SAFETY-52: a battery that refuses is asked for 0 W, with a 200 W probe every 15 min
checks / unit (pull_request) Failing after 9m32s
infra-pin / fresh (pull_request) Successful in 1s

At the BMS cut-out the saturation freeze held the last request for hours
(+5000 W for 11.87 h on 09-06, +1940 W for 3 h 55 m on 09-19) until the grid
error changed sign. compute() now carries a refused direction: after
SETTLE_CYCLES of a ~0 W reading against a larger ask it holds that direction
at 0 W, probes 200 W (below the gate's 250 W) every 15 minutes, and lets go
on charge, an answered probe, stop/start or another writer. A stale-reading
trip on a frozen ~0 W reading seeds the same hold, which ends SAFETY-59's
~70 s cycle. One inverter only; no SoC in the loop (SAFETY-25 Option A).

test_refusal.py drives the real Controller and gate; tools/replay_refusal.py
replays the 16-day capture. test_clamp.py and test_delivery.py re-baselined
where they asserted the old hold. DECISIONS/SAFETY-52 has the record and what
SAFETY-58/59 must take into account.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
glenn schrooyen
2026-09-27 16:43:27 +02:00
co-authored by Claude Opus 5.5
parent d1f7e3336e
commit 1f97c7652f
10 changed files with 756 additions and 6 deletions
+4 -1
View File
@@ -1158,7 +1158,10 @@ async def _one():
c.inverters[0].dev_min, c.inverters[0].dev_max = -2000.0, 2000.0
c.auto = True
hass.ctrl = c
await _drive(c, n=20, dt=0.1)
# SAFETY-52: from SETTLE_CYCLES of delivering nothing the loop asks 0 W
# (the battery refuses), so the run stops just before that. 11 cycles is
# still 1.1 s against the 1 s hold.
await _drive(c, n=11, dt=0.1)
return c, hass, store