Skip to content

sml_eco_shelly.tc

sml_eco_shelly.tc — SML meter + EcoTracker / Shelly Pro 3EM emulator,

Source on GitHub

// @name: SML + EcoTracker / Shelly
// @info: https://ottelo9.github.io/tasmota-sml-script/eco_shelly/
// sml_eco_shelly.tc — SML meter + EcoTracker / Shelly Pro 3EM emulator,
// no charts. Feeds a PV battery (Marstek, Jackery, NOAH, Hoymiles, ...) for
// zero feed-in. For the charts variant use sml_chart_eco_shelly.tc.
// Docs: https://ottelo9.github.io/tasmota-sml-script/eco_shelly/
// Needs ESP32, TinyC in SLOT 0 (Tasmota dispatches every webOn to slot 0),
// USE_SML.

#include "sml_descriptor.tc"
#include "sml_modbus_common.tc"
#include "eco_shelly_common.tc"

char sml_vers[] = "28.09.2026";

// ─── Settings ──────────────────────────────────────────────────────────────
persist int sml_sndpwr;      // 1 = publish power2 via MQTT

int sml_e_sndpwr;             // edit buffer, applied by Save

// ─── Runtime ───────────────────────────────────────────────────────────────
int   sml_pwr_last;
char  sml_mtopic[64];
byte  sml_pub[128];
int   do_reset;

// ─── Callbacks ─────────────────────────────────────────────────────────────

void Every100ms() {
    esPoll();
    mbPoll();
}

void EverySecond() {
    uiApplyBegin();
    if (tc_opt_first == 1) {
        sml_sndpwr = tcOptGet(10, sml_sndpwr);
        tcOptSet(10, sml_sndpwr);
    }
    if (ui_reloading) sml_e_sndpwr = sml_sndpwr;
    if (ui_applying)  { sml_sndpwr = sml_e_sndpwr; tcOptSet(10, sml_sndpwr); }
    sml_descriptor_apply();
    esTick();
    mbTick();

    if (do_reset) {
        tasmCmd("Sensor53 r", sml_buf);
        do_reset = 0;
    }

    if (tasm_uptime - sml_pwr_last >= 5) {
        sml_pwr_last = tasm_uptime;
        sml_power2 = (0.9 * sml_power2) + (0.1 * smlGet(1));
        if (sml_sndpwr > 0) {
            sprintf(sml_ftopic, "stat/%s/script/power2", sml_mtopic);
            sprintf(sml_pub, "%.0f", sml_power2);
            smlPublish(sml_ftopic, sml_pub);
            sprintf(sml_ftopic, "stat/%s/script/cpwr", sml_mtopic);
            sprintf(sml_pub, "%.0f", es_cpwr);
            smlPublish(sml_ftopic, sml_pub);
        }
    }
}

void WebOn() {
    int h = webHandler();
    if (esWebOn(h)) return;          // /v1/json, /rpc/*
    if (h != 7) return;              // /pwr
    sml_cpwr = es_cpwr; sml_hascp = 1;
    if (smlVars()) return;          // /pwr?v=... single values
    smlPwrJson(sml_power2);
    sprintfAppend(sml_buf, ",\"cpwr\":%.0f}", es_cpwr);
    smlSendJson(sml_buf);
}

void WebCall() {
    if (!sml_activ) {
        uiText("{s}SML{m}off (Rule1){e}", "{s}SML{m}aus (Rule1){e}");
        return;
    }
    uiSep();
    if (ui_de) {
        sprintf(sml_buf, "{s}Leistung{m}%.0f W{e}", smlGet(1));      webSend(sml_buf);
        sprintf(sml_buf, "{s}Bezug{m}%.3f kWh{e}", smlGet(2));       webSend(sml_buf);
        sprintf(sml_buf, "{s}Einspeisung{m}%.3f kWh{e}", sml_export()); webSend(sml_buf);
    } else {
        sprintf(sml_buf, "{s}Power{m}%.0f W{e}", smlGet(1));         webSend(sml_buf);
        sprintf(sml_buf, "{s}Import{m}%.3f kWh{e}", smlGet(2));      webSend(sml_buf);
        sprintf(sml_buf, "{s}Export{m}%.3f kWh{e}", sml_export());      webSend(sml_buf);
    }
    sml_phase_rows();
    uiSep();
    esRows();
    mbRows();
}

void WebUI() {
    if (webPage() == 5) { webSend("<script>location.replace('/tc')</script>"); return; }
    if (webPage() != 0) return;
    sml_render_settings_panel();
    esPanel();

    uiHead("&#x1F4E1;", "Telemetry", "Telemetrie", "sml/#telemetry");
    if (ui_de) webCheckbox(sml_e_sndpwr, "power2 + cpwr senden (MQTT)");
    else       webCheckbox(sml_e_sndpwr, "Send power2 + cpwr (MQTT)");
    sprintf(sml_buf, "<div class='n'>power2: <b>%.0f W</b></div>", sml_power2);
    webSend(sml_buf);
    sprintf(sml_buf, "<div class='n'>cpwr: <b>%.0f W</b></div>", es_cpwr);
    webSend(sml_buf);
    webSend("<b>HTTP API</b>");
    webSend("<div class='n'>HTTP: <a href='/pwr' target='_blank'>/pwr</a> &nbsp; <a href='/pwr?v=all' target='_blank'>/pwr?v=all</a> &nbsp; <a href='/pwr?v=sml[1]' target='_blank'>/pwr?v=sml[1]</a></div>");
    mbPanel();

    webSend("<hr>");
    uiSaveButton();
    if (ui_de) webButton(do_reset, "SML Neustart|OK");
    else       webButton(do_reset, "Restart SML|OK");

    uiUptime();
    sprintf(sml_buf, "sml_eco_shelly.tc &middot; Version %s<br>", sml_vers);
    webSend(sml_buf);
    webSend("by <a href='https://ottelo.jimdofree.com' target='_blank'>ottelo.jimdofree.com</a> &amp; gemu2015 (TinyC <span id='tcv'>...</span>)</div></div>");
    webSend("<script>if(window.__tcv){document.getElementById('tcv').textContent=window.__tcv;}else{fetch('/tc_api?cmd=status').then(function(r){return r.json();}).then(function(j){window.__tcv=j.release;var e=document.getElementById('tcv');if(e)e.textContent=j.release;}).catch(function(){});}</script>");
}

void JsonCall() {
    esJson();
}

int main() {
    if (sml_rx_pin == 0 && sml_tx_pin == 0) {
        sml_rx_pin = -1;
        sml_tx_pin = -1;
    }
    if (sml_activ != tasm_rule) tasm_rule = sml_activ;

    sml_e_sndpwr = sml_sndpwr;
    tasmInfo(0, sml_mtopic);
    sml_power2 = smlGet(1);
    sml_pwr_last = 0;

    esInit(1, 2, 3, 5, 6);       // /v1/json, EM, Shelly, EM1, EMData
    mbInit();
    webPageLabel(0, "&#x2699;&#xFE0F; SML /&#x1F50B;");
    webPageLabel(5, "&#x1F4BB; TinyC");   // hands over to /tc - kept last so it stays
                                          // last when a program adds a page
    webOn(7, "/pwr");
    smlScripterLoad("/sml_meter.def");

    addLog("sml_eco_shelly: %s started mode=%d rx=%d modbus=%d",
           sml_vers, es_mode, sml_rx_pin, mb_on);
    return 0;
}