MAINT-21: comments only - trim restated notes, add DECISIONS pointers
checks / unit (pull_request) Successful in 25m17s
infra-pin / fresh (pull_request) Successful in 1s

No code change: tools/maint21_same_ast.py and tools/maint20_same_code.py HEAD~
both print OK, and the -OO code objects are the same.

- Trims 56 comment and docstring lines (2088 -> 2032) in app/main.py and the
  new files that only restated their MAIN-orchestration.md bullet, each cut to
  its first sentence and a pointer. What the cut comments said and the bullet
  did not is now in the bullet: why a SAFETY-02 latch is restored, why the
  SAFETY-30 site check is not armed at one inverter, the SAFETY-18 re-derive,
  the MAINT-05 one-config rule, the OPT-09 sizing rules, the "no plan" row,
  the 5 400 s age bound, and the guard being the only latch repair.
- Adds the missing DECISIONS pointers on the SMA-10, SAFETY-137 and SMA-2
  comments (FALLBACK_BUDGET_S, write_setpoint, _await_inflight,
  _release_last, the retry wait, the failed-release branch, release_zero_sent,
  the in-flight write, the stop_event hand-back, the SMA-2 read and driver).
- DECISIONS/SAFETY-32: the stale `main.py:932-936` now names
  Controller.pack_floor_soc.
- main.py's docstring and import block say where the Controller's parts are.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
glenn schrooyen
2026-11-09 00:41:34 +01:00
co-authored by Claude
parent 27a867f34b
commit 902da3f723
8 changed files with 87 additions and 141 deletions
+14 -12
View File
@@ -15,12 +15,8 @@ from . import shadow
_LOG = logging.getLogger("goodwe")
# DEV-02: the per-write budget: every write is dispatched concurrently, each
# bounded by this, so one hung `hass.call()` cannot eat a sibling's layer-1
# budget. DERIVED: `heartbeat_s` max 25 s + 4 <
# `validate.FIRMWARE_INGEST_VETO_S` (30 s), graded by test_inverter.py; below
# hass.py's 10 s HTTP timeout on purpose (abandon, re-issue next tick;
# `last_write` moves only on an ACCEPTED write).
# DEV-02: the per-write budget, DERIVED: `heartbeat_s` max 25 s + 4 <
# `validate.FIRMWARE_INGEST_VETO_S` (30 s), graded by test_inverter.py.
# DECISIONS/MAIN-orchestration.md.
WRITE_BUDGET_S = 4.0
@@ -33,6 +29,7 @@ RELEASE_BUDGET_S = WRITE_BUDGET_S
# SMA-10: the one fallback write after a failed release (default 0 W). Worst
# case RELEASE_BUDGET_S + FALLBACK_BUDGET_S = 6 s, inside the Supervisor's 10 s.
# DECISIONS/SMA-10-two-idle-states.md.
FALLBACK_BUDGET_S = 2.0
@@ -345,7 +342,8 @@ class WritesMixin:
"""Put `value` on ONE inverter, or with `unit=None` on all. `unit=None`
is for a number that is not a share (stop button, `shutdown()`,
`maint_abort()`: 0 W means the same to every inverter). Writes are
concurrent, each under its own budget (DEV-02). `last`: SAFETY-137."""
concurrent, each under its own budget (DEV-02). `last`:
DECISIONS/SAFETY-137-last-write-is-zero.md."""
if unit is None:
targets = [i for i in self.inverters]
if len(targets) > 1 and is_commandable(value) and float(value) != 0.0:
@@ -375,14 +373,16 @@ class WritesMixin:
async def _await_inflight(self, inv, budget_s: float) -> None:
"""Wait, at most `budget_s`, for writes already on their way to `inv`; one
still running is left to its own WRITE_BUDGET_S. SAFETY-137."""
still running is left to its own WRITE_BUDGET_S.
DECISIONS/SAFETY-137-last-write-is-zero.md."""
pending = [t for t in inv.inflight if not t.done()]
if pending:
await asyncio.wait(pending, timeout=budget_s)
async def _release_last(self, inv) -> bool:
"""`release()` once any write in flight to `inv` has finished (at most
SETTLE_BUDGET_S): the release is the LAST thing the unit hears."""
SETTLE_BUDGET_S): the release is the LAST thing the unit hears.
DECISIONS/SMA-10-two-idle-states.md."""
await self._await_inflight(inv, SETTLE_BUDGET_S)
return await self._driver(inv).release()
@@ -397,7 +397,7 @@ class WritesMixin:
if not inv.driver.configured():
return False
# After a failure the next try waits one heartbeat; Stop and shutdown
# (`force`) always try.
# (`force`) always try. DECISIONS/SMA-10-two-idle-states.md.
now_m = time.monotonic()
if not force and now_m < inv.release_retry_at:
return False
@@ -421,6 +421,7 @@ class WritesMixin:
self.log_event(f"{inv.name}: handed back",
hint=messages.hint("handed_back", unit=inv.name))
return True
# SMA-10: a failed hand-back is tried again, and sent 0 W once below.
inv.released = False
inv.release_fails += 1
inv.release_retry_at = now_m + float(self.o.get("heartbeat_s", 10))
@@ -433,7 +434,7 @@ class WritesMixin:
hint=messages.hint("hand_back_failed", unit=inv.name))
if not inv.release_zero_sent:
# ⚠️ ONCE: a 0 W re-sent on every try would reset the inverter's own
# fallback timer for ever.
# fallback timer for ever. DECISIONS/SMA-10-two-idle-states.md.
inv.release_zero_sent = True
try:
if await _bounded(self._driver(inv).release_fallback(),
@@ -539,7 +540,8 @@ class WritesMixin:
# ACCEPTED write).
try:
_w = self._driver(inv).write(value)
# SMA-10 / SAFETY-137: kept so a release or last 0 W can wait for it.
# SMA-10 / SAFETY-137: kept so a release or last 0 W can wait for
# it. DECISIONS/SAFETY-137-last-write-is-zero.md.
_w = asyncio.ensure_future(_w)
inv.inflight.add(_w)
_w.add_done_callback(inv.inflight.discard)