Last active
May 11, 2026 11:44
-
-
Save Davc0m/dc419aa3147ec3c3d6e7289f89dc0ed8 to your computer and use it in GitHub Desktop.
Shelly Pro 3EM: Saldierende Energiemessung (Net Metering) mit Home Assistant Auto-Discovery
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * Shelly Pro 3EM - Net Metering (Saldierung) & Home Assistant Auto-Discovery | |
| * Version: 1.1.8 | |
| * | |
| * DISCLAIMER: | |
| * Use this script entirely at your own risk! I assume absolutely no liability | |
| * for any direct, indirect, or consequential damages. This includes, but is | |
| * not limited to, damage to the Shelly device, any connected electrical | |
| * equipment, other devices in your network, data loss, or system malfunctions. | |
| * By using this script, you acknowledge that you alone are responsible for | |
| * your hardware and setup. | |
| * | |
| * CHANGELOG (v1.1.1 → v1.1.8): | |
| * - Corrected HA Discovery MQTT topic (removed duplicate device prefix) | |
| * - Added null/type-checks for MQTT config, em component and KVS values | |
| * - Fixed race condition: main loop waits until KVS counters are fully loaded | |
| * - Replaced fixed startup delay with MQTT.setConnectHandler() | |
| * - Counter topics are now published retained for reliable HA recovery | |
| * - Availability uses native Shelly LWT topic (<id>/online, true/false) | |
| * - HA Discovery payload switched to short keys to stay under 512-byte MQTT limit | |
| * - Removed unused helper functions and objects to reduce RAM usage | |
| */ | |
| let CONFIG = { | |
| updateInterval: 500, // Calculation cycle in ms | |
| enablePersistence: true, // true = Save counter states to flash memory | |
| saveInterval: 1800, // Save to KVS every 1800 cycles (~15 min.) | |
| mqttPrefix: "homeassistant" // Standard HA Discovery Prefix | |
| }; | |
| let VERSION = "1.1.8"; | |
| let SHELLY_ID = null; | |
| let energyReturnedWs = 0.0; | |
| let energyConsumedWs = 0.0; | |
| let energyReturnedKWh = 0.0; | |
| let energyConsumedKWh = 0.0; | |
| let saveCounter = 0; | |
| let lastPublishedConsumed = ""; | |
| let lastPublishedReturned = ""; | |
| let countersLoaded = false; | |
| // ───────────────────────────────────────────── | |
| // 1. Helper Functions | |
| // ───────────────────────────────────────────── | |
| function TryAnnounceAndPublish() { | |
| if (!SHELLY_ID || !MQTT.isConnected()) return; | |
| AnnounceHA(); | |
| if (countersLoaded) PublishCounters(true); | |
| } | |
| function PublishCounters(force) { | |
| if (!SHELLY_ID || !countersLoaded) return; | |
| let valC = energyConsumedKWh.toFixed(3); | |
| let valR = energyReturnedKWh.toFixed(3); | |
| if (!force && valC === lastPublishedConsumed && valR === lastPublishedReturned) return; | |
| let okC = MQTT.publish(SHELLY_ID + "/energy_counter/consumed", valC, 0, true); | |
| let okR = MQTT.publish(SHELLY_ID + "/energy_counter/returned", valR, 0, true); | |
| if (okC) lastPublishedConsumed = valC; | |
| if (okR) lastPublishedReturned = valR; | |
| } | |
| // ───────────────────────────────────────────── | |
| // 2. MQTT Event Handlers | |
| // ───────────────────────────────────────────── | |
| MQTT.setConnectHandler(function () { | |
| print("MQTT connected."); | |
| TryAnnounceAndPublish(); | |
| }); | |
| MQTT.setDisconnectHandler(function () { | |
| // Cannot publish here – connection is already gone. | |
| // HA uses native Shelly LWT on <topic_prefix>/online for offline detection. | |
| print("MQTT disconnected."); | |
| }); | |
| // ───────────────────────────────────────────── | |
| // 3. Get Device ID and Initialize | |
| // ───────────────────────────────────────────── | |
| Shelly.call("Mqtt.GetConfig", {}, function (res, err_code, err_msg) { | |
| if (!res) { | |
| print("ERROR: Mqtt.GetConfig returned null! Code: " + err_code + " | " + err_msg); | |
| return; | |
| } | |
| SHELLY_ID = res.topic_prefix ? res.topic_prefix : null; | |
| if (!SHELLY_ID) { | |
| print("ERROR: No MQTT topic_prefix set. Please check MQTT configuration."); | |
| return; | |
| } | |
| print("Shelly ID: " + SHELLY_ID + " | Script v" + VERSION); | |
| if (CONFIG.enablePersistence) { | |
| LoadCounters(); | |
| } else { | |
| countersLoaded = true; | |
| print("Persistence disabled. Counters start at 0."); | |
| } | |
| // Handles script restarts when MQTT is already connected | |
| TryAnnounceAndPublish(); | |
| }); | |
| // ───────────────────────────────────────────── | |
| // 4. Home Assistant Auto-Discovery | |
| // ───────────────────────────────────────────── | |
| function AnnounceHA() { | |
| if (!SHELLY_ID) return; | |
| let haTopic = CONFIG.mqttPrefix + "/sensor/" + SHELLY_ID; | |
| let avtyTopic = SHELLY_ID + "/online"; | |
| let dev = { | |
| "ids": [SHELLY_ID], | |
| "name": "Shelly Pro 3EM", | |
| "mf": "Shelly", | |
| "mdl": "Pro 3EM", | |
| "sw": "Saldierung v" + VERSION | |
| }; | |
| let okImport = MQTT.publish( | |
| haTopic + "-import/config", | |
| JSON.stringify({ | |
| "name": "Saldierend Import", | |
| "uniq_id": SHELLY_ID + "_sald_import", | |
| "stat_t": SHELLY_ID + "/energy_counter/consumed", | |
| "unit_of_meas": "kWh", | |
| "dev_cla": "energy", | |
| "stat_cla": "total_increasing", | |
| "avty_t": avtyTopic, | |
| "pl_avail": "true", | |
| "pl_not_avail": "false", | |
| "dev": dev | |
| }), | |
| 0, true | |
| ); | |
| let okExport = MQTT.publish( | |
| haTopic + "-export/config", | |
| JSON.stringify({ | |
| "name": "Saldierend Export", | |
| "uniq_id": SHELLY_ID + "_sald_export", | |
| "stat_t": SHELLY_ID + "/energy_counter/returned", | |
| "unit_of_meas": "kWh", | |
| "dev_cla": "energy", | |
| "stat_cla": "total_increasing", | |
| "avty_t": avtyTopic, | |
| "pl_avail": "true", | |
| "pl_not_avail": "false", | |
| "dev": dev | |
| }), | |
| 0, true | |
| ); | |
| if (okImport && okExport) { | |
| print("HA Auto-Discovery sent."); | |
| } else { | |
| print("WARNING: HA Discovery publish failed (MQTT not ready?)."); | |
| } | |
| } | |
| // ───────────────────────────────────────────── | |
| // 5. Load / Save Persistence (KVS) | |
| // ───────────────────────────────────────────── | |
| function LoadCounters() { | |
| let loadedCount = 0; | |
| function checkDone() { | |
| loadedCount++; | |
| if (loadedCount === 2) { | |
| countersLoaded = true; | |
| lastPublishedConsumed = ""; | |
| lastPublishedReturned = ""; | |
| print("Counters ready. Consumed: " + energyConsumedKWh + " kWh | Returned: " + energyReturnedKWh + " kWh"); | |
| if (MQTT.isConnected()) PublishCounters(true); | |
| } | |
| } | |
| Shelly.call("KVS.Get", { "key": "EnergyConsumedKWh" }, function (res, err_code) { | |
| if (res && res.value !== undefined && res.value !== null) { | |
| energyConsumedKWh = Number(res.value); | |
| if (isNaN(energyConsumedKWh)) energyConsumedKWh = 0.0; | |
| print("Loaded EnergyConsumedKWh: " + energyConsumedKWh); | |
| } else if (err_code !== 0) { | |
| print("INFO: EnergyConsumedKWh not in KVS yet (first run?)."); | |
| } | |
| checkDone(); | |
| }); | |
| Shelly.call("KVS.Get", { "key": "EnergyReturnedKWh" }, function (res, err_code) { | |
| if (res && res.value !== undefined && res.value !== null) { | |
| energyReturnedKWh = Number(res.value); | |
| if (isNaN(energyReturnedKWh)) energyReturnedKWh = 0.0; | |
| print("Loaded EnergyReturnedKWh: " + energyReturnedKWh); | |
| } else if (err_code !== 0) { | |
| print("INFO: EnergyReturnedKWh not in KVS yet (first run?)."); | |
| } | |
| checkDone(); | |
| }); | |
| } | |
| function SaveCounters() { | |
| Shelly.call("KVS.Set", { "key": "EnergyConsumedKWh", "value": energyConsumedKWh.toFixed(3) }); | |
| Shelly.call("KVS.Set", { "key": "EnergyReturnedKWh", "value": energyReturnedKWh.toFixed(3) }); | |
| print("Counters saved to KVS."); | |
| } | |
| // ───────────────────────────────────────────── | |
| // 6. Main Calculation Loop | |
| // ───────────────────────────────────────────── | |
| Timer.set(CONFIG.updateInterval, true, function () { | |
| if (!SHELLY_ID || !countersLoaded) return; | |
| let em = Shelly.getComponentStatus("em", 0); | |
| if (!em || typeof em.total_act_power !== "number") return; | |
| let power = em.total_act_power; | |
| let energyStep = power * (CONFIG.updateInterval / 1000.0); | |
| if (power >= 0) { | |
| energyConsumedWs += energyStep; | |
| } else { | |
| energyReturnedWs += Math.abs(energyStep); | |
| } | |
| if (energyConsumedWs >= 3600) { | |
| let chunkC = Math.floor(energyConsumedWs / 3600); | |
| energyConsumedKWh += chunkC / 1000.0; | |
| energyConsumedWs -= chunkC * 3600; | |
| } | |
| if (energyReturnedWs >= 3600) { | |
| let chunkR = Math.floor(energyReturnedWs / 3600); | |
| energyReturnedKWh += chunkR / 1000.0; | |
| energyReturnedWs -= chunkR * 3600; | |
| } | |
| PublishCounters(false); | |
| if (CONFIG.enablePersistence) { | |
| saveCounter++; | |
| if (saveCounter >= CONFIG.saveInterval) { | |
| saveCounter = 0; | |
| SaveCounters(); | |
| } | |
| } | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Danke für das Skript. Ich habe es implementiert und es liefert auch Werte an Home Assistant. Jedoch haben diese ständig Aussetzer und setzen sich wieder zurück (siehe Screenshot). Das sollte anders aussehen oder?
