Files
MeterVault/CLAUDE.md
T
schmidt.florian 48a7f5a825 M0: scaffold solution, EF+Timescale schema, /healthz
Five-project Clean Architecture solution (Core/Infrastructure/App + Core.Tests/
Integration.Tests) on .NET 10 with central package management, snake_case EF mapping,
and shared build/style config.

- Full domain entity set + EF DbContext for the SDD §5.3 schema (singular table names).
- InitialSchema migration (relational) + TimescaleHypertables migration (raw SQL:
  create_hypertable + compression on reading, hypertable on consumption).
- App wiring: Serilog (actually wired, unlike MQTTower), DbContext, migrate-on-startup,
  /healthz. Serves plain HTTP behind a reverse proxy (no HTTPS redirect).
- deploy/Dockerfile (2-stage, ICU-capable) + docker-compose (app + timescaledb).
- Integration.Tests: shared TimescaleFixture (Testcontainers) — migrations, hypertables,
  compression policy, and /healthz all verified green (4/4).

Claude-Session: https://claude.ai/code/session_01WujdMtMJPbxDpDnMeK22rr
2026-07-13 11:02:21 +02:00

7.4 KiB
Raw Blame History

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What this repo is

MeterVault is a self-hosted, local-first energy & utility metering platform: it ingests meter data from Home Assistant, Tasmota and MQTT on a schedule, stores every reading timestamped and immutable, normalizes it into consumption, and turns it into cost dashboards. Energy types (electricity, water, heating oil, gas, …) and meters are user-defined, never hardcoded.

Status: pre-code. The repository currently contains only the design spec and reference data — no solution, projects, or build yet. The first coding task is milestone M0 (scaffold) from the spec.

Source of truth

docs/SDD.md is the authoritative spec and build brief — read it before implementing anything. Key protocol from §0 that governs all work here:

  • Build strictly in milestone order (§12, M0→M7). Each milestone is independently runnable and testable; do not start Mn+1 until Mn's tests pass.
  • The four CSVs in sampledata/ are golden fixtures. Every parsing / consumption / cost rule must reconcile against them (§13). If a computed number disagrees with the spreadsheet, the spreadsheet wins unless the discrepancy is a deliberately documented correctness fix.
  • When a design decision is ambiguous, check §14 (open questions): if listed, take the stated default and flag it; if not listed, ask before guessing.
  • Keep the domain layer free of infrastructure concerns (the domain model and DB schema are UI-agnostic by design).

Committed tech stack (do not re-litigate; see SDD §4.1)

.NET (current LTS — .NET 10, .NET 8 acceptable), C# · ASP.NET Core + Blazor Server · MudBlazor components · ApexCharts (Blazor-ApexCharts) · MQTTnet · PostgreSQL + TimescaleDB · EF Core (Npgsql) for schema/CRUD + Dapper for hot-path time-series reads · BackgroundService hosted services for ingestion/aggregation · xUnit + Testcontainers (Timescale image) · Docker Compose + GHCR.

Intended project layout (created during M0, per SDD §11)

/src/Core            domain entities, enums, interfaces, expression evaluator  (no infra deps)
/src/Infrastructure  EF Core + Npgsql, Dapper repos, Timescale raw-SQL migrations, MQTT/HA clients, CSV importer
/src/App             ASP.NET Core host: Blazor Server UI + REST API + hosted workers
/tests/Core.Tests            unit: deltas, swaps, tariff resolution, oil rate, CSV parsing
/tests/Integration.Tests     Testcontainers (Timescale): ingest→aggregate→cost e2e
/tests/fixtures              the 4 reference CSVs + expected outputs
/deploy              Dockerfile, docker-compose.yml (app + timescaledb), unraid-template.xml

Commands (apply once M0 has scaffolded the solution)

dotnet build                                   # build the solution
dotnet test                                    # all tests (Integration.Tests needs Docker for Testcontainers)
dotnet test tests/Core.Tests                   # unit tests only (no Docker needed)
dotnet test --filter "FullyQualifiedName~Csv"  # a single test / class by name filter
dotnet run --project src/App                   # run app + workers locally
docker compose -f deploy/docker-compose.yml up # app + TimescaleDB together

