Live ingestion wrote the raw reading and stopped there. Import, the REST push endpoint and the meter editor all recompute afterwards; the MQTT/Tasmota/HA path was the one that did not, so a polled reading landed in `reading` and every derived figure stayed frozen at the last import. Observed on a GenerationCounter: 45 readings, 44 consumption rows, generation pinned to the register value of the last imported reading. Recompute inline rather than behind a debounce. Normalizing a whole meter is cheap at metering cadence and a background dirty-set worker is machinery this does not yet need; the remark on RenormalizeAsync records when it would. Fixes a latent bug this surfaced in NormalizationService: ExecuteDelete drops the consumption rows in the database but leaves them in the change tracker, so a second recompute on the same context threw an identity conflict on (meter, time, kind). One worker scope ingesting two readings was enough to hit it. Detach the stale entries after the delete. Poll interval is now minutes, default 60, replacing seconds/60. A meter answers "how much this month, what will it cost" — an hourly sample answers that exactly as well as a per-second one, with far less raw volume (SDD §5.5). The `pollSeconds` key no longer binds, so existing sources fall back to the 60 default and move from every-60-seconds to hourly, which is the intent. A source that had deliberately set e.g. 300 seconds also lands on 60 minutes. Two test cleanups now delete consumption before the meter: live ingestion never produced any before, so the FK had nothing to trip on. Claude-Session: https://claude.ai/code/session_01V6joyergfvVLFEizH1hJLd
2.7 KiB
Wiring up sources
MeterVault ingests from MQTT/Tasmota and Home Assistant. Sources are attached to meters; each
source's config JSON says where the value comes from. Secrets are referenced by environment
variable name, never stored in the database.
1. Create an MQTT broker endpoint
ingestion_endpoint (type MqttBroker) config:
{
"host": "192.168.1.10",
"port": 1883,
"usernameEnv": "MQTT_USER",
"passwordEnv": "MQTT_PASS",
"extraTopics": ["tele/+/SENSOR"]
}
Set MQTT_USER / MQTT_PASS in the container environment. The worker connects on startup and
resubscribes automatically after outages.
2. Tasmota plug → electricity meter
Create a Tasmota source on the meter with:
{ "topic": "tele/plug1/SENSOR", "path": "ENERGY.Total" }
Tasmota publishes e.g. {"Time":"2026-01-01T12:00:00","ENERGY":{"Total":1234.56,"Today":1.2,"Power":50}}.
MeterVault reads ENERGY.Total, and uses the payload's Time field as the timestamp. Use
ENERGY.Today for daily-delta meters (direct_delta mode) or ENERGY.Power for instant_rate.
scale/offset on the source convert units (e.g. Wh → kWh with scale: 0.001).
3. Raw MQTT sensor
Same as Tasmota but source type Mqtt; point path at the JSON field, or omit it for a bare
numeric payload. timePath names a timestamp field in the payload if present.
4. Home Assistant
Two options:
A — HA pushes to MQTT. Configure an HA MQTT sensor/automation to publish to a topic and treat it as an MQTT source (above). No HA endpoint needed.
B — MeterVault polls HA. Create an ingestion_endpoint (type HomeAssistant):
{ "baseUrl": "http://homeassistant.local:8123", "tokenEnv": "HA_TOKEN" }
and a HomeAssistant source on the meter:
{ "entityId": "sensor.house_power", "attribute": null, "pollMinutes": 60 }
Set HA_TOKEN (a long-lived access token) in the environment, or type the token into the connector
dialog to have it encrypted at rest (SDD §6.4). Numeric state (or a named attribute) is read every
pollMinutes — default 60, because monthly totals and cost are identical whether a meter is sampled
hourly or per-second. unavailable/unknown states are skipped.
C — HA pushes to the REST API. POST to /api/v1/readings with an X-Api-Key header (see the
README). Good when HA should drive the cadence.
Notes
- Cumulative registers reject spurious decreases unless a
counter_reset/meter_swapevent explains them — record swaps viaPOST /api/v1/events. - High-frequency sources: MeterVault stores raw readings idempotently on
(meter, time); use Tasmota'sTelePeriodand per-source sampling to bound volume.