SAFETY-81 review: 10 s hold, ERROR line per episode, more scenarios
checks / unit (pull_request) Successful in 20m31s
infra-pin / fresh (pull_request) Successful in 2s

- The alarm must hold for peakguard.ALARM_HOLD_S (10 s) on the projection's
  clock. A load that tops cap + battery for ~5 s every 30 s alarmed at every
  quarter start while the quarter landed under the aim (sine4500_fast).
- A cycle with no battery reading leaves the hold alone.
- Notified once per quarter, but the ERROR line is written per episode.
- New sim scenarios (sine4500_fast, twice7000, step6500_gap) with 5a4cdc2
  digests; control still byte-identical. Unit checks for export and the
  hold across a missing reading. test_peakguard's saturated fixture moves
  the hold start back instead of waiting it out.
- DECISIONS and DOCS updated: costs of the hold, tolerance reasoning, the
  double count in control left to a follow-up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-29 20:58:37 +02:00
co-authored by Claude Opus 5.5
parent 6eb67c0f9c
commit f967073cfe
7 changed files with 170 additions and 47 deletions
+7
View File
@@ -1259,6 +1259,13 @@ check(f"the saturating fixture ({NET2:.0f} W) is a load the real ingest "
f"accepts, not one it rejects as implausible", NET2 < PLAUSIBLE_MAX_W)
print(f" saturated case: {c2.peak_guard.explain()}")
check("a load larger than the inverter saturates the guard", c2.peak_guard.saturated)
# SAFETY-81 review: the alarm must hold ALARM_HOLD_S first. The fixture runs on
# the wall clock, so the start of the hold is moved back instead of waited out.
check("...but not on its first cycle: it must hold peakguard.ALARM_HOLD_S "
"(SAFETY-81)", hass2.notifications.count("goodwe_peak_saturated") == 0
and c2._peak_high_since is not None)
c2._peak_high_since -= timedelta(seconds=peakguard.ALARM_HOLD_S)
asyncio.get_event_loop_policy().new_event_loop().run_until_complete(c2.cycle())
check("...and THAT raises exactly one notification, with its own id - the one "
"state where the money will be spent and a human is the only remaining "
"actuator", hass2.notifications.count("goodwe_peak_saturated") == 1)