Files
glenn schrooyenandClaude 2929ece7da
infra-pin / fresh (pull_request) Successful in 1s
checks / unit (pull_request) Successful in 1h0m42s
SAFETY-125 round 2: the one-inverter maintenance charge is sized to the room too
Owner 2026-10-29: fix N=1 in this hotfix. _charge_headroom() no longer
returns early at one inverter, so the N=1 cheap-window maintenance charge
is held to charge_room_w() as at N=2; SAFETY-84's site-stop rule stays N>=2
only (at N=1 such a stop outranks the shaping anyway). The guard no longer
steps in and gives power at each quarter's end during that charge.

test_safety125.py: N=1 maintenance on 5 s and 10 s meters with late
readings, with and without a banked peak; the row in an N=2 maintenance
charge and in shadow mode; edge cases (blind past the hold, a start
mid-quarter, the summer-time change, the forecast in W, a 10 s meter, the
last second). test_safety84/87: three N=1 pins move. test_safety119: its
quarter-edge hand-back runs keep the old N=1 sizing as a fixture, plus a
check that with the new sizing nothing hands back. CHANGELOG, DECISIONS,
DOCS follow.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-29 14:57:39 +01:00

2.8 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. (SAFETY-125, 0.19.2: the owner said yes, and N=1 is sized the same way.)
  • 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).