Skip to content

Instantly share code, notes, and snippets.

@Davc0m
Last active August 16, 2026 20:18
Show Gist options
  • Select an option

  • Save Davc0m/dc419aa3147ec3c3d6e7289f89dc0ed8 to your computer and use it in GitHub Desktop.

Select an option

Save Davc0m/dc419aa3147ec3c3d6e7289f89dc0ed8 to your computer and use it in GitHub Desktop.
Shelly Pro 3EM: Saldierende Energiemessung (Net Metering) mit Home Assistant Auto-Discovery
/**
* 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();
}
}
});
@Davc0m

Davc0m commented Dec 3, 2025

Copy link
Copy Markdown
Author

Anleitung: Shelly Pro 3EM Saldierung (Net Metering) für Home Assistant

⚠️ Haftungsausschluss / Disclaimer:
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:

  • ✅ 100% korrekter Eigenverbrauch
  • ✅ Entlastet Home Assistant (keine Template-Sensoren nötig)
  • Auto-Discovery: Die neuen Sensoren tauchen automatisch in HA auf.

Neu in Version 1.1.0:

  • Bugfix PV-Einspeisung: Daten werden nun auch bei reiner Netzeinspeisung (wenn der Bezug bei 0 stagniert) zuverlässig in Echtzeit an Home Assistant gesendet.
  • Speicherschonung: Das Intervall für die Speicherung im internen Flash-Speicher des Shellys wurde auf 15 Minuten erhöht, um die Lebensdauer des Chips massiv zu verlängern. Die Persistenz lässt sich im Code nun auch komplett abschalten.

Neu in Version 1.1.1–1.1.8:

  • Stabilitätsverbesserungen: Mehrere Absturzquellen beim Start und bei Verbindungsproblemen wurden behoben.
  • Bugfix Zähler-Reset: Ein Timing-Problem beim Booten konnte dazu führen, dass Home Assistant einen Rücksprung auf 0 kWh als Zähler-Reset interpretierte und die Energiestatistik dauerhaft verfälschte.
  • Zuverlässigerer Start: Das Script reagiert nun auf die tatsächliche MQTT-Verbindung statt blind eine feste Zeit zu warten.
  • Retained Zählerwerte: Home Assistant bekommt den letzten Zählerstand sofort nach einem Neustart, ohne auf den nächsten Messzyklus warten zu müssen.
  • Optimierungen: Geringerer RAM-Verbrauch und sicherere MQTT-Payload-Größe für lange Gerätenamen.

Schritt 1: MQTT im Shelly aktivieren

Damit das Script die Daten an Home Assistant senden kann, muss MQTT aktiv sein.

  1. Öffne das Web-Interface deines Shelly Pro 3EM.
  2. Gehe auf Settings -> MQTT.
  3. Setze folgende Haken:
    • Enable MQTT Control
    • Enable RPC over MQTT
    • RPC status notifications over MQTT
  4. WICHTIG: Trage bei Server die IP-Adresse deines Home Assistant Brokers ein (z.B. 192.168.178.XX:1883).
  5. Falls nötig, User & Passwort eintragen.
  6. Klicke auf Save und starte den Shelly neu (Settings -> Device Reboot).

Schritt 2: Script installieren

  1. Gehe im Shelly-Menü links auf Scripts.
  2. Klicke auf "Create Script".
  3. Gib dem Script einen Namen, z.B. Saldierung HA.
  4. Kopiere den Code aus der Datei shelly_pro3em_net_metering.js hier oben.
  5. Füge den Code in das Editor-Fenster im Shelly ein.
  6. Klicke oben auf "Save".

(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 auf false setzen. 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!)

  1. Setze im Script-Menü den Schalter "Enable" (neben dem Namen), damit es nach einem Neustart automatisch läuft.
  2. Klicke auf den Start-Button (Play-Symbol).
  3. Der Status sollte auf "Running" wechseln.

Schritt 4: In Home Assistant einbinden

Das Script unterstützt Auto-Discovery. Du musst keine YAML-Dateien bearbeiten!

  1. Gehe in Home Assistant auf Einstellungen -> Geräte & Dienste.
  2. Suche die Integration MQTT -> Klicke auf Geräte.
  3. Suche nach deinem Shelly Pro 3EM.
  4. Du findest dort nun zwei neue Entitäten:
    • Saldierend Import
    • Saldierend Export

Schritt 5: Energie-Dashboard umstellen

  1. Gehe zu Einstellungen -> Dashboards -> Energie.
  2. Ändere bei "Netzverbrauch" die Quelle auf den neuen Sensor: sensor.shelly_pro_3em_saldierend_import.
  3. Ändere bei "Zurück ins Netz" die Quelle auf: sensor.shelly_pro_3em_saldierend_export.

Fertig! Ab sofort stimmen dein Eigenverbrauch und deine Einspeisung exakt.

@Kinggrass

Copy link
Copy Markdown

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.

@jubie25

jubie25 commented Mar 5, 2026

Copy link
Copy Markdown

Andere Scriptlösungen stressen das Flash. Sehr ich richtig, dass du keine/kaum Flashressourcen benötigst?

@Davc0m

Davc0m commented Mar 6, 2026

Copy link
Copy Markdown
Author

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.

@jubie25

jubie25 commented Mar 9, 2026

Copy link
Copy Markdown

@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?

@christian1982ks

christian1982ks commented Mar 9, 2026

Copy link
Copy Markdown

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.

@jubie25

jubie25 commented Mar 12, 2026

Copy link
Copy Markdown

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...

@jubie25

jubie25 commented Mar 12, 2026

Copy link
Copy Markdown

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 :-)

@Davc0m

Davc0m commented Mar 12, 2026

Copy link
Copy Markdown
Author

@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.8) 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.8 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!

@arbenhaliti-web

Copy link
Copy Markdown

THANK YOU!!

@leepfrog-ger

Copy link
Copy Markdown

This is gold, thank you for your work!

@lord-icon

Copy link
Copy Markdown

unnütz bei mehr als 1 Sensor da die namen nicht ersichtlich sind. ich habe 7 Geräte.
in Geräte und Dienste gibt es nun 7x "3EM Pro" und somit dann auch 7x Pro3EM-Saldierend Export.
Gut für Personen, die gerne "Raten Sie mal" spielen.

Ich hab mir selbst Linderung verschafft.

/**
 * Shelly Pro 3EM - Net Metering (Saldierung) & Home Assistant Auto-Discovery
 * Version: 1.1.9
 *
 * 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.8 → v1.1.9):
 * - Added dynamic device name via Sys.GetConfig()
 * - Discovery now waits until MQTT topic_prefix and device name are initialized
 * - Fallback to "Shelly Pro 3EM" if no custom device name is set
 */

