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
4.7 KiB
MeterVault
A self-hosted, local-first energy & utility metering platform. MeterVault pulls meter data from Home Assistant, Tasmota and raw 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, district heat, …) and meters are user-defined — nothing is hardcoded.
Successor to a hand-maintained Energiebilanz spreadsheet. See docs/SDD.md for the
full design.
Features
- Automatic ingestion from MQTT/Tasmota (persistent subscriptions) and Home Assistant (REST poll or push), plus manual entry, a REST push API, and CSV import.
- Immutable raw readings on a TimescaleDB hypertable; a normalized, append-only consumption layer on top — reproducible, auditable.
- Seven measurement modes (cumulative/generation registers, burner runtime, tank/consumable, direct delta, instant rate, virtual). Handles meter swaps, counter resets, tank dip-sticks with calibration, and virtual meters defined by an expression (PV self-consumption, savings, net).
- Tariff engine with time-ranged price history (unit/base/feed-in), scoped global / per type / per meter; cost categories decoupled from energy types; meterless manual costs.
- Continuous aggregates (daily/monthly/yearly, local timezone) so dashboards never scan raw.
- Dashboard: cost KPIs with period-over-period deltas, "what costs most", a "what cost more/ less" difference view, trends, a PV/Solar panel (generation, self-consumption, autarky %, savings), an oil/consumable panel (tank gauge, deliveries, burner runtime, effective L/h, forecast-to-empty) and a per-meter detail view (raw readings, consumption, sources, tariff timeline, events), one-click reference-data load, CSV dry-run.
- Admin UI: full create/edit/delete for energy types, meters (with consumption recompute on mode/baseline change), ingest sources, tariffs, cost categories, and MQTT/Home-Assistant connectors; a "Test connection" for Home Assistant; effective-settings view.
- REST API + OpenAPI/Swagger, API-key auth, reverse-proxy trust (Authelia/Traefik).
- JSON config export/import for portability; Docker Compose + multi-arch image.
Quick start (Docker)
docker compose -f deploy/docker-compose.yml up -d
# open http://localhost:8080 → Import → "Load reference data" for a populated demo
# ...or start pre-populated: METERVAULT_SEED=true docker compose -f deploy/docker-compose.yml up -d
# API docs at http://localhost:8080/swagger
Configuration is via environment variables (Section__Key double-underscore mapping), e.g.:
| Variable | Purpose |
|---|---|
ConnectionStrings__Default |
PostgreSQL/Timescale connection string |
MeterVault__TimeZone |
Local timezone for buckets/display (default Europe/Berlin) |
MeterVault__ApiKeys__0 |
An API key accepted on the X-Api-Key header |
MeterVault__AllowAnonymousApi |
true to open the REST API without a key (trusted LAN only) |
MeterVault__ReverseProxyTrust |
true to honour X-Forwarded-User behind an auth proxy |
MeterVault__EnableLiveIngestion |
false to disable the MQTT/HA workers |
MeterVault__SeedReferenceData |
true to load the bundled demo dataset on first start (idempotent) |
The REST API is closed by default: with no ApiKeys configured and AllowAnonymousApi off, it
returns 401. Set at least one API key (or open it explicitly for a trusted network).
Secrets (broker/HA tokens) are never stored in the database — endpoint configs hold the name of an environment variable, resolved at runtime.
Pushing readings (Home Assistant)
curl -X POST http://localhost:8080/api/v1/readings \
-H "X-Api-Key: $METERVAULT_API_KEY" -H "Content-Type: application/json" \
-d '[{"meterId": 1, "time": "2026-01-01T12:00:00Z", "value": 47200}]'
See docs/wiring.md for wiring up Tasmota, MQTT and Home Assistant.
Development
dotnet build
dotnet test # integration tests spin a TimescaleDB via Testcontainers (needs Docker)
dotnet test tests/Core.Tests # fast unit tests, no Docker
dotnet run --project src/App
Architecture, project layout and conventions live in CLAUDE.md.
Releasing
Edit the VERSION file on master; Gitea Actions tags vX.Y.Z and builds/pushes a
multi-arch image to the Gitea container registry (.gitea/workflows/). Locally:
pwsh deploy/build-and-push.ps1 -Registry git.finalfactory.de -Image finalfactory/metervault -Push.
Requires a Docker-capable act_runner; the image build itself is self-contained.
License
Not yet chosen (see SDD §14). Add a LICENSE before the first public tag.