SAFETY-126: a landing error on the aim is not an overrun

During the night grid charge under the peak reserve the guard alarmed
(ERROR PEAK_GUARD_FLOOR_LIMITED, "Battery stopped limiting the peak") and the
Batteries "Why" said "The peak limit needs power" at quarter ends, while every
quarter landed on the aim. The charge lands the period 0-7 W over the aim and
excess_w()'s 900 / remaining turns that into tens of kW in the last second;
with nothing able to give, check_peak_floor_limited() alarmed on any
wanted > 0.

Each cycle the guard wants power, _peak_landing() asks misses_aim(): with the
batteries at 0 W, does the period end more than ALARM_MISS_W over the aim? If
not, the decision is within_aim: no reserve alarm, no red peak row, no "Why"
peak. The claim and the commands are unchanged. A real overrun under the
reserve alarms on the same cycle as before.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
glenn schrooyen
2026-10-31 01:26:02 +01:00
co-authored by Claude
parent cc9bbeb73e
commit 40595e42ae
8 changed files with 526 additions and 8 deletions
@@ -0,0 +1,42 @@
# SAFETY-126 - a landing error on the aim is not an overrun
**Status: FIXED on release/1.0 (after 0.19.2).** Tests: `test_safety126.py`.
**Cause.** A grid charge under the peak limit is sized to land the 15-minute
period on the aim (SAFETY-125), so the period ends 0-7 W over it. In the last
second `peakguard.excess_w()` multiplies that by 900 / remaining (3 000 at
0.3 s left, 90 000 at 0.01 s), so `decide()` wants tens of kW
(`wanted > 0`, peakguard.py `decide()`). With every battery under the peak
reserve that is floor-limited at 0 W, and `check_peak_floor_limited()`
alarmed on it at once: SAFETY-87 graded only the case where another battery
could give. With one battery able, the guard asked it for its rating for that
cycle and the Batteries "Why" said "The peak limit needs power". The
add-on's own charge is not the cause: `wanted` already counts it as
stoppable.
**Rule.** Each cycle the guard wants power, `Controller._peak_landing()`
asks `peakguard.misses_aim()`: with the batteries at 0 W from now on, does
the period end more than `ALARM_MISS_W` (10 W) over the aim? This is
`load_too_high()` with nothing to give, without `decide()`'s gain. If not, and
no real miss was seen since the guard took hold, the decision is
`within_aim`. Such a decision raises no reserve alarm (ERROR,
notification, red row), no red "Load too high" row, no "Why" peak and
no "Battery stopped limiting the peak" event hint. The claim, the floor and the
commands are unchanged.
**Still alarms.** House load the batteries cannot cover under the reserve:
the period would end over the aim with the batteries at 0 W, so the alarm
fires on the same cycle as before (both under the reserve; a 6 500 W house
during the night charge). SAFETY-87's case (others able, graded on the limit
for `ALARM_HOLD_S`) and SAFETY-81's 'Load too high' notification are unchanged.
A period heading at most 10 W over the aim no longer alarms with nothing able
to give. That is SAFETY-81's landing tolerance. `misses_aim()` takes the house
as meter plus batteries, so an export nets off. `decide()`'s projection counts
offtake only. So a re-alarm while the loop's draw on held batteries covers the
house with some export, and the period lands under the aim, is gone too
(test_safety126.py part 2, modes off: 2 notifications, was 4).
**Not changed.** The one-cycle floor itself: at a period's last instant the
guard still asks an able battery for its rating, or stops a charge, for a
landing error. That is control, outside the alarm path. Raise a card if it
should go.