growatt.tc¶
growatt.tc — gemu's Growatt-inverter Modbus reader + feed-in control
// ============================================================================
// growatt.tc — gemu's Growatt-inverter Modbus reader + feed-in control
// (growatt_garten, .63)
// ============================================================================
//
// Port of growatt_garten.tas. Two parts:
// 1. SML/Modbus reading — the GRW descriptor + pin management via
// sml_descriptor.tc (same pattern as deye.tc). 7 Growatt values publish
// in the standard SML sensor block.
// 2. Feed-in control — write the Growatt active-power-rate register 0x0003:
// • Manual limit slider (gl, %): on change → reg 0x0003 = gl directly
// (0..100 %; 100 % = full feed-in, now permitted by German law).
// • Auto-throttle (Scripter #setlimit): when the house meter (zwzc)
// reports a grid feed-in above `limit`, drop the inverter to
// (mainsw - excess)/42 %. Edge-triggered, armed by the g_auto
// checkbox (a kill switch for autonomous inverter writes). Every
// write is capped at mproz % (inverter safety ceiling).
//
// The Modbus write telegram "r0106000300<proz>" is byte-identical to the
// Scripter sml(1 3 "r0106000300"+hn(proz)): the SML driver sees the 'r' raw
// prefix on an 'm' meter → appends only the Modbus-CRC16 (no read-count).
//
// AUTO-THROTTLE IS DORMANT while the (new) house meter reports zwzc=0: the
// overflow branch (zwzc<0 && |zwzc|>limit) can never be reached, so no
// autonomous write happens until the meter starts exporting zwzc again.
//
// The GRW descriptor is custom (NOT on the ottelo repo), so /growatt_backup.def
// holds a copy and the restore button rebuilds /sml_meter.def from it.
// ============================================================================
#include "sml_descriptor.tc"
int do_reset; // webButton → Sensor53 r
int do_restore; // webButton → restore /sml_meter.def from /growatt_backup.def
char restore_buf[1024]; // own buffer (sml_buf is only 160 B)
// ── House meter (ZRZ / Zweirichtungszähler) — Scripter g:zwz* via UDP-multicast.
// `global` = the direct TinyC equivalent of Scripter `g:`: auto-updates by NAME
// from the house meter's broadcasts. READ-ONLY here (never assigned → never
// re-broadcast, so we don't fight the meter for ownership of the value).
global float zwzc; // current grid power (W): - = feed-in, + = consumption
global float zwzi; // total consumption (kWh)
global float zwzo; // total feed-in (kWh)
float limit = 7250.0; // grid feed-in limit (W) — Scripter's `limit`
char zbuf[200]; // ZRZ web-tile scratch
// ── Growatt feed-in control ────────────────────────────────────────────────
persist watch int gl = 100; // manual limit slider (%), 0..100 → written straight to reg 0x0003
persist watch int g_auto = 0; // auto-throttle armed (1) / off (0). OFF by default: German law now
// allows 100 % feed-in, so throttling is no longer required — we
// never reduce the inverter autonomously unless gemu arms this.
int mproz = 87; // hard % ceiling for register 0x0003 (inverter safety max)
int g_ovf; // current over-limit feed-in state (Scripter `ovf`)
int g_ovf_prev; // edge detector for g_ovf (Scripter `chg[ovf]`)
int g_last_proz = -1; // last % written to reg 0x0003 (for tile/log; -1 = none yet)
char gcmd[40]; // Modbus write-telegram scratch
// ── Phase 3: UDP global-array broadcast (Scripter `acp(xsml sml); gvrsa(xsml)`) ──
// Re-broadcasts the GRW readings as the multicast float array "xsml" (8 slots:
// [mainsv,mainsc,mainsw,s1w,s2w,limit,tot,spare]) so the house devices receive
// them by name. udpSendArray's wire format (=>xsml:[2-byte LE count][N×float])
// is byte-identical to Scripter gvrsa, and smlGet(i) mirrors Scripter sml[i].
persist watch int g_send = 1; // broadcast on/off (0/1)
float gxml[8]; // the 8-float array (mirrors Scripter M:xsml=0 8)
int g_udp_tick; // 1 Hz divider → 2 s cadence (Scripter `upsecs%2==0`)
int g_udp_cnt; // packets sent (tile counter)
// Write Growatt active-power-rate register 0x0003 = proz % (clamped 0..mproz).
// Telegram: r 01 06 0003 00 <proz>; driver appends Modbus-CRC16 (lo,hi).
void growatt_set_proz(int proz, int isauto) {
if (proz > 100) proz = 100; // hard register bound; the auto path pre-caps at mproz
if (proz < 0) proz = 0;
sprintf(gcmd, "r0106000300%02X", proz);
int rc = smlWrite(1, gcmd);
g_last_proz = proz;
if (isauto) { addLog("growatt AUTO: set %d%% (reg 0x0003) cmd=%s rc=%d", proz, gcmd, rc); }
else { addLog("growatt: limit -> %d%% (reg 0x0003) cmd=%s rc=%d", proz, gcmd, rc); }
}
// Scripter #setlimit: throttle so total grid feed-in returns to `limit`.
// over = |zwzc| - limit (W above the feed-in limit)
// pcurr = mainsw - over (target inverter output, W)
// proz = pcurr / 42 (capped) (active-power-rate %)
void growatt_setlimit() {
float over = (-zwzc) - limit;
float mainsw = smlGet(3); // current Growatt output (W) = Scripter sml[3]
float pcurr = mainsw - over;
if (pcurr > 0.0) {
int proz = (int)(pcurr / 42.0);
if (proz > mproz) { proz = mproz; }
if (g_auto) {
growatt_set_proz(proz, 1);
} else {
addLog("growatt AUTO(off): would set %d%% (zwzc=%.0f over=%.0f mainsw=%.0f)", proz, zwzc, over, mainsw);
}
}
}
// Copy /growatt_backup.def → /sml_meter.def, re-apply the chosen pins, reload.
void growatt_restore() {
int h = fileOpen("/growatt_backup.def", 0); // 0 = read
if (h < 0) { addLog("growatt: /growatt_backup.def missing"); return; }
int n = fileRead(h, restore_buf, 1020);
fileClose(h);
if (n <= 0) { addLog("growatt: backup empty"); return; }
int w = fileOpen("/sml_meter.def", 1); // 1 = write (truncate)
if (w < 0) { addLog("growatt: 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("growatt: descriptor restored from /growatt_backup.def");
}
void EverySecond() {
sml_descriptor_apply(); // pins / filter / activ
if (do_reset) { tasmCmd("Sensor53 r", sml_buf); do_reset = 0; }
if (do_restore) { growatt_restore(); do_restore = 0; }
// ── Manual limit slider → write register 0x0003 (Scripter `if chg[gl]>0`) ──
// gl is already a percent (0..100) → straight to the active-power-rate register.
if (changed(gl)) {
snapshot(gl);
growatt_set_proz(gl, 0);
saveVars(); // persist the new limit across reboot
}
// Persist the auto-throttle arm + UDP-broadcast toggles when they change.
if (changed(g_auto)) { snapshot(g_auto); saveVars(); }
if (changed(g_send)) { snapshot(g_send); saveVars(); }
// ── Phase 3: broadcast the GRW readings as UDP global array "xsml" every 2 s ──
// (Scripter `if upsecs%2==0 { acp(xsml sml); gvrsa(xsml) }`)
if (g_send && sml_activ) {
g_udp_tick = g_udp_tick + 1;
if (g_udp_tick >= 2) {
g_udp_tick = 0;
smlCopy(gxml, 8); // = Scripter acp(xsml sml): gxml[0..6]=SML vals 1..7
gxml[7] = 0.0; // spare (xsml[7]); smlCopy fills only the 7 GRW values
udpSendArray("xsml", gxml, 8);
g_udp_cnt = g_udp_cnt + 1;
}
}
// ── Auto-throttle overflow detection (Scripter >S lines 40-60) ──
// DORMANT while zwzc==0: the (-zwzc)>limit branch can never be reached.
if (zwzc < 0.0 && (-zwzc) > limit) { g_ovf = 1; } else { g_ovf = 0; }
if (g_ovf != g_ovf_prev) { // Scripter `if chg[ovf]>0`
g_ovf_prev = g_ovf;
if (g_ovf != 0) { growatt_setlimit(); } // just entered over-limit → #setlimit
}
}
// ── Main sensor page ──
// The SML driver publishes all 7 Growatt values in its own sensor block. Here
// we surface the off-state, the house-meter (ZRZ) tiles, and the current limit.
void WebCall() {
if (!sml_activ) {
webSend("{s}Growatt SML{m}aus (Rule1 off){e}");
return;
}
// ── House-meter (ZRZ) tiles, from the UDP globals (Scripter >W section) ──
sprintf(zbuf, "{s}ZRZ Verbrauch total{m}<b style='color:#c0392b'>%.1f kWh</b>{e}", zwzi);
webSend(zbuf);
sprintf(zbuf, "{s}ZRZ Einspeisung total{m}<b style='color:#27ae60'>%.1f kWh</b>{e}", zwzo);
webSend(zbuf);
if (zwzc < 0.0 && -zwzc > limit) { // feeding in over the limit → yellow
sprintf(zbuf, "{s}ZRZ aktuell{m}<b style='color:#e0a800'>%.0f W (Grenze!)</b>{e}", zwzc);
} else if (zwzc < 0.0) { // feeding in → green
sprintf(zbuf, "{s}ZRZ aktuell{m}<b style='color:#27ae60'>%.0f W</b>{e}", zwzc);
} else { // consuming → red
sprintf(zbuf, "{s}ZRZ aktuell{m}<b style='color:#c0392b'>%.0f W</b>{e}", zwzc);
}
webSend(zbuf);
// ── Growatt limit setpoint + auto-throttle state ──
sprintf(zbuf, "{s}Growatt Limit (Soll){m}<b>%d %%</b>{e}", gl);
webSend(zbuf);
if (g_auto) {
webSend("{s}Auto-Drosselung{m}<b style='color:#27ae60'>aktiv</b>{e}");
} else {
webSend("{s}Auto-Drosselung{m}<b style='color:#888'>aus</b>{e}");
}
if (g_send) {
sprintf(zbuf, "{s}UDP xsml{m}<b style='color:#27ae60'>aktiv (%d Pakete)</b>{e}", g_udp_cnt);
webSend(zbuf);
} else {
webSend("{s}UDP xsml{m}<b style='color:#888'>aus</b>{e}");
}
}
// ── Settings page (SML-Einstellungen menu button) ──
void WebUI() {
sml_render_settings_panel(); // SML aktiv + pins + repo + filter
webSend("<hr><b>⚙️ Growatt-Drosselung</b>");
webSlider(gl, 0, 100, "Growatt Limit (%)");
webCheckbox(g_auto, "Auto-Drosselung bei Netz-Einspeisung");
webCheckbox(g_send, "GRW-Werte als UDP-Array \"xsml\" senden (alle 2s)");
webSend("<div class='hint'>Schiebt das Limit (%) sofort in Register 0x0003 (Wirkleistungsbegrenzung). 100 % = volle Einspeisung (gesetzlich erlaubt). Die Auto-Drosselung (standardmässig aus) greift nur, wenn der Hauszähler eine Einspeisung über der Grenze meldet (zwzc).</div>");
webSend("<hr><b>🔄 SML neu initialisieren</b>");
webButton(do_reset, "Sensor53 r|SML neu geladen");
webSend("<hr><b>♻ Growatt-Descriptor</b>");
webSend("<div class='hint'>Falls versehentlich ein Repo-Descriptor geladen wurde: stellt /sml_meter.def aus /growatt_backup.def wieder her, Pins bleiben.</div>");
webButton(do_restore, "Growatt-Descriptor wiederherstellen|wiederhergestellt");
webSend("<div style='text-align:center;font-size:10px;color:#777;margin-top:10px'><b>growatt.tc</b> — growatt_garten</div></div>");
}
int main() {
if (sml_rx_pin == 0 && sml_tx_pin == 0) { // first-run (fresh deploy: persist reset to 0)
sml_rx_pin = -1;
sml_tx_pin = -1;
gl = 100; // default the limit to full power (law-compliant)
g_send = 1; // default the UDP "xsml" broadcast on
}
if (sml_activ != tasm_rule) { tasm_rule = sml_activ; }
webPageLabel(0, "SML-Einstellungen");
smlScripterLoad("/sml_meter.def");
addLog("growatt: started rx=%d tx=%d active=%d gl=%d auto=%d", sml_rx_pin, sml_tx_pin, sml_activ, gl, g_auto);
return 0;
}