-
-
Save Davc0m/dc419aa3147ec3c3d6e7289f89dc0ed8 to your computer and use it in GitHub Desktop.
| /** | |
| * 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(); | |
| } | |
| } | |
| }); |
Would like to say thank your really much. 😄
Was a long time, I had this on my todo list but dont had really much time to do.
Andere Scriptlösungen stressen das Flash. Sehr ich richtig, dass du keine/kaum Flashressourcen benötigst?
Andere Scriptlösungen stressen das Flash. Sehr ich richtig, dass du keine/kaum Flashressourcen benötigst?
Nicht ganz: Das Script nutzt Flash über KVS.Set, weil es die kWh‑Zähler persistent speichert. Mit updateInterval=500ms und saveInterval=300 wird etwa alle 150 s, also 2,5 min, in den Flash geschrieben. Das ist weniger als manche Lösungen, aber eben nicht keine oder kaum Flash‑Nutzung. Wer schonen will, kann das Save‑Intervall erhöhen oder das Persistieren extern, zum Beispiel in Home Assistant, machen.
@Davc0m: danke dir für das Feedback. Ich glaube zu wissen, dass Home Assistant Verbrauchszähler unterstützt, die immer wieder auf 0 stellen (im Text heisst es, dass das z.B. bei Restart der Messeinrichtung passieren kann). Mit so einer Konfiguration ergäbe sich keine Notwendigkeit der Persistenz der Verbrauchswerte im Shelly selbst. Damit liefen die veränderlichen Werte des Shelly ausschließlich im RAM. Siehst du das auch so?
Wie deaktiviere ich das Persistieren in deinem Script? Einfach die Funktion SaveCounters() deaktivieren und über LoadCounters() die beiden Variablen mit 0 versorgen?
Hallo,
vielen Dank für das Skript.
Ich habe es seit letzter Woche im Einsatz und es ist genau das, was ich gesucht habe. Eine kleine Anmerkung: Ich habe festgestellt, dass im Laufe des Tages (wenn die PV-Anlage Strom produziert und aufgrunddessen keine Energie aus dem Netz bezogen wird) keine Aktualisierung der Daten über mqtt erfolgt.
Frage mich, ob es an diesem Teil des Codes if (valC !== lastPublishedConsumed) { MQTT.publish(SHELLY_ID + "/energy_counter/consumed", valC, 0, false); MQTT.publish(SHELLY_ID + "/energy_counter/returned", valR, 0, false); lastPublishedConsumed = valC; liegt.
Für mich sieht es so aus, dass die Daten nur übertragen werden, wenn sich der "Netzbezug" ändert. Aufgrund des Eigenverbrauchs im Laufe des Tages und der Überschusseinspeisung, wird nach meiner Vermutung die if-Bedingung nicht erfüllt. In der Praxis passiert die Aktualisierung dann erst abends, wenn es wieder zum Netzbezug kommt.
Mich treibt noch immer das Flashzyklen-Thema. Mit der Einstellung, alle 2.5min zu speichern, und der Info, dass ein typisches Flash wie im ESP32 nur 10-100k Schreibzyklen verträgt, kann ich eine Standzeit für die einzelne Zelle abschätzen. Da im KVS sicherlich kein WearLeveling gemacht wird, wäre die Zelle nach spätestens ca. 170 Tagen "durch". In der Praxis halten Flashzellen vermutlich länger, aber selbst ein Jahr wäre Größenordnungen zu wenig für ein hausinstalliertes Energiemessgerät.
In der Shelly Doku wird von unconditional updates bei Aufruf der Methode KVS.Set gesprochen. Es scheint also kein Mechanismus implementiert sein, der vor dem Flashen einen Wertevergleich durchführt. Dieser wäre in der Methode SaveCounters() wohl gut aufgehoben und würde alleine für den Export das Flash um die ganze Nachtzeit (keine PV) entlasten.
Das Thema ist brisant, denn Flashes werden in Bänken gelöscht (was den eigentlichen Stress der Zelle macht). Kann gut sein, dass bei einer KV-Änderung also die ganze Bank gelöscht wird, in der möglicherweise andere KVs liegen. Das verdoppelte den Stress pro Zelle bei Update von zwei KVs hintereinander. Wäre schön, wenn man wüsste, was Shelly an Zyklen garantiert.
Am besten, man nutzt das KVS gar nicht als Messdatenspeicher, nur zur Konfiguration...
Mich treibt noch immer das Flashzyklen-Thema. Mit der Einstellung, alle 2.5min zu speichern, und der Info, dass ein typisches Flash wie im ESP32 nur 10-100k Schreibzyklen verträgt, kann ich eine Standzeit für die einzelne Zelle abschätzen. Da im KVS sicherlich kein WearLeveling gemacht wird, wäre die Zelle nach spätestens ca. 170 Tagen "durch". In der Praxis halten Flashzellen vermutlich länger, aber selbst ein Jahr wäre Größenordnungen zu wenig für ein hausinstalliertes Energiemessgerät.
In der Shelly Doku wird von unconditional updates bei Aufruf der Methode KVS.Set gesprochen. Es scheint also kein Mechanismus implementiert sein, der vor dem Flashen einen Wertevergleich durchführt. Dieser wäre in der Methode SaveCounters() wohl gut aufgehoben und würde alleine für den Export das Flash um die ganze Nachtzeit (keine PV) entlasten.
Das Thema ist brisant, denn Flashes werden in Bänken gelöscht (was den eigentlichen Stress der Zelle macht). Kann gut sein, dass bei einer KV-Änderung also die ganze Bank gelöscht wird, in der möglicherweise andere KVs liegen. Das verdoppelte den Stress pro Zelle bei Update von zwei KVs hintereinander. Wäre schön, wenn man wüsste, was Shelly an Zyklen garantiert.
Am besten, man nutzt das KVS gar nicht als Messdatenspeicher, nur zur Konfiguration...
EDIT: da gibt es einen interessanten Beitrag, der Vermutungen aus reverse engineering anstellt. Bisher das beste, was ich gefunden habe. Demnach wären meine Bedenken ein Stück weit entkräftet (und so wie dort vermutet würde ich es auch tun, wenn ich bei Allterco was zu sagen hätte).
https://www.facebook.com/groups/ShellyIoTCommunitySupport/posts/6329889113777066/
Demnach wäre bei 2,5 minütiger unbedingter Speicherung das Flash nach ca. 5 Jahren kaputt.
Wäre also noch immer Potenzial für Optimierung gegeben :-)
@jubie25
Deine Bedenken sind absolut berechtigt. Auch wenn das zugrundeliegende Espressif NVS (Non-Volatile Storage) ein "Wear Leveling" nutzt, um Schreibzugriffe auf den ganzen Speicherbereich zu verteilen, ist ein 2,5-Minuten-Intervall auf Dauer unnötiger Stress für den Chip.
Du hast auch völlig recht, was Home Assistant angeht: Da die Sensoren beim Auto-Discovery mit stat_cla: "total_increasing" angelegt werden, erkennt Home Assistant einen Reset der Werte auf 0 (nach einem Shelly-Neustart) automatisch und führt die internen Summen im Energy Dashboard völlig nahtlos weiter.
Lösung: Ich habe das Skript (Version 1.1.0) entsprechend angepasst.
Du findest ganz oben im CONFIG-Block nun den Schalter enablePersistence. Setzt du diesen auf false, wird die KVS-Speicherung komplett deaktiviert und das Skript läuft zu 100% im flüchtigen RAM des Shellys. Für diejenigen, die das Skript ohne Home Assistant nutzen und die Werte behalten wollen, habe ich das Intervall im Beispielcode jetzt deutlich schonender auf 15 Minuten (saveInterval: 1800) angehoben.
@christian1982ks
Hallo Christian, vielen Dank fürs Testen und vor allem für diesen super Hinweis! Du hast den Fehler absolut exakt analysiert.
Da lag tatsächlich ein Logikfehler in meiner if-Bedingung vor. Wenn du reinen PV-Überschuss einspeist, ändert sich nur der Wert für die Einspeisung (valR), während der Netzbezug (valC) stagniert. Da das alte Skript nur valC auf Änderungen geprüft hat, wurden die MQTT-Nachrichten tagsüber einfach verschluckt.
Lösung: Ich habe das in der neuen Version 1.1.0 gefixt. Das Skript speichert nun beide Werte (lastPublishedConsumed und lastPublishedReturned) und triggert das MQTT-Publishing, sobald sich einer der beiden Werte ändert. Damit kommen deine Einspeisedaten jetzt auch im Sommer wieder in Echtzeit in Home Assistant an!
Anleitung: Shelly Pro 3EM Saldierung (Net Metering) für Home Assistant
Die Nutzung dieses Skripts und dieser Anleitung erfolgt ausdrücklich und vollständig auf eigene Gefahr! Ich übernehme absolut keine Haftung für direkte, indirekte oder Folgeschäden. Dies umfasst, aber ist nicht beschränkt auf: Schäden am Shelly-Gerät, an angeschlossener Elektrik, an anderen Geräten in deinem Netzwerk, Datenverlust oder Systemausfälle. Mit der Nutzung erklärst du dich damit einverstanden, dass du allein für deine Hardware und dein Setup verantwortlich bist.
Wer den Shelly Pro 3EM mit einer PV-Anlage (Balkonkraftwerk etc.) nutzt, kennt das Problem: Der Shelly saldiert nicht von Haus aus. Das verfälscht den Eigenverbrauch in Home Assistant massiv.
Hier ist die Lösung per Script direkt auf dem Shelly. Das Script verrechnet die Phasen intern in Echtzeit und sendet nur noch zwei saubere Werte (Import & Export) an Home Assistant.
Vorteile:
Neu in Version 1.1.0:
Neu in Version 1.1.1–1.1.8:
Schritt 1: MQTT im Shelly aktivieren
Damit das Script die Daten an Home Assistant senden kann, muss MQTT aktiv sein.
192.168.178.XX:1883).Schritt 2: Script installieren
Saldierung HA.shelly_pro3em_net_metering.jshier oben.(Optional zur Flash-Schonung): Oben im Code steht
enablePersistence: true. Das bedeutet, der Shelly merkt sich die Zählerstände bei einem Stromausfall (Speicherung alle 15 Min). Wer Home Assistant nutzt, kann dies bedenkenlos auffalsesetzen. Home Assistant rechnet nach einem Neustart des Shellys nahtlos weiter, auch wenn der Zähler im Shelly wieder bei 0 anfängt. Das schont den Flash-Speicher maximal.Schritt 3: Script aktivieren (Wichtig!)
Schritt 4: In Home Assistant einbinden
Das Script unterstützt Auto-Discovery. Du musst keine YAML-Dateien bearbeiten!
Saldierend ImportSaldierend ExportSchritt 5: Energie-Dashboard umstellen
sensor.shelly_pro_3em_saldierend_import.sensor.shelly_pro_3em_saldierend_export.Fertig! Ab sofort stimmen dein Eigenverbrauch und deine Einspeisung exakt.