let CONFIG = {
    updateInterval: 500,          // Calculation cycle in ms
    enablePersistence: false,     // 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.9";
let SHELLY_ID = null;
let DEVICE_NAME = "Shelly Pro 3EM";

let mqttConfigLoaded = false;
let sysConfigLoaded = false;

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) return;
    if (!mqttConfigLoaded) return;
    if (!sysConfigLoaded) return;
    if (!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 / Name 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;
    }

    mqttConfigLoaded = true;
    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();
});

Shelly.call("Sys.GetConfig", {}, function (res, err_code, err_msg) {
    if (!res) {
        print("ERROR: Sys.GetConfig returned null! Code: " + err_code + " | " + err_msg);
        print("INFO: Using fallback device name: " + DEVICE_NAME);
        sysConfigLoaded = true;
        TryAnnounceAndPublish();
        return;
    }

    if (res.device && res.device.name) {
        DEVICE_NAME = res.device.name;
        print("Device name loaded: " + DEVICE_NAME);
    } else {
        print("INFO: No custom device name set. Using fallback: " + DEVICE_NAME);
    }

    sysConfigLoaded = true;
    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": DEVICE_NAME,
        "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. Device name: " + DEVICE_NAME);
    } 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();
        }
    }
});

Ausgehend von meiner Solaranlage

  • Pro3EM-Solaranlage
  • Pro3EM-Solaranlage Saldierend Export

So wird es dann übersichtlich.

@esrauscht

Copy link
Copy Markdown

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?
IMG_7729

@jubie25

jubie25 commented Jun 6, 2026

Copy link
Copy Markdown

Seit den neueren Shelly-Firmwaren kann man virtuelle Komponenten recht einfach über den Costomize-Menüpunkt anlegen, u.a. auch numbers, switches, boolean. Gefüttert werden die durch Scripte. Diese Komponenten tauchen dann über die shelly-API zusammen mit den nativen anderen als Entitäten auf, alles ohne MQTT. Deshalb Anregung: um einen Protokollbruch zu vermeiden: wäre es nicht eine Vereinfachung und Verschönerung des Saldierungsscripts, dessen Werte ebenfalls über die API zu exportieren? Sogar Import von Settings wären sehr einfach machbar.
Ok, du hast auch die Nicht-HA'ler auf dem Schirm, aber das eine schließt das andere ja nicht aus...

grafik

