When release() fails (False, raises or hangs past RELEASE_BUDGET_S) the unit
gets one bounded write(0) through the new Driver.release_fallback() hook
(default write(0)), then nothing more, so the inverter's own fallback timer is
not reset. The release is retried once per heartbeat, not on every idle
write. One error line and one action-log event per failure streak, one line on
recovery. Dutch default "terug naar de eigen sturing". DECISIONS says what a
unit is left at.
test_messages.py: the RELEASE_BUDGET_S data entry is gone again (the line
that needed it is gone).
Co-Authored-By: Claude <noreply@anthropic.com>
A driver now says whether its release() hands the inverter back
(Driver.hands_back; GoodWe is False, so release() stays write(0) and the
Controller never calls it). command_site_release() sits next to
command_site_zero(): the Stop button and shutdown() (so SIGTERM, restart,
control task death and a late config refusal) hand a hand-back unit back,
once, and zero every other unit as before. Safety stops and refusals still
hold 0 W for every unit. Shadow mode releases a hand-back unit. After a
release nothing is written to the unit (heartbeat and cycle included) until
control starts again. release() is bounded by RELEASE_BUDGET_S (= the write
budget), runs concurrently per unit, and a raising or hanging release on one
unit does not stop the others.
The "set to 0 W" sentences for Stop, restart, shadow mode and the stopped
banner are built from the roster (app/handback.py, EN and NL, words from the
driver); a GoodWe-only roster keeps every old literal. A handed-back unit
shows "Back under <its control>" on Home.
Existing test edited: test_messages.py (two classification entries for the
CRITICAL line that moved into a roster builder, and the new release bound).
Co-Authored-By: Claude <noreply@anthropic.com>
app/drivers.py holds one Driver per roster entry (configured, actuator_id,
limits, read, write, release, probe, link_stale_s, batt_bound, settle_s, brand,
words). GoodWeEsphomeDriver makes the calls the code it replaced made, in the
same order. Inverter gets driver; has_writer, is_live and link_note read it. The
SoC/power read, the write, the device range (run_control, amain, the settings
panel), rule 2 per unit and the SAFETY-28 census go through it.
setpoint_entity is optional per entry beside driver, host, port, unit_id and
maintenance; only goodwe_esphome is selectable, the rest are checked and unused.
Co-Authored-By: Claude <noreply@anthropic.com>