TEL-05: read the meter, not Home Assistant's opinion of the meter
A fourth meter_source, `homewizard_local`, polling a HomeWizard P1's own local API (GET /api/v1/data) instead of watching an HA entity. The point is the age sensor. sensor.p1_sample_age_s is FW-01's watchdog input, and on every transport we had it measured "time since the value CHANGED", not "time since the meter REPORTED". Home Assistant offers nothing better: a repeated reading emits no state_changed, advances last_reported on neither serialiser, and state_reported cannot be subscribed to at all. Measured twice - 70 s of a frozen meter on the ENV-01 rig, and ten repeated readings against the live house. Our own capture of this house's meter goes 42.2 s and 97.0 s between changes, both past the default meter_max_age_s of 30, so the age sensor would have commanded 0 W on a perfectly healthy meter. Here every HTTP response is an arrival. The meter answered, now, with its current reading; whether the number moved is not consulted. Five identical readings are five arrivals. Reuses TEL-01's pipeline rather than restructuring it: same split_signed sign convention as ha_signed, same make_sample, same ingest stamping, meter_max_age_s, clock-recomputed age, plausibility bounds and the §20 unsigned-decode rejection. A failed or timed-out poll submits nothing, so it is a missing reading - never 0 W - and does not reset the age. meter_poll_s (default 5 s, the meter's own rate) is checked against meter_max_age_s once at startup, like the ha_signed entity ids. ⚠️ An arrival stamp cannot see a FROZEN meter, and no arrival detector can - one answering 200 OK with a stale number is arriving. The local API does expose what HA never had (the total_power_*_kwh registers stop advancing) and the transport tracks it as `unchanged_s`, but it is deliberately not folded into the age and not thresholded: this controller regulates grid toward ~0 W, and at a converged -10 W the export register needs six minutes to move by its 1 Wh resolution while the power figure legitimately repeats. Thresholding that would rebuild the false-trip limit cycle at the exact operating point we aim for. test_p1.py 179 -> 236 checks. Still defaults to off. 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
ad9c5772a4
commit
98109a9b91
@@ -70,6 +70,13 @@ Either way, do not build the missing shape out of template sensors. The point of
|
||||
`meter_source` is that the sign convention is derived in one tested place rather
|
||||
than in YAML nobody reviews underneath a safety input.
|
||||
|
||||
> ✅ **If your meter is a HomeWizard P1, use `homewizard_local`, not
|
||||
> `ha_signed`.** It reads the same meter and produces the same numbers, but it
|
||||
> polls the meter directly instead of watching a Home Assistant entity — and
|
||||
> that is the difference between an age sensor a watchdog can threshold and one
|
||||
> it cannot. See "`sensor.p1_sample_age_s`" below; `ha_signed` remains for
|
||||
> installs where the meter is only reachable through Home Assistant.
|
||||
|
||||
> ⚠️ **`ha_dsmr` and `mqtt_p1` have never processed a telegram from real
|
||||
> hardware.** No meter in this installation uses either one. Both were written
|
||||
> to the assumption in `specs.md` §5.2 that a Belgian P1 exposes two unsigned
|
||||
@@ -84,9 +91,11 @@ than in YAML nobody reviews underneath a safety input.
|
||||
|
||||
| option | default | meaning |
|
||||
|---|---|---|
|
||||
| `meter_source` | `off` | `off` keeps `meter_entity`. `ha_dsmr` subscribes to the DSMR integration over the HA WebSocket; `mqtt_p1` reads a topic; `ha_signed` subscribes to one signed entity over the HA WebSocket |
|
||||
| `meter_source` | `off` | `off` keeps `meter_entity`. `ha_dsmr` subscribes to the DSMR integration over the HA WebSocket; `mqtt_p1` reads a topic; `ha_signed` subscribes to one signed entity over the HA WebSocket; `homewizard_local` polls a HomeWizard P1's own local API, bypassing Home Assistant |
|
||||
| `meter_phases` | 1 | 1 or 3. Must match the telegram, or every telegram is rejected and logged |
|
||||
| `meter_max_age_s` | 30 | Beyond this the reading is stale and grid power reads as *missing*. On its own it does **not** command 0 W — see the timing note below. It is also the longest a reading is held forward into the 15-minute average |
|
||||
| `meter_poll_s` | 5 | `homewizard_local` only. Seconds between polls. **Must be well under `meter_max_age_s`** — see below |
|
||||
| `p1_host` | | `homewizard_local` only. The meter's own address, `host` or `host:port` (e.g. `192.168.2.250`) |
|
||||
| `meter_mqtt_topic` | | `mqtt_p1` only |
|
||||
| `p1_import_entity` | | The **unsigned** consumption sensor. Do not point this at a signed template |
|
||||
| `p1_export_entity` | | The **unsigned** injection sensor |
|
||||
@@ -143,6 +152,50 @@ tested, and guessing a key here means guessing a kilowatt:
|
||||
it is present it is used for the age, which is what stops a retained message
|
||||
replayed on reconnect from presenting a ten-minute-old reading as current.
|
||||
|
||||
#### `homewizard_local` — polling the meter instead of Home Assistant
|
||||
|
||||
Set `p1_host` to the meter's address and the add-on does `GET /api/v1/data` on
|
||||
it every `meter_poll_s` seconds, reading `active_power_w` (signed, same
|
||||
convention as `ha_signed`) and the three `active_power_l{1,2,3}_w` fields. Home
|
||||
Assistant is not involved: no entity, no WebSocket, no integration to
|
||||
mis-configure. Per-phase figures are used only when the meter serves all
|
||||
`meter_phases` of them — a single-phase meter returns `null` for L2/L3, and the
|
||||
connection-level reading is still accepted on its own.
|
||||
|
||||
**Why this mode exists:** every HTTP response is an *arrival*. The meter
|
||||
answered, now, with its current reading — whether or not the number moved. That
|
||||
is the signal `sensor.p1_sample_age_s` needs and the one Home Assistant cannot
|
||||
give it at all (see the note below). It is also simply fewer moving parts: the
|
||||
five-second cadence is the meter's own, rather than an integration's polling of
|
||||
it re-published as a state change.
|
||||
|
||||
**Cadence.** The age is never fresher than the poll interval, so:
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| meter's own update rate | ~5.0 s (measured 4.97 s) |
|
||||
| `meter_poll_s` default | 5 s — nothing to gain below the meter's own rate |
|
||||
| `meter_max_age_s` default | 30 s, i.e. six polls of headroom |
|
||||
| `meter_poll_s >= meter_max_age_s` | **refused at startup** — every reading would be stale before its successor arrived |
|
||||
| `meter_poll_s > meter_max_age_s / 2` | warned — one missed poll makes the reading stale |
|
||||
|
||||
A failed poll — timeout, connection refused, non-200, unparseable body — is a
|
||||
**missing** reading. It submits nothing, so the reading does not become 0 W, the
|
||||
last good value and its timestamp are left alone, and the age goes on climbing.
|
||||
That is exactly what a dead meter should look like.
|
||||
|
||||
**What it still cannot see: a frozen meter.** A meter that answers `200 OK`
|
||||
forever with a stale number is arriving, so no arrival detector — this one
|
||||
included — can tell it from a healthy one. The local API does expose the raw
|
||||
material the HA path never had (the `total_power_*_kwh` registers stop
|
||||
advancing), and the transport tracks it as `unchanged_s`, but it is deliberately
|
||||
*not* folded into the age and *not* thresholded: this controller regulates grid
|
||||
power toward ~0 W, and at a converged −10 W the export register needs six
|
||||
minutes to move by its 1 Wh resolution while the power figure legitimately
|
||||
repeats. Thresholding that at 30 s would rebuild the false-trip limit cycle at
|
||||
the exact operating point the controller aims for. Freeze detection is a
|
||||
separate problem and needs the low-power case solved first.
|
||||
|
||||
#### `sensor.p1_sample_age_s`
|
||||
|
||||
Published over MQTT discovery whenever a broker is available: **seconds since the
|
||||
@@ -189,8 +242,19 @@ behind it would trip the firmware watchdog on a system that is working fine.
|
||||
> the ESP32 stale-input watchdog: a quiet house would trip the battery to 0 W.
|
||||
> Raising `meter_max_age_s` is **not** the fix — the two conditions produce an
|
||||
> identical signal, so a bigger number only chooses which of the two errors you
|
||||
> get. The real fix is an arrival stamp the meter itself provides, i.e. reading
|
||||
> the HomeWizard local API directly rather than through an HA entity.
|
||||
> get. The real fix is an arrival stamp the meter itself provides — and that now
|
||||
> exists: **`meter_source: homewizard_local`**. If you have a HomeWizard P1,
|
||||
> switch to it. If your meter is only reachable through Home Assistant, this
|
||||
> limit still applies to you and the watchdog threshold still must not be armed.
|
||||
|
||||
**Where the age is trustworthy:**
|
||||
|
||||
| mode | the age measures | safe to threshold from firmware |
|
||||
|---|---|---|
|
||||
| `homewizard_local` | time since the meter **answered** | **yes** — every HTTP response is an arrival |
|
||||
| `ha_dsmr` | time since one of several entities changed | no — statistically usually fine, which is a masked bug, not an absent one |
|
||||
| `ha_signed` | time since the one entity changed | **no** — see above |
|
||||
| `mqtt_p1` | time since a message arrived | arrivals yes, but a stuck bridge republishing keeps arriving |
|
||||
|
||||
### Control
|
||||
|
||||
|
||||
Reference in New Issue
Block a user