-
-
Save Davc0m/dc419aa3147ec3c3d6e7289f89dc0ed8 to your computer and use it in GitHub Desktop.
| /** | |
| * Shelly Pro 3EM - Net Metering (Saldierung) & Home Assistant Auto-Discovery | |
| * Version: 1.2.2 | |
| * | |
| * 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.2.2): | |
| * - Automatic move of the Home Assistant sensors from the old v1.1.0 | |
| * discovery topics to the current ones. Entity IDs, settings and | |
| * statistics are kept (needs a Home Assistant version that supports | |
| * migrate_discovery; see CONFIG.migrateLegacyDiscovery). | |
| * - Counters no longer jump back after a restart: the start value is the | |
| * highest of Script.storage, the KVS backup and the last retained MQTT | |
| * value. Measuring starts immediately, publishing only once the start | |
| * value is known. | |
| * - Counters are saved before planned restarts and firmware updates: | |
| * synchronously to Script.storage (KVS.Set is asynchronous and does not | |
| * finish before the restart), then published via MQTT. | |
| * - KVS is only written when a value has changed; interval in minutes. | |
| * - Energy is integrated over the measured time between two cycles instead | |
| * of assuming exactly 500 ms per cycle. | |
| * - Invalid power readings (NaN, implausible values) are skipped. | |
| * - Synchronous start-up; a missing MQTT topic_prefix falls back to the | |
| * device id. | |
| * - The Home Assistant device name is taken from the Shelly settings. | |
| * - Test mode to run next to the production script with its own MQTT | |
| * topics, KVS keys and Home Assistant entities. | |
| * - getDiagnostics() for troubleshooting via Script.Eval. | |
| */ | |
| let CONFIG = { | |
| testMode: false, // true = run alongside the production script (own topics, KVS keys, HA entities) | |
| updateInterval: 500, // Calculation cycle in ms | |
| enablePersistence: true, // true = back up counter states in flash memory (KVS, Script.storage) | |
| saveIntervalMinutes: 60, // KVS backup interval; only changed values are written | |
| minPublishInterval: 5000, // Minimum time between two MQTT counter updates in ms | |
| restoreTimeout: 5000, // Max. wait for retained MQTT values at start in ms | |
| maxGap: 5000, // Longer pauses between two cycles are capped to this value in ms | |
| migrateLegacyDiscovery: true, // true = move sensors that were registered by v1.1.0 to the current topics | |
| migrationStepDelay: 5000, // Pause between the steps of that move in ms | |
| mqttPrefix: "homeassistant" // Standard HA Discovery Prefix | |
| }; | |
| let VERSION = "1.2.2"; | |
| let SUFFIX = CONFIG.testMode ? "_test" : ""; | |
| let KVS_KEY_C = (CONFIG.testMode ? "Test" : "") + "EnergyConsumedKWh"; | |
| let KVS_KEY_R = (CONFIG.testMode ? "Test" : "") + "EnergyReturnedKWh"; | |
| let STORE_KEY = "wh" + SUFFIX; | |
| let MAX_POWER = 500000; // W; readings beyond this are treated as invalid | |
| let SHELLY_ID = null; | |
| let DEVICE_NAME = "Shelly Pro 3EM"; | |
| let TOPIC_C = ""; | |
| let TOPIC_R = ""; | |
| // Counters in whole Wh: start value + energy measured since script start | |
| let baseWhC = 0; | |
| let baseWhR = 0; | |
| let sessionWhC = 0; | |
| let sessionWhR = 0; | |
| let restWsC = 0.0; | |
| let restWsR = 0.0; | |
| let kvsPending = 0; | |
| let kvsDone = false; | |
| let kvsBlocked = false; | |
| let kvsWhC = null; | |
| let kvsWhR = null; | |
| let savedC = null; | |
| let savedR = null; | |
| let storeWhC = null; | |
| let storeWhR = null; | |
| let retainedStarted = false; | |
| let retainedDone = false; | |
| let retainedTimer = null; | |
| let mqttWhC = null; | |
| let mqttWhR = null; | |
| let gotC = false; | |
| let gotR = false; | |
| let migrationStarted = false; | |
| let legacyTopics = []; | |
| let ready = false; | |
| let pubWhC = -1; | |
| let pubWhR = -1; | |
| let lastPublish = 0; | |
| let lastTick = null; | |
| let diag = { | |
| ticks: 0, | |
| sumDt: 0, | |
| maxDt: 0, | |
| gaps: 0, | |
| invalid: 0, | |
| kvsWrites: 0, | |
| storageWrites: 0, | |
| restore: null | |
| }; | |
| // ───────────────────────────────────────────── | |
| // 1. Helper Functions | |
| // ───────────────────────────────────────────── | |
| function nowMs() { | |
| if (typeof Shelly.getUptimeMs === "function") return Shelly.getUptimeMs(); | |
| return Date.now(); | |
| } | |
| function whToKWh(wh) { | |
| return (wh / 1000).toFixed(3); | |
| } | |
| function kwhToWh(value) { | |
| let n = Number(value); | |
| if (typeof n !== "number" || n !== n || n < 0) return null; | |
| return Math.round(n * 1000); | |
| } | |
| function maxWh(current, candidate) { | |
| if (candidate === null) return current; | |
| return Math.max(current, candidate); | |
| } | |
| function fmtWh(wh) { | |
| if (wh === null) return "-"; | |
| return whToKWh(wh); | |
| } | |
| function validWh(value) { | |
| if (typeof value !== "number" || value !== value || value < 0) return null; | |
| return Math.round(value); | |
| } | |
| function getDiagnostics() { | |
| return JSON.stringify({ | |
| version: VERSION, | |
| testMode: CONFIG.testMode, | |
| ready: ready, | |
| consumed: whToKWh(baseWhC + sessionWhC), | |
| returned: whToKWh(baseWhR + sessionWhR), | |
| published: [pubWhC, pubWhR], | |
| saved: [savedC, savedR], | |
| restore: diag.restore, | |
| ticks: diag.ticks, | |
| avgCycleMs: diag.ticks > 0 ? Math.round(diag.sumDt / diag.ticks * 100) / 100 : null, | |
| maxCycleMs: diag.maxDt, | |
| gaps: diag.gaps, | |
| invalid: diag.invalid, | |
| kvsWrites: diag.kvsWrites, | |
| storageWrites: diag.storageWrites, | |
| kvsBlocked: kvsBlocked | |
| }); | |
| } | |
| // ───────────────────────────────────────────── | |
| // 2. Publish / Save | |
| // ───────────────────────────────────────────── | |
| function publishCounters(force) { | |
| if (!ready || !MQTT.isConnected()) return; | |
| let whC = baseWhC + sessionWhC; | |
| let whR = baseWhR + sessionWhR; | |
| if (!force && whC === pubWhC && whR === pubWhR) return; | |
| let t = nowMs(); | |
| if (!force && t - lastPublish < CONFIG.minPublishInterval) return; | |
| lastPublish = t; | |
| if ((force || whC !== pubWhC) && MQTT.publish(TOPIC_C, whToKWh(whC), 0, true)) pubWhC = whC; | |
| if ((force || whR !== pubWhR) && MQTT.publish(TOPIC_R, whToKWh(whR), 0, true)) pubWhR = whR; | |
| } | |
| function onKvsSet(result, errCode, errMsg, key) { | |
| if (errCode === 0) return; | |
| print("ERROR: KVS.Set " + key + " failed (" + errCode + "): " + errMsg); | |
| if (key === KVS_KEY_C) { | |
| savedC = null; | |
| } else { | |
| savedR = null; | |
| } | |
| } | |
| function saveCounters(reason) { | |
| if (!CONFIG.enablePersistence || !kvsDone || kvsBlocked) return; | |
| let valC = whToKWh(baseWhC + sessionWhC); | |
| let valR = whToKWh(baseWhR + sessionWhR); | |
| let writes = 0; | |
| if (valC !== savedC) { | |
| savedC = valC; | |
| Shelly.call("KVS.Set", { "key": KVS_KEY_C, "value": valC }, onKvsSet, KVS_KEY_C); | |
| writes++; | |
| } | |
| if (valR !== savedR) { | |
| savedR = valR; | |
| Shelly.call("KVS.Set", { "key": KVS_KEY_R, "value": valR }, onKvsSet, KVS_KEY_R); | |
| writes++; | |
| } | |
| if (writes > 0) { | |
| diag.kvsWrites += writes; | |
| print("Counters saved to KVS (" + reason + "): " + valC + " / " + valR + " kWh"); | |
| } | |
| } | |
| function onSaveTimer() { | |
| saveCounters("interval"); | |
| } | |
| // Script.storage writes synchronously, so the value is stored even if the | |
| // device restarts right after the restart announcement. | |
| function hasStorage() { | |
| return CONFIG.enablePersistence && typeof Script !== "undefined" && !!Script.storage; | |
| } | |
| function writeStorage() { | |
| if (!hasStorage()) return false; | |
| try { | |
| Script.storage.setItem(STORE_KEY, JSON.stringify([baseWhC + sessionWhC, baseWhR + sessionWhR])); | |
| diag.storageWrites++; | |
| return true; | |
| } catch (e) { | |
| print("ERROR: Script.storage.setItem failed."); | |
| return false; | |
| } | |
| } | |
| function readStorage() { | |
| if (!hasStorage()) return; | |
| try { | |
| let raw = Script.storage.getItem(STORE_KEY); | |
| if (raw === null || raw === undefined) return; | |
| let values = JSON.parse(raw); | |
| storeWhC = validWh(values[0]); | |
| storeWhR = validWh(values[1]); | |
| } catch (e) { | |
| print("WARNING: Script.storage holds an invalid value."); | |
| } | |
| } | |
| // ───────────────────────────────────────────── | |
| // 3. Restore start values (Script.storage + KVS + retained MQTT) | |
| // ───────────────────────────────────────────── | |
| function tryFinishRestore() { | |
| if (ready || !kvsDone || !retainedDone) return; | |
| ready = true; | |
| diag.restore = { | |
| storage: [storeWhC, storeWhR], | |
| kvs: [kvsWhC, kvsWhR], | |
| mqtt: [mqttWhC, mqttWhR], | |
| start: [baseWhC, baseWhR], | |
| atMs: nowMs() | |
| }; | |
| print("Start values | consumed " + whToKWh(baseWhC) + " kWh (storage " + fmtWh(storeWhC) + ", KVS " + fmtWh(kvsWhC) + | |
| ", MQTT " + fmtWh(mqttWhC) + ") | returned " + whToKWh(baseWhR) + " kWh (storage " + fmtWh(storeWhR) + | |
| ", KVS " + fmtWh(kvsWhR) + ", MQTT " + fmtWh(mqttWhR) + ")"); | |
| publishCounters(true); | |
| } | |
| function onKvsGet(result, errCode, errMsg, key) { | |
| let wh = null; | |
| if (errCode === 0 && result && result.value !== undefined && result.value !== null) { | |
| wh = kwhToWh(result.value); | |
| if (wh === null) print("WARNING: KVS " + key + " holds an invalid value: " + result.value); | |
| } else if (errCode !== 0 && errCode !== -105) { | |
| kvsBlocked = true; | |
| print("ERROR: KVS.Get " + key + " failed (" + errCode + "): " + errMsg + " - KVS writes disabled for this run."); | |
| } | |
| if (key === KVS_KEY_C) { | |
| kvsWhC = wh; | |
| if (wh !== null) savedC = whToKWh(wh); | |
| } else { | |
| kvsWhR = wh; | |
| if (wh !== null) savedR = whToKWh(wh); | |
| } | |
| kvsPending--; | |
| if (kvsPending > 0) return; | |
| kvsDone = true; | |
| baseWhC = maxWh(baseWhC, kvsWhC); | |
| baseWhR = maxWh(baseWhR, kvsWhR); | |
| tryFinishRestore(); | |
| } | |
| function finishRetainedCheck() { | |
| if (retainedDone) return; | |
| retainedDone = true; | |
| if (retainedTimer !== null) { | |
| Timer.clear(retainedTimer); | |
| retainedTimer = null; | |
| } | |
| try { | |
| MQTT.unsubscribe(TOPIC_C); | |
| MQTT.unsubscribe(TOPIC_R); | |
| } catch (e) { | |
| print("WARNING: MQTT.unsubscribe failed."); | |
| } | |
| baseWhC = maxWh(baseWhC, mqttWhC); | |
| baseWhR = maxWh(baseWhR, mqttWhR); | |
| tryFinishRestore(); | |
| } | |
| function onRetained(topic, message, which) { | |
| if (retainedDone) return; | |
| let wh = kwhToWh(message); | |
| if (which === "c") { | |
| mqttWhC = wh; | |
| gotC = true; | |
| } else { | |
| mqttWhR = wh; | |
| gotR = true; | |
| } | |
| if (gotC && gotR) finishRetainedCheck(); | |
| } | |
| function startRetainedCheck() { | |
| if (retainedStarted || retainedDone) return; | |
| retainedStarted = true; | |
| try { | |
| MQTT.subscribe(TOPIC_C, onRetained, "c"); | |
| MQTT.subscribe(TOPIC_R, onRetained, "r"); | |
| } catch (e) { | |
| print("WARNING: MQTT.subscribe failed - continuing without retained values."); | |
| finishRetainedCheck(); | |
| return; | |
| } | |
| retainedTimer = Timer.set(CONFIG.restoreTimeout, false, finishRetainedCheck); | |
| } | |
| // ───────────────────────────────────────────── | |
| // 4. Home Assistant Auto-Discovery | |
| // ───────────────────────────────────────────── | |
| function AnnounceHA() { | |
| let haTopic = CONFIG.mqttPrefix + "/sensor/" + SHELLY_ID + SUFFIX; | |
| let avtyTopic = SHELLY_ID + "/online"; | |
| let dev = { | |
| "ids": [SHELLY_ID + SUFFIX], | |
| "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" + SUFFIX, | |
| "stat_t": TOPIC_C, | |
| "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" + SUFFIX, | |
| "stat_t": TOPIC_R, | |
| "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. MQTT and System Event Handlers | |
| // ───────────────────────────────────────────── | |
| // v1.1.0 registered the sensors under topics with a doubled device prefix. | |
| // Home Assistant keeps owning the entities through those topics and rejects the | |
| // current registration as a duplicate. migrate_discovery makes HA unload the | |
| // entities but keep their registry entries (entity ID, settings, statistics); | |
| // after the old retained topics are cleared, the current registration attaches | |
| // to the same entries. Without anything on the old topics this does nothing. | |
| function startLegacyMigration() { | |
| migrationStarted = true; | |
| let base = CONFIG.mqttPrefix + "/sensor/shellypro3em-" + SHELLY_ID + SUFFIX; | |
| legacyTopics = [base + "-import/config", base + "-export/config"]; | |
| for (let i = 0; i < legacyTopics.length; i++) { | |
| MQTT.publish(legacyTopics[i], "{\"migrate_discovery\":true}", 0, false); | |
| } | |
| Timer.set(CONFIG.migrationStepDelay, false, clearLegacyDiscovery); | |
| } | |
| function clearLegacyDiscovery() { | |
| for (let i = 0; i < legacyTopics.length; i++) { | |
| MQTT.publish(legacyTopics[i], "", 0, true); | |
| } | |
| Timer.set(CONFIG.migrationStepDelay, false, finishLegacyMigration); | |
| } | |
| function finishLegacyMigration() { | |
| if (MQTT.isConnected()) AnnounceHA(); | |
| // Second registration in case HA was slower than the pause; unchanged ones are ignored. | |
| Timer.set(30000, false, AnnounceHA); | |
| } | |
| function onMqttConnect() { | |
| print("MQTT connected."); | |
| if (CONFIG.migrateLegacyDiscovery && !migrationStarted) { | |
| startLegacyMigration(); | |
| } else { | |
| AnnounceHA(); | |
| } | |
| if (!retainedDone) { | |
| startRetainedCheck(); | |
| } else { | |
| publishCounters(true); | |
| } | |
| } | |
| function onMqttDisconnect() { | |
| // Cannot publish here - connection is already gone. | |
| // HA uses native Shelly LWT on <topic_prefix>/online for offline detection. | |
| print("MQTT disconnected."); | |
| } | |
| function onEvent(ev) { | |
| if (!ev || ev.component !== "sys" || !ev.info) return; | |
| let name = ev.info.event; | |
| if (name === "scheduled_restart" || name === "ota_begin" || name === "ota_success") { | |
| // Synchronous storage first: the device may restart within a second. | |
| let stored = writeStorage(); | |
| publishCounters(true); | |
| saveCounters(name); | |
| print("System event " + name + ": counters stored" + (stored ? "" : " (without Script.storage)") + "."); | |
| } | |
| } | |
| // ───────────────────────────────────────────── | |
| // 6. Main Calculation Loop | |
| // ───────────────────────────────────────────── | |
| function onTick() { | |
| let t = nowMs(); | |
| if (lastTick === null) { | |
| lastTick = t; | |
| return; | |
| } | |
| let dt = t - lastTick; | |
| lastTick = t; | |
| if (dt <= 0) return; | |
| diag.ticks++; | |
| diag.sumDt += dt; | |
| if (dt > diag.maxDt) diag.maxDt = dt; | |
| if (dt > CONFIG.maxGap) { | |
| diag.gaps++; | |
| dt = CONFIG.maxGap; | |
| } | |
| let em = Shelly.getComponentStatus("em", 0); | |
| if (!em) return; | |
| let power = em.total_act_power; | |
| if (typeof power !== "number" || power !== power || power > MAX_POWER || power < -MAX_POWER) { | |
| diag.invalid++; | |
| return; | |
| } | |
| let ws = power * dt / 1000.0; | |
| if (ws >= 0) { | |
| restWsC += ws; | |
| if (restWsC >= 3600) { | |
| let fullC = Math.floor(restWsC / 3600); | |
| sessionWhC += fullC; | |
| restWsC -= fullC * 3600; | |
| } | |
| } else { | |
| restWsR -= ws; | |
| if (restWsR >= 3600) { | |
| let fullR = Math.floor(restWsR / 3600); | |
| sessionWhR += fullR; | |
| restWsR -= fullR * 3600; | |
| } | |
| } | |
| publishCounters(false); | |
| } | |
| // ───────────────────────────────────────────── | |
| // 7. Initialization | |
| // ───────────────────────────────────────────── | |
| function init() { | |
| let info = Shelly.getDeviceInfo(); | |
| let mqttCfg = Shelly.getComponentConfig("mqtt"); | |
| let sysCfg = Shelly.getComponentConfig("sys"); | |
| if (mqttCfg && mqttCfg.topic_prefix) { | |
| SHELLY_ID = mqttCfg.topic_prefix; | |
| } else if (info && info.id) { | |
| SHELLY_ID = info.id; | |
| } | |
| if (!SHELLY_ID) { | |
| print("ERROR: Neither MQTT topic_prefix nor device id available."); | |
| return; | |
| } | |
| if (sysCfg && sysCfg.device && sysCfg.device.name) DEVICE_NAME = sysCfg.device.name; | |
| if (CONFIG.testMode) DEVICE_NAME = DEVICE_NAME + " (Test)"; | |
| TOPIC_C = SHELLY_ID + "/energy_counter" + SUFFIX + "/consumed"; | |
| TOPIC_R = SHELLY_ID + "/energy_counter" + SUFFIX + "/returned"; | |
| print("Saldierung v" + VERSION + (CONFIG.testMode ? " [TEST MODE]" : "") + " | ID: " + SHELLY_ID + " | Name: " + DEVICE_NAME); | |
| if (!Shelly.getComponentStatus("em", 0)) { | |
| print("ERROR: Component em:0 not found. The device must use the triphase profile."); | |
| } | |
| if (CONFIG.enablePersistence) { | |
| readStorage(); | |
| baseWhC = maxWh(baseWhC, storeWhC); | |
| baseWhR = maxWh(baseWhR, storeWhR); | |
| kvsPending = 2; | |
| Shelly.call("KVS.Get", { "key": KVS_KEY_C }, onKvsGet, KVS_KEY_C); | |
| Shelly.call("KVS.Get", { "key": KVS_KEY_R }, onKvsGet, KVS_KEY_R); | |
| } else { | |
| kvsDone = true; | |
| print("Persistence disabled. Start values come from MQTT only."); | |
| } | |
| if (mqttCfg && mqttCfg.enable) { | |
| MQTT.setConnectHandler(onMqttConnect); | |
| MQTT.setDisconnectHandler(onMqttDisconnect); | |
| if (MQTT.isConnected()) onMqttConnect(); | |
| } else { | |
| retainedDone = true; | |
| print("WARNING: MQTT is disabled - counters cannot be published."); | |
| } | |
| Shelly.addEventHandler(onEvent); | |
| Timer.set(CONFIG.updateInterval, true, onTick); | |
| if (CONFIG.enablePersistence && CONFIG.saveIntervalMinutes > 0) { | |
| Timer.set(CONFIG.saveIntervalMinutes * 60000, true, onSaveTimer); | |
| } | |
| tryFinishRestore(); | |
| } | |
| init(); |
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.
@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.
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).

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?