Skip to content

Instantly share code, notes, and snippets.

@Davc0m
Last active May 11, 2026 11:44
Show Gist options
  • Select an option

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

Select an option

Save Davc0m/dc419aa3147ec3c3d6e7289f89dc0ed8 to your computer and use it in GitHub Desktop.
Shelly Pro 3EM: Saldierende Energiemessung (Net Metering) mit Home Assistant Auto-Discovery
/**
* Shelly Pro 3EM - Net Metering (Saldierung) & Home Assistant Auto-Discovery
* Version: 1.1.8
*
* DISCLAIMER:
* Use this script entirely at your own risk! I assume absolutely no liability
* for any direct, indirect, or consequential damages. This includes, but is
* not limited to, damage to the Shelly device, any connected electrical
* equipment, other devices in your network, data loss, or system malfunctions.
* By using this script, you acknowledge that you alone are responsible for
* your hardware and setup.
*
* CHANGELOG (v1.1.1 → v1.1.8):
* - Corrected HA Discovery MQTT topic (removed duplicate device prefix)
* - Added null/type-checks for MQTT config, em component and KVS values
* - Fixed race condition: main loop waits until KVS counters are fully loaded
* - Replaced fixed startup delay with MQTT.setConnectHandler()
* - Counter topics are now published retained for reliable HA recovery
* - Availability uses native Shelly LWT topic (<id>/online, true/false)
* - HA Discovery payload switched to short keys to stay under 512-byte MQTT limit
* - Removed unused helper functions and objects to reduce RAM usage
*/
let CONFIG = {
updateInterval: 500, // Calculation cycle in ms
enablePersistence: true, // true = Save counter states to flash memory
saveInterval: 1800, // Save to KVS every 1800 cycles (~15 min.)
mqttPrefix: "homeassistant" // Standard HA Discovery Prefix
};
let VERSION = "1.1.8";
let SHELLY_ID = null;
let energyReturnedWs = 0.0;
let energyConsumedWs = 0.0;
let energyReturnedKWh = 0.0;
let energyConsumedKWh = 0.0;
let saveCounter = 0;
let lastPublishedConsumed = "";
let lastPublishedReturned = "";
let countersLoaded = false;
// ─────────────────────────────────────────────
// 1. Helper Functions
// ─────────────────────────────────────────────
function TryAnnounceAndPublish() {
if (!SHELLY_ID || !MQTT.isConnected()) return;
AnnounceHA();
if (countersLoaded) PublishCounters(true);
}
function PublishCounters(force) {
if (!SHELLY_ID || !countersLoaded) return;
let valC = energyConsumedKWh.toFixed(3);
let valR = energyReturnedKWh.toFixed(3);
if (!force && valC === lastPublishedConsumed && valR === lastPublishedReturned) return;
let okC = MQTT.publish(SHELLY_ID + "/energy_counter/consumed", valC, 0, true);
let okR = MQTT.publish(SHELLY_ID + "/energy_counter/returned", valR, 0, true);
if (okC) lastPublishedConsumed = valC;
if (okR) lastPublishedReturned = valR;
}
// ─────────────────────────────────────────────
// 2. MQTT Event Handlers
// ─────────────────────────────────────────────
MQTT.setConnectHandler(function () {
print("MQTT connected.");
TryAnnounceAndPublish();
});
MQTT.setDisconnectHandler(function () {
// Cannot publish here – connection is already gone.
// HA uses native Shelly LWT on <topic_prefix>/online for offline detection.
print("MQTT disconnected.");
});
// ─────────────────────────────────────────────
// 3. Get Device ID and Initialize
// ─────────────────────────────────────────────
Shelly.call("Mqtt.GetConfig", {}, function (res, err_code, err_msg) {
if (!res) {
print("ERROR: Mqtt.GetConfig returned null! Code: " + err_code + " | " + err_msg);
return;
}
SHELLY_ID = res.topic_prefix ? res.topic_prefix : null;
if (!SHELLY_ID) {
print("ERROR: No MQTT topic_prefix set. Please check MQTT configuration.");
return;
}
print("Shelly ID: " + SHELLY_ID + " | Script v" + VERSION);
if (CONFIG.enablePersistence) {
LoadCounters();
} else {
countersLoaded = true;
print("Persistence disabled. Counters start at 0.");
}
// Handles script restarts when MQTT is already connected
TryAnnounceAndPublish();
});
// ─────────────────────────────────────────────
// 4. Home Assistant Auto-Discovery
// ─────────────────────────────────────────────
function AnnounceHA() {
if (!SHELLY_ID) return;
let haTopic = CONFIG.mqttPrefix + "/sensor/" + SHELLY_ID;
let avtyTopic = SHELLY_ID + "/online";
let dev = {
"ids": [SHELLY_ID],
"name": "Shelly Pro 3EM",
"mf": "Shelly",
"mdl": "Pro 3EM",
"sw": "Saldierung v" + VERSION
};
let okImport = MQTT.publish(
haTopic + "-import/config",
JSON.stringify({
"name": "Saldierend Import",
"uniq_id": SHELLY_ID + "_sald_import",
"stat_t": SHELLY_ID + "/energy_counter/consumed",
"unit_of_meas": "kWh",
"dev_cla": "energy",
"stat_cla": "total_increasing",
"avty_t": avtyTopic,
"pl_avail": "true",
"pl_not_avail": "false",
"dev": dev
}),
0, true
);
let okExport = MQTT.publish(
haTopic + "-export/config",
JSON.stringify({
"name": "Saldierend Export",
"uniq_id": SHELLY_ID + "_sald_export",
"stat_t": SHELLY_ID + "/energy_counter/returned",
"unit_of_meas": "kWh",
"dev_cla": "energy",
"stat_cla": "total_increasing",
"avty_t": avtyTopic,
"pl_avail": "true",
"pl_not_avail": "false",
"dev": dev
}),
0, true
);
if (okImport && okExport) {
print("HA Auto-Discovery sent.");
} else {
print("WARNING: HA Discovery publish failed (MQTT not ready?).");
}
}
// ─────────────────────────────────────────────
// 5. Load / Save Persistence (KVS)
// ─────────────────────────────────────────────
function LoadCounters() {
let loadedCount = 0;
function checkDone() {
loadedCount++;
if (loadedCount === 2) {
countersLoaded = true;
lastPublishedConsumed = "";
lastPublishedReturned = "";
print("Counters ready. Consumed: " + energyConsumedKWh + " kWh | Returned: " + energyReturnedKWh + " kWh");
if (MQTT.isConnected()) PublishCounters(true);
}
}
Shelly.call("KVS.Get", { "key": "EnergyConsumedKWh" }, function (res, err_code) {
if (res && res.value !== undefined && res.value !== null) {
energyConsumedKWh = Number(res.value);
if (isNaN(energyConsumedKWh)) energyConsumedKWh = 0.0;
print("Loaded EnergyConsumedKWh: " + energyConsumedKWh);
} else if (err_code !== 0) {
print("INFO: EnergyConsumedKWh not in KVS yet (first run?).");
}
checkDone();
});
Shelly.call("KVS.Get", { "key": "EnergyReturnedKWh" }, function (res, err_code) {
if (res && res.value !== undefined && res.value !== null) {
energyReturnedKWh = Number(res.value);
if (isNaN(energyReturnedKWh)) energyReturnedKWh = 0.0;
print("Loaded EnergyReturnedKWh: " + energyReturnedKWh);
} else if (err_code !== 0) {
print("INFO: EnergyReturnedKWh not in KVS yet (first run?).");
}
checkDone();
});
}
function SaveCounters() {
Shelly.call("KVS.Set", { "key": "EnergyConsumedKWh", "value": energyConsumedKWh.toFixed(3) });
Shelly.call("KVS.Set", { "key": "EnergyReturnedKWh", "value": energyReturnedKWh.toFixed(3) });
print("Counters saved to KVS.");
}
// ─────────────────────────────────────────────
// 6. Main Calculation Loop
// ─────────────────────────────────────────────
Timer.set(CONFIG.updateInterval, true, function () {
if (!SHELLY_ID || !countersLoaded) return;
let em = Shelly.getComponentStatus("em", 0);
if (!em || typeof em.total_act_power !== "number") return;
let power = em.total_act_power;
let energyStep = power * (CONFIG.updateInterval / 1000.0);
if (power >= 0) {
energyConsumedWs += energyStep;
} else {
energyReturnedWs += Math.abs(energyStep);
}
if (energyConsumedWs >= 3600) {
let chunkC = Math.floor(energyConsumedWs / 3600);
energyConsumedKWh += chunkC / 1000.0;
energyConsumedWs -= chunkC * 3600;
}
if (energyReturnedWs >= 3600) {
let chunkR = Math.floor(energyReturnedWs / 3600);
energyReturnedKWh += chunkR / 1000.0;
energyReturnedWs -= chunkR * 3600;
}
PublishCounters(false);
if (CONFIG.enablePersistence) {
saveCounter++;
if (saveCounter >= CONFIG.saveInterval) {
saveCounter = 0;
SaveCounters();
}
}
});
@Davc0m
Copy link
Copy Markdown
Author

