Skip to content

Instantly share code, notes, and snippets.

@Davc0m
Last active August 29, 2026 18:28
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();
}
}
});
@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.

@fgz874

fgz874 commented Aug 19, 2026

Copy link
Copy Markdown

@jubie25 verstehe ich richtig, dass du das Script angepasst hast und nun die Werte über den Shelly Adapter in IoBroker bekommst? Ich habe es versucht nach deiner "Anleitung" nachzubauen bin bisher aber immer gescheitert. Wenn es so klappt wäre ja wirklich super.

@jubie25

jubie25 commented Aug 19, 2026

Copy link
Copy Markdown

Home Assistant, nicht ioBroker.
Aber ja, es gibt die virtuellen Komponenten, die kann man nach Anleitung oben (die ich auch nur von der Shelly Seite gezogen habe) anlegen und erhält die Werte dann über die Shelly Integration.
ABER (und deshalb bin ich wieder weg davon):
Hier werden nur Zahlenwerte übermittelt, keine Metadata. Damit beschreibt dann die Entität nicht, welche Einheit, Messart, Geräteklasse usw der Wert hat. Denn das sieht Shelly in Home Assistant-Form (und in jeder anderen) nicht vor. Da die Integration diese neuen Werte auch nicht kennt, sind es zustandslose Daten. Im Fall Home Assistant müsste man damit Template Sensoren füllen, die ihrerseits die Daten dann nachfüttern. Oder man macht das über die configuration.yaml nach solcher Art:

homeassistant:
  customize:
    sensor.weathersb5_daily_rain:
      state_class: total_increasing

Ginge wohl auch. Das ist aber dann das Gegenteil von Vereinfachung.
Deshalb bleibe ich nun doch bei mqtt: dort kann mit dem publish-Kommando per json jede Metainfo mitgegeben werden, was das Script auch nutzt.
Vielleicht macht Shelly hier mal was, ist HA doch inzwischen eine echte Größe im Markt und Shelly den Makern und Heimautomatisierern traditionell auch recht verbunden (hoffentlich auch, wenn sie demnächst aufgekauft werden).

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