Files
goodwe-addon/goodwe_controller/DECISIONS/SMA-10-two-idle-states.md
T
glenn schrooyenandClaude 6cae5a2f8f
checks / unit (pull_request) Successful in 30m41s
infra-pin / fresh (pull_request) Successful in 2s
SMA-10 round 4: pin the Dutch default "de eigen sturing"
The NL default for a hand-back driver without words (handback._BACK_NL) was
changed to "de eigen sturing" in round 2 at the PR #239 review's request
(comment 2495: the article as in the generic clause). The brief does not name
it, so DECISIONS now says where it came from and a test checks the exact Dutch
sentence. No app code change.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-11-07 22:45:08 +01:00

3.8 KiB

SMA-10 - two idle states: hold 0 W, or hand the inverter back

Status: for review. Tests: test_sma10_two_idle_states.py. Source: SMA-9 report sections 1.4, 3.2; owner decision 2026-10-20 (Q1, "as proposed").

A unit is idle in one of two ways, and the driver says which: Driver.hands_back. False (GoodWe): the add-on holds 0 W, release() is write(0) and the Controller never calls it, so Stop keeps today's 0 W write and today's 0 W heartbeat. True: release() gives the unit back to whatever steers it by itself (SMA energy management), once, and then nothing is written to it until control starts again.

Why two. When the add-on goes away (Stop, shutdown, SIGTERM, restart, task death, a config refusal) nobody is left to hold a number, so the unit goes back to its own controller. When the add-on itself decides to stop (sign trip, site sign, circulating power, a refused or non-finite write, missing readings, another writer, an inverter fault) it has just said it cannot trust that battery, and handing it to something else would let that something do what it likes with it. Those hold 0 W through write(0) for every unit, whatever its driver. Shadow mode on a unit that hands back uses release(): it must leave the unit alone, and 0 W would be a command.

Where. Controller.command_site_release() sits beside command_site_zero() (unchanged). Stop and shutdown() call it; the two maintenance-abort paths keep zero (the add-on runs on, the sentence says 0 W for a moment). With no hand-back unit it is command_site_zero(). _write_unit() also refuses to write a hand-back unit while the add-on is idle (stopping, stopped, or shadow mode): it releases once and returns, so the heartbeat and the cycle's own "stopped" 0 W cannot reach it. A safety stop runs while control runs, so it falls through to the write. Start re-arms: released drops and last_write clears.

Bounds. RELEASE_BUDGET_S equals WRITE_BUDGET_S (4 s): units are released together, so one slow unit costs the kill window no more than a hung GoodWe write does. A release at the bound is abandoned, not awaited. A release that raises, hangs or returns false leaves that unit un-handed (the next idle write tries again) and does not touch the others. Exit 78 before the Controller exists has no driver to release and keeps today's path.

Words. Every sentence that said "set to 0 W" for the add-on going away or for shadow mode is built from the roster in app/handback.py (EN and NL): a roster with no hand-back unit gets None and keeps its old literal, so a GoodWe site sees no changed byte. A hand-back unit's words (Driver.words: back, back_nl) come from its driver; one without them gets "its own control" / "de eigen sturing" (the article as in the generic clause; PR #239 review comment 2495, not the brief). MQTT status is unchanged; the Home battery row of a handed-back unit says "Back under SMA control" and carries handed_back.

Not done here (SMA-4, SMA-5, SMA-11): any SMA/FoxESS driver, the read-back after release(), per-driver timing, DOCS.md's safety model and the "control stalled" sentence, the site-level "Held at 0 W" tiles in shadow mode on a mixed roster, and the MQTT per-unit setpoint of a handed-back unit (it still publishes 0).

After a failed release (returns False, raises or hangs past the bound) the unit gets one write(0) through Driver.release_fallback() (default write(0); a driver where 0 W is not always safe overrides it), then nothing: a repeated 0 W would keep resetting the inverter's own fallback timer, so after that the unit is left to its own fallback. The release is retried once per heartbeat; one error line and one action-log event per failure streak, one line on recovery. Worst case at shutdown is RELEASE_BUDGET_S + FALLBACK_BUDGET_S (6 s).