Davc0m commented Mar 12, 2026

@jubie25
Deine Bedenken sind absolut berechtigt. Auch wenn das zugrundeliegende Espressif NVS (Non-Volatile Storage) ein "Wear Leveling" nutzt, um Schreibzugriffe auf den ganzen Speicherbereich zu verteilen, ist ein 2,5-Minuten-Intervall auf Dauer unnötiger Stress für den Chip.

Du hast auch völlig recht, was Home Assistant angeht: Da die Sensoren beim Auto-Discovery mit stat_cla: "total_increasing" angelegt werden, erkennt Home Assistant einen Reset der Werte auf 0 (nach einem Shelly-Neustart) automatisch und führt die internen Summen im Energy Dashboard völlig nahtlos weiter.

Lösung: Ich habe das Skript (Version 1.1.8) entsprechend angepasst.
Du findest ganz oben im CONFIG-Block nun den Schalter enablePersistence. Setzt du diesen auf false, wird die KVS-Speicherung komplett deaktiviert und das Skript läuft zu 100% im flüchtigen RAM des Shellys. Für diejenigen, die das Skript ohne Home Assistant nutzen und die Werte behalten wollen, habe ich das Intervall im Beispielcode jetzt deutlich schonender auf 15 Minuten (saveInterval: 1800) angehoben.

