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>
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).