Last active
May 11, 2026 11:44
-
-
Save Davc0m/dc419aa3147ec3c3d6e7289f89dc0ed8 to your computer and use it in GitHub Desktop.
Shelly Pro 3EM: Saldierende Energiemessung (Net Metering) mit Home Assistant Auto-Discovery
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * 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(); | |
| } | |
| } | |
| }); |
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.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment

This is gold, thank you for your work!