Files
goodwe-addon/goodwe_controller/DECISIONS/SAFETY-84-charge-at-two.md
T
glenn schrooyenandClaude Opus 5.5 ba31e7c8b1
checks / unit (pull_request) Successful in 24m17s
infra-pin / fresh (pull_request) Successful in 1s
SAFETY-86: peak guard outranks the maintenance hold
During a HOLD the holding battery gives what keeps the quarter at the peak
aim. N=1: the peak floor is refiled at P_MAINTENANCE, as for a drain
(SAFETY-83). N>=2: the hold's pin is raised to the floor minus what the
other live batteries may place, so they give first. A site stop still
decides alone. The hold ends on its clock even if the level dipped.
Below the aim nothing changes (rows identical).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 00:56:34 +02:00

2.7 KiB

SAFETY-84 - a maintenance charge at two inverters

Status: DECIDED 2026-09-30. Follows SAFETY-78's "Not changed" list. Tests: test_safety84.py; identity: tools/safety84_identity.py.

The defect. In a cheap window the charging pack files its own hi = -charge (P_MAINT_SHAPE). At N>=2 the loop's site target sat above it (a house to cover, or its 400 W slew step from 0 W), so the other pack had to give while this one charged. The allocator refuses opposite directions (SAFETY-31), so the site sat at 0 W whatever the house (tools/safety84_identity.py on 9d5b7d1: 436 of 480 cycles refused). N=1 was fine.

The fix.

  • While a pack's own limits make it charge at N>=2, main.py files a site limit charge, hi = what that pack's own hi places (allocator.hi_w(), whole W as the allocator rounds). The site target is never above it, so the other pack only adds power in the same direction. Opposite splits stay forbidden. Mirror of SAFETY-78's drain floor.
  • Peak guard first (owner order, SAFETY-83). The charge is sized to the quarter's headroom under the aim (charge_headroom_w(), cap or banked peak minus peak_margin_w), so it charges less instead of stopping: the meter sits on the aim and the guard does not need to step in. Where the house alone is over the aim the guard engages, the shaping is suspended as before (shaping_suspended()) and no charge is filed, so the guard's lo and this hi never meet in one cycle (no lo > hi, no park at 0 W). Without the headroom sizing the aim still held (the suspension did it), but a battery gave power back on 285 of 540 cycles at a 3000 W house (test_safety84.py part 2).
  • A site stop (a safety SET above P_MAINT_SHAPE) sizes the charge to 0 W: the unit hi is a hard limit, so a charge under a 0 W stop needs the other pack to give. It was refused, with an ARBITER line, on most cycles of the stop (test_safety84.py part 4). The stop still parked the site at 0 W.
  • A pack that cannot take the charge (a refusal, a full pack, a stale link) files no ceiling; the other pack follows the loop.
  • Not filed at N=1: byte-identical (tools/safety84_identity.py, which hashes every claim, in and out of a cheap window and under a banked peak).

Not changed.

  • At N=1 the charge is not sized to the headroom: the guard still suspends it and the battery gives until the quarter is safe. The same sizing would fit there; that is an owner question, not this fix.
  • Outside a cheap window (hi = 0, solar only) nothing is filed, at any N.
  • A HOLD (later fixed by SAFETY-86) and the "Battery is not keeping up" alarm (SAFETY-85) are untouched. In the identity runs that alarm's log line comes as often at N=2 as at N=1 (2 in 480 cycles).