Files
goodwe-addon/goodwe_controller/DECISIONS/SAFETY-128-late-readings.md
T
glenn schrooyenandClaude 09e1fbb898
checks / unit (pull_request) Successful in 19m36s
infra-pin / fresh (pull_request) Successful in 1s
SAFETY-128: control does not act on late Home Assistant readings
Readings that Home Assistant delivers late but still changing passed
every staleness gate, so the loop corrected again each cycle an error
its last command had already fixed (10 s late: the give run swung and
exported to -1.9 kW), and the peak guard pushed to the inverter limit
on the same late figures (OPT-34 F-2).

app/lateness.py fits how long after a power request the meter shows
it. Over 8 s the readings are late: a request the battery reading has
not answered holds the loop (it may not lead the reading by more than
100 W) and caps the peak guard at what stands; unanswered for 23 s it
holds, then 0 W after stale_input_s. An amber row and one log line per
episode say so. With readings on time nothing changes (digests equal).

DECISIONS/SAFETY-128-late-readings.md; test_safety128.py.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
2026-11-01 23:45:03 +01:00

4.8 KiB

SAFETY-128 - a reading that does not answer the last command is not current

Status: FIXED on release/1.0 (after 0.20.0). Code: app/lateness.py, control.compute(hold=, hold_w=), peakguard.cap_floor(), Controller._readings_late(). Tests: test_safety128.py (tools/safety128_rig.py, TEST-23's plant).

Cause (SAFETY-127 report). Home Assistant can deliver readings late but still changing. Every gate judges arrival (SAFETY-01) or a value that stops changing (SAFETY-18, SAFETY-37), so a late reading passes all of them. The loop then corrects, each cycle, an error its last command has already fixed: a loop with a dead time far longer than its cycle, which swings and grows. The peak guard projects the quarter from the same late figures and pushes to the inverter limit in its last seconds (OPT-34 rig F-2).

Rule.

  1. Find late readings. Delay fits, every 4 s over the last 40 s of new meter values, how long after a power request the meter shows it: the meter is the house less the batteries, so meter(t) + request(t - d) is flattest at the right d. Over DELAY_S the readings are late (late_seen). The fit counts only if the requests explain half the meter's swing; a late d only if it fits 30 % better than any fresh one and better than a slow battery answering at once (10 or 20 s lag, the SAFETY-85/94 rigs). A silent meter adds no values (SAFETY-99/125). A battery that gives nothing under its ask, or refuses, spoils the fit and is left out for 40 s. With no fit for 120 s (a steady site shows nothing) the verdict is dropped. Nothing below acts unless late_seen, so with readings on time control is unchanged.
  2. Hold. While late_seen, a request more than ANSWER_W from the battery reading is pending until that reading comes within ANSWER_W of it, or half way. Meanwhile the loop may not lead the reading by more than ANSWER_W that way (compute(hold=, hold_w=); it may come back), and the peak guard's floor is capped at the request that stands (cap_floor()). The loop is not held while a battery gives nothing under its ask or refuses: SAFETY-52/94 need it to go on asking.
  3. Late. Pending for LATE_S with no reading moving ANSWER_W towards it is the late verdict: control holds, then after stale_input_s goes to 0 W ("readings late"), as for a missing input. It ends when a reading starts towards the request.
  4. Seen. While late_seen an amber row says "Home Assistant readings arrive about N seconds late, so control holds the batteries." (NL too), and "every inverter is at 0 W" past the hold. One WARNING (READINGS_LATE) and one action-log line per episode, and one when it ends.

Constants (app/lateness.py).

Value Why
ANSWER_W 100 W control.PROBE_ANSWER_W: a reading this far towards the ask has answered it (SAFETY-52's probe).
DELAY_S 8 s freshness.PLANT_SETTLE_S 6 s + one LOOP_TICK_S 2 s: a fresh meter shows a request within the plant's settle (SAFETY-75's 6 s dead time) and one meter tick. The fit reads 0 s at 1/2/5 s meters, 5-7 s with a 6 s dead time, 9 s at 10 s late.
LATE_S 23 s 2 x PLANT_SETTLE_S + PUBLISH_PERIOD_S 10 s + READ_PERIOD_S 1 s: SAFETY-75's 6 s dead time, the 6 s settle, one publish and one read, the slowest healthy battery answer.
FIT_S, FIT_EVERY_S 40 s, 4 s Short enough to find 10 s and 30 s late within 50-60 s of the start (the swing has not grown yet); one 2 s cycle in two.
MAX_DELAY_S 90 s F-2 measured 17-78 s.
FIT_SHARE, FIT_BETTER 0.5, 0.7 Below these a battery that stops following (a refusal at N=2) was read as 23 s late.
SLOW_TAUS_S 10 s, 20 s The SAFETY-85/94 lagging battery: without this check 16 of 36 of its reversals read as 11-56 s late.
FIT_GONE_S 120 s The verdict outlives the lag by at most FIT_S + this when the site goes steady.

What it costs.

  • Late readings are found only after the requests have moved 100 W and the meter has shown it late: 48-68 s after the lag starts in the tests. Until then control is as before.
  • The row can stay up to 160 s after the readings are on time again, while the site is steady.
  • A battery that cannot follow while readings are late (at a limit, not reading 0 W) is held, then set to 0 W after 38 s, until it moves.
  • The fit costs about 93 x 20 sums every 4 s.
  • With readings late, a large step is followed in steps of the lag: the loop waits for each request to show before the next.

Left out. The round trip setpoint -> Home Assistant: the fitted delay gives the same figure from readings the add-on already has. The arbitrage charge's own sizing on late readings (charge_room_w()) is not capped here; the test shows it swinging while the peak guard holds (see the pull request).