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>
48 lines
2.8 KiB
Markdown
48 lines
2.8 KiB
Markdown
# 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).
|