a6edec2b12
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
83 lines
3.8 KiB
Markdown
83 lines
3.8 KiB
Markdown
# 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`](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)
|
|
|
|
```bash
|
|
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__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 |
|
|
|
|
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)
|
|
|
|
```bash
|
|
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`](docs/wiring.md) for wiring up Tasmota, MQTT and Home Assistant.
|
|
|
|
## Development
|
|
|
|
```bash
|
|
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`](CLAUDE.md).
|
|
|
|
## Releasing
|
|
|
|
Edit the [`VERSION`](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.
|