Files
goodwe-addon/goodwe_controller/DECISIONS/SAFETY-78-drain-at-two.md
T
Claude 5d27f8496f
checks / unit (pull_request) Failing after 17m13s
infra-pin / fresh (pull_request) Successful in 2s
SAFETY-78: maintenance drain at N=2 no longer stalls
At two inverters the drain pins its pack and the site target is split
around the pin. Below the pin the other pack had to charge, the allocator
refused the opposite directions, and the site sat at 0 W with an ARBITER
line every cycle (house below or above the drain rate, or a grid charge).

main.py now files a site limit `drain` (lo = what the pin places, at
P_MAINTENANCE) at N>=2 while a pack drains, so the other pack only adds
power in the same direction. A drain that cannot give power waits with
one log line. With battery modes on, no grid charge while a pack drains,
and the other battery's card says why. N=1 is byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 19:52:51 +02:00

2.1 KiB

SAFETY-78 - a maintenance drain at two inverters

Status: DECIDED 2026-09-29. Fixes MAINT-05 §6 (OPT-11 P1 F1, UX-13 P3 check 5, OPT-11 P3 F4). Tests: test_safety78.py.

The defect. At N>=2 the drain pins its pack (a unit SET at P_MAINTENANCE) and the site target is split around the pin. Whenever the site target was below the pin, the other pack had to charge. The allocator refuses opposite directions (SAFETY-25 §0a, SAFETY-31), so the site went to 0 W and logged ARBITER: REFUSED every cycle. That covers a house below the drain rate, a grid-charge trade, and a house above it too: after a refusal the loop restarts from 0 W and its 400 W slew step is below the pin again.

The fix. While a pack drains at N>=2, main.py files a site limit drain, lo = what the pin really places (allocator.pin_w(): the drain rate inside that pack's own hard limits), at P_MAINTENANCE. So the site target is never below the pin and the other pack only adds power in the same direction. Opposite splits stay forbidden.

  • The order is the arbiter's own: a safety SET (100) outranks the limit and the pin, so everything parks; the peak floor (50) and the drain floor are both applied, so the higher wins; a trade or the loop below the drain is lifted to it.
  • Below the drain rate the drained pack exports the rest, as the drain does at N=1 (maintenance.py: "forced discharge, exports the surplus"). Holding it to the house load would make N=2 drain differently from N=1; that is an owner question, not this fix.
  • A draining pack that can place nothing (a refusal, a stale link) adds no floor, and the wait is logged once per episode.
  • With battery modes on, no pack is a grid charger while another drains (modetrade.decide(draining=), why cycle), and the other battery's card says why (other_cycle).
  • Not filed at N=1, where the drain is the site SET itself: byte-identical (tools/safety78_identity.py, which hashes the claims too).

Not changed. At N=1 a drain (60) still outranks the peak floor (50), so the peak guard does not add to a drain there. That is pre-existing and kept by the N=1 identity.