Quellen:
https://shelly-api-docs.shelly.cloud/gen2/DynamicComponents/Virtual/
https://shelly-api-docs.shelly.cloud/gen2/Scripts/APIs/Virtual

Edit:
Ich habe das mal durchexerziert und es funktioniert. Zuerst bei den Variablendeklarationen im Script:

//  Get handles to virtual components
let hEnergyConsumedKWh = Virtual.getHandle("number:200");
let hEnergyReturnedKWh = Virtual.getHandle("number:201");

Dann dort, wo die mqtt-Werte gepusht werden:

// push API values
hEnergyConsumedKWh.setValue(valC);
hEnergyReturnedKWh.setValue(valR);

Die Indizes 200 und 201 zeigen auf virtual components, die ich vorher im dortigen Menü als Number mit dem View Label angelegt habe. Die dort hinterlegten Namen sind die, die später im HA als Entitäten erscheinen.
Nach Neustart des Scripts haben wir die Werte nun als Entität in HA. Allerdings als number ohne Klassifizierung. Selbige nachzurüsten geht auf verschiedene Weisen (yaml, Template-Helfer). Ich habe mich für letzteres entschieden und erhalte mit einem Helper-Template vom Typ Sensor dann letztlich eine Energie-Entität, die genau das macht was sie soll. Per Shelly-API.

grafik

Nun ist es im Gegensatz zu mqtt-Anbindung über die Shelly-Components wie gesagt nicht möglich, vom Shelly-Gerät aus gleich Klassifizierungen mitzugeben. Solange Shelly da nichts ändert, muss man das über den beschriebenen Weg händisch in HA nachpäppeln. Das verkompliziert es an dieser Stelle etwas, aber dafür fällt eben das ganze Thema mqtt weg.

@MMA150475

MMA150475 commented Jun 15, 2026

Copy link
Copy Markdown

Vielen Dank für das Script, ich probiere es gerade aus. Mit der Alternativlösung im HA habe ich bisher das Problem, dass die Saldierung bes Brzugswertes manchmal um gut 20-30% von dem abweicht was der Stromzähler sagt - ich finde dafür einfach keinen Grund... Jetzt mal auf diesem Weg direkt im Shelly. Mal schauen ob das nun genauer ist.

Was mir aber fehlt ist ein saldierter Leistungswert, den ich im Energy Dashboard angeben kann - könnte man den im Script auch anlegen?

@jubie25

jubie25 commented Jun 15, 2026

Copy link
Copy Markdown

Vielen Dank für das Script, ich probiere es gerade aus. Mit der Alternativlösung im HA habe ich bisher das Problem, dass die Saldierung bes Brzugswertes manchmal um gut 20-30% von dem abweicht was der Stromzähler sagt - ich finde dafür einfach keinen Grund... Jetzt mal auf diesem Weg direkt im Shelly. Mal schauen ob das nun genauer ist.

Das kommt wahrscheinlich vom zeitlichen Versatz der Summierung ggü der Momentanwerte. Deshalb sehe ich es auch als vorteilhaft an, dies so nahe wie möglich und so oft wie nötig am Ort der Quelle zu tun - im Script auf dem Shelly

Was mir aber fehlt ist ein saldierter Leistungswert, den ich im Energy Dashboard angeben kann - könnte man den im Script auch anlegen?

Das macht keinen Sinn. Die Saldierung gilt nur für die Energie, nicht für die Leistung. Die Summenleistung liest du direkt aus dem Shelly oder addierst einfach alle Phasen. Da gibt es keine synchron-kritische Komponente

@MMA150475

Copy link
Copy Markdown

Ja das stimmt schon, da HA "nur" alle 6-7 Sekunden den Wert aktualisiert kommt da ein kleiner Versatz zustande, aber in der Größenordnung 20-30 % hätte ich das nicht erwartet, vor allem weil der Wert für die Einspeisung ja stimmt. Es ist nur der Wert für den Bezug falsch wenn ichs im HA saldieren lasse. Am YAML Code im HA dürfte es nicht liegen, das habe ich nun mehrfach kontrolliert (und auch mal von einer KI kontrollieren lassen). Naja, werds nun mal mit dem Shelly Script ein paar Tage beobachten.

Was den Wert für die Leistung angeht: HA hätte gerne eine Momentanleistung, saldiert über alle Phasen, für die Echtzeitbetrachtung. Da für die Energiesaldierung eh die Leistung zum Messzeitpunkt saldiert wird dachte ich es wäre gut wenn das Shelly auch gleich mitliefert. Es sei denn, der Parameter "Leistung" des Shellys ist schon der saldierte Wert - ich bin da inzwischen sehr skeptisch ob das alles so hinkommt, weil meine Messergebnisse so falsch waren.

