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
+16 -1
View File
@@ -526,7 +526,7 @@ accumulator is doing:
| `Loop integrator lead` | W | that value minus **the command it was computed against**, i.e. the command the previous cycle left standing |
| `Loop reason` | — | the law's own word for this cycle |
`Loop reason` takes one of eight values:
`Loop reason` takes one of ten values:
| value | what happened |
|---|---|
@@ -538,6 +538,21 @@ accumulator is doing:
| `saturated-freeze` | the inverter is not following, so the command's magnitude may fall but not rise |
| `freeze-released` | the freeze had run out of plant-shaped explanations and the command was let move again |
| `not-running` | control is stopped **and** shadow mode is off, so the loop did not run |
| `battery refuses` | the battery gave nothing for a minute while asked for more than 500 W, so the add-on asks 0 W in that direction (see below) |
| `battery probe` | the add-on asks 200 W to test whether a refusing battery gives power again |
**When the battery refuses.** At its cut-out level the battery gives nothing,
whatever it is asked. The add-on then asks 0 W instead of repeating its last
request, and the `Why` row reads *The battery refuses to give power: 0 W*. Every
15 minutes it asks 200 W for about half a minute; if the battery answers,
control resumes. It also resumes when the grid needs charging instead, and when
control is stopped or started. 200 W is below the 250 W the stale-reading check
needs, so a test never trips it. One inverter only: with several, the battery
level limit already moves an empty battery's share to the others.
*For installers:* SAFETY-52. The event log reads `BATTERY REFUSES …` and
`BATTERY REFUSES ENDED: …`, and the INFO line ends in `BATTERY_REFUSES`.
`DECISIONS/SAFETY-52-a-battery-that-refuses.md` has the replay figures.
⚠️ **`Loop reason` is not the `Why` row on the status page, and the two will
often disagree.** `Why` is the *arbiter's* answer in words — *Keeping the grid