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
This commit is contained in:
co-authored by
Claude Opus 5
parent
680461c9bf
commit
f47f1f0129
@@ -48,6 +48,60 @@ Use the ESP32's readings rather than the inverter's cloud or dongle sensors:
|
||||
those serve cached values, and a stale reading here ends the maintenance charge
|
||||
phase having charged nothing.
|
||||
|
||||
### P1 meter ingestion
|
||||
|
||||
`meter_entity` above expects one signed sensor, which usually means a template
|
||||
someone wrote by hand. A Belgian P1 meter does not publish one: it publishes two
|
||||
**unsigned** registers, consumption and injection. Setting `meter_source` moves
|
||||
that subtraction into the add-on, where it is done once and tested, and replaces
|
||||
`meter_entity` entirely.
|
||||
|
||||
| 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 |
|
||||
| `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: grid power reads as *missing*, and the existing failsafe commands 0 W |
|
||||
| `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 |
|
||||
| `p1_phase_import_entities` | `[]` | L1..L3, in order. Needed for the capacity-tariff peak on a three-phase connection |
|
||||
| `p1_phase_export_entities` | `[]` | L1..L3, in order |
|
||||
|
||||
There is **no fallback to an inverter-side power figure**, deliberately. The
|
||||
inverter's own AC power tracks its battery almost perfectly and the real meter
|
||||
hardly at all, so a controller that failed over to it would be regulating
|
||||
against its own output while looking healthy.
|
||||
|
||||
The `mqtt_p1` payload is one JSON object per telegram, and the schema is strict —
|
||||
a key it does not recognise is a telegram from something other than what was
|
||||
tested, and guessing a key here means guessing a kilowatt:
|
||||
|
||||
```json
|
||||
{"import_w": 1234.0,
|
||||
"export_w": 0.0,
|
||||
"phases": [{"import_w": 500, "export_w": 0},
|
||||
{"import_w": 400, "export_w": 0},
|
||||
{"import_w": 334, "export_w": 0}],
|
||||
"timestamp": "2026-08-24T18:00:05+02:00"}
|
||||
```
|
||||
|
||||
`phases` and `timestamp` are optional; `timestamp` must carry a UTC offset. Where
|
||||
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.
|
||||
|
||||
#### `sensor.p1_sample_age_s`
|
||||
|
||||
Published over MQTT discovery whenever a broker is available: **seconds since the
|
||||
newest accepted telegram**, refreshed every second rather than only when a
|
||||
telegram lands. The ESP32's stale-input watchdog subscribes to this exact entity
|
||||
id, so do not rename it.
|
||||
|
||||
The reason it is recomputed against the clock is that Home Assistant only pushes
|
||||
a state when the state *changes*. A meter sitting at a genuinely constant reading
|
||||
emits nothing, which is indistinguishable — to anything watching the value — from
|
||||
a meter that has died. Watching the age instead separates the two: it climbs when
|
||||
telegrams stop and resets when they arrive, whatever the reading says.
|
||||
|
||||
### Control
|
||||
|
||||
| option | default | meaning |
|
||||
|
||||
Reference in New Issue
Block a user