@jubie25

jubie25 commented Jun 15, 2026

Copy link
Copy Markdown

Ja das stimmt schon, da HA "nur" alle 6-7 Sekunden den Wert aktualisiert kommt da ein kleiner Versatz zustande, aber in der Größenordnung 20-30 % hätte ich das nicht erwartet, vor allem weil der Wert für die Einspeisung ja stimmt. Es ist nur der Wert für den Bezug falsch wenn ichs im HA saldieren lasse. Am YAML Code im HA dürfte es nicht liegen, das habe ich nun mehrfach kontrolliert (und auch mal von einer KI kontrollieren lassen). Naja, werds nun mal mit dem Shelly Script ein paar Tage beobachten.

Mach mal und berichte. Bis zu 5% würde ich auch den Toleranzen zurechnen. Die kann man dann aber, wenn es schließlich ein statischer Offset würde, rauskalibrieren...

Was den Wert für die Leistung angeht: HA hätte gerne eine Momentanleistung, saldiert über alle Phasen, für die Echtzeitbetrachtung. Da für die Energiesaldierung eh die Leistung zum Messzeitpunkt saldiert wird dachte ich es wäre gut wenn das Shelly auch gleich mitliefert. Es sei denn, der Parameter "Leistung" des Shellys ist schon der saldierte Wert - ich bin da inzwischen sehr skeptisch ob das alles so hinkommt, weil meine Messergebnisse so falsch waren.

Wie gesagt, es gibt keine saldierte Leistung. Das ist immer ein Momentansummenwert über alle Phasen. Den kriegst du über die Hauptinstanz deines Shellys (also nicht aus den Phasen-Instanzen). Das ist es, was dein Energieboard will.

Vielleicht nochmal zum Verständnis: eine Saldierung kann nur anhand der Summenleistung passieren. Also erst die Phasen- leistungen zusammenzählen ("gegeneinander aufrechnen") und dann aufintegrieren: Das Integral einer Leistung ist eine Energie.
Was der Shelly aber macht ist, die Leistung erst zu Energien zu integrieren (pro Phase) und diese dann zusammenzuzählen. Da dann bei den drei Phasen durchaus Werte unterschiedlichen Vorzeichens rauskommen können, wachsen pro Zeiteinheit auch gern mal die rausgehende und reinkommende Energieanteile gleichzeitig. Das passiert bei saldierender Rechnung nicht. Deshalb sind bei ungleichem Leistungsvorzeichen über die Phasen (z.B. wegen Einspeisung auf einer Phase) die saldierten Energien bei Im- und Export auch immer niedriger als die nichtsaldierende.

Die jedoch geforderte (saldierte) Energiegröße kann man später nicht mehr aus den vorhandenen Daten "auseinanderfieseln", da der zeitliche Bezug der Leistungswerte zueinander durch die Integration verlorengeht. Deshalb fehlt uns das Feature auch so sehr.

@jubie25

jubie25 commented Jun 15, 2026

Copy link
Copy Markdown

Ja das stimmt schon, da HA "nur" alle 6-7 Sekunden den Wert aktualisiert kommt da ein kleiner Versatz zustande,

HA aktualisiert, wenn die Quelle neue Daten schickt. Fragt sich also, wann der Shelly das tut. Da bist du mit dem Script potenziell besser dran, denn das rechnet alle 0.5 s den aktuellen (intern gemessenen Wert) und integriert in diesem Zeittakt. Egal, in welcher Frequenz die Ergebnisse dann an HA gesendet werden: die Integrale haben ein viel höheres Abtastintervall und sollten deshalb genauer sein. Ich hoffe, ich habe das Script so richtig verstanden (...) und entschuldige mich für die mathematische Sprache (ist halt eher meine Vorstellungsebene).

@MMA150475

Copy link
Copy Markdown

Keine Sorge, mit mathematischer Sprache und der Technik dahinter habe ich kein Problem.

Ich hab auch verstanden wie der Shelly rechnet und auch wie es eigentlich sein sollte. Aber "Saldo" ist per se erst mal das Ergebnis eine Summe (natürlich Vorzeichenbehaftet) - nicht mehr und nicht weniger. Ein Saldo kann also sowohl über die Energie also auch über die Leistung erfolgen. Über die Energie ist es halt wichtig zu wissen, dass man entweder so rechen kann wie es der Shelly tut - Phasenbezogen - oder das Gesamtsaldo macht - also über alle Phasen hinweg. Ein Saldo über die Leistung ist das schon unspektakulärer, da man nur drei Sachen addieren muss.

