SAFETY-02: a loop can be fresh, tracking and inside its rating and still push the wrong way
The stale-input watchdog asks whether the meter is still talking. The saturation freeze asks whether the inverter can follow. The output clamp asks whether the number is too big. A loop can pass all three while charging the battery out of the grid, because the only thing wrong is the sign. On 2026-08-20 a commercial controller did exactly that four times in one day, for up to 77 minutes at a stretch, reaching a commanded -14 768 W against a ±5000 W inverter. app/watchdog.py compares the emitted setpoint against the authoritative P1 figure and, when they oppose each other with both over 300 W for 180 s continuously, files a failsafe SET claim at P_SAFETY, commands 0 W and latches there until an operator starts control again. Validated against that day rather than against an argument: sim/replay_sign.py in the infra repo drives this module over the committed captures of both 2026-08-20 and the healthy 2026-08-23 and reaches 18 trips on the fault day - every one inside a window NOTES.md §4.7 accounts for - and 0 on the healthy day. The four episodes do share one signature: 0.3 / 0.9 / 10.5 / 0.0 % sign agreement inside them against 92.9 % healthy. Three decisions the ticket could not make: - It is DISARMED whenever the grid-following loop is not what is driving. Sign agreement is a property of a loop that follows the meter, and a maintenance grid charge opposes the meter by design - 2500 W of charge into a house drawing 500 W is +3000 W on the meter against a -2500 W command, for hours. The gate tests the emitted number, not the winning claim's name, because a limit claim can let the loop "win" and still replace its value. - The trip LATCHES. Commanding 0 W removes the command side of the comparison, so the disagreement always clears the moment the trip works. - sign_disagree_w at or above max_w is refused at startup. The arbiter never lets a command exceed max_w, so such a threshold leaves the watchdog incapable of firing while the status page still reports it armed. What it does not catch, said out loud: an inverter that ACTS opposite to its command moves the meter further the same way, leaving command and meter in agreement. That is the saturation freeze's job. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Du77usMj8XNKNFZGmUiWDa
This commit is contained in:
co-authored by
Claude Opus 5
parent
35856a73ae
commit
9aed43990b
@@ -1,5 +1,58 @@
|
||||
# Changelog
|
||||
|
||||
## 0.5.0
|
||||
|
||||
**SAFETY-02 — the sign-disagreement watchdog.** The add-on now watches whether
|
||||
the setpoint it is emitting and the grid figure it is regulating on point the
|
||||
same way. If they oppose each other, both over `sign_disagree_w` (300 W), for
|
||||
`sign_disagree_s` (180 s) **continuously**, it files a failsafe claim at
|
||||
`P_SAFETY`, commands **0 W**, and latches there until an operator starts control
|
||||
again. New module `app/watchdog.py`; new options `sign_disagree_w` and
|
||||
`sign_disagree_s`.
|
||||
|
||||
This is the gap the existing failsafes leave open. The stale-input watchdog asks
|
||||
whether the meter is still talking, the saturation freeze asks whether the
|
||||
inverter can follow, the output clamp asks whether the number is too big — a
|
||||
loop can pass all three while charging the battery out of the grid, because the
|
||||
only thing wrong is the sign. On 2026-08-20 a commercial controller did exactly
|
||||
that four times in one day, for up to 77 minutes at a stretch, and reached a
|
||||
commanded −14 768 W against a ±5000 W inverter.
|
||||
|
||||
Validated against that day. `sim/replay_sign.py` in the infra repo drives this
|
||||
module over the committed RS485 captures of both 2026-08-20 (the four episodes)
|
||||
and 2026-08-23 (healthy): **18 trips on the fault day, every one inside a window
|
||||
`NOTES.md` §4.7 accounts for, and 0 on the healthy day.** The four episodes do
|
||||
share one signature rather than merely resembling each other — sign agreement
|
||||
runs 0.3 / 0.9 / 10.5 / 0.0 % inside them against 92.9 % on the healthy day.
|
||||
|
||||
Three decisions worth knowing about:
|
||||
|
||||
- **It is disarmed whenever the grid-following loop is not what is driving.**
|
||||
Sign agreement is a property of a loop that follows the meter. A maintenance
|
||||
grid charge opposes the meter *by design* — 2500 W of charge into a house
|
||||
drawing 500 W is +3000 W on the meter against a −2500 W command, for hours —
|
||||
and armed unconditionally this would trip on every maintenance cycle. The gate
|
||||
tests the emitted number, not the winning claim's name: a limit claim can let
|
||||
the loop "win" and still replace its value with one pointing the other way.
|
||||
- **The trip latches.** Commanding 0 W removes the command side of the
|
||||
comparison, so the disagreement always clears the moment the trip works.
|
||||
Un-latched, this would trip, clear, and hand the actuator back to the loop that
|
||||
was driving the wrong way, every three minutes forever.
|
||||
- **`sign_disagree_w` at or above `max_w` is refused at startup** (validator rule
|
||||
5). The arbiter never lets a command exceed `max_w`, so such a threshold makes
|
||||
the watchdog incapable of firing while the status page still reports it armed.
|
||||
A disarmed guard that advertises itself as armed is the failure mode this
|
||||
project keeps refusing to ship.
|
||||
|
||||
What it does **not** catch, stated so nobody assumes otherwise: an inverter that
|
||||
acts opposite to what it was told moves the meter *further* the same way, so
|
||||
command and meter agree while everything runs away. That is the saturation
|
||||
freeze's job, which compares the command against the battery's actual power.
|
||||
|
||||
`test_validate.py`'s "the smallest sane max_w still starts" case now names a
|
||||
reachable sign threshold, because a 1 W ceiling genuinely does disarm the 300 W
|
||||
default and is now correctly refused.
|
||||
|
||||
## 0.4.0
|
||||
|
||||
**CFG-01.** The add-on now refuses to start on a configuration it cannot honour,
|
||||
|
||||
Reference in New Issue
Block a user