@christian1982ks
Hallo Christian, vielen Dank fürs Testen und vor allem für diesen super Hinweis! Du hast den Fehler absolut exakt analysiert.

Da lag tatsächlich ein Logikfehler in meiner if-Bedingung vor. Wenn du reinen PV-Überschuss einspeist, ändert sich nur der Wert für die Einspeisung (valR), während der Netzbezug (valC) stagniert. Da das alte Skript nur valC auf Änderungen geprüft hat, wurden die MQTT-Nachrichten tagsüber einfach verschluckt.

Lösung: Ich habe das in der neuen Version 1.1.8 gefixt. Das Skript speichert nun beide Werte (lastPublishedConsumed und lastPublishedReturned) und triggert das MQTT-Publishing, sobald sich einer der beiden Werte ändert. Damit kommen deine Einspeisedaten jetzt auch im Sommer wieder in Echtzeit in Home Assistant an!

@arbenhaliti-web
Copy link
Copy Markdown

THANK YOU!!

@leepfrog-ger
Copy link
Copy Markdown

This is gold, thank you for your work!

@lord-icon
Copy link
Copy Markdown

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

Ich hab mir selbst Linderung verschafft.

/**
 * Shelly Pro 3EM - Net Metering (Saldierung) & Home Assistant Auto-Discovery
 * Version: 1.1.9
 *
 * DISCLAIMER:
 * Use this script entirely at your own risk! I assume absolutely no liability
 * for any direct, indirect, or consequential damages. This includes, but is
 * not limited to, damage to the Shelly device, any connected electrical
 * equipment, other devices in your network, data loss, or system malfunctions.
 * By using this script, you acknowledge that you alone are responsible for
 * your hardware and setup.
 *
 * CHANGELOG (v1.1.8 → v1.1.9):
 * - Added dynamic device name via Sys.GetConfig()
 * - Discovery now waits until MQTT topic_prefix and device name are initialized
 * - Fallback to "Shelly Pro 3EM" if no custom device name is set
 */

let CONFIG = {
    updateInterval: 500,          // Calculation cycle in ms
    enablePersistence: false,     // true = Save counter states to flash memory
    saveInterval: 1800,           // Save to KVS every 1800 cycles (~15 min.)
    mqttPrefix: "homeassistant"   // Standard HA Discovery Prefix
};

