TEL-04 review: unshadow the helper, validate config at startup, and record
what the rig proved about the age sensor
Review findings 1, 3, 5 and 6. Finding 2 is deliberately untouched - it is
its own ticket.
3. `built` was rebound at test_p1.py:816 by `built = build_source(...)`,
silently disarming the build() wrapper for anything appended below it.
Renamed to `sel`. Reproduced the reviewer's failure before fixing:
appending a check that calls built() after that line gives
`TypeError: 'HaSignedSource' object is not callable` and aborts at 163 of
180; with the rename the same probe reaches 180 and passes.
5. DOCS.md now states the "length must equal meter_phases" constraint that
config.yaml already carried, plus what leaving the list empty actually
costs: on the surveyed reading the phases carry 2769 W of import while the
connection nets 187 W, so the tariff quantity is understated ~15x.
6. build_source now checks the ha_signed wiring once at startup instead of
once per telegram: a blank p1_net_entity, or a phase list whose length
disagrees with meter_phases, logs an error and disables ingestion. Both
otherwise fail in the single way indistinguishable from a healthy source
nobody has fed yet - no samples, a climbing age, the watchdog holding the
battery at 0 W, and nothing in the log.
1. THE AGE SENSOR. Measured on the ENV-01 rig against the real HomeWizard
integration, meter frozen via hwsim's `?fault=freeze` seam (cleared in a
finally:, rig verified restored):
- websocket state_changed for the meter over 70 s : 0
- last_reported advanced (REST serialiser) : no
- last_reported advanced (websocket serialiser) : no
- subscribe_events(state_reported) : rejected,
"Event filter is required for event state_reported"
So Home Assistant exposes NO arrival signal for a repeated reading, and
the proposed fix - stamp from last_reported via subscribe_entities - is
not available. subscribe_entities listens only to EVENT_STATE_CHANGED, and
as_compressed_state carries no last_reported at all.
The age is therefore "time since the value changed", which on ha_dsmr is
mostly harmless (a telegram moves several entities) and on ha_signed is
not: one entity means a healthy meter under a flat load is
indistinguishable from a dead one. Recorded loudly in DOCS.md, in the
HaSignedSource docstring and in the CHANGELOG, with the measured 42.2 s
and 97.0 s gaps from our own capture.
meter_max_age_s is deliberately NOT widened. The two conditions produce an
identical signal, so a larger number does not separate them - it only
chooses which of the two errors you get, and it would disarm the watchdog
for a genuinely dead meter as well. The honest fix is an arrival stamp the
meter itself provides.
test_p1.py: 174 -> 179 checks, all green. Other three suites unchanged.
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
632be44f6c
commit
e663e10245
@@ -813,13 +813,18 @@ check("sensor.p1_sample_age_s is a live number on this transport too",
|
||||
print("ha_signed: selection by config")
|
||||
|
||||
check("ha_signed is enabled", is_enabled({"meter_source": SOURCE_HA_SIGNED}) is True)
|
||||
built = build_source({"meter_source": SOURCE_HA_SIGNED,
|
||||
"p1_net_entity": "sensor.p1_meter_active_power"},
|
||||
P1Ingest(), None, None)
|
||||
# ⚠️ `sel`, not `built` - that name is the build() wrapper defined at the top of
|
||||
# this file, and rebinding it here silently disarms every check appended below
|
||||
# this line. Caught in review: an added check went `TypeError: 'HaSignedSource'
|
||||
# object is not callable` and aborted the suite, which is the exact failure the
|
||||
# wrapper exists to prevent, reintroduced by a name collision.
|
||||
sel = build_source({"meter_source": SOURCE_HA_SIGNED,
|
||||
"p1_net_entity": "sensor.p1_meter_active_power"},
|
||||
P1Ingest(), None, None)
|
||||
check("meter_source ha_signed selects the signed transport",
|
||||
isinstance(built, HaSignedSource))
|
||||
isinstance(sel, HaSignedSource))
|
||||
check("...wired to p1_net_entity, and subscribed to exactly that one entity",
|
||||
built.ids == {"sensor.p1_meter_active_power"})
|
||||
sel.ids == {"sensor.p1_meter_active_power"})
|
||||
# ⚠️ The three modes must not bleed into each other: ha_dsmr must keep ignoring
|
||||
# p1_net_entity, or a half-configured install silently reads the wrong sensor.
|
||||
plain = build_source({"meter_source": SOURCE_HA,
|
||||
@@ -833,6 +838,30 @@ check("meter_source off still selects nothing",
|
||||
check("an unrecognised meter_source selects nothing rather than guessing",
|
||||
build_source({"meter_source": "ha_signd"}, P1Ingest(), None, None) is None)
|
||||
|
||||
# ⚠️ Caught once at startup, not once per telegram. A source that is wired up
|
||||
# wrong otherwise fails in the one way indistinguishable from a healthy source
|
||||
# nobody has sent anything to yet: no samples, a climbing age, the watchdog
|
||||
# holding the battery at 0 W, and nothing in the log saying why.
|
||||
check("a blank p1_net_entity is refused rather than silently never receiving",
|
||||
build_source({"meter_source": SOURCE_HA_SIGNED, "p1_net_entity": ""},
|
||||
P1Ingest(), None, None) is None)
|
||||
check("...and whitespace does not sneak past it",
|
||||
build_source({"meter_source": SOURCE_HA_SIGNED, "p1_net_entity": " "},
|
||||
P1Ingest(), None, None) is None)
|
||||
check("a phase list that disagrees with meter_phases is refused at startup",
|
||||
build_source({"meter_source": SOURCE_HA_SIGNED, "p1_net_entity": "sensor.n",
|
||||
"p1_phase_net_entities": ["sensor.a", "sensor.b"]},
|
||||
P1Ingest(phases=3), None, None) is None)
|
||||
check("a phase list that agrees with meter_phases is accepted",
|
||||
isinstance(build_source(
|
||||
{"meter_source": SOURCE_HA_SIGNED, "p1_net_entity": "sensor.n",
|
||||
"p1_phase_net_entities": ["sensor.a", "sensor.b", "sensor.c"]},
|
||||
P1Ingest(phases=3), None, None), HaSignedSource))
|
||||
check("no phase list at all is still fine - per-phase billing is optional",
|
||||
isinstance(build_source(
|
||||
{"meter_source": SOURCE_HA_SIGNED, "p1_net_entity": "sensor.n"},
|
||||
P1Ingest(phases=3), None, None), HaSignedSource))
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
print("the age sensor must not exist when P1 is off")
|
||||
# ⚠️ This is a fleet-wide regression guard, not a nicety. The ESP32 watchdog
|
||||
|
||||
Reference in New Issue
Block a user