SAFETY-126 round 2: state what the 10 W tolerance costs; test the latch reset
checks / unit (pull_request) Successful in 18m11s
infra-pin / fresh (pull_request) Successful in 2s

Text only (DECISIONS, DOCS, CHANGELOG): a step load alarms on the same cycle
as before; a steadily rising load alarms a little later; with peak_margin_w 0
a period can end up to 10 W over the limit without this alarm, as for 'Load
too high' (SAFETY-81). Owner accepted 2026-10-31.

test_safety126.py part 2: after a real overrun ends, _peak_landing()'s latch
is reset and the night's later landings are quiet (review F3).

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
glenn schrooyen
2026-10-31 11:52:27 +01:00
co-authored by Claude
parent 8e2463bc32
commit a66a7f1a0f
4 changed files with 62 additions and 14 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
### Not in a release yet
- The night grid charge no longer raises "Battery stopped limiting the peak" when the 15-minute period stays on the aim ([[SAFETY-126]]).
- The night grid charge no longer raises "Battery stopped limiting the peak" when the 15-minute period stays on the aim. A steadily rising load now alarms a little later, and with a peak margin of 0 a period can end up to 10 W over the limit without this alarm ([[SAFETY-126]]).
**A patch bump: one fix for charging from the grid under the peak limit, and no option changes.**
No option was added, removed or renamed, and no default changed (86 options).
@@ -25,16 +25,21 @@ 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).
the period would end over the aim with the batteries at 0 W. A step load
alarms on the same cycle as before. SAFETY-87's case and SAFETY-81's 'Load too
high' notification are unchanged.
**What the 10 W tolerance costs** (review, owner accepted 2026-10-31):
- A steadily rising load alarms a little later, once the projected miss passes
10 W (3 000 -> 4 000 W over 45 min: 56 s later). With the default 50 W
margin the alarm still comes before the limit.
- With `peak_margin_w` 0, a period can end up to 10 W over the limit without
this alarm, the tolerance 'Load too high' already accepts (SAFETY-81).
`misses_aim()` takes the house as meter plus batteries, so an export nets
off (`decide()`'s projection counts offtake only). A re-alarm while the loop's
draw on held batteries covers the house 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
+1 -1
View File
@@ -3458,7 +3458,7 @@ Charging from the grid stays blocked meanwhile. With several inverters the
notification names the battery at its limit (SAFETY-32), each graded against
its own cut-out level. That battery is held (weight 0 while the site gives)
and the guard's need goes to the others, up to their rating; the alarm fires
only if they miss the limit at full power for `ALARM_HOLD_S` (SAFETY-87). With no battery able to give it fires at once, but only if the period would end more than 10 W over the aim with the batteries at 0 W. A grid charge the add-on runs counts as one it can stop, and the last second's landing error on the aim is not an alarm (SAFETY-126). ⚠️ **The reserve binds the peak guard only
only if they miss the limit at full power for `ALARM_HOLD_S` (SAFETY-87). With no battery able to give it fires at once, but only if the period would end more than 10 W over the aim with the batteries at 0 W. A grid charge the add-on runs counts as one it can stop, and the last second's landing error on the aim is not an alarm. A steadily rising load therefore alarms a little later. With `peak_margin_w` 0 a period can end up to 10 W over the limit without this alarm, as for “Load too high” (SAFETY-126). ⚠️ **The reserve binds the peak guard only
(SAFETY-25).** Self-consumption has no such floor. See *What `peak_soc_floor`
does not protect*.
+45 -2
View File
@@ -18,8 +18,9 @@ quarter's edge as on the rig.
red peak row, no "Why" peak, every quarter on the aim; the guard did want
power at the quarter's end (not vacuous); with the fix reverted the same
run alarms. The commands are the same with and without the fix.
2. A real overrun under the reserve still alarms, at the same cycle as
before, and names both batteries.
2. A real overrun under the reserve still alarms, a step load at the same
cycle as before, and names both batteries. After it, the latch is reset:
the night's later landings are quiet again.
3. `misses_aim()` on its own.
4. The page and the "Why" on a decision within the aim.
@@ -345,6 +346,48 @@ for tag, onset, kw in (
any(s[4] for s in head["seen"]))
def burst(t):
"""600 W, a 6 500 W load from 20 to 25 minutes, then 600 W again while
the night charge goes on."""
return 6500.0 if 1200 <= t < 1500 else 600.0
# F3 (review): `_peak_landing()` resets its latch when the guard lets go, so
# a real overrun does not keep the later landing errors alarming.
_bh = drive(opts(), house=burst, off=0.99, minutes=61)
_bb = without_fix(lambda: drive(opts(), house=burst, off=0.99,
minutes=61))
show("head real overrun, then the night again", _bh)
show("fix reverted", _bb)
# The burst banked a new month peak, which the later quarters stay under.
_new_peak = _bh["c"].peak_guard.ceiling_w
def _late(r):
"""From the next quarter on: the overrun's own quarter ends over."""
return [x for x in r["seen"] if x[0] >= 1800]
print(f" notifications at {[t for t, _n in _bh['notes']]} s; with the fix "
f"reverted {[t for t, _n in _bb['notes']]} s; limit after the burst "
f"{_new_peak:.0f} W")
check("⭐ after a real overrun ends, the latch is reset: the alarm came "
"during the overrun, and the later quarters' landings are quiet "
"(no notification, no 'Why' peak, no red row) and under the new peak",
any(1200 <= t < 1500 for t, _n in _bh["notes"])
and not [t for t, _n in _bh["notes"] if t >= 1800]
and not any(x[3] or x[4] or x[5] for x in _late(_bh))
and present(nth([v for k, v in _bh["quarters"].items()
if k >= "22:30"], 0))
and all(v <= _new_peak for k, v in _bh["quarters"].items()
if k >= "22:30"))
check("...not vacuous: the guard wanted power at those landings, and with "
"the fix reverted they show the 'Why' peak or an alarm",
any(x[2] for x in _late(_bh))
and (any(x[3] or x[4] or x[5] for x in _late(_bb))
or bool([t for t, _n in _bb["notes"] if t >= 1800])))
# ============================================================================ #
print("\n3. misses_aim() on its own")
START = datetime(2026, 10, 30, 21, 15, tzinfo=timezone.utc)