# 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, 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. - **Per-energy-type flow pages** (Electricity, Water, …): a **Sankey diagram** of the meter chain — a downstream meter is a *subsection* of an upstream one (main → car, pool, garden, …), arrow thickness ∝ amount, with an auto-computed "Other/unmetered" remainder. Meters can have several upstreams (a merge, e.g. grid + solar → house). - **Admin UI**: full create/edit/delete for energy types, meters (with consumption recompute on mode/baseline change, and cycle-safe upstream-meter wiring), 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) ```bash docker compose -f deploy/docker-compose.yml up -d # open http://localhost:8760 → 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:8760/swagger ``` ## Quick start (Proxmox VE LXC) A community-scripts–style installer builds a self-contained LXC (Debian + PostgreSQL/TimescaleDB + the app as a systemd service). Run **on the Proxmox host**: ```bash bash -c "$(curl -fsSL https://git.finalfactory.de/FinalFactory/MeterVault/raw/branch/master/deploy/ct/metervault.sh)" ``` It asks the standard container questions, optionally loads the demo dataset, and prints the URL (`http://:8760`) plus the generated DB password. Re-run `update` inside the container to pull the latest source and rebuild. It builds from this public Gitea repo (there is no prebuilt tarball — releases ship as a container image). See [`deploy/ct/metervault.sh`](deploy/ct/metervault.sh) and [`deploy/install/metervault-install.sh`](deploy/install/metervault-install.sh). 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) | | `MeterVault__DataProtectionKeyPath` | Where the key ring for UI-entered connector secrets lives (default `/var/lib/metervault/keys`) | | `MeterVault__UpdateCheckEnabled` | `false` to stop the dashboard checking for a newer release | | `MeterVault__AllowInAppUpdate` | `true` to allow updates triggered from the UI/API — grants root-equivalent access to anyone with an API key; see below | | `MeterVault__UpdateCheckUrl` | Tag listing consulted by that check (repoint at a fork; blank also disables it) | 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). > **The web UI has no authentication.** There is no login: anything that can reach the port can read > and change everything, including connectors and their stored secrets. Put it behind a reverse proxy > with auth (Authelia, Traefik forward-auth, …) — `MeterVault__ReverseProxyTrust` then honours the > user header — or keep it on a trusted network. The dashboard compares the running build against the newest tag in the source repository and shows a banner when it is behind. That is a plain GET of a public tag list — nothing about the instance is sent — cached for six hours, and it never blocks or fails a page render. Turn it off with `MeterVault__UpdateCheckEnabled=false`. ### Updating from the UI (opt-in) `MeterVault__AllowInAppUpdate=true` adds an **Update now** button to that banner, and a `POST /api/v1/system/update` endpoint for scripting it from Home Assistant or `curl`: ```bash curl -X POST http://metervault:8760/api/v1/system/update -H "X-Api-Key: $METERVAULT_API_KEY" ``` Both pull the latest source, rebuild, and restart the service — a few minutes during which MeterVault is unavailable. Readings are untouched; ingestion resumes on restart. LXC only: containers are replaced by pulling a new image, and the endpoint reports that rather than pretending. > **Understand what this grants before enabling it.** The updater builds whatever is on the branch, > and the LXC runs MeterVault as **root** — so a valid API key becomes arbitrary code execution on > that host. Three things must all hold before anything runs: the opt-in above, at least one > configured API key, and a caller presenting one. In particular `MeterVault__AllowAnonymousApi` can > **never** reach it — opening reads must not open root — and the UI button asks for the key every > time rather than remembering it, because the UI itself has no login. Leave this off unless the UI > is behind an authenticating proxy or on a network you fully trust. Secrets (broker/HA tokens) are **never** stored in the database as plaintext. Each connector picks one of two forms: the *name* of an environment variable, resolved at runtime, or the secret typed into the admin UI and encrypted at rest under the data-protection key ring. Either way a `pg_dump` or JSON export carries nothing usable. Keep the key ring on persistent storage outside the app directory — the default `/var/lib/metervault/keys` survives an LXC update, and the Compose file mounts a named volume for it. Lose it and every UI-entered secret must be re-entered. The key ring is on disk, so this protects against leaked database content, not against an attacker who already has the host; that is the same trust boundary an environment variable has. ## Pushing readings (Home Assistant) ```bash curl -X POST http://localhost:8760/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 `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.