Files
goodwe-addon/goodwe_controller/DECISIONS/SAFETY-96-dropped-battery.md
T
glenn schrooyenandClaude Opus 5.5 956711b801
checks / unit (pull_request) Successful in 31m53s
infra-pin / fresh (pull_request) Successful in 2s
SAFETY-96 review: keep the refusal count over a refused write; test a fast board
F1: resetting the count on each refused write hid a battery that really
refuses behind a link refusing 1 write in 3 (never learned; learned at
358 s with the count kept). F2: a board at 0 W before its link is stale
learned no refusal untested (mutation R7 survived); now a 1 s-lag case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 05:46:32 +02:00

3.0 KiB

SAFETY-96 - a battery the loop cannot move does not freeze it

Status: implemented 2026-10-06, for review. Tests: test_safety96.py.

The defect (SAFETY-93 rig test). At N=2 the saturation freeze compares the site target with the site sum of the readings. After A's link went stale, A was out of the split but its reading (-2000 W, still charging) was in the sum: "saturation freeze (800 W vs -1903 W)", B held at 1200 W for ~236 cycles until the circulation trip. After a charge timeout or Off at 6000 W the loop froze ~14 s at 800 W while A came out of its charge. At a drop, B also took A's whole share in one step.

The rule (main.py, _held_out_w(), _reseed_drop(), N>=2 only). A battery is held out of the freeze test, counted at its command instead of its reading, while:

  • it is out of the split (writes refused for LINK_STALE_S);
  • its last write was refused (its board may have gone to 0 W on its own watchdog);
  • it settles after a step the loop did not make: a charge end (SAFETY-93: HOLD, timeout, Off, refusal, stale link) or a rejoin (then every battery, as the split is made again). Settling ends when its reading is within the deadband of its command, or after PLANT_SETTLE_DOWN_S (23 s, the measured plant settle).

Its real power is not lost and not counted twice: the meter sees it, and the loop follows the meter. Only the freeze stops reading it as the loop's. The freeze still fires on the other batteries' own lag and on unplaced watts.

At a drop the battery's share leaves the loop's start point, so the others take over one slew step a cycle. At a rejoin it enters at its reading.

Not a drop. A missing reading or a stale register stops the site (inputs_ok), as before. A refused battery (SAFETY-73) stays in the sum: it is asked ~0 W and reads ~0 W. A watchdog-stopped battery (SAFETY-02/30) is pinned at 0 W and stays in the sum. New: no refusal is learned on a battery whose writes are refused; its board going to 0 W is no refusal. Its count is kept, not reset, so a link that refuses 1 write in 3 does not hide a battery that really refuses (review).

Before -> after (2 x 2500 W, 10 s lag, 2 s meter; freeze cycles not about the remaining battery's own lag):

case before after
A drops while giving, A holds 1200 W 128, B steps 1210 W 0, B stays
A drops while charging, A holds -2000 W 78 0 (circulation trip stays: 4 kW goes round)
A drops while charging, board to 0 W 5 0
A drops then comes back, A holds 1200 W 46, grid +1.1 kW 0, grid within 250 W
charge timeout at 6000 W 7, B held at 400 W 0, B climbs
A switched Off at 6000 W 6, B held at 800 W 0, B climbs
N=1, N=2 with no drop byte-identical

Not changed. At a rejoin the allocator splits again at once (A 0 -> 1205, B 2410 -> 1205 W); the site total does not step. A battery that keeps charging on its own after its link drops still trips the circulation check (it is real circulation). A trade end (_reseed_run_edge) still starts from the full site sum.