English · Deutsch
Self-hosted web application for recording and analysing your own energy and water consumption. Single-file PHP backend, vanilla-JS SPA frontend, flat-file JSON persistence — no database, no server setup, no external runtime dependencies (other than Chart.js via CDN).
Up to eight utilities in parallel: gas, electricity, water, district heating, heating oil and wood pellets (heating oil/pellets are delivery-based rather than meter-based), plus, since v1.7.0, PV feed-in and PV generation for photovoltaic owners (combined electricity balance, self-sufficiency rate). Per utility: multiple meters with fully modelled meter swaps, an arbitrary contract history with tariff changes, base-price history, advance payments and bonuses. From this the app computes monthly consumption (linearly interpolated resp. energetically balanced), heating degree days against the local climate, five regression models (linear, polynomial, robust, segmented with a data-driven breakpoint, sigmoid), weather adjustment, an efficiency class (kWh/m²·a) and a balance per contract — both the current state and the expected year-end settlement. On top of that: a statistical recommendation engine, reminder/maintenance management, a tariff comparison with shadow contracts and a PDF annual report.
Status: v2.1.5 is the current public version (initial release was v1.0.2). If you want to migrate from a privately run v0.9.0 backup, see Migration from v0.9.0 — the v0.9.0 backup format is supported by the migrator.
📚 Full documentation: the separate technical and functional compendium (installation, API, data model, each utility with formulas, user scenarios, UI reference) lives under
docs/en/.
🌐 Languages: the app interface ships in German, English, French, Italian, Spanish, Portuguese and Dutch, switchable under Settings → Language. The documentation is maintained bilingually (English + German); the compendium is fully available in English as well — German remains canonical and is kept in sync at every release.
- Features
- Documentation
- Quick start
- Data model
- Directory layout
- Migration from v0.9.0
- Further documentation
- Contributing & licence
- Meter-reading table per utility with inline edit, delete, optional per-reading notes and an "estimated" flag for corrections.
- Future readings to pre-record planned billing dates (ignored in the consumption calculation but kept visible).
- Meter swap as a first-class data model: a meter bundles one or more
devices, including serial number, installation date, initial/final counter and
reason. Consumption is computed correctly across swap boundaries
(
(old_final − previous_reading) + (current_reading − new_initial)). - Multiple meters per utility with independent contracts (e.g. main meter + garden-water sub-meter).
- Temperature import as CSV (format
DD.MM.YYYY"avg"min"max, double-quote-separated) or via Open-Meteo sync using the location coordinates in the settings. - CSV import of readings per meter: read a file of
date;reading;note;estimated— existing readings on the same date are overwritten and reported in the result.
- Contract history per meter: provider, tariff name, start/end, free-text note.
- Date-accurate price history for working price (ct/consumption unit), base price (€/month) and the monthly advance (€). Several effective dates per contract are applied to the months as a forward fill.
- Bonuses with credit date, amount and label (new-customer, loyalty bonus, etc.).
- Balance calculation per contract:
- Current balance = costs incurred so far − advances paid so far
- Expected year-end balance = current balance + (remaining months × avg monthly cost − monthly advance). Open contracts are projected to the next settlement date (configurable per utility, default 1 January).
- Verdict: refund / surcharge / balanced with a ±5 € threshold
- Contract-end reminder: contracts whose end falls within a configurable window (three levels, default 90 / 30 / 1 days) are shown as a staged hint in the correlation view.
- Heating degree days (HDD) per month against the configured base temperature (default 15 °C, adjustable in the settings).
- Five regression models on HDD vs. consumption:
- Linear: classic OLS fit, fast and robust for evenly heated households
- Polynomial degree 2: captures non-linear effects (e.g. summer base load from hot water)
- Robust (Huber): iteratively reweighted OLS, ignores outliers
- Segmented: two separate linear fits above/below a threshold (typically HDD = 50), distinguishing the heating season from summer load
- Sigmoid: S-curve for households with pronounced saturation at high HDD
- Anomalies: months in which consumption deviates more than 2σ (adjustable) from the model's expected value.
- Forecast over 12 months as an R²-weighted blend of the regression model and the seasonal profile. The cost forecast is contract-based: per month the then-valid working and base price from the contract history is used, plus the projected advance and running balance.
- Year-on-year comparison with monthly deltas: month-by-month difference of the two most recent years, absolute and as a percentage.
- Water saving index
(litres/person/day) / reference × 100with configurable band limits.
- Heating oil & pellets are recorded via deliveries instead of meter readings (date, quantity, price/unit or total amount, supplier, note, "planned" flag). The monthly consumption is energetically balanced and distributed via a base-load share plus HDD weighting.
- Tank stock curve: modelled remaining stock (initial stock + deliveries − HDD-weighted consumption) with a warning threshold. An estimate, not a tank gauge.
- District heating as an additional cumulative, HDD-relevant utility.
- Weather adjustment: "consumed more, or just colder?" — consumption normalised to the long-term calendar-month HDD, plus the regression expectation and the deviation in per cent.
- Efficiency class A+…H from the heating energy demand in kWh/m²·a, configurable band limits, living area/year built/building type maintainable.
- Recommendation engine: seven statistical rule families from your own data, with severity and individually dismissible.
- Sigmoid and auto-segmented regression model in addition to linear/polynomial/robust.
- Reminders & maintenance: recurring reminders (heating service, chimney sweep, meter calibration deadlines …) with a due status.
- Tariff comparison with shadow contracts: compute hypothetical tariffs on your real consumption without affecting the balance/forecast.
- PDF annual report as a file download (dependency-free PDF writer, no composer/mPDF needed).
- Toggleable utilities: hide unused utilities without losing data.
- Sub-meters / series connection (F1006): a meter can sit behind another (e.g. a heat pump behind household electricity). Its consumption is subtracted from the parent meter — no double counting in the total.
- Meter groups (F1006): several meters (off-peak + peak electricity, several wallboxes) can be bundled into a group for the dashboard; a merge wizard combines existing meters. See Meter topology.
- Home Assistant integration (F1009): Home Assistant pushes meter readings
automatically via
POST /api/ingest(idempotent, upsert per day). Optional API token (opt-in) and freely assignable meter aliases. Full guide with use cases: Home Assistant.
- Backup & restore via the UI: a full JSON backup in the new format
(
backup_version: "3.0"), for restoring or moving. - Migration from v0.9.0: an old backup format (
version: "2.1") can be imported directly, either replacing or merging with existing data. See Migration from v0.9.0. - CSV export for the monthly overview, readings and the temperature series — semicolon-separated, UTF-8 with BOM, directly usable in Excel/LibreOffice. Complements the full JSON backup.
- Light/dark toggle on the right in the top bar. Respects
prefers-color-schemeon the first visit, then persists the choice inlocalStorage. - System diagnostics under Settings: PHP version, data directory, write permissions, schema version, number of meters/readings per utility.
- CI pipeline (GitHub Actions): four jobs on every push/PR to
main— PHP syntax lint, the PHPUnit service suite, frontend-API-shape + browser-render against a real backend server, and a Docker image smoke test. Version tags automatically publish the multi-arch image (amd64 + arm64) to GHCR.
The full documentation lives as a compendium under
docs/en/ — split into a technical and a functional part
plus a UI reference with real screenshots of all 12 views:
- 🚀 New here? → Getting started (a guided example from installation to the first forecast) · Use cases (shared flat, smart home, PV, landlord)
- 🔧 Technical: installation · architecture · API · data model · tests · release process → see the compendium index
- 🏠 Home Assistant integration — push meter readings automatically from Home Assistant (token, meter alias, REST command, scenarios) → see the compendium index
- 📚 Functional: fundamentals & formulas · each utility (gas … pellets) · flat scenario · house scenario · glossary → see the compendium index
- 🖥️ UI reference: all views → see the compendium index
The English compendium is complete — see
docs/en/README.md. The German documentation underdocs/remains canonical and is kept in sync at every release.
- PHP ≥ 8.4 (the CLI plus any web server: Apache, nginx, Caddy, or the built-in PHP server for local work)
- A web browser with ES modules (anything from 2020 onwards)
git clone https://github.com/Bingerminger/energietracker.git
cd energietracker
# Local test server (document root = project root)
php -S 127.0.0.1:8080Open http://127.0.0.1:8080 in the browser → the dashboard appears with an empty state.
On first start the app automatically initialises data/meta.json,
data/settings.json, an empty temperatures.json and the utility subfolders
(data/gas/, data/strom/, data/wasser/) including meters.json,
readings.json, contracts.json. The data/ directory must be writable by the
web server.
Reproducible operation as a single container (nginx + php-fpm). Data lives in
the mounted volume ./data.
docker compose up -d # → http://localhost:8080Or without Compose, directly with the published image:
docker run -d --name energietracker -p 8080:80 \
-v "$PWD/data:/data" \
ghcr.io/bingerminger/energietracker:2.1.5Without
--name energietrackerDocker assigns a random name (e.g.thirsty_archimedes). With--namethe container is always calledenergietracker— exactly as when starting viadocker compose.
Logs (JSON Lines) appear via docker logs; configuration via ET_LOG_LEVEL /
ET_LOG_DEST / ET_DATA_DIR. Details:
INSTALL.md → Production: Docker.
- Check Settings → System constants: gas conversion factor (default 11.5 kWh/m³), HDD base temperature (default 15 °C), CO₂ factors, your own location (lat/lon, default Leipzig).
- Open Consumption → Gas/Electricity/Water → ⚙️ Meters and create the first meter (a default device is created automatically). For existing meters, enter a serial number + approximate installation date.
- Consumption → Gas → 📑 Contracts → create the first contract with provider/tariff/start/end, and enter the working price, base price and monthly advance.
- First reading via the
+ Readingbutton. As soon as at least two readings exist, the monthly consumption calculation begins. - Temperatures → sync Open-Meteo for local climate data (or upload a CSV file).
💡 Fastest way (no file system needed): the demo data is also available as a JSON backup under
demo-data/energietracker-demo-backup.json. In an empty Energietracker, upload it via Settings → Backup & restore → Import backup (since v1.7.4 also via the "Load demo data" button). A snapshot is created automatically beforehand.
The repository ships demo data under demo-data/ (Leipzig detached-house
scenario 2023–2026, three utilities, several contracts with tariff changes). To
try it out, copy it into data/:
rm -rf data/gas data/strom data/wasser data/meta.json data/settings.json data/temperatures.json
cp -r demo-data/gas demo-data/strom demo-data/wasser data/
cp demo-data/meta.json demo-data/settings.json demo-data/temperatures.json data/Before copying, back up your own data if needed (Settings → Backup & restore → Download JSON backup).
Everything is stored as JSON under data/. The schema version (1.1.0 — since
v1.3.0; v1.0.3 introduced the water contract model, v1.1.0 added the
delivery-based utilities and reminders.json) is in data/meta.json and in
every exported backup under backup_version.
data/
├── meta.json ← {schema_version, created_at, …}
├── settings.json ← {gas_conversion_factor, hdd_base_temp, …}
├── temperatures.json ← {"YYYY-MM-DD": {avg, min, max}}
├── gas/
│ ├── meters.json ← [{id, name, icon, devices: [...], …}]
│ ├── readings.json ← [{id, meter_id, device_id, date, counter, …}]
│ └── contracts.json ← [{id, meter_id, provider, working_prices: [...], …}]
├── strom/
│ ├── meters.json
│ ├── readings.json
│ └── contracts.json
├── wasser/
│ ├── meters.json
│ ├── readings.json
│ └── contracts.json
└── backups/ ← [optional UI snapshots]
{
"id": "r_abc12345",
"meter_id": "m_gas_main",
"device_id": "d_gas_001",
"date": "2025-04-15",
"counter": 23545.0,
"price_cents": null,
"note": "After heating service",
"is_estimated": false,
"is_future": false
}price_cents is optional and (when set) applied as a forward fill to later
readings — as a fallback when no matching contract exists. In practice you leave
price_cents empty and maintain contracts instead.
{
"id": "m_gas_main",
"name": "Main meter",
"icon": "🔥",
"created_at": "2023-01-01T00:00:00+01:00",
"active": true,
"notes": "",
"devices": [
{
"id": "d_gas_001",
"serial": "GAS-2019-AB7321",
"installed_on": "2019-04-12",
"initial_counter": 0.0,
"removed_on": "2024-08-22",
"final_counter": 18432.5,
"reason": "Calibration period expired"
},
{
"id": "d_gas_002",
"serial": "GAS-2024-CD8945",
"installed_on": "2024-08-22",
"initial_counter": 0.0,
"removed_on": null,
"final_counter": null,
"reason": ""
}
]
}The devices list is chronological. The active one (= most recently installed and
not removed) is the one with removed_on === null. Consumption between two
readings on different devices is bridged correctly via
ConsumptionService::consumptionBetween().
{
"id": "c_gas_004",
"meter_id": "m_gas_main",
"provider": "Vattenfall",
"tariff_name": "Easy Gas 12 — 2026",
"start": "2026-01-01",
"end": "2026-12-31",
"notes": "Price adjustment at the start of 2026",
"working_prices": [{"from": "2026-01-01", "ct_per_kwh": 8.2}],
"base_prices": [{"from": "2026-01-01", "eur_per_month": 10.50}],
"advance_payments": [{"from": "2026-01-01", "amount_eur": 130.00}],
"bonuses": [{"credit_date": "2026-06-30", "amount_eur": 75, "type": "neukunde", "label": "New-customer bonus"}]
}For water the field ct_per_kwh semantically means ct/m³ — the unit is derived
from the utility config (consumption_unit), not from the field name.
end: null corresponds to an open contract. For the balance projection the
effective end is then set to the next settlement date of the respective utility
(settings billing_cycle_anchor_*, default 1 January).
For the full list of configurable values see
docs/technical/04-data-model.md →
Settings (English mirror: docs/en/technical/04-data-model.md).
energietracker/
├── api.php ← 20-line entry point, delegates to src/bootstrap.php
├── index.php ← SPA shell (sidebar + top bar, loads /public/js/app.js)
├── VERSION ← "2.1.5"
├── README.md ← this file (English)
├── README.de.md ← German version
├── CHANGELOG.md
├── LICENSE
├── docs/ ← German compendium (canonical)
│ ├── en/ ← English mirror (complete)
│ ├── API.md ← REST endpoint reference with examples
│ ├── ARCHITECTURE.md ← service map, data-model details, calculations
│ ├── MIGRATION-FROM-V090.md
│ └── screenshots/ ← real PNG screenshots of all views
├── src/ ← PHP backend
│ ├── bootstrap.php ← app container, routing
│ ├── Config/
│ │ └── Utilities.php ← single source of truth for all 6 utilities
│ ├── Http/ ← Router, Request, Response, ErrorHandler
│ ├── Storage/
│ │ ├── JsonStore.php ← LOCK_EX writes, atomic reads
│ │ └── Migrator.php ← bootstrap logic for an empty `data/`
│ ├── Services/ ← 24 services (Consumption, DeliveryConsumption, Forecast, Auth, Ingest, …)
│ └── Controllers/ ← 20 controllers, 1 class per file
├── public/
│ ├── css/ ← tokens.css + app.css + components.css
│ └── js/ ← vanilla-JS SPA
│ ├── app.js ← entry
│ ├── api.js ← fetch wrapper
│ ├── router.js ← hash router
│ ├── state.js ← lightweight cache
│ ├── views/ ← dashboard, utility, meters, contracts, …
│ ├── components/ ← chart, modal, toast
│ └── lib/ ← format, i18n
├── public/locales/ ← language catalogs (de, en, fr, it, es, pt, nl)
├── data/ ← runtime data (gitignored, .gitkeep stubs)
├── demo-data/ ← optional copyable demo dataset
└── scripts/
└── init_data.py ← Python helper for bulk import from Excel
If you want to migrate from a v0.9.0 backup:
- In v0.9.0, export a full JSON backup (format version
2.1with the top-level keysgas,strom,temperatures,settings,contracts). - Install v1.2.0 fresh (see Quick start) or delete the demo data.
- Open Settings → Backup & restore → 📦 Migration from v0.9.0 and upload the JSON file.
- The migration dialog shows what would be imported (readings per utility, contracts, temperatures, settings, warnings, detected meter-swap candidates).
- Choose Replace or Merge → import. Before writing, a safety snapshot of
the current data is created automatically under
data/backups/.
A complete step-by-step guide including schema mapping and error handling is in
docs/MIGRATION-FROM-V090.md (English:
docs/en/MIGRATION-FROM-V090.md).
docs/en/README.md— compendium index (technical · functional · UI), with English/German availabilitydocs/technical/03-api-reference.md— full API reference (German)docs/technical/04-data-model.md— data model & schemas (German)docs/MIGRATION-FROM-V090.md— migration from v0.9.0 (German)CHANGELOG.md— version history
Pull requests welcome. Before larger changes to the data model, please open an issue so that a migration can be planned cleanly.
MIT License — see LICENSE.