Ich hätte jetzt erwartet, dass durch die regelmäßige Abtastung - wenn auch nur im 6-7 Sekunden Raster - der natürliche Jitter ausgeglichen ist und das Ergebnis im HA schon ziemlich genau dem 0.5 Sekunden Intervall des Shelly entspricht. Statistisch gesehen sollte auch eine 6-7 Sekunden Abtastung ein ähnliches Ergebnis liefern wie die 0.5s Abtastung - jedenfalls über einen Tag hinweg gesehen. Das scheint aber komischerweise nur für den Wert der Einspeisung bei mir zu stimmen, dort passt es ganz gut obwohl ich dort von der gleichen Quelle aus rechne. Den Fehler hier beim Bezug konnte weder ich noch die KI bisher finden, daher nun der Versuch mit dem Script im Shelly. Obs was bringt? Ich weiß es nicht.

Scripting ist nicht so mein Ding, ich kanns zwar auch ein bisschen aber wenn sich hier schlauere Menschen schon die nötigen Gedanken dazu gemacht haben probiere ich das gerne aus und berichte von meinem Vergleich.

@jubie25

jubie25 commented Jun 15, 2026

Copy link
Copy Markdown

Keine Sorge, mit mathematischer Sprache und der Technik dahinter habe ich kein Problem.

:-)

Ich hab auch verstanden wie der Shelly rechnet und auch wie es eigentlich sein sollte. Aber "Saldo" ist per se erst mal das Ergebnis eine Summe (natürlich Vorzeichenbehaftet) - nicht mehr und nicht weniger. Ein Saldo kann also sowohl über die Energie also auch über die Leistung

Exakt! Das Saldo der Leistung stellt schon der Shelly zur Verfügung. Ich hab's Leistungssumme genannt. Brauchst nix anderes.

Ich hätte jetzt erwartet, dass durch die regelmäßige Abtastung - wenn auch nur im 6-7 Sekunden Raster - der natürliche Jitter ausgeglichen ist und das Ergebnis im HA schon ziemlich genau dem 0.5 Sekunden Intervall des Shelly entspricht. Statistisch gesehen sollte auch eine 6-7 Sekunden Abtastung ein ähnliches Ergebnis liefern wie die 0.5s Abtastung - jedenfalls über einen Tag hinweg gesehen. Das scheint aber

Der Unterschied liegt im Wort "statistisch". Sonst hättest du Recht! Die Verläufe sind gegeneinander jedoch nicht konstant, sprich: sie haben einen Zeitbezug. Deshalb passt die Statistik nicht zu den realen Ergebnissen. Dafür bräuchte es eben kürzere Samplingintervalle.

Scripting ist nicht so mein Ding, ich kanns zwar auch ein bisschen aber wenn sich hier schlauere Menschen schon die nötigen Gedanken dazu gemacht haben probiere ich das gerne aus und berichte von meinem Vergleich.

Deshalb sind wir froh, dass es Davc0m so tolle Arbeit geleistet hat. JavaScript (hier in der Shelly-Variante) kenn ich auch nicht wirklich, man fuchst sich halt so rein.

@MMA150475

Copy link
Copy Markdown

Kurzer Zwischenstand nach einem Tag: Das Script funktioniert perfekt! Und: Ich hab in HA einen neuen Integralsensor zum Vergleich aufgebaut, mit der linken Riemann-Summe - der ist nun nahezu identisch, weicht um nur noch ca. 1% ab.

Vielen Dank an @Davc0m für das tolle Script!

@jubie25

jubie25 commented Aug 4, 2026

Copy link
Copy Markdown

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.8) 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.

Hier eine deutliche Warnung, die diese Einstellung für Home Assistant Nutzer hat, wenn sie einen Verbrauchssensor (Utility Meter) dahinter hängen: mein Shelly machte zum ersten Mal seit der Implementierung einen Reset (FW Update). Folge: der erste vom Script berichtete Wert war nicht 0, sondern ein geringerer Wert als der letzte vor dem Reboot. Das triggert den Verbrauchszähler nicht, er summiert nun die Differenz auf. Folge: kaputte Historie. Wer also einen Verbrauchszähler einrichtet, um monoton ansteigende Werte zu erhalten, der schaltet die Speicherung im Shelly komplett ab, sorgt dafür, dass auch der evtl. noch im KV-Speicher gehaltene Wert weg ist und stellt im Verbrauchszähler die "periodische Rückstellung" auf aktiv.
Getriggert auf die Rückstellung wird im Verbrauchszähler anscheinend alleine ein Wert von 0, nicht ein kleinerer Wert als der zuletzt gemeldete.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment