Owner decision 2026-10-04 (review R1). A wrong idle reading more than about 100 W off hid every refusal on that battery. Clamped under the band, the band keeps 0 W +-25 W, so a battery reading about 0 W while refusing is caught whatever was learned or restored. Samples are still taken to 250 W. Beyond about +-170 W of idle a battery behaves as in 0.14.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6.1 KiB
SAFETY-97 - "gives nothing" is graded against each battery's idle reading
Status: implemented 2026-10-04, folded into PR #171 (SAFETY-94), for
review. Owner decision 2026-10-04: the band's centre is clamped to
+-75 W (see Bounds). Tests: test_safety97.py (rig: tools/safety94_rig.py).
Closes SAFETY-94 review finding F1.
The defect
B's power reading (board B's inverter_ac_power, 0x0518) shows -95 to -118 W
at idle: its standby draw (the GoodWe app shows 0 W). A's shows -2 to -4 W.
Every "the battery gives nothing" test was |reading| < 100 W, so a refusing
B sat on the band's edge and never latched:
- N=2: never caught, before or after SAFETY-94.
- N=1: the old one-instant check caught it in 17-105 s; SAFETY-94's shared rule (12 cycles in the band in a row) never did (review probe, 1200 s).
Design
- Offset source: learned per unit (
app/idle.py,Inverter.idle). No new option: a required one is a setup step every client gets wrong, and an optional one is a second source to reconcile. An override can be added later if a site needs one. - When it learns: control runs; the unit is not refused (unit or site),
stale or sign-tripped; its command has been within 50 W (
IDLE_ASK_W) for 120 s (IDLE_QUIET_S, 6 lags of 20 s). A refusing battery is asked more than 50 W, and while refused it does not learn, so its refusal cannot be learned as idle. A capped battery under a big ask is not sampled either. - How: the first value is the mean of 120 s of samples; then it follows
drift with a 30 min time constant. A sample beyond 250 W (
IDLE_SAMPLE_W) is dropped (a register held at a giving value). The band is centred on the value clamped to 75 W (IDLE_MAX_W; owner decision below). - Restart: saved per unit (
unit/<name>/idle_w, on first learn and each 5 W move) and restored, clamped to 250 W. Without it a battery refusing from the start is never quiet, so never learned, so never caught. - Until learned the idle reading is 0 W: the rule as it was.
Not chosen: the GoodWe LAN integration's DC battery power
Both inverters also appear through HA's GoodWe integration (B:
sensor.kelder_dyness_battery_power, 0 W at idle; found read-only on the
live HA 2026-10-04). Pointing batt_entity at it removes B's offset by
configuration, but:
- the band must be safe without it: other installs may not have it;
- it is polled over UDP on the inverter's Wi-Fi, a second channel that can drop while the board keeps reading (HW-01 lost both once); a missing reading stops the site;
- SAFETY-37's 35 s bound is derived from the board's 10 s publish; the integration's poll interval (believed 10 s, not checked here) and latency are not measured;
- DC power is not 0x0518, which SAFETY-30/31/42 were measured against.
It stays an install choice to test on the rig, not a code change here. With it, the learned idle reading is simply ~0 W.
Where it applies
Every test of "gives nothing / gives / takes" now uses reading - idle:
| check | change |
|---|---|
shared refusal evidence _gave_nothing (N=1 and N>=2) |
centred |
N=1 engage and end in compute() (idle_w, the site sum) |
centred |
N=1 end in _refusal_update |
centred |
| N>1 unit end (probe answered, takes the other way) | centred; uncentred, B's idle -100 W read as "takes 100 W" and ended a charge refusal at once |
stale-path refusal (_refusal_from_stale, N=1 and N>1) |
centred |
| SAFETY-42 "parked" (small ask, gives nothing) | centred |
| SAFETY-37 stale gate | unchanged: it grades register changes against command travel, not a level |
| SAFETY-57 release | unchanged: it reads the command only |
| SAFETY-30 per-unit sign | unchanged: opposition needs both over 250 W; B's 100 W idle cannot make or hide one alone |
| SAFETY-42 shortfall | unchanged: B reads ~100 W short at every power, which errs towards reporting (report only) |
Bounds: owner decision 2026-10-04 (review R1)
Band: |reading - centre| < 100 W, centre = idle clamped to +-75 W.
- Why a clamp under the band. A wrong idle reading (taught during a rest while the reading shows something else, e.g. a grid top-up of a unit held at 0 W, or restored onto a swapped inverter) more than about 100 W off hid every refusal on that battery, at N=1 and N=2; with the first +-250 W clamp the rig missed a refusing A whenever the error was 110-250 W. At +-75 W the band always keeps 0 W +-25 W, so a battery reading about 0 W while refusing is caught whatever was learned or restored, and a wrong idle cannot refuse a healthy 0 W battery (its ask is over 200 W).
- The trade-off. Options: keep +-250 W (covers idle readings to about +-350 W, any 100 W mis-learn hides refusals); clamp to +-75 W (chosen); "old band or centred band" (rejected: a correct B would need to give 200 W to answer its 200 W probe).
- Cost. A battery whose idle reading is beyond about +-170 W is centred at most 75 W out, so its refusal falls outside the band: it behaves as in 0.14.0 (not caught by this rule). B (-95 to -118 W) reads 20-43 W from its clamped centre. B is caught across -150..+90 W of error in its idle reading; learned 110 W or more too high it is missed, as in 0.14.0 at N=2.
- A frozen register is still caught by the stale gate at 35 s of asking.
- Recovery from a wrong value needs quiet time: tau 30 min.
Catch times (rig, refusing from 200 s after a 400 s rest, +-4 W)
| refusing | meter 1 s | 2 s | 5 s |
|---|---|---|---|
| A, N=1 | 28 s (before 28) | 28 (28) | 65 (65) |
| A, N=2 | 28 (28) | 28 (28) | 110 (110) |
| B, N=1 | 28 (never) | 28 (never) | 65 (never) |
| B, N=2 | 24 (never) | 24 (never) | 60 (never) |
A at N=2 on a 5 s meter is 110 s here because the rig's grid goes flat once A is cut, so the loop cycles on the 15 s floor; the same run on the PR before SAFETY-97 gives 110 s. B restored after a restart, refusing from the start: 24 s; with nothing saved: never.
Before -> after
- Healthy B with its offset, SAFETY-94's four scenarios, N=1 and N=2, 1/2/5 s meters, both register shapes, idle learned or restored (96 runs): 0 stale, 0 refused. Reversals, lag 10/20 s, idle restored: 0.
- Live replay (SAFETY-94 part 1): 0 trips (the gate is untouched).