-
-
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.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!
THANK YOU!!
This is gold, thank you for your work!
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.
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...
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.
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.
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?
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
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.
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.
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).
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.
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.
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!
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.

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.