ecotracker.tc¶
EcoTracker Emulator for Marstek battery systems (Jupiter, Venus, B2500) and Hoymiles (MS-A2)
// 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;
}