let VERSION = "1.1.9";
let SHELLY_ID = null;
let DEVICE_NAME = "Shelly Pro 3EM";

let mqttConfigLoaded = false;
let sysConfigLoaded = false;

let energyReturnedWs = 0.0;
let energyConsumedWs = 0.0;
let energyReturnedKWh = 0.0;
let energyConsumedKWh = 0.0;

let saveCounter = 0;
let lastPublishedConsumed = "";
let lastPublishedReturned = "";
let countersLoaded = false;

// ─────────────────────────────────────────────
// 1. Helper Functions
// ─────────────────────────────────────────────

function TryAnnounceAndPublish() {
    if (!SHELLY_ID) return;
    if (!mqttConfigLoaded) return;
    if (!sysConfigLoaded) return;
    if (!MQTT.isConnected()) return;

    AnnounceHA();

    if (countersLoaded) {
        PublishCounters(true);
    }
}

function PublishCounters(force) {
    if (!SHELLY_ID || !countersLoaded) return;

    let valC = energyConsumedKWh.toFixed(3);
    let valR = energyReturnedKWh.toFixed(3);

    if (!force && valC === lastPublishedConsumed && valR === lastPublishedReturned) return;

    let okC = MQTT.publish(SHELLY_ID + "/energy_counter/consumed", valC, 0, true);
    let okR = MQTT.publish(SHELLY_ID + "/energy_counter/returned", valR, 0, true);

    if (okC) lastPublishedConsumed = valC;
    if (okR) lastPublishedReturned = valR;
}

// ─────────────────────────────────────────────
// 2. MQTT Event Handlers
// ─────────────────────────────────────────────

MQTT.setConnectHandler(function () {
    print("MQTT connected.");
    TryAnnounceAndPublish();
});

MQTT.setDisconnectHandler(function () {
    // Cannot publish here – connection is already gone.
    // HA uses native Shelly LWT on <topic_prefix>/online for offline detection.
    print("MQTT disconnected.");
});

// ─────────────────────────────────────────────
// 3. Get Device ID / Name and Initialize
// ─────────────────────────────────────────────

Shelly.call("Mqtt.GetConfig", {}, function (res, err_code, err_msg) {
    if (!res) {
        print("ERROR: Mqtt.GetConfig returned null! Code: " + err_code + " | " + err_msg);
        return;
    }

    SHELLY_ID = res.topic_prefix ? res.topic_prefix : null;

    if (!SHELLY_ID) {
        print("ERROR: No MQTT topic_prefix set. Please check MQTT configuration.");
        return;
    }

    mqttConfigLoaded = true;
    print("Shelly ID: " + SHELLY_ID + " | Script v" + VERSION);

    if (CONFIG.enablePersistence) {
        LoadCounters();
    } else {
        countersLoaded = true;
        print("Persistence disabled. Counters start at 0.");
    }

    // Handles script restarts when MQTT is already connected
    TryAnnounceAndPublish();
});

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

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

    sysConfigLoaded = true;
    TryAnnounceAndPublish();
});

// ─────────────────────────────────────────────
// 4. Home Assistant Auto-Discovery
// ─────────────────────────────────────────────

function AnnounceHA() {
    if (!SHELLY_ID) return;

    let haTopic = CONFIG.mqttPrefix + "/sensor/" + SHELLY_ID;
    let avtyTopic = SHELLY_ID + "/online";

    let dev = {
        "ids": [SHELLY_ID],
        "name": DEVICE_NAME,
        "mf": "Shelly",
        "mdl": "Pro 3EM",
        "sw": "Saldierung v" + VERSION
    };

    let okImport = MQTT.publish(
        haTopic + "-import/config",
        JSON.stringify({
            "name": "Saldierend Import",
            "uniq_id": SHELLY_ID + "_sald_import",
            "stat_t": SHELLY_ID + "/energy_counter/consumed",
            "unit_of_meas": "kWh",
            "dev_cla": "energy",
            "stat_cla": "total_increasing",
            "avty_t": avtyTopic,
            "pl_avail": "true",
            "pl_not_avail": "false",
            "dev": dev
        }),
        0, true
    );

    let okExport = MQTT.publish(
        haTopic + "-export/config",
        JSON.stringify({
            "name": "Saldierend Export",
            "uniq_id": SHELLY_ID + "_sald_export",
            "stat_t": SHELLY_ID + "/energy_counter/returned",
            "unit_of_meas": "kWh",
            "dev_cla": "energy",
            "stat_cla": "total_increasing",
            "avty_t": avtyTopic,
            "pl_avail": "true",
            "pl_not_avail": "false",
            "dev": dev
        }),
        0, true
    );

    if (okImport && okExport) {
        print("HA Auto-Discovery sent. Device name: " + DEVICE_NAME);
    } else {
        print("WARNING: HA Discovery publish failed (MQTT not ready?).");
    }
}

