TEL-04: a meter source for the meter this house actually has #4

Open
admin wants to merge 0 commits from TEL-04 into release/1.0
Owner

TEL-01 shipped ha_dsmr and mqtt_p1. Neither fits the installed meter - a HomeWizard P1 publishing one signed figure, where ha_dsmr requires two unsigned registers and refuses negatives (p1.py:149). So sensor.p1_sample_age_s could not be produced here, which blocked the FW-01 flash.

meter_source: ha_signed reads one signed entity. It is a subclass of HaDsmrSource overriding only _wanted() and build(), so ingest timestamping, staleness, the clock-recomputed age, plausibility bounds and unavailable-is-not-zero all behave identically by construction rather than by copy.

122 -> 174 checks. Ten mutations, nine red; the tenth is a genuine equivalent mutant, documented at the site rather than left for a reviewer to rediscover.

Sign convention asserted against real captured values from sim/scenarios/: -5710 W at 13:46 local under full sun, +775 W at midnight. The argument is physical - night draws, midday feeds back.

Worth deciding separately: neither original transport is reachable by any meter in the estate, and neither has run against real hardware.

TEL-01 shipped `ha_dsmr` and `mqtt_p1`. Neither fits the installed meter - a HomeWizard P1 publishing **one signed** figure, where `ha_dsmr` requires two unsigned registers and refuses negatives (`p1.py:149`). So `sensor.p1_sample_age_s` could not be produced here, which blocked the FW-01 flash. `meter_source: ha_signed` reads one signed entity. It is a **subclass of `HaDsmrSource` overriding only `_wanted()` and `build()`**, so ingest timestamping, staleness, the clock-recomputed age, plausibility bounds and unavailable-is-not-zero all behave identically by construction rather than by copy. 122 -> 174 checks. Ten mutations, nine red; the tenth is a genuine equivalent mutant, documented at the site rather than left for a reviewer to rediscover. Sign convention asserted against real captured values from `sim/scenarios/`: -5710 W at 13:46 local under full sun, +775 W at midnight. The argument is physical - night draws, midday feeds back. Worth deciding separately: neither original transport is reachable by any meter in the estate, and neither has run against real hardware.
admin added 1 commit 2026-08-25 16:46:22 +02:00
TEL-01 shipped ha_dsmr and mqtt_p1, and neither can read the meter that is
actually fitted here. The house has a HomeWizard P1 exposing ONE signed
entity, sensor.p1_meter_active_power (+ import, - export); ha_dsmr wants two
unsigned registers and refuses a negative one outright, which is every
exporting telegram. So sensor.p1_sample_age_s could not be produced at this
site, and FW-01's watchdog needs it - measured, not theoretical: the house P1
went 51.1 s and 36.2 s without a state change overnight, both past
meter_max_age_s 30, so without the age sensor the watchdog would false-trip
the battery to 0 W.

Adds meter_source: ha_signed, reading p1_net_entity (and optionally
p1_phase_net_entities in L1..L3 order for the capacity-tariff peak). The
derivation is split_signed(), sitting next to make_sample's subtraction for
the same reason it does - the moment a user is asked to write two template
sensors that split a signed value, the sign convention is back in unreviewed
YAML underneath a safety input, which is exactly what TEL-01 removed.

The transport is a subclass of HaDsmrSource overriding only _wanted() and
build(), so every rule TEL-01 established is inherited rather than
re-implemented: ingest timestamping, meter_max_age_s, the clock-recomputed
sensor.p1_sample_age_s republished ~1 Hz, the plausibility ceiling, the
"prime the cache from get_states but never build a sample out of it" rule,
"a reconnect emits nothing", and unavailable/unknown treated as a MISSING
reading and never as 0 W.

Defaults to off. An existing install is unaffected until it opts in.

test_p1.py: 122 -> 174 checks. Includes an end-to-end run of the new
transport against a fake Home Assistant websocket, and the sign convention
asserted against real captured readings from
sim/scenarios/ha-p1_meter_active_power-2026-08-{20,23}.json (-5710 W at
13:46 local under full sun is export; +775 W at midnight is import).

Non-vacuity: ten mutations of the new rules, each applied alone and reverted
byte-identical. Nine turn the suite red. The tenth - splitting the per-phase
signed values rather than passing them through - is an equivalent mutant,
because make_sample subtracts the two lists again and does not sign-check
per-phase figures. That is recorded in a ponytail: comment at the site rather
than left for the next reviewer to rediscover.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Du77usMj8XNKNFZGmUiWDa
admin added 1 commit 2026-08-25 17:01:41 +02:00
Review point from the non-vacuity run. Deleting build()'s "is the net entity
cached at all" guard makes build() raise KeyError rather than return False.
That still failed the suite, but by aborting it with a traceback at whichever
check ran first - a red that costs the next person ten minutes deciding
whether the suite is broken or the code is.

New `built()` helper in test_p1.py wraps build() and turns an escaping
exception into a returned value, so the comparison against True/False fails
by name. Applied only to the ha_signed section; TEL-01's own checks are
untouched.

Mutation 4 before: 0 named checks red, aborted at check 123 of 174.
Mutation 4 after:  2 named checks red - "nothing cached yet builds nothing"
and "an unavailable entity does not build a sample" - all 174 reached.

Still 174 checks, all green, and the other nine mutations are unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Du77usMj8XNKNFZGmUiWDa
admin added 2 commits 2026-08-25 17:47:53 +02:00
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
No meter in this installation uses either transport. Both were written to
specs.md 5.2's assumption that a Belgian P1 exposes two unsigned registers,
which the meter actually fitted here does not - it is the HomeWizard P1 that
ha_signed reads. Their only coverage is test_p1.py and an end-to-end test
against a fake Home Assistant.

Deliberately not called "experimental". That word says the design is
unfinished, which is not the defect and is vaguer than the truth; these are
complete and reviewed, they have simply never had a real telegram through
them. The failure this note is guarding against is a future session debugging
a meter problem, treating those two paths as proven, and looking elsewhere.

Placed where the mode is chosen rather than in a footnote, because the choice
is the decision it should inform.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Du77usMj8XNKNFZGmUiWDa
This branch is already included in the target branch. There is nothing to merge.
This branch is out-of-date with the base branch
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin TEL-04:TEL-04
git checkout TEL-04
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: admin/goodwe-addon#4