Files
goodwe-addon/goodwe_controller/DECISIONS/SAFETY-78-drain-at-two.md
T
goodwe-review 044fc0d5bd
checks / unit (pull_request) Successful in 16m15s
infra-pin / fresh (pull_request) Successful in 1s
SAFETY-78 review: card truth while the drain waits, more checks
- homeview: no "This one does not charge" while the draining battery waits
  (no floor then, so the other battery may charge from solar).
- test_safety78: one wait line across a probe, a second episode logs again,
  the card while waiting, and a seeded sweep of the floor against every
  other claim (part 6).
- DECISIONS: the wait case, and the measured N=1 peak and N=2 charge
  follow-ups.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:13:31 +02:00

2.6 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 while the drain waits: no floor is filed then, so the other battery may charge from solar.
  • 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 (measured in the PR #154 review, follow-ups).

  • At N=1 a drain (60) outranks the peak floor (50), so the guard does not add to it: a 5000 W house imports 3000 W against a 2500 W limit, a 6000 W house 4000 W. Pre-existing, kept by the N=1 identity.
  • At N=2 a maintenance CHARGE in a cheap window has the same stall: the charging pack's hi = -floor against the loop asking the other to give is refused on 141 of 261 cycles. The mirror fix is a site hi, which can meet the peak floor's lo (0 W), so it is not this card.