deye.tc¶
deye.tc — Rolf's Deye hybrid-inverter Modbus reader (.198) + go-e wallbox
// ============================================================================
// deye.tc — Rolf's Deye hybrid-inverter Modbus reader (.198) + go-e wallbox
// ============================================================================
//
// Reads the Deye's 13 values via the SML/Modbus driver (sml_descriptor.tc) and
// publishes them in the standard SML sensor block.
//
// EXTENSION — go-e wallbox PV-surplus control (Rolf, charger @ 192.168.1.114):
// when "E-Auto laden" is checked, every 5 s push the live Deye power values to
// the charger so it does PV-surplus charging:
// GET /api/set?ids={"pGrid":<TotalGrid>,"pPv":<PV1+PV2+TotalGen>,"pAkku":<Batt>}
// pGrid = smlGet(1) Total_Grid_Power
// pPv = smlGet(8)+smlGet(9)+smlGet(10) PV1 + PV2 + Total_Gen
// pAkku = smlGet(7) Battery_Power
// Every second read /api/status?filter=nrg,wh → show charging power (nrg[11], W)
// + session energy (wh). Buttons start/stop charging via go-e force-state frc
// (2 = charge, 1 = off). The JSON ids are URL-encoded (%7B…%7D) so httpGet's URL
// parser is happy.
//
// Plus the original one-click "Deye-Descriptor wiederherstellen" (restores
// /sml_meter.def from /deye_backup.def, since the Deye descriptor is not on the
// ottelo repo).
// ============================================================================
#include "sml_descriptor.tc"
#define GOE "http://192.168.1.114"
int do_reset; // webButton → Sensor53 r
int do_restore; // webButton → restore /sml_meter.def from /deye_backup.def
char restore_buf[1024]; // own buffer (sml_buf is only 160 B)
// ── go-e wallbox state (two-level control, Rolf) ──
// goe_on = MASTER "E-Auto laden": only when set do we talk to the wallbox at
// all (read Ladeleistung/Energiemenge + show tiles + charge).
// goe_pv = "PV-Überschuss laden": ON → Eco/PV regulation (lmo=4, frc=0, push);
// OFF → normal full charge (lmo=3, frc=2).
persist watch int goe_on = 0; // master: "E-Auto laden"
persist watch int goe_pv = 0; // sub: "PV-Überschuss laden"
int goe_w; // charging power (W) = nrg[11]
int goe_wh; // session energy (Wh) = wh
char goe_url[224]; // request URL
char goe_resp[320]; // response (filtered status is small)
char goe_elem[16]; // one parsed nrg element
char goe_tile[80]; // WebCall tile scratch
int goe_tick; // 5 s assert/push counter
// Copy /deye_backup.def → /sml_meter.def, re-apply the chosen pins, reload.
void deye_restore() {
int h = fileOpen("/deye_backup.def", 0); // 0 = read
if (h < 0) { addLog("deye: /deye_backup.def missing"); return; }
int n = fileRead(h, restore_buf, 1020);
fileClose(h);
if (n <= 0) { addLog("deye: backup empty"); return; }
int w = fileOpen("/sml_meter.def", 1); // 1 = write (truncate)
if (w < 0) { addLog("deye: cannot write /sml_meter.def"); return; }
fileWrite(w, restore_buf, n);
fileClose(w);
if (sml_rx_pin >= 0 || sml_tx_pin >= 0) { // re-substitute the current pins
int fv = sml_filter ? 16 : 0;
smlApplyPins("/sml_meter.def", sml_rx_pin, sml_tx_pin, fv);
}
tasmCmd("Sensor53 r", sml_buf);
addLog("deye: descriptor restored from /deye_backup.def");
}
// ── go-e helpers ──
// Force-state: 2 = charge (start), 1 = off (stop), 0 = neutral (go-e decides).
// NB the go-e ignores `frc` inside the ids={} batch (returns {"ids":true} but
// doesn't apply it) — it must be a DIRECT query param: /api/set?frc=N.
void goe_set_frc(int v) {
sprintf(goe_url, "%s/api/set?frc=%d", GOE, v);
httpGet(goe_url, goe_resp);
addLog("goe: frc=%d", v);
}
// Logic mode: 3 = Default (chargest full, IGNORES pGrid!), 4 = Eco/PV-surplus.
// lmo=4 is what makes the go-e actually regulate the current from our pushed
// pGrid (web-confirmed + live-verified: lmo=3 drains the battery, lmo=4 throttles).
void goe_set_lmo(int v) {
sprintf(goe_url, "%s/api/set?lmo=%d", GOE, v);
httpGet(goe_url, goe_resp);
addLog("goe: lmo=%d", v);
}
// fup = "use PV surplus" (boolean). Needed together with lmo=4 + acp=true.
void goe_set_fup(int v) {
if (v) { sprintf(goe_url, "%s/api/set?fup=true", GOE); }
else { sprintf(goe_url, "%s/api/set?fup=false", GOE); }
httpGet(goe_url, goe_resp);
addLog("goe: fup=%d", v);
}
// Push live Deye power to the charger for PV-surplus: pGrid/pPv/pAkku.
// CRITICAL — battery-aware pGrid: the go-e has no own meter and regulates the
// car current to drive pGrid → 0. Sending the RAW grid power drains the house
// battery: when the battery discharges to cover the car, the grid stays ≈0, so
// the go-e thinks the surplus is perfectly used and keeps charging at full from
// the battery. Fix: treat battery DISCHARGE as grid draw, so the go-e backs off
// and the car charges ONLY from true export surplus, never from the battery.
// Deye signs: Total_Grid_Power neg=export/pos=import; Battery_Power neg=charge/pos=discharge.
// pGrid_send = grid + max(0, battery_discharge)
void goe_push() {
int pg = (int)smlGet(1); // Total_Grid_Power (neg=export)
int pb = (int)smlGet(7); // Battery_Power (neg=charge, pos=discharge)
int pp = (int)smlGet(8) + (int)smlGet(9) + (int)smlGet(10); // PV1 + PV2 + Total_Gen
int pgr = pg;
if (pb > 0) { pgr = pg + pb; } // battery discharging → count as grid draw
sprintf(goe_url, "%s/api/set?ids=%%7B%%22pGrid%%22%%3A%d%%2C%%22pPv%%22%%3A%d%%2C%%22pAkku%%22%%3A%d%%7D",
GOE, pgr, pp, pb);
httpGet(goe_url, goe_resp);
}
// Extract the want-th (0-based) comma element of the nrg[] array in goe_resp.
// goe_resp = {...,"nrg":[235,235,239,0,...,0]} — the only '['. nrg[11] = total W.
int goe_nrg(int want) {
int p = strFind(goe_resp, "[");
if (p < 0) return 0;
p = p + 1;
int idx = 0;
int start = p;
while (goe_resp[p] != 0 && goe_resp[p] != ']') {
if (goe_resp[p] == ',') {
if (idx == want) { strSub(goe_elem, goe_resp, start, p - start); return strToInt(goe_elem); }
idx = idx + 1;
start = p + 1;
}
p = p + 1;
}
if (idx == want) { strSub(goe_elem, goe_resp, start, p - start); return strToInt(goe_elem); }
return 0;
}
// Integer part of the "wh" value (current charging-session energy) in goe_resp.
// NB: (int)jsonNum(...,"wh") returns the raw float BITS, not the value (~2150 →
// 1158194312) — so parse it by hand like goe_nrg(). wh is a float in the JSON
// ("wh":2186.67); we want the whole Wh, so read up to the decimal point.
int goe_wh_val() {
int p = strFind(goe_resp, "\"wh\":");
if (p < 0) return 0;
p = p + 5; // past "wh":
int start = p;
if (goe_resp[p] == '-') { p = p + 1; }
while (goe_resp[p] >= '0' && goe_resp[p] <= '9') { p = p + 1; } // stop at '.'/','/'}'
strSub(goe_elem, goe_resp, start, p - start);
return strToInt(goe_elem);
}
// Read charging power + session energy for the display tiles.
void goe_status() {
sprintf(goe_url, "%s/api/status?filter=nrg,wh", GOE);
if (httpGet(goe_url, goe_resp) > 0) {
goe_wh = goe_wh_val(); // wh = current session energy (Wh)
goe_w = goe_nrg(11); // nrg[11] = total active power (W)
}
}
void EverySecond() {
sml_descriptor_apply(); // pins / filter / activ
if (do_reset) { tasmCmd("Sensor53 r", sml_buf); do_reset = 0; }
if (do_restore) { deye_restore(); do_restore = 0; }
// React fast to a checkbox toggle: re-assert the mode next second, and when the
// MASTER is switched OFF send one final frc=1 (stop) before going silent.
int chg = 0;
if (changed(goe_on)) { snapshot(goe_on); chg = 1; if (!goe_on) { goe_set_frc(1); } }
if (changed(goe_pv)) { snapshot(goe_pv); chg = 1; }
if (chg) { goe_tick = 5; }
// MASTER "E-Auto laden": only talk to the wallbox at all when it's set.
if (goe_on) {
goe_status(); // read Ladeleistung/Energiemenge (tiles), 1 s
goe_tick = goe_tick + 1;
if (goe_tick >= 5) { // every 5 s: assert the charging mode
goe_tick = 0;
if (goe_pv) {
// PV-surplus regulation: Eco mode + neutral + battery-aware surplus push.
// lmo=4 makes the go-e regulate the current to the true surplus (lmo=3
// would ignore pGrid + charge full + drain the battery). fup=use-surplus.
goe_set_lmo(4); goe_set_fup(1); goe_set_frc(0); goe_push();
} else {
// Normal charging: Default mode + force full charge (no surplus regulation).
goe_set_lmo(3); goe_set_frc(2);
}
}
}
}
// ── Main sensor page ──
void WebCall() {
if (!sml_activ) {
webSend("{s}Deye SML{m}aus (Rule1 off){e}");
}
if (goe_on) { // wallbox tiles only when "E-Auto laden" is on
sprintf(goe_tile, "{s}Wallbox Ladeleistung{m}%d W{e}", goe_w); webSend(goe_tile);
sprintf(goe_tile, "{s}Wallbox Energiemenge{m}%d Wh{e}", goe_wh); webSend(goe_tile);
}
}
// ── Settings page (Einstellungen menu button) ──
void WebUI() {
sml_render_settings_panel(); // SML aktiv + pins + repo + filter
webSend("<hr><b>🔌 go-e Wallbox</b>");
webCheckbox(goe_on, "E-Auto laden (Wallbox aktiv: Anzeige + Laden)");
webCheckbox(goe_pv, "└ PV-Überschuss laden (sonst normales Vollladen)");
webSend("<div class='hint'><b>E-Auto laden</b> EIN → Wallbox-Werte werden angezeigt und das Auto wird geladen (AUS → keine Kommunikation mit der Wallbox). <b>PV-Überschuss laden</b> EIN → der go-e regelt auf den echten Solar-Überschuss (Eco); AUS → normales Vollladen.</div>");
webSend("<hr><b>🔄 SML neu initialisieren</b>");
webButton(do_reset, "Sensor53 r|SML neu geladen");
webSend("<hr><b>♻ Deye-Descriptor</b>");
webSend("<div class='hint'>Falls versehentlich ein Repo-Descriptor geladen wurde: stellt /sml_meter.def aus /deye_backup.def wieder her, Pins bleiben.</div>");
webButton(do_restore, "Deye-Descriptor wiederherstellen|wiederhergestellt");
webSend("<div style='text-align:center;font-size:10px;color:#777;margin-top:10px'><b>deye.tc</b> — Rolf (+ go-e)</div></div>");
}
int main() {
if (sml_rx_pin == 0 && sml_tx_pin == 0) { // first-run: leave placeholders
sml_rx_pin = -1;
sml_tx_pin = -1;
}
if (sml_activ != tasm_rule) { tasm_rule = sml_activ; }
webPageLabel(0, "SML-Einstellungen");
smlScripterLoad("/sml_meter.def");
addLog("deye: started rx=%d tx=%d active=%d goe_on=%d goe_pv=%d", sml_rx_pin, sml_tx_pin, sml_activ, goe_on, goe_pv);
return 0;
}