// ─────────────────────────────────────────────
// 5. Load / Save Persistence (KVS)
// ─────────────────────────────────────────────

function LoadCounters() {
    let loadedCount = 0;

    function checkDone() {
        loadedCount++;
        if (loadedCount === 2) {
            countersLoaded = true;
            lastPublishedConsumed = "";
            lastPublishedReturned = "";
            print("Counters ready. Consumed: " + energyConsumedKWh + " kWh | Returned: " + energyReturnedKWh + " kWh");
            if (MQTT.isConnected()) PublishCounters(true);
        }
    }

    Shelly.call("KVS.Get", { "key": "EnergyConsumedKWh" }, function (res, err_code) {
        if (res && res.value !== undefined && res.value !== null) {
            energyConsumedKWh = Number(res.value);
            if (isNaN(energyConsumedKWh)) energyConsumedKWh = 0.0;
            print("Loaded EnergyConsumedKWh: " + energyConsumedKWh);
        } else if (err_code !== 0) {
            print("INFO: EnergyConsumedKWh not in KVS yet (first run?).");
        }
        checkDone();
    });

    Shelly.call("KVS.Get", { "key": "EnergyReturnedKWh" }, function (res, err_code) {
        if (res && res.value !== undefined && res.value !== null) {
            energyReturnedKWh = Number(res.value);
            if (isNaN(energyReturnedKWh)) energyReturnedKWh = 0.0;
            print("Loaded EnergyReturnedKWh: " + energyReturnedKWh);
        } else if (err_code !== 0) {
            print("INFO: EnergyReturnedKWh not in KVS yet (first run?).");
        }
        checkDone();
    });
}

function SaveCounters() {
    Shelly.call("KVS.Set", { "key": "EnergyConsumedKWh", "value": energyConsumedKWh.toFixed(3) });
    Shelly.call("KVS.Set", { "key": "EnergyReturnedKWh", "value": energyReturnedKWh.toFixed(3) });
    print("Counters saved to KVS.");
}

// ─────────────────────────────────────────────
// 6. Main Calculation Loop
// ─────────────────────────────────────────────

Timer.set(CONFIG.updateInterval, true, function () {
    if (!SHELLY_ID || !countersLoaded) return;

    let em = Shelly.getComponentStatus("em", 0);
    if (!em || typeof em.total_act_power !== "number") return;

    let power = em.total_act_power;
    let energyStep = power * (CONFIG.updateInterval / 1000.0);

    if (power >= 0) {
        energyConsumedWs += energyStep;
    } else {
        energyReturnedWs += Math.abs(energyStep);
    }

    if (energyConsumedWs >= 3600) {
        let chunkC = Math.floor(energyConsumedWs / 3600);
        energyConsumedKWh += chunkC / 1000.0;
        energyConsumedWs -= chunkC * 3600;
    }

    if (energyReturnedWs >= 3600) {
        let chunkR = Math.floor(energyReturnedWs / 3600);
        energyReturnedKWh += chunkR / 1000.0;
        energyReturnedWs -= chunkR * 3600;
    }

    PublishCounters(false);

    if (CONFIG.enablePersistence) {
        saveCounter++;
        if (saveCounter >= CONFIG.saveInterval) {
            saveCounter = 0;
            SaveCounters();
        }
    }
});

Ausgehend von meiner Solaranlage

  • Pro3EM-Solaranlage
  • Pro3EM-Solaranlage Saldierend Export

So wird es dann übersichtlich.

@esrauscht
Copy link
Copy Markdown

Danke für das Skript. Ich habe es implementiert und es liefert auch Werte an Home Assistant. Jedoch haben diese ständig Aussetzer und setzen sich wieder zurück (siehe Screenshot). Das sollte anders aussehen oder?
IMG_7729

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