Core architecture (the part that spans multiple files)

Data pipeline — one direction, layered (SDD §4.2, §5, §7):

sources (Tasmota/HA/MQTT/manual/CSV)
  → Ingestion workers write raw `reading` rows (immutable audit truth)
  → Normalization derives append-only `consumption` (deltas in base unit)
  → TimescaleDB continuous aggregates roll consumption to hourly/daily/monthly/yearly
  → Cost engine joins aggregates with time-ranged `tariff`
  → Blazor dashboard + REST API read aggregates + cost views

Invariants that shape everything:

  • Raw reading is immutable audit truth. Everything derived (consumption, cost, balances, forecasts) is computed on top and must be reproducible. Never mutate readings to fix a derived number.
  • Dashboards and charts read aggregates only — never scan reading. This is what makes 1000 meters × 50 years feasible (§5.5). Raw is kept for a bounded window (default 3y); consumption + aggregates are the long-term source of truth.
  • meter.mode (measurement mode) is the central abstraction for how raw readings become consumption (SDD §5.2): cumulative_counter, generation_counter, runtime_counter (Δhours × rate), consumable_balance (tank: deliveries usage + forecast), direct_delta, instant_rate, virtual (expression over other meters). New ingestion/normalization logic dispatches on mode.
  • Nothing domain-specific is hardcoded. Energy types are data. Cost categories are decoupled from energy types (Heizung may be oil today, heat-pump tomorrow). PV self-consumption/savings/net are virtual meters with user-defined expressions, not special-cased code. Tariffs are time-ranged (price history), scoped global / per-type / per-meter.

Timescale vs EF split (SDD §5.3): EF Core migrations own the relational tables. Timescale-specific DDL — create_hypertable, compression policies, continuous aggregates, retention — is not expressible via EF's model builder and must live in raw-SQL migrations. reading and consumption are hypertables.

Time & DST (SDD §10): store UTC everywhere; bucket and display in the instance timezone (default Europe/Berlin). "Daily cost" boundaries are local-midnight — use time_bucket(..., 'Europe/Berlin').

Secrets (SDD §6.4): broker/HA tokens are never stored in DB plaintext. ingestion_endpoint.config holds a reference (env var name / Docker secret path) resolved at runtime.

Reference-data behaviours the code must reproduce (from sampledata/)

These CSVs are the German-dialect Energiebilanz spreadsheet export and define the minimum feature bar (SDD §2, Appendix A). When writing the importer or normalization, honour:

  • German number dialect: decimal comma (180,8244706), thousands dot (2.940,19), trailing- currency (120,00 €), unit suffixes on values (411kWh, 49 cm, 2287 L) — strip and validate.
  • Two date formats: Monat YYYY (German month names, monthly tables) and DD.MM.YYYY (event rows).
  • Skip inline summary rows (Total, Heute, Seitbeginn Tage, Seit YYYY) and all-zero future placeholder rows (e.g. Dec 2026) — do not ingest them. Negatives are valid (savings, grid balance).
  • Water register swaps mid-series (…861 → 2 → 15): consumption must stay continuous across the boundary via a meter_swap event.
  • Electricity has 5 meters (Haus, Netz, Auto, Solar 1, Solar 2) plus derived columns. Verified relations to reproduce: Netz Einsparung = Haus Netz, Ersparnis = Netz Einsparung × €/kWh, Kosten = Verbrauchskosten Ersparnis — but implement these as user-definable virtual-meter expressions, not hardcoded formulas.
  • Heating oil is the versatility stress test: a consumable/tank model where consumption is derivable two ways — tank-level Δ, or burner runtime × rate (rate fixed from nozzle spec, or empirical = Δlevel ÷ Δhours). Early rows (19972004) carry deliveries only (no burner hours yet). Includes cm→litre dipstick calibration and forecast-to-empty.

Git

Remote origin is https://git.finalfactory.de/FinalFactory/MeterVault.git (default branch main).