- Minimal API under /api/v1 (SDD §9): POST /readings (idempotent HA push), GET /meters,
/energy-types, /consumption, /cost, /dashboard/summary, POST /events (records + recomputes),
GET+POST /tariffs, GET /sources/status.
- IngestionService.IngestByMeterAsync for direct REST push (batch-safe upsert via Local cache).
- ApiKeyFilter: X-Api-Key enforced against configured keys (open only when none set).
- ReverseProxyTrust middleware: adopt X-Forwarded-User/Remote-User behind Authelia/Traefik.
- Swagger/OpenAPI (Swashbuckle) at /swagger.
- Tests: push rejected without key (401), accepted + persisted with key; meters + swagger live.
94 tests green (56 Core + 38 integration).
Claude-Session: https://claude.ai/code/session_01WujdMtMJPbxDpDnMeK22rr
- PayloadExtractor: dot-path value/time extraction (Tasmota ENERGY.Total, bare scalars).
- MqttTopicMatcher: standard +/# wildcard matching.
- IngestionService: scale/offset, idempotent upsert on (meter_id, time), and a spurious-
decrease guard for monotonic registers (allowed only with a reset/swap event) + source
last-seen status.
- MqttMessageRouter + MqttIngestionWorker (MQTTnet 5): per-endpoint persistent connections,
topic subscription, graceful degradation; secrets resolved by env-var reference.
- Home Assistant: HaStateClient (REST /api/states parse) + HomeAssistantWorker polling on
each source's interval. HA-via-MQTT also works through the MQTT path.
- Ingestion workers gated by MeterVault:EnableLiveIngestion (off in tests).
85 tests green (53 Core + 32 integration): Tasmota payload → reading verified end to end.
Follow-up (polish): HA WebSocket push (state_changed) as an alternative to REST poll;
source-topic index caching in the router.
Claude-Session: https://claude.ai/code/session_01WujdMtMJPbxDpDnMeK22rr