Commit Graph

4 Commits

Author SHA1 Message Date
schmidt.florian 8550ed8d9e Ingestion: Home Assistant WebSocket push path
ci / build-test (push) Successful in 1m16s
HomeAssistantWebSocketWorker holds a persistent state_changed subscription per HA endpoint that opts in via the connector's WebSocket toggle (HaEndpointConfig.UseWebSocket): auth handshake, subscribe, ingest in real time, capped-backoff reconnect. The REST poll worker skips WS endpoints so each is served once. HaWebSocketProtocol holds the pure handshake/parse logic. Verified by 11 protocol unit tests + a live integration test against an in-process fake HA server. CLAUDE.md updated.

Claude-Session: https://claude.ai/code/session_01Kib2MniVFbD95fkgLgBBnB
2026-07-17 11:05:01 +02:00
schmidt.florian 09cd435c2b Admin write-CRUD, Home Assistant connector config, wiring audit
ci / build-test (push) Successful in 1m19s
Three requested phases.

1) Admin section (SDD §8.7) — MudBlazor inline-dialog CRUD, consistent pattern,
   delete guards, snackbar feedback, shared Confirm helper:
   - Energy types: create/edit/delete (blocks delete when meters reference it).
   - Meters: create/edit/delete; recomputes consumption when mode/baseline
     changes (NormalizationService over a fresh factory context, in a tx);
     delete cascades data (consumption+readings are Restrict → removed first).
   - A meter's ingest sources: manage on the meter-detail Sources tab
     (add/edit/delete MQTT/Tasmota/HA sources with typed config).
   - Tariffs: full CRUD (scope/component/value/validity).
   - Cost categories: CRUD + member management (meter or energy-type members).
   - Connectors: ingestion_endpoint CRUD (MQTT broker + Home Assistant);
     secrets referenced by env-var name only, never stored.
   - Settings: read-only effective-config view (settings are env-driven and
     reproducible, so an editable form would change nothing — kept honest).
   PV role is now editable on meters (MeterMeta.SetRole can clear a role).

2) Read Home Assistant — extracted a shared public HaEndpointConfig (was a
   private record in the worker), added HaConnectionTester (powers the connector
   "Test connection": checks base URL + env-resolved token, optionally reads one
   entity). Configuring an HA connector + an HA source on a meter drives the
   existing REST-poll worker end to end. (WebSocket push stays a future
   optimization; REST poll already reads HA.)

3) Wiring/placeholder audit — swept every OnClick/Href: all handlers are real,
   all internal links resolve to real routes, no TODO/stub/placeholder code.
   Fixed one genuine gap: MainLayout had no drawer toggle, so the nav was
   unreachable on narrow screens — added a hamburger button.

Tests: +6 (MeterMeta.SetRole role-removal; HaConnectionTester fail-closed
guard branches with a throwing HttpClientFactory proving no network on bad
config); render test now covers all admin routes. 69 Core + 45 Integration =
114 green. Live-verified in Docker: all admin pages 200, drawer toggle present,
Settings shows real effective config.

Claude-Session: https://claude.ai/code/session_01Lz2RqAsnQhetqWNoCDfexK
2026-07-14 11:22:17 +02:00
schmidt.florian a6edec2b12 Polish/audit: fix bugs found by 3 subsystem audits
ci / build-test (push) Successful in 2m45s
Correctness/data:
- Fix demo cost double-count: reference importer no longer imports the Kosten Strom/Wasser
  columns for categories that are metered (only Heizung), so Wasser rollup is 70€ not 140€.
- Spurious-decrease guard: only a reset/swap in the window (prevReading, thisReading] explains
  a decrease — an old historical reset no longer permanently disables the guard.
- Gate swap auto-detection on MappingProfile.DetectCumulativeSwaps (flag was ignored).
- Prorate basePrice by bucket length (day/month/year); guard virtual expressions against NaN/Inf.

Concurrency/infra:
- Blazor: register a DbContextFactory; CostService/DashboardService and the read pages now use
  short-lived per-operation contexts (no shared circuit DbContext); guard Trends re-entrancy.
- /events: wrap event insert + consumption recompute in one transaction (atomic); 404 (not 500)
  on unknown meter.
- MQTT worker: subscribe to newly-added topics on each tick; move client cleanup into finally.
- Migrations: CREATE MATERIALIZED VIEW IF NOT EXISTS + if_not_exists on CAgg/compression/
  hypertable calls (re-run-safe after a mid-migration crash).
- HA worker: prune stale poll-schedule entries; export: null dangling ImportBatchIds on restore.

API/security:
- API fail-closed by default: with no keys and AllowAnonymousApi off, /api/v1 returns 401
  (protects /export and /import). New MeterVault:AllowAnonymousApi opt-in.
- Cap /readings batch at 5000; report ignored (unknown-meter) count; enums as strings in JSON.

+4 regression tests (guard window, API closed, /events 404, no demo double-count). 98 tests
green; Docker deploy re-verified healthy with the API fail-closed.

Claude-Session: https://claude.ai/code/session_01WujdMtMJPbxDpDnMeK22rr
2026-07-13 12:56:51 +02:00
schmidt.florian 4b0cad67df M3: live ingestion (MQTT/Tasmota + Home Assistant)
- 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
2026-07-13 11:45:20 +02:00