- Easy docker push (MQTTower ergonomics + the image push it lacks): VERSION file → version-tag.yml (semver-guard auto-tag) → docker-publish.yml (buildx multi-arch → GHCR, registry centralized for one-line retarget to git.finalfactory.de) + ci.yml + build-and-push.ps1. - Verified end to end: deploy/Dockerfile builds; docker compose stack (app + timescaledb) comes up healthy; /healthz and the dashboard respond in-container. - JSON config export/import (ExportService) with id remapping on restore + GET /export, POST /import endpoints; round-trip test preserves meter→type, meter-scoped tariff, category links. - README, HA/Tasmota/MQTT wiring guide (docs/wiring.md), Unraid template. - CLAUDE.md updated to reflect the built codebase. - i18n: locale-aware number/currency formatting (de-DE); full de UI string localization deferred. 95 tests green (56 Core + 39 integration). Claude-Session: https://claude.ai/code/session_01WujdMtMJPbxDpDnMeK22rr
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, meter list, one-click reference-data load, CSV dry-run.
- 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
# 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__ReverseProxyTrust |
true to honour X-Forwarded-User behind an auth proxy |
MeterVault__EnableLiveIngestion |
false to disable the MQTT/HA workers |
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 main; CI tags vX.Y.Z and builds/pushes a multi-arch image
(.github/workflows/). Locally: pwsh deploy/build-and-push.ps1 -Push.
License
Not yet chosen (see SDD §14). Add a LICENSE before the first public tag.