Review fixes.
A. test_loop.py takes 73 s, not the ~35 s claimed in three places. That number
is the whole defence against a future developer deleting a slow test, so it was
understated by 2x in exactly the place it matters. Corrected - and it is now
~95 s (measured 93 s), because this commit adds the scenario below rather than
trimming windows to make an inconvenient number smaller. Both call sites carry
an instruction to re-measure if the windows change.
B. "242 checks" was test_p1's own count quoted as the total for all five
suites. The real figure is 395 across the five, plus 14 here.
And the finding this PR should have led with. stale_input_s and the
`inputs missing -> 0 W` claim both live INSIDE cycle(), so pre-fix, on a meter
whose value happened to repeat, that claim could not fire at all. Constant
meter, SoC entity dying at t=4 s, one binary:
pre-fix final target 220.0 W, reason 'loop -> 220 W'
post-fix final target 0.0 W, reason 'safety -> 0 W'
The old controller held 220 W through the whole input outage with its status
line still reading loop -> 220 W, and layer 1 does not cover that on a
`meter_source: off` install - no P1 age sensor is published, so the firmware has
nothing to veto on, while the heartbeat keeps its watchdog fed. The pre-fix
behaviour was not "holds a stale command safely"; it was a failsafe a repeating
meter switched off. Asserted in test_loop.py, both halves.
Three points of reasoning tightened, none of them wrong before:
- the re-tune is unreachable by construction, not merely by the reset: the
combined cycle rate is max(arrival rate, 1/floor), never the sum, and 1/15 s
is strictly slower than the ~1/5 s arrival rate. That holds only while the
floor exceeds the meter's update interval - and the schema minimum of 5 s is
exactly the reference meter's interval. Flagged for validate.py, not fixed
here.
- the ~11 s lower bracket is not only a dropped poll: a 5.0 s poller against a
4.97 s meter beats, so about every 14 minutes the poller steps over an update
entirely. Reaching 15 s takes a beat-skip AND a failed poll.
- the "freshness is decided upstream" guarantee is transport-conditional. On
`meter_source: off` the grid figure comes straight from hass.number() with no
age gate. Running the full law is still right; the claim is narrower than it
was stated.
The 15 s default is not independently justified - it rests on the firmware's
30 s ingest veto and moves if that ever does. Said beside the constant.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Du77usMj8XNKNFZGmUiWDa