Skip to content

ecotracker.tc

EcoTracker Emulator for Marstek battery systems (Jupiter, Venus, B2500) and Hoymiles (MS-A2)

Source on GitHub

// EcoTracker Emulator for Marstek battery systems (Jupiter, Venus, B2500) and Hoymiles (MS-A2)
// TinyC translation of ottelo's Scripter EcoTrackerEmuSimple
// Emulates Everhome EcoTracker via mDNS + REST API /v1/json
// Requires: USE_SML (or USE_SML_M)
// SML meter descriptor must be loaded separately via IDE SML tab or /sml_meter.def

// ── Persistent variables (auto-saved to /tinyc_pvars.bin) ──
persist float dval;     // daily counter in (kWh) at midnight
persist float dval2;    // daily counter out (kWh) at midnight
persist float mval;     // monthly counter in
persist float mval2;    // monthly counter out
persist float yval;     // yearly counter in
persist float yval2;    // yearly counter out
persist int pwroffset;  // power offset [W] for zero-feed regulation

// ── Runtime variables ──
float cpwr;         // current power (smoothed)
int once;           // mDNS registered flag
int last_hr;        // last seen hour (for midnight detect)
char buf[256];      // general purpose buffer

// ── Settings (WebUI widgets) ──
int do_init;        // button: initialize counters from meter
int do_save;        // button: manually save data

void initVars() {
    // Initialize counters from current meter readings
    float ein = smlGet(2);   // energy in (kWh)
    float eout = smlGet(3);  // energy out (kWh)
    dval  = ein;  dval2  = eout;
    mval  = ein;  mval2  = eout;
    yval  = ein;  yval2  = eout;
    saveVars();
    addLog("EcoTracker: counters initialized from meter");
}

// ── EcoTracker REST API handler ──
// Marstek polls /v1/json every ~13s
void WebOn() {
    int h = webHandler();
    if (h == 1) {
        float ein  = smlGet(2) * 1000.0;   // kWh -> Wh
        float eout = smlGet(3) * 1000.0;
        // Build JSON: {"power":N,"powerAvg":N,"energyCounterIn":N,"energyCounterOut":N}
        sprintf(buf, "{\"power\":%.0f,\"powerAvg\":%.0f,\"energyCounterIn\":%.0f,\"energyCounterOut\":%.0f}", cpwr, cpwr, ein, eout);
        webSend(buf);
    }
}

// ── Every second: midnight check + power smoothing ──
void EverySecond() {
    // Wait for NTP and meter data
    if (tasm_year < 2020) return;
    float pwr = smlGet(1);
    if (pwr == 0.0) return;  // no meter data yet

    // Register mDNS once
    if (once == 0) {
        mdnsRegister("ecotracker-", "-", "everhome");
        once = 1;
        addLog("EcoTracker: mDNS registered");
    }

    // Handle button presses from settings page
    if (do_init) {
        initVars();
        do_init = 0;
    }
    if (do_save) {
        saveVars();
        do_save = 0;
    }

    // Power smoothing — reduce peaks for better battery regulation
    float diff = pwr - cpwr;
    if (diff > 500.0) {
        cpwr = pwr - (diff / 2.0);
    } else {
        cpwr = pwr + (float)pwroffset;
    }

    // Midnight counter update (check every second, act on hour change to 0)
    int hr = tasm_hour;
    if (hr == 0 && last_hr != 0) {
        // Day change
        dval  = smlGet(2);
        dval2 = smlGet(3);
        if (tasm_day == 1) {
            // Month change
            mval  = smlGet(2);
            mval2 = smlGet(3);
        }
        if (tasm_day == 1 && tasm_month == 1) {
            // Year change
            yval  = smlGet(2);
            yval2 = smlGet(3);
        }
        saveVars();
    }
    last_hr = hr;
}

// ── Web display: sensor rows on Tasmota main page ──
void WebCall() {
    sprintf(buf, "{s}Leistung (Marstek){m}%.0f W{e}", cpwr);
    webSend(buf);

    float ein = smlGet(2);
    float eout = smlGet(3);

    sprintf(buf, "{s}Tagesverbrauch{m}%.2f kWh{e}", ein - dval);
    webSend(buf);
    sprintf(buf, "{s}Monatsverbrauch{m}%.2f kWh{e}", ein - mval);
    webSend(buf);
    sprintf(buf, "{s}Jahresverbrauch{m}%.2f kWh{e}", ein - yval);
    webSend(buf);
    sprintf(buf, "{s}Tageseinspeisung{m}%.2f kWh{e}", eout - dval2);
    webSend(buf);
    sprintf(buf, "{s}Monatseinspeisung{m}%.2f kWh{e}", eout - mval2);
    webSend(buf);
    sprintf(buf, "{s}Jahreseinspeisung{m}%.2f kWh{e}", eout - yval2);
    webSend(buf);
    sprintf(buf, "{s}Uptime{m}%d min{e}", tasm_uptime / 60);
    webSend(buf);
}

// ── Settings page (WebUI widgets) ──
void WebUI() {
    int page = webPage();
    if (page == 0) {
        // Settings
        webNumber(pwroffset, -100, 100, "Offset [W] Nulleinspeisung");
        webButton(do_init, "Zaehler initialisieren");
        webButton(do_save, "Daten speichern");
    }
}

// ── MQTT telemetry ──
void JsonCall() {
    float ein = smlGet(2);
    float eout = smlGet(3);
    sprintf(buf, ",\"EcoTracker\":{\"Power\":%.0f,\"EnergyIn\":%.3f,\"EnergyOut\":%.3f,\"DayIn\":%.3f,\"DayOut\":%.3f}", cpwr, ein, eout, ein - dval, eout - dval2);
    responseAppend(buf);
}

// ── Main: register endpoint ──
// Persist vars are auto-loaded from /tinyc_pvars.bin before main() runs
int main() {
    once = 0;
    last_hr = -1;
    cpwr = 0.0;
    do_init = 0;
    do_save = 0;

    // Register settings page button on Tasmota main page
    webPageLabel(0, "EcoTracker Einstellungen");

    // Register /v1/json endpoint for Marstek polling
    webOn(1, "/v1/json");

    // If no saved data, initialize from meter
    if (dval == 0.0 && smlGet(2) > 0.0) {
        initVars();
    }

    // Set timezone (same as Scripter version)
    tasmCmd("Backlog2 Timezone 99;TimeStd 0,0,10,1,3,60;TimeDst 0,0,3,1,2,120", buf);

    addLog("EcoTracker: started");
    return 0;
}