TEL-04: a meter source for the meter this house actually has #4
Closed
admin
wants to merge 0 commits from
TEL-04 into release/1.0
pull from: TEL-04
merge into: :release/1.0
:master
:release/1.0
:PRICE-02
:UX-46
:SAFETY-121
:SAFETY-119
:UX-45
:DOC-8
:deploy-0.18.2
:release-0.18.2
:SAFETY-118
:deploy-0.18.1
:release-0.18.1
:UX-44
:UX-43
:deploy-0.18.0
:release-0.18.0
:OPT-20
:PRICE-01
:MAINT-11
:opt15-price-run-fixes
:SAFETY-112
:OPT-17-forecast-entities
:deploy-0.17.0
:release-0.17.0
:MAINT-15
:SAFETY-107
:MAINT-14
:MAINT-13-full-pack-complete
:SAFETY-103
:deploy-0.16.0
:release-0.16.0
:safety-95-drain-end-reseed
:safety-96-stale-unit-site-total
:maint-10-local-start-hour
:safety-90-peak-guard-gap
:deploy-0.15.0
:release-0.15.0
:ci-14-infra-pin-3b4ff84
:safety-94-no-false-stale-or-refusal
:deploy-0.14.0
:release-0.14.0
:ci-13-infra-pin-0c04488
:safety-93-no-grid-charge-at-charge-end
:safety-91-held-battery-last
:deploy-0.13.0
:release-0.13.0
:safety-85-no-false-not-keeping-up
:safety-87-guard-uses-able-batteries
:safety-86-peak-over-hold
:safety-84-n2-maint-charge
:safety-83-peak-over-drain
:opt11-p4-solar-forecast
:safety-78-drain-at-two
:safety-81-load-too-high
:opt11-p3-price-runs
:safety-79-peak-margin
:opt11-p2-ha-entities
:UX-13-p3-batteries
:opt11-p1-modes
:UX-13-p5
:UX-15-setup-only
:UX-13-p4-setup
:UX-13-p2-installer
:SAFETY-73-per-unit-refusal
:UX-13-p1-home
:UX-13-P0-foundation
:SAFETY-52-refused-hold
:UX-11-short-safety-lines
:pin-bump-d637d0b
:UX-10-plain-leftovers
:pin-bump-ecd3864
:UX-08-2d-logs-docs
:UX-08-2c-refusals
:UX-09-2a-fixes
:UX-08-status-rows
:pin-bump-7081df4
:UX-08-plain-notifications
:SAFETY-40-clamp-alarm
:pin-bump-7689816
:SAFETY-34
:pin-bump-a1d0618
:TOOL-9
:CI-12
:TEL-14-pv-sign
:DOC-5-watchdog
:TEST-26
:SAFETY-62-refuse-unread-input
:safety-61-control-task-death
:doc-1-operator-docs
:PKG-18-cut-0.12.0
:TEL-12
:PKG-17
:TEL-11
:ci9-gateproof
:SAFETY-56
:SEC-2-release-procedure
:PKG-16-release-0.10.1
:SAFETY-57
:pkg15-changelog
:CI-9
:SAFETY-48
:TEL-10
:PKG-14
:SAFETY-47
:SAFETY-10
:SAFETY-45
:TARIFF-16
:TARIFF-18
:TARIFF-17
:TARIFF-11
:TARIFF-13
:TARIFF-15
:TARIFF-07
:TARIFF-14
:TARIFF-12
:TARIFF-10
:TARIFF-09
:TARIFF-08
:FCST-06
:FCST-05
:FCST-04
:INSTR-01
:SAFETY-44
:ARCH-02
:SAFETY-43
:TEST-21
:SAFETY-31-attribution
:UX-04
:PKG-13
:PKG-12
:PKG-09
:OPT-09
:UX-02
:PKG-07
:SAFETY-42
:SAFETY-13
:SAFETY-37
:SAFETY-39
:SAFETY-38
:CI-06
:PKG-06-version-0.7.0
:SAFETY-41-reassert
:TEL-07
:UX-01
:SAFETY-32
:SPIKE-04a
:MAINT-05
:SAFETY-30
:CFG-04
:DATA-01
:DATA-02
:SAFETY-24
:SAFETY-26
:OPT-04
:FCST-03
:SAFETY-07
:TARIFF-03
:TARIFF-02
:TARIFF-01
:TEST-06
:SAFETY-03
:SAFETY-18
:MAINT-04
:safety-02-latch-persistence
:safety-02-sign-watchdog
:safety-12-loop-floor
:CFG-01
:TEL-05
:TEL-04
:DEPLOY-01
:TEL-01
:SAFETY-04
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
TEL-01 shipped
ha_dsmrandmqtt_p1. Neither fits the installed meter - a HomeWizard P1 publishing one signed figure, whereha_dsmrrequires two unsigned registers and refuses negatives (p1.py:149). Sosensor.p1_sample_age_scould not be produced here, which blocked the FW-01 flash.meter_source: ha_signedreads one signed entity. It is a subclass ofHaDsmrSourceoverriding only_wanted()andbuild(), 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, 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_01Du77usMj8XNKNFZGmUiWDawhat 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_01Du77usMj8XNKNFZGmUiWDaClosing: the content of this branch is already on
release/1.0—git rev-list --count origin/release/1.0..returns 0.The work shipped, but it reached the release branch by a different route (a direct push, or carried in with another merge) rather than through this PR, so Gitea never auto-closed it. That is a process failure on my part: it left the PR list looking like nothing had shipped, and it orphans any review discussion held here.
Nothing is lost and nothing needs re-doing. Going forward, work lands by merging its PR.
Pull request closed