Show 3EM power consumption on Wall Display
- Home
- Show 3EM power consumption on Wall Display
Introduction
Right now, it is not possible to display a virtual device on a Shelly Wall Display, but with this solution we can show the total energy consumption from for example a Shelly Pro 3EM directly on the screen.
Script Functions
Sends total energy consumption to a virtual component on the Wall Display.
With two virtual components on the energy meter you can also:
Displays the same consumption value locally – Virtual Number Component.
Reset counter – Virtual Button Component.
There are 2 methods
1. Newer devices (Beta 2.6 for WDXL and WD X2i)
With the latest beta 2.6 firmware for WDXL and WD X2i, it is possible to use a Virtual Number Component that the script writes values to.
2. Older devices
On older devices where only Virtual Buttons are available, the script can rename the button label to show the consumption value instead.
(It will not be displayed as large as a number component, but it still works well.)
Connection Types
There are two ways to use the script:
1. Manual IP Address
You manually enter the Wall Display IP address in the script, and the data is sent there.
2. Automatic Detection via Range Extender.
The energy meter can be configured as a range extender, where the Wall Display connects to it.
The script will automatically detect the Wall Display IP address and send data to it.
This can be an advantage if you do not have the option to assign a fixed IP address in the router.
Quick Setup Guide
Wall Display
Create a virtual component:
Virtual Number (recommended for newer devices) together with a virtual group and place the number VC into it (If shown as a group the label will be a little larger)
or
Virtual Button (for older firmware/devices)
3-Phase Energy Meter
– Create a Virtual Number Component
– Create a Virtual Button Component
– Insert the script
– Adjust the settings in the script
– Save and run the script
– Enable Run on Update
Script for newer devices with firmware 2.6+
/*************************************************
* Period energy counter (reset via virtual button)
*
* Features:
* - Reads total_act from EMData.GetStatus
* - Calculates consumption since last reset
* - Converts the result to kWh
* - Virtual button reset stores a new start point in KVS
* - Writes current value to local Virtual Number
* - Writes current value to remote Virtual Number via HTTP RPC
* - Remote number host can be set manually or auto-detected
* from the first WiFi AP client connected to this device
*************************************************/
let CONFIG = {
emdata_id: 0, // EMData component ID to read total_act from
virtual_button_id_reset: 200, // Local virtual button ID used to reset the counter
virtual_number_id_period: 200, // Local virtual number ID used to display the period value
update_interval_s: 10, // Update interval in seconds
virtual_number_decimals: 4, // Number of decimals written to the local and remote virtual number
remote_number_host_mode: "auto", // "manual" = use remote_number_host_manual, "auto" = detect from AP client list
remote_number_host_manual: "192.168.2.195", // Manual IP address of the remote Shelly used for number update
remote_number_id: 200, // Remote virtual number ID to write to
ap_client_refresh_on_start: true, // Refresh auto-detected AP client IP on boot
debug: true, // Enable or disable debug logging
};
let resolvedApClientHost = null;
function logd() {
if (!CONFIG.debug) return;
try {
print.apply(null, arguments);
} catch (e) {}
}
function kvsKey(name) {
return "period_" + name;
}
function floorToDecimals(v, decimals) {
let factor = 1;
let i;
for (i = 0; i < decimals; i++) factor *= 10;
return Math.floor(v * factor) / factor;
}
function roundTo4Decimals(v) {
return Math.round(v * 10000) / 10000;
}
function formatFixed(num, decimals) {
let s;
try {
s = String(num.toFixed(decimals));
} catch (e) {
s = String(num);
}
return s;
}
function normalizeHost(host) {
try {
host = String(host);
if (host.indexOf("http://") === 0) host = host.slice(7);
else if (host.indexOf("https://") === 0) host = host.slice(8);
while (host.length > 0 && host.charAt(host.length - 1) === "/") {
host = host.slice(0, host.length - 1);
}
return host;
} catch (e) {
print("normalizeHost error:", e);
return "";
}
}
function urlEncodeSimple(str) {
try {
str = String(str);
let out = "";
let i, ch;
for (i = 0; i < str.length; i++) {
ch = str.charAt(i);
if (ch === "%") out += "%25";
else if (ch === " ") out += "%20";
else if (ch === '"') out += "%22";
else if (ch === "{") out += "%7B";
else if (ch === "}") out += "%7D";
else if (ch === ":") out += "%3A";
else if (ch === ",") out += "%2C";
else if (ch === "[") out += "%5B";
else if (ch === "]") out += "%5D";
else if (ch === "\\") out += "%5C";
else if (ch === "+") out += "%2B";
else out += ch;
}
return out;
} catch (e) {
print("urlEncodeSimple error:", e);
return "";
}
}
/*** Sequential call queue ***/
let Q = [];
let Qbusy = false;
let Qindex = 0;
function qCall(method, params, cb) {
try {
Q.push({ method: method, params: params, cb: cb });
qRun();
} catch (e) {
print("qCall error:", e);
}
}
function qRun() {
try {
if (Qbusy) return;
if (Qindex >= Q.length) {
Q = [];
Qindex = 0;
return;
}
Qbusy = true;
let item = Q[Qindex];
Qindex++;
Shelly.call(item.method, item.params || {}, function(res, err) {
try {
if (item.cb) item.cb(res, err);
} catch (e) {
print("Callback error in", item.method, ":", e);
}
Timer.set(120, false, function() {
Qbusy = false;
qRun();
});
});
} catch (e) {
print("Queue error:", e);
Qbusy = false;
}
}
/*** KVS helpers ***/
function kvsGet(key, cb) {
qCall("KVS.Get", { key: key }, function(res, err) {
try {
if (err || !res || typeof res.value === "undefined") {
cb(null);
return;
}
cb(res.value);
} catch (e) {
print("kvsGet error:", e);
cb(null);
}
});
}
function kvsSet(key, value, cb) {
qCall("KVS.Set", { key: key, value: String(value) }, function(res, err) {
try {
if (cb) cb(res, err);
} catch (e) {
print("kvsSet callback error:", e);
}
});
}
/*** Read total_act from EMData ***/
function readTotalAct(cb) {
qCall("EMData.GetStatus", { id: CONFIG.emdata_id }, function(res, err) {
try {
if (err || !res) {
cb(null, err || { message: "No response" });
return;
}
if (typeof res.total_act === "undefined") {
cb(null, { message: "total_act missing" });
return;
}
cb(res.total_act, null);
} catch (e) {
print("readTotalAct error:", e);
cb(null, { message: "readTotalAct exception" });
}
});
}
/*** Get first AP client IP ***/
function getFirstApClientHost(cb) {
try {
if (resolvedApClientHost) {
cb(resolvedApClientHost);
return;
}
qCall("WiFi.ListAPClients", {}, function(res, err) {
try {
let clients;
let i;
let ip = null;
if (err || !res || !res.ap_clients) {
print("WiFi.ListAPClients failed:", JSON.stringify(err || res));
cb(null);
return;
}
clients = res.ap_clients;
for (i = 0; i < clients.length; i++) {
if (clients[i] && clients[i].ip) {
ip = clients[i].ip;
break;
}
}
if (!ip) {
print("No AP client IP found.");
cb(null);
return;
}
resolvedApClientHost = normalizeHost(ip);
logd("Auto-detected AP client host:", resolvedApClientHost);
cb(resolvedApClientHost);
} catch (e) {
print("getFirstApClientHost callback error:", e);
cb(null);
}
});
} catch (e) {
print("getFirstApClientHost error:", e);
cb(null);
}
}
function refreshApClientHost(cb) {
try {
resolvedApClientHost = null;
getFirstApClientHost(function(host) {
if (cb) cb(host);
});
} catch (e) {
print("refreshApClientHost error:", e);
if (cb) cb(null);
}
}
function resolveRemoteNumberHost(cb) {
try {
if (CONFIG.remote_number_host_mode === "manual") {
cb(normalizeHost(CONFIG.remote_number_host_manual));
return;
}
if (CONFIG.remote_number_host_mode === "auto") {
getFirstApClientHost(function(host) {
cb(host);
});
return;
}
print("Invalid remote_number_host_mode:", CONFIG.remote_number_host_mode);
cb(null);
} catch (e) {
print("resolveRemoteNumberHost error:", e);
cb(null);
}
}
/*** Write value to remote virtual number via HTTP.GET ***/
function updateRemoteNumber(value) {
try {
resolveRemoteNumberHost(function(host) {
try {
let url;
if (!host) {
print("Remote number host unavailable. Skipping update.");
return;
}
url =
"http://" + host +
"/rpc/Number.Set?id=" + CONFIG.remote_number_id +
"&value=" + urlEncodeSimple(String(value));
logd("Remote number URL:", url);
qCall("HTTP.GET", { url: url }, function(res, err) {
try {
if (err) {
print("Remote Number.Set failed:", JSON.stringify(err));
if (CONFIG.remote_number_host_mode === "auto") {
resolvedApClientHost = null;
}
return;
}
logd("Remote number set to:", value);
} catch (e) {
print("updateRemoteNumber callback error:", e);
}
});
} catch (e) {
print("updateRemoteNumber host callback error:", e);
}
});
} catch (e) {
print("updateRemoteNumber error:", e);
}
}
/*** Write current period value to local Virtual Number ***/
function writePeriodValue(kwh, totalAct, cb) {
try {
let displayValue = floorToDecimals(kwh, CONFIG.virtual_number_decimals);
let logValue = roundTo4Decimals(kwh);
let logText = formatFixed(logValue, 4);
let totalText = String(totalAct);
qCall("Number.Set", {
id: CONFIG.virtual_number_id_period,
value: displayValue
}, function(_r, _e) {
logd("Updated period value:", logText, "kWh | total_act:", totalText, "| number_value:", displayValue);
updateRemoteNumber(displayValue);
if (cb) cb(displayValue);
});
} catch (e) {
print("writePeriodValue error:", e);
if (cb) cb(null);
}
}
/*** Initialize start value if missing ***/
function initStartValue(cb) {
try {
kvsGet(kvsKey("start_total_act"), function(storedStartStr) {
try {
let storedStart = null;
if (storedStartStr !== null) {
storedStart = parseFloat(storedStartStr);
}
if (storedStart !== null && !isNaN(storedStart)) {
logd("Existing start_total_act:", storedStart);
if (cb) cb(storedStart);
return;
}
readTotalAct(function(totalAct, err) {
try {
if (err || totalAct === null) {
print("Initialization failed, could not read total_act:", JSON.stringify(err));
if (cb) cb(null);
return;
}
kvsSet(kvsKey("start_total_act"), totalAct, function() {
try {
print("Initialized period start at total_act:", totalAct);
qCall("Number.Set", {
id: CONFIG.virtual_number_id_period,
value: 0
}, function() {
updateRemoteNumber(0);
if (cb) cb(totalAct);
});
} catch (e) {
print("initStartValue finalize error:", e);
if (cb) cb(null);
}
});
} catch (e) {
print("initStartValue readTotalAct error:", e);
if (cb) cb(null);
}
});
} catch (e) {
print("initStartValue KVS parse error:", e);
if (cb) cb(null);
}
});
} catch (e) {
print("initStartValue error:", e);
if (cb) cb(null);
}
}
/*** Update displayed period consumption ***/
function updatePeriodConsumption() {
try {
kvsGet(kvsKey("start_total_act"), function(storedStartStr) {
try {
let storedStart = null;
if (storedStartStr !== null) {
storedStart = parseFloat(storedStartStr);
}
if (storedStart === null || isNaN(storedStart)) {
logd("No valid start value found, initializing...");
initStartValue(function() {});
return;
}
readTotalAct(function(totalAct, err) {
try {
if (err || totalAct === null) {
print("Update failed, EMData.GetStatus error:", JSON.stringify(err));
return;
}
let periodWh = totalAct - storedStart;
if (periodWh < 0) periodWh = 0;
let periodKwh = periodWh / 1000;
writePeriodValue(periodKwh, totalAct, function(displayValue) {
if (displayValue === null) return;
});
} catch (e) {
print("updatePeriodConsumption logic error:", e);
}
});
} catch (e) {
print("updatePeriodConsumption KVS parse error:", e);
}
});
} catch (e) {
print("updatePeriodConsumption error:", e);
}
}
/*** Reset period counter ***/
function resetPeriodConsumption() {
try {
print("Reset requested from virtual button.");
readTotalAct(function(totalAct, err) {
try {
if (err || totalAct === null) {
print("Reset failed, could not read total_act:", JSON.stringify(err));
return;
}
kvsSet(kvsKey("start_total_act"), totalAct, function() {
try {
print("Period counter reset. New start_total_act:", totalAct);
qCall("Number.Set", {
id: CONFIG.virtual_number_id_period,
value: 0
}, function() {
logd("Period value reset to 0");
updateRemoteNumber(0);
});
} catch (e) {
print("resetPeriodConsumption finalize error:", e);
}
});
} catch (e) {
print("resetPeriodConsumption readTotalAct error:", e);
}
});
} catch (e) {
print("resetPeriodConsumption error:", e);
}
}
/*** Listen for virtual button press ***/
function setupButtonHandler() {
try {
Shelly.addEventHandler(function(e) {
try {
if (!e) return;
let comp = "";
if (typeof e.component !== "undefined") comp = e.component;
else if (typeof e.src !== "undefined") comp = e.src;
let ev = "";
if (typeof e.event !== "undefined") ev = e.event;
else if (e.info && typeof e.info.event !== "undefined") ev = e.info.event;
if (comp.indexOf("btn:") === 0 || comp.indexOf("button:") === 0) {
print("Button event detected:", JSON.stringify(e));
}
let match1 = "btn:" + CONFIG.virtual_button_id_reset;
let match2 = "button:" + CONFIG.virtual_button_id_reset;
if (comp !== match1 && comp !== match2) return;
if (
ev === "single_push" ||
ev === "btn_down" ||
ev === "toggle" ||
ev === "short_push"
) {
print("Reset button triggered:", comp, ev);
resetPeriodConsumption();
}
} catch (err) {
print("Event handler error:", err);
}
});
print("Button event handler ready for:", "btn:" + CONFIG.virtual_button_id_reset);
} catch (e) {
print("setupButtonHandler error:", e);
}
}
/*** Start periodic updater ***/
function startUpdater() {
try {
Timer.set(CONFIG.update_interval_s * 1000, true, function() {
try {
updatePeriodConsumption();
} catch (e) {
print("Periodic update error:", e);
}
});
print("Periodic updater started. Interval:", CONFIG.update_interval_s, "sec");
} catch (e) {
print("startUpdater error:", e);
}
}
/*** Boot ***/
function boot() {
try {
if (CONFIG.ap_client_refresh_on_start &&
CONFIG.remote_number_host_mode === "auto") {
refreshApClientHost(function(host) {
if (host) logd("AP client host ready:", host);
});
}
initStartValue(function() {
try {
updatePeriodConsumption();
} catch (e) {
print("Initial update error:", e);
}
});
setupButtonHandler();
startUpdater();
print(
"Period kWh counter started. Reset button ID:",
CONFIG.virtual_button_id_reset,
"Local number ID:",
CONFIG.virtual_number_id_period
);
} catch (e) {
print("Boot error:", e);
}
}
boot(); Script for older devices with firmware below 2.6
/*************************************************
* Period energy counter (reset via virtual button)
*
* Features:
* - Reads total_act from EMData.GetStatus
* - Calculates consumption since last reset
* - A virtual button resets the counter by saving
* a new starting point in KVS
*
*
* Maintained by: Shelly Nordics - Ronni
*************************************************/
let CONFIG = {
emdata_id: 0, // EMData component ID used for reading total_act
virtual_button_id_reset: 200, // Virtual button ID used to reset the period counter
virtual_number_id_period: 200, // Virtual number ID used to show the calculated period consumption in kWh
update_interval_s: 10, // Update interval in seconds for refreshing the displayed period value
virtual_number_decimals: 4, // Number of decimals shown on the virtual number and remote label
// "manual" = use remote_button_host
// "auto" = use the first AP client found with an IP address
remote_host_mode: "auto", // Select how the remote host IP should be resolved
remote_button_host: "192.168.33.37", // Manual IP address of the remote device when remote_host_mode is set to "manual"
remote_button_id: 200, // Virtual button ID on the remote device that should be renamed
debug: true, // Enable or disable debug logging
};
function logd() {
if (!CONFIG.debug) return;
try {
print.apply(null, arguments);
} catch (e) {}
}
function floorToDecimals(v, decimals) {
let factor = 1;
let i;
for (i = 0; i < decimals; i++) factor *= 10;
return Math.floor(v * factor) / factor;
}
function roundTo4Decimals(v) {
return Math.round(v * 10000) / 10000;
}
function formatFixed(num, decimals) {
let s;
try {
s = String(num.toFixed(decimals));
} catch (e) {
s = String(num);
}
return s;
}
/*** Regex-free escaping ***/
function escapeJsonString(str) {
try {
str = String(str);
let out = "";
let i, ch;
for (i = 0; i < str.length; i++) {
ch = str.charAt(i);
if (ch === "\\") out += "\\\\";
else if (ch === '"') out += '\\"';
else out += ch;
}
return out;
} catch (e) {
print("escapeJsonString error:", e);
return "";
}
}
function urlEncodeSimple(str) {
try {
str = String(str);
let out = "";
let i, ch;
for (i = 0; i < str.length; i++) {
ch = str.charAt(i);
if (ch === "%") out += "%25";
else if (ch === " ") out += "%20";
else if (ch === '"') out += "%22";
else if (ch === "{") out += "%7B";
else if (ch === "}") out += "%7D";
else if (ch === ":") out += "%3A";
else if (ch === ",") out += "%2C";
else if (ch === "[") out += "%5B";
else if (ch === "]") out += "%5D";
else if (ch === "\\") out += "%5C";
else out += ch;
}
return out;
} catch (e) {
print("urlEncodeSimple error:", e);
return "";
}
}
/*** Sequential call queue ***/
let Q = [];
let Qbusy = false;
let Qindex = 0;
function qCall(method, params, cb) {
try {
Q.push({ method: method, params: params, cb: cb });
qRun();
} catch (e) {
print("qCall error:", e);
}
}
function qRun() {
try {
if (Qbusy) return;
if (Qindex >= Q.length) {
Q = [];
Qindex = 0;
return;
}
Qbusy = true;
let item = Q[Qindex];
Qindex++;
Shelly.call(item.method, item.params || {}, function(res, err) {
try {
if (item.cb) item.cb(res, err);
} catch (e) {
print("Callback error in", item.method, ":", e);
}
Timer.set(120, false, function() {
Qbusy = false;
qRun();
});
});
} catch (e) {
print("Queue error:", e);
Qbusy = false;
}
}
/*** KVS helpers ***/
function kvsGet(key, cb) {
qCall("KVS.Get", { key: key }, function(res, err) {
try {
if (err) {
logd("KVS.Get error for key", key, ":", JSON.stringify(err));
cb({ ok: false, found: false, value: null, err: err });
return;
}
if (!res) {
logd("KVS.Get no response for key:", key);
cb({ ok: false, found: false, value: null, err: { message: "No response" } });
return;
}
if (typeof res.value === "undefined") {
logd("KVS key missing:", key);
cb({ ok: true, found: false, value: null, err: null });
return;
}
logd("KVS key found:", key, "value:", res.value);
cb({ ok: true, found: true, value: res.value, err: null });
} catch (e) {
print("kvsGet error:", e);
cb({ ok: false, found: false, value: null, err: { message: "kvsGet exception" } });
}
});
}
function kvsSet(key, value, cb) {
qCall("KVS.Set", { key: key, value: String(value) }, function(res, err) {
try {
if (err) {
print("KVS.Set failed for key", key, ":", JSON.stringify(err));
} else {
logd("KVS.Set OK:", key, "=", String(value));
}
if (cb) cb(res, err);
} catch (e) {
print("kvsSet cb error:", e);
}
});
}
/*** Read total_act ***/
function readTotalAct(cb) {
qCall("EMData.GetStatus", { id: CONFIG.emdata_id }, function(res, err) {
try {
if (err || !res) {
cb(null, err || { message: "No response" });
return;
}
if (typeof res.total_act === "undefined") {
cb(null, { message: "total_act missing" });
return;
}
cb(res.total_act, null);
} catch (e) {
print("readTotalAct error:", e);
cb(null, { message: "readTotalAct exception" });
}
});
}
/*** Remote host cache ***/
let cachedRemoteHost = null;
/*** Find remote host ***/
function resolveRemoteButtonHost(cb) {
try {
if (CONFIG.remote_host_mode === "manual") {
logd("Using manual remote host:", CONFIG.remote_button_host);
cb(CONFIG.remote_button_host);
return;
}
if (cachedRemoteHost) {
logd("Using cached remote host:", cachedRemoteHost);
cb(cachedRemoteHost);
return;
}
qCall("Wifi.ListAPClients", {}, function(res, err) {
try {
if (err || !res || !res.ap_clients) {
print("Wifi.ListAPClients failed:", JSON.stringify(err || { message: "No response" }));
cb(null);
return;
}
let clients = res.ap_clients;
let i, client, ip, mac;
logd("AP clients found:", clients.length);
for (i = 0; i < clients.length; i++) {
client = clients[i];
if (!client) continue;
ip = client.ip || "";
mac = client.mac || "";
logd("AP client", i, "| IP:", ip, "| MAC:", mac);
}
for (i = 0; i < clients.length; i++) {
client = clients[i];
if (!client) continue;
ip = client.ip;
if (ip) {
cachedRemoteHost = ip;
logd("Auto remote host found:", ip);
cb(ip);
return;
}
}
print("No AP client with IP found");
cb(null);
} catch (e) {
print("resolveRemoteButtonHost callback error:", e);
cb(null);
}
});
} catch (e) {
print("resolveRemoteButtonHost error:", e);
cb(null);
}
}
/*** Write current period consumption to Number.Set ***/
function writePeriodValue(kwh, totalAct, cb) {
try {
let displayValue = floorToDecimals(kwh, CONFIG.virtual_number_decimals);
let logValue = roundTo4Decimals(kwh);
let logText = formatFixed(logValue, 4);
let totalText = String(totalAct);
qCall("Number.Set", {
id: CONFIG.virtual_number_id_period,
value: displayValue
}, function(_r, _e) {
logd("Updated period value:", logText, "kWh | total_act:", totalText, "| number_value:", displayValue);
if (cb) cb(displayValue);
});
} catch (e) {
print("writePeriodValue error:", e);
if (cb) cb(null);
}
}
/*** Remote virtual button rename via HTTP.GET ***/
let lastRemoteButtonName = null;
let lastResolvedRemoteHost = null;
function updateRemoteButtonName(name) {
try {
if (name === lastRemoteButtonName && lastResolvedRemoteHost) return;
resolveRemoteButtonHost(function(host) {
try {
if (!host) {
print("Remote button host could not be resolved");
return;
}
let safeName = escapeJsonString(name);
let cfgJson = '{"name":"' + safeName + '"}';
let url =
"http://" + host +
"/rpc/Button.SetConfig?id=" + CONFIG.remote_button_id +
"&config=" + urlEncodeSimple(cfgJson);
logd("Remote button URL:", url);
qCall("HTTP.GET", { url: url }, function(res, err) {
try {
if (err) {
print("Remote Button.SetConfig failed:", JSON.stringify(err));
cachedRemoteHost = null;
lastResolvedRemoteHost = null;
return;
}
lastRemoteButtonName = name;
lastResolvedRemoteHost = host;
logd("Remote button renamed to:", name, "| host:", host);
} catch (e) {
print("updateRemoteButtonName cb error:", e);
}
});
} catch (e) {
print("updateRemoteButtonName resolve callback error:", e);
}
});
} catch (e) {
print("updateRemoteButtonName error:", e);
}
}
/*** Update only remote button name ***/
function updateRemoteLabel(kwh) {
try {
let displayValue = floorToDecimals(kwh, CONFIG.virtual_number_decimals);
let name = "kWh: " + formatFixed(displayValue, CONFIG.virtual_number_decimals);
updateRemoteButtonName(name);
} catch (e) {
print("updateRemoteLabel error:", e);
}
}
/*** Init start value if missing ***/
function initStartValue(cb) {
try {
kvsGet("period_start_total_act", function(result) {
try {
if (!result || !result.ok) {
print("KVS.Get failed during init, not resetting counter.");
if (result && result.err) print("KVS error:", JSON.stringify(result.err));
if (cb) cb(null);
return;
}
if (result.found) {
let storedStart = parseFloat(result.value);
if (!isNaN(storedStart)) {
logd("Existing start_total_act:", storedStart);
if (cb) cb(storedStart);
return;
}
print("KVS value exists but is invalid:", result.value);
if (cb) cb(null);
return;
}
readTotalAct(function(totalAct, err) {
try {
if (err || totalAct === null) {
print("Init failed, could not read total_act:", JSON.stringify(err));
if (cb) cb(null);
return;
}
kvsSet("period_start_total_act", totalAct, function(_res, setErr) {
try {
if (setErr) {
print("Failed to initialize start_total_act");
if (cb) cb(null);
return;
}
print("Initialized period start at total_act:", totalAct);
qCall("Number.Set", {
id: CONFIG.virtual_number_id_period,
value: 0
}, function() {
updateRemoteLabel(0);
if (cb) cb(totalAct);
});
} catch (e) {
print("initStartValue finalize error:", e);
if (cb) cb(null);
}
});
} catch (e) {
print("initStartValue readTotalAct error:", e);
if (cb) cb(null);
}
});
} catch (e) {
print("initStartValue kvsGet error:", e);
if (cb) cb(null);
}
});
} catch (e) {
print("initStartValue error:", e);
if (cb) cb(null);
}
}
/*** Update displayed period consumption ***/
function updatePeriodConsumption() {
try {
kvsGet("period_start_total_act", function(result) {
try {
if (!result || !result.ok) {
print("KVS.Get failed during update, skipping update.");
if (result && result.err) print("KVS error:", JSON.stringify(result.err));
return;
}
if (!result.found) {
logd("No start value found in KVS, initializing...");
initStartValue(function() {});
return;
}
let storedStart = parseFloat(result.value);
if (isNaN(storedStart)) {
print("Stored start_total_act is invalid:", result.value);
return;
}
readTotalAct(function(totalAct, err) {
try {
if (err || totalAct === null) {
print("Update failed, EMData.GetStatus error:", JSON.stringify(err));
return;
}
let periodWh = totalAct - storedStart;
if (periodWh < 0) periodWh = 0;
let periodKwh = periodWh / 1000;
writePeriodValue(periodKwh, totalAct, function(displayValue) {
if (displayValue === null) return;
updateRemoteLabel(periodKwh);
});
} catch (e) {
print("updatePeriodConsumption logic error:", e);
}
});
} catch (e) {
print("updatePeriodConsumption error:", e);
}
});
} catch (e) {
print("updatePeriodConsumption outer error:", e);
}
}
/*** Reset period counter ***/
function resetPeriodConsumption() {
try {
print("Reset requested from virtual button.");
readTotalAct(function(totalAct, err) {
try {
if (err || totalAct === null) {
print("Reset failed, could not read total_act:", JSON.stringify(err));
return;
}
kvsSet("period_start_total_act", totalAct, function(_res, setErr) {
try {
if (setErr) {
print("Reset failed, could not write new start_total_act");
return;
}
print("Period counter reset. New start_total_act:", totalAct);
qCall("Number.Set", {
id: CONFIG.virtual_number_id_period,
value: 0
}, function() {
logd("Period value reset to 0");
lastRemoteButtonName = null;
updateRemoteLabel(0);
});
} catch (e) {
print("resetPeriodConsumption finalize error:", e);
}
});
} catch (e) {
print("resetPeriodConsumption readTotalAct error:", e);
}
});
} catch (e) {
print("resetPeriodConsumption error:", e);
}
}
/*** Listen for virtual button press ***/
function setupButtonHandler() {
try {
Shelly.addEventHandler(function(e) {
try {
if (!e) return;
let comp = "";
if (typeof e.component !== "undefined") comp = e.component;
else if (typeof e.src !== "undefined") comp = e.src;
let ev = "";
if (typeof e.event !== "undefined") ev = e.event;
else if (e.info && typeof e.info.event !== "undefined") ev = e.info.event;
if (comp.indexOf("btn:") === 0 || comp.indexOf("button:") === 0) {
print("Button event seen:", JSON.stringify(e));
}
let match1 = "btn:" + CONFIG.virtual_button_id_reset;
let match2 = "button:" + CONFIG.virtual_button_id_reset;
if (comp !== match1 && comp !== match2) return;
if (
ev === "single_push" ||
ev === "btn_down" ||
ev === "toggle" ||
ev === "short_push"
) {
print("Reset button triggered:", comp, ev);
resetPeriodConsumption();
}
} catch (err) {
print("Event handler error:", err);
}
});
print("Button event handler ready for:", "btn:" + CONFIG.virtual_button_id_reset);
} catch (e) {
print("setupButtonHandler error:", e);
}
}
/*** Periodic updater ***/
function startUpdater() {
try {
Timer.set(CONFIG.update_interval_s * 1000, true, function() {
try {
updatePeriodConsumption();
} catch (e) {
print("Periodic update error:", e);
}
});
print("Periodic updater started. Interval:", CONFIG.update_interval_s, "sec");
} catch (e) {
print("startUpdater error:", e);
}
}
/*** Boot ***/
function boot() {
try {
initStartValue(function() {
try {
updatePeriodConsumption();
} catch (e) {
print("Initial update error:", e);
}
});
setupButtonHandler();
startUpdater();
print(
"Period kWh counter started. Button:",
CONFIG.virtual_button_id_reset,
"Number:",
CONFIG.virtual_number_id_period
);
} catch (e) {
print("Boot error:", e);
}
}
boot();
