diff --git a/goodwe_controller/CHANGELOG.md b/goodwe_controller/CHANGELOG.md index 331eb36..d0a9970 100644 --- a/goodwe_controller/CHANGELOG.md +++ b/goodwe_controller/CHANGELOG.md @@ -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). diff --git a/goodwe_controller/DECISIONS/SAFETY-126-landing-error-is-not-an-overrun.md b/goodwe_controller/DECISIONS/SAFETY-126-landing-error-is-not-an-overrun.md index a2099c4..feda5a4 100644 --- a/goodwe_controller/DECISIONS/SAFETY-126-landing-error-is-not-an-overrun.md +++ b/goodwe_controller/DECISIONS/SAFETY-126-landing-error-is-not-an-overrun.md @@ -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 diff --git a/goodwe_controller/DOCS.md b/goodwe_controller/DOCS.md index f055592..2f08c2f 100644 --- a/goodwe_controller/DOCS.md +++ b/goodwe_controller/DOCS.md @@ -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*. diff --git a/goodwe_controller/test_safety126.py b/goodwe_controller/test_safety126.py index a76f152..5d3275c 100644 --- a/goodwe_controller/test_safety126.py +++ b/goodwe_controller/test_safety126.py @@ -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)