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 ### 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.** **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). 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. commands are unchanged.
**Still alarms.** House load the batteries cannot cover under the reserve: **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 the period would end over the aim with the batteries at 0 W. A step load
fires on the same cycle as before (both under the reserve; a 6 500 W house alarms on the same cycle as before. SAFETY-87's case and SAFETY-81's 'Load too
during the night charge). SAFETY-87's case (others able, graded on the limit high' notification are unchanged.
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 **What the 10 W tolerance costs** (review, owner accepted 2026-10-31):
to give. That is SAFETY-81's landing tolerance. `misses_aim()` takes the house - A steadily rising load alarms a little later, once the projected miss passes
as meter plus batteries, so an export nets off. `decide()`'s projection counts 10 W (3 000 -> 4 000 W over 45 min: 56 s later). With the default 50 W
offtake only. So a re-alarm while the loop's draw on held batteries covers the margin the alarm still comes before the limit.
house with some export, and the period lands under the aim, is gone too - With `peak_margin_w` 0, a period can end up to 10 W over the limit without
(test_safety126.py part 2, modes off: 2 notifications, was 4). 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 **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 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 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) 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 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` (SAFETY-25).** Self-consumption has no such floor. See *What `peak_soc_floor`
does not protect*. 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 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 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. 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 2. A real overrun under the reserve still alarms, a step load at the same
before, and names both batteries. 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. 3. `misses_aim()` on its own.
4. The page and the "Why" on a decision within the aim. 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"])) 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") print("\n3. misses_aim() on its own")
START = datetime(2026, 10, 30, 21, 15, tzinfo=timezone.utc) START = datetime(2026, 10, 30, 21, 15, tzinfo=timezone.utc)