SMA-10: two idle states, hold 0 W or hand the inverter back
checks / unit (pull_request) Successful in 24m9s
infra-pin / fresh (pull_request) Successful in 1s

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>
This commit is contained in:
glenn schrooyen
2026-11-07 18:21:28 +01:00
co-authored by Claude
parent 57c8d1b912
commit a1be9c48b3
12 changed files with 1720 additions and 22 deletions
+8 -1
View File
@@ -793,6 +793,7 @@ from datetime import datetime as _dt, timedelta as _td, timezone as _tz # noqa:
from app import clamp as _clamp, freshness as _fresh, peakguard as _pg # noqa: E402
from app import store as _store # noqa: E402
from app import handback # noqa: E402
from app.circulation import CirculationConfig, CirculationWatchdog # noqa: E402
from app.control import Tuning, compute # noqa: E402
from app.delivery import (DeliveryConfig, DeliveryWatchdog, UnitDelivery, # noqa: E402
@@ -850,7 +851,7 @@ DATA = {
"main.py:inv.batt_fresh.bound.release_s", "main.py:inv.batt_fresh.bound.move_w",
"main.py:inv.batt_fresh.bound.unchanged_s", "main.py:inv.batt_fresh.held",
"main.py:value", "main.py:float(value)",
"main.py:entity", "main.py:WRITE_BUDGET_S",
"main.py:entity", "main.py:WRITE_BUDGET_S", "main.py:RELEASE_BUDGET_S",
"main.py:_e",
"main.py:messages.times(self.clamp_wd.engagements)",
"main.py:messages.when(brussels(self.tariff_at))",
@@ -1058,6 +1059,12 @@ TEXT = {
"main.py:messages.reason_code(inv.batt_fresh.reason)": _coded(_stale_texts),
"main.py:rep.explain()": _migration_texts,
"store.py:rep.explain()": _migration_texts,
# SMA-10: the control-task CRITICAL line's state, for a GoodWe-only roster
# (the sentence it always was) and a mixed one.
"main.py:handback.task_died_state(handback.current())": lambda: [
handback.task_died_state(()),
handback.task_died_state((handback.Unit("inverter-a"),
handback.Unit("inverter-b", True)))],
"main.py:plan.words": _tariff_texts,
# OPT-11 P2: why an MQTT command was ignored.
"main.py:why": lambda: list(messages.PACK_REFUSED.values()),