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
@@ -722,6 +722,25 @@ class HaSignedSource(HaDsmrSource):
|
||||
ingest timestamping, meter_max_age_s, the clock-recomputed age sensor, and
|
||||
`unavailable` treated as a missing reading rather than 0 W.
|
||||
|
||||
⚠️ THE AGE ON THIS TRANSPORT MEASURES TIME SINCE THE VALUE CHANGED, not time
|
||||
since the meter reported, and on one entity those are very different things.
|
||||
Home Assistant offers no arrival signal for a repeated reading: it emits no
|
||||
`state_changed`, it does not advance `last_reported` on either serialiser,
|
||||
and `state_reported` cannot be subscribed to over the websocket at all
|
||||
("Event filter is required for event state_reported"). All three measured on
|
||||
the ENV-01 rig against the real HomeWizard integration with the meter frozen
|
||||
- 0 state_changed in 70 s, no timestamp movement anywhere.
|
||||
|
||||
`ha_dsmr` mostly escapes it because a DSMR telegram moves several entities at
|
||||
once. This transport has ONE, so a healthy meter under a flat load looks
|
||||
exactly like a dead one - and our own capture has the real meter going 42.2 s
|
||||
and 97.0 s between changes, both past the default max_age_s of 30. Hence
|
||||
DOCS.md: sensor.p1_sample_age_s is correct while the value moves and must not
|
||||
yet be thresholded by the firmware watchdog on this transport. Raising
|
||||
meter_max_age_s does not fix it, it only chooses which of the two errors you
|
||||
get. The fix is an arrival stamp from the meter itself - reading the
|
||||
HomeWizard local API rather than an HA entity - which is a separate ticket.
|
||||
|
||||
⚠️ The debounce is inherited but does nothing useful here, and that is fine:
|
||||
one telegram is one entity, so there is no burst of per-entity events to
|
||||
coalesce and no window in which a new reading sits beside a stale one. It
|
||||
@@ -811,10 +830,27 @@ def build_source(opts: dict, ingest: P1Ingest, session, broker: dict | None):
|
||||
"phase_export": opts.get("p1_phase_export_entities") or [],
|
||||
})
|
||||
if source == SOURCE_HA_SIGNED:
|
||||
return HaSignedSource(session, ingest, {
|
||||
"net": opts.get("p1_net_entity", ""),
|
||||
"phase_net": opts.get("p1_phase_net_entities") or [],
|
||||
})
|
||||
net = str(opts.get("p1_net_entity", "") or "").strip()
|
||||
phase_net = [str(e).strip() for e in
|
||||
(opts.get("p1_phase_net_entities") or []) if str(e).strip()]
|
||||
# ⚠️ Both of these are checked ONCE here rather than per telegram. A
|
||||
# misconfigured source otherwise fails silently in the only way that
|
||||
# looks exactly like a healthy one that has not been sent anything yet:
|
||||
# no samples, a climbing age, and the firmware watchdog holding the
|
||||
# battery at 0 W with nothing in the log saying why.
|
||||
if not net:
|
||||
_LOG.error("meter_source %s needs p1_net_entity - P1 ingestion "
|
||||
"disabled (sensor.p1_sample_age_s would otherwise be "
|
||||
"announced with nothing feeding it)", SOURCE_HA_SIGNED)
|
||||
return None
|
||||
if phase_net and len(phase_net) != ingest.phases:
|
||||
_LOG.error("p1_phase_net_entities has %d entities but meter_phases "
|
||||
"is %d - P1 ingestion disabled. Every telegram would be "
|
||||
"rejected on the phase-count check.",
|
||||
len(phase_net), ingest.phases)
|
||||
return None
|
||||
return HaSignedSource(session, ingest,
|
||||
{"net": net, "phase_net": phase_net})
|
||||
if source == SOURCE_MQTT:
|
||||
broker = broker or {}
|
||||
return MqttP1Source(ingest, str(opts.get("meter_mqtt_topic", "")),
|
||||
|
||||
Reference in New Issue
Block a user