Four follow-ups on the reviewed and approved TEL-05 work. Additive; no shipped
behaviour changes except the two failure paths below.
1. unchanged_s had no operator surface. DOCS.md told a reader "the transport
tracks it as unchanged_s" and there was nowhere to look: main.py built the
transport, scheduled run(), and never read the object again. The status page
now shows it on the healthy P1 line. Still NOT thresholded and NOT folded
into the age - that refusal was reviewed and upheld, because at the converged
-10 W this controller aims for a 1 Wh register needs ~6 minutes to move, so
any limit false-trips at the target operating point. The whole argument for
leaving it to a human requires the human being able to see it.
2. The "equivalent mutant" note on the content_type guard was wrong, and the
comment is downgraded to say so. web.Response(text=...) defaults to
text/plain, so the fake meter CAN serve valid JSON under the wrong mimetype.
Test added; shipped behaviour was already correct.
3. A timed-out poll logged an empty reason: str(asyncio.TimeoutError()) is "",
so the status page read "last error:" and then nothing, on a hung meter, at
the moment the battery had just gone to 0 W. Falls back to the class name.
Note str(err), not `err or ...` - an exception object is always truthy.
4. submit() sat outside the try in poll_once() and run() had no except, so a
raise would kill the poll task permanently and SILENTLY - safe (the age
climbs, the controller commands 0 W) but indistinguishable from a dead meter.
Both wrapped; poll_s is already the retry cadence, so no backoff.
Also a comment at the parse_homewizard range(phases) slice: a 3-phase meter
configured as 1-phase understates the capacity-tariff figure. Filed separately,
not fixed here.
242 checks in test_p1.py (236 before, 6 new). test_control 55, test_arbiter 18,
test_maintenance 21, all untouched and green. Each new check proved non-vacuous:
six mutations, six named reds, no suite aborts, sources restored byte-identical.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Du77usMj8XNKNFZGmUiWDa