TEL-01: P1 ingestion - two transports, the EMS-owned sign derivation, and sensor.p1_sample_age_s #2
Closed
admin
wants to merge 2 commits from
TEL-01 into SAFETY-04
pull from: TEL-01
merge into: :SAFETY-04
: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
2
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4bd659c499 |
TEL-01 review fixes: stop the age sensor tripping installs that have no meter
T-1, and it was a fleet-wide trip to zero. publish() emitted p1_age unconditionally, and published_age_s counts from P1Ingest.__init__ when no sample has ever arrived. With meter_source defaulting to off, every existing install would have published sensor.p1_sample_age_s climbing without bound; the ESP32 does `has_state() && state >= max_age_s` and forces the layer-1 failsafe, so each of them would have pinned its inverter at 0 W within 30 s. Exactly the opposite of the zero-regression the off default was for. The key is now omitted from the payload AND from MQTT discovery when P1 is off, so the entity does not exist at all - which is the status quo, and what has_state() is testing for. The predicate is one function, is_enabled(), because the grid reading, the task start and the discovery announcement have to agree or this comes back. T-2, connect no longer manufactures a sample. get_states returns whatever HA currently holds, which after a Core restart is a RestoreEntity value of unknown age; stamping it with ingest_ts=now reset the age and reported a fresh meter that could have been dead for an hour. run()'s own docstring already said a reconnect must emit nothing - the code disagreed with it, and a test asserted the violation. The cache is still primed, so the first real state_changed builds a complete sample; the age just stays honest until one arrives. T-3, gaps are no longer filled with the last held value. The averager held a sample forward across any interval, so a meter dying at 5 kW and returning ten minutes later credited 5 kW x 600 s to the capacity-tariff accumulator - a fabricated peak on a permanent record. The hold is capped at max_age_s: past that the stretch is walked so block boundaries still land correctly, but nothing accumulates and elapsed does not grow, which is what finally makes the comment about a gap dragging the billed average down true. Same threshold for control and billing: a reading too old to steer by is too old to bill by. T-4, the out-of-order/duplicate guard is covered. It was untested, and the reason is worth recording: the obvious assertion passes without the guard, because the negative interval is separately refused by the covered > 0 test. What the guard prevents is the timestamp REWIND, which only shows up one sample later as a re-integrated window. The test now goes one sample later. T-6, DOCS was wrong about latency. meter_max_age_s and stale_input_s stack, so meter death to 0 W is 45 s and not 30. Documented as a table with both clocks. Also documented the T-5 asymmetry rather than papering over it: the age measures arrival, not change, so a stuck MQTT bridge republishing its last telegram still looks fresh. Correct on ha_dsmr, not detectable on mqtt_p1 without a change-detector. Written up as a known limit. Writing the T-1 test caught a second defect in the test itself: it recorded only MQTT topics, and object_id lives in the payload, so "the age sensor is not announced" had been passing for the wrong reason. test_p1.py: 99 -> 122 checks. 14 mutations run, all 14 red, files restored byte-identical - including one per fix above. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Du77usMj8XNKNFZGmUiWDa |
||
|
|
f47f1f0129 |
TEL-01: P1 ingestion, with the derivation and the age the EMS owns
A Belgian P1 meter publishes two UNSIGNED registers, not one signed figure. Until now the add-on asked the installer to bridge that gap with a template sensor, which put the sign convention of the whole control loop in a text box. This moves it into the EMS: net = import - export, derived once, in one place, with a test that fails if anyone inverts it. Two transports behind one contract, chosen by `meter_source`: the HA WebSocket subscribing to the DSMR integration's entities, and MQTT on a configurable topic. Everything downstream reads P1Ingest, so switching is a config edit. `meter_source: off` is the default and keeps the existing meter_entity path, so no installed system changes until it opts in. The other half is the timestamp. Every accepted sample is stamped at ingest with a monotonic clock, `meter_max_age_s` is applied to it, and the age is published as sensor.p1_sample_age_s for the ESP32's stale-input watchdog. That entity is recomputed against the clock every second rather than only when a telegram lands, because HA pushes state only on change: a meter frozen at a constant reading emits nothing and looks, to anything watching the value, exactly like a meter that has died. The age tells them apart. Deliberately absent: any fallback to an inverter-side power figure. The inverter's own AC power correlates 0.998 with battery power and 0.09 with the real meter, so failing over to it means regulating against your own output. A gap stays a gap - a reconnect emits no synthetic sample, and a rejected telegram never resolves to 0 W or refreshes the timestamp. Quarter-hour averages are time-weighted over clock-aligned blocks rather than a mean of samples, so a cadence change cannot bias the capacity-tariff figure, and only offtake is accumulated so a quarter of pure export averages to 0 kW. Per-phase import is kept separately: on an unbalanced three-phase load the phase sum and the connection net are different numbers, and only one of them is billed. test_p1.py: 99 checks, runnable with a bare interpreter and no meter. Includes an end-to-end run of the HA transport against a fake Home Assistant websocket. Stacked on SAFETY-04; nothing here touches control.py. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Du77usMj8XNKNFZGmUiWDa |