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();
}
}
});
@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