/** * Shelly Gen2 Script: Poll & Send Measurements + Daily kWh Accumulator * * - Polls all available component measurements every POLL_INTERVAL_MS * - Tracks cumulative kWh per component for the current day * - Resets the daily kWh counter at 00:00 (midnight) each day * - POSTs everything as JSON to TARGET_URL * * Enable "Run on startup" in the Scripts UI to persist across reboots. */ var TARGET_URL = "http://123.123.123.123:5000"; var POLL_INTERVAL_MS = 10000; // 10 seconds // Components that can carry measurement data on Gen2 devices. // The script will silently skip any that are not present on your device. var COMPONENTS = [ { type: "switch", maxId: 3 }, { type: "cover", maxId: 1 }, { type: "light", maxId: 3 }, { type: "temperature", maxId: 3 }, { type: "humidity", maxId: 3 }, { type: "voltmeter", maxId: 3 }, { type: "input", maxId: 3 }, { type: "em", maxId: 0 }, { type: "emdata", maxId: 0 }, ]; var EXTRACT = { "switch": ["output", "apower", "voltage", "current", "aenergy", "temperature"], "cover": ["state", "apower", "voltage", "current", "aenergy", "temperature"], "light": ["output", "brightness", "apower", "voltage", "current", "aenergy"], "temperature": ["tC", "tF"], "humidity": ["rh"], "voltmeter": ["voltage"], "input": ["state", "counts"], "em": ["a_act_power", "b_act_power", "c_act_power", "a_voltage", "b_voltage", "c_voltage", "a_current", "b_current", "c_current", "total_act_power", "total_act_energy"], "emdata": ["a_total_act_energy", "b_total_act_energy", "c_total_act_energy", "total_act_energy"], }; // --------------------------------------------------------------------------- // Daily kWh accumulator // --------------------------------------------------------------------------- // Strategy: on each poll we read the device's own lifetime Wh counter // (aenergy.total for switch/cover/light, total_act_energy for em/emdata). // We store the lifetime value at the START of the day and subtract it each // poll cycle. This is accurate regardless of poll interval and survives // reboots as long as the device's internal counter is not reset. var dayBaseline = {}; // { "switch:0": , ... } var lastDay = -1; // tracks current day-of-month to detect rollover function getTodayDay() { return new Date().getDate(); // 1-31 } // Extract the lifetime energy Wh value from a component status object. function getLifetimeWh(compType, status) { if (!status) return null; // switch / cover / light → aenergy.total (Wh) if (status.aenergy && typeof status.aenergy.total === "number") { return status.aenergy.total; } // em / emdata → total_act_energy (Wh) if (typeof status.total_act_energy === "number") { return status.total_act_energy; } return null; } // Called every poll. Returns daily Wh consumed so far today, or null. function updateDailyWh(compKey, compType, status) { var lifetimeWh = getLifetimeWh(compType, status); if (lifetimeWh === null) return null; var today = getTodayDay(); // Day rollover → wipe all baselines if (today !== lastDay) { console.log("[daily] New day – resetting kWh baselines."); dayBaseline = {}; lastDay = today; } // First sighting of this component today → record baseline if (typeof dayBaseline[compKey] === "undefined") { dayBaseline[compKey] = lifetimeWh; } var dailyWh = lifetimeWh - dayBaseline[compKey]; if (dailyWh < 0) dailyWh = 0; // guard: internal counter reset or reboot return Math.round(dailyWh * 100) / 100; // Wh, 2 dp } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function pickFields(obj, fields) { if (!obj) return null; var out = {}, found = false; for (var i = 0; i < fields.length; i++) { var key = fields[i]; if (typeof obj[key] !== "undefined") { out[key] = obj[key]; found = true; } } return found ? out : null; } function round3(v) { return Math.round(v * 1000) / 1000; } // --------------------------------------------------------------------------- // Core: collect + send // --------------------------------------------------------------------------- function collectPayload() { var info = Shelly.getDeviceInfo(); var sys = Shelly.getComponentStatus("sys"); var measurements = {}; var daily_kwh = {}; // per component, in kWh var totalDailyWh = 0; for (var c = 0; c < COMPONENTS.length; c++) { var compType = COMPONENTS[c].type; var fields = EXTRACT[compType]; if (!fields) continue; for (var id = 0; id <= COMPONENTS[c].maxId; id++) { var status = Shelly.getComponentStatus(compType, id); if (!status) continue; var picked = pickFields(status, fields); if (!picked) continue; var key = compType + ":" + id; measurements[key] = picked; // Daily energy tracking var dailyWh = updateDailyWh(key, compType, status); if (dailyWh !== null) { daily_kwh[key] = round3(dailyWh / 1000); // Wh → kWh totalDailyWh += dailyWh; } } } return { device_id: info.id, device_app: info.app, fw_version: info.ver, uptime_s: sys ? sys.uptime : null, timestamp: Math.floor(Date.now() / 1000), // Unix epoch seconds measurements: measurements, daily_kwh: daily_kwh, // per-component kWh today daily_kwh_total: round3(totalDailyWh / 1000), // all metered components summed }; } function sendData() { var payload = collectPayload(); if (Object.keys(payload.measurements).length === 0) { console.log("[poll] No measurement data found on this device."); return; } Shelly.call( "HTTP.Request", { method: "POST", url: TARGET_URL, headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }, function(result, error_code, error_msg) { if (error_code !== 0) { console.log("[poll] HTTP error " + error_code + ": " + error_msg); } else { console.log("[poll] OK HTTP " + result.code + " | today total: " + payload.daily_kwh_total + " kWh"); } } ); } // --------------------------------------------------------------------------- // Boot // --------------------------------------------------------------------------- lastDay = getTodayDay(); // seed so first poll sets baselines, not a reset sendData(); Timer.set(POLL_INTERVAL_MS, true, sendData); console.log("[poll] Started – every " + (POLL_INTERVAL_MS / 1000) + "s → " + TARGET_URL);