Solar-Live shows my private PV system on the web in real time – without anything reaching into the home network from outside. Home Assistant pushes the readings every 5 minutes, encrypted, to a Cloudflare Worker that validates them, integrates them into daily totals and stores them in a D1 database. The browser fetches the prepared data from two lean, read-only JSON endpoints.
The chain is deliberately simple: no servers, no inbound port, no third-party frameworks – just Home Assistant, a Worker at the edge and static HTML with vanilla JavaScript.
The connection only ever goes from inside to outside – the home network stays completely closed; there is no port forwarding and no VPN access for the website.
Template sensors bundle PV power, house consumption, grid import/export, battery, wallbox/car and the solar forecast into a JSON payload. Every 5 minutes an automation calls a rest_command that sends the payload over HTTPS to the ingest endpoint – authenticated with a bearer token from
secrets.yaml.
The Worker runs in the domain’s Pages project (advanced mode). It checks the token, normalises the values (including comma decimal separators), integrates power into daily kWh using the trapezoidal rule and writes three things atomically as one batch: the latest state, a 5-minute chart point and the daily aggregate.
power_latest holds exactly one row with the latest payload,
power_chart the 5-minute history (kept forever, no pruning),
power_day the summed-up daily values including import/export and battery charge/discharge. D1 instead of KV, because the KV free tier of 1,000 writes/day is not enough for periodic pushes.
A single static HTML page without frameworks. Charts (daily curve, energy flow animation) are rendered as SVG directly in the browser. Polling is tied to the push interval: after each response the page schedules the next fetch for last push + 5 min + 20 s – so new values show up seconds after they arrive, with no empty requests in between.
| Endpoint | Method | Purpose |
|---|---|---|
/api/power/ingest | POST, bearer | Accepts the readings payload from Home Assistant, integrates and stores it. |
/api/power/latest | GET, public | Latest state plus daily balance (kWh, self-sufficiency, self-consumption) and a stale flag. |
/api/power/series?h=24 | GET, public | Time series for the chart; aggregated in SQL to at most ~600 points, cached at the edge. |
Push, not pull. The home network initiates every connection itself. That removes port forwarding, DynDNS and the entire attack surface of a Home Assistant reachable from outside.
5-minute interval, fetched adaptively. The source only delivers new values every 5 minutes – polling faster only creates load without information. Stale detection only kicks in after two missed pushes (11 min), so single dropouts stay invisible.
Integration at the edge, not at home. Daily kWh are computed from the power readings in the Worker using the trapezoidal rule. Home Assistant does not need to keep long-term statistics for that, and the balance (PV + import − export = consumption) adds up to within 0.01 kWh.
Keep raw data, aggregate on delivery. The 5-minute points are never deleted; for long periods an SQL aggregation condenses them to chart resolution, with the edge cache in front. Storage is cheap, compute time per request is not.
Ready for multi-tenancy. For the planned company version (info panels with their own data source) a sitefield is added to payload and keys – the frontend stays unchanged.