Static page that plots Home Assistant on/off history of the heatpump compressor with pan/zoom, per-phase hover durations and statistics (min/max/mean/median, total on-time, duty cycle) for the visible range. English/German UI. Served by nginx in Docker with basic auth, CSP and a healthcheck; Plotly is vendored locally. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
107 lines
4.1 KiB
Markdown
107 lines
4.1 KiB
Markdown
# Heatpump cycle analysis
|
|
|
|
A small static web app that visualises when a heat pump compressor is running,
|
|
based on a Home Assistant history export (`binary_sensor.*` on/off states).
|
|
|
|
- Interactive timeline (pan, zoom, mouse-wheel, quick ranges 6 h / 24 h / 3 d / 7 d)
|
|
- Hover an on-phase to see its start, end and duration
|
|
- Statistics for the visible range: number of phases, min / max / mean / median
|
|
duration, total on-time, duty cycle, mean pause between phases
|
|
- Table of all on-phases in the visible range
|
|
- English / German UI (auto-detected from the browser, switchable, remembered)
|
|
- Light / dark mode following the OS setting
|
|
- Served by nginx in Docker, protected with HTTP basic auth; no external requests
|
|
(Plotly is vendored in `html/vendor/`)
|
|
|
|
## Quick start
|
|
|
|
```sh
|
|
# 1. put your export next to docker-compose.yml
|
|
cp /path/to/export.csv history.csv
|
|
|
|
# 2. create the login (prompts for the password)
|
|
./set-password.sh admin
|
|
|
|
# 3. start
|
|
docker compose up -d
|
|
```
|
|
|
|
Open <http://localhost:8080> and log in.
|
|
|
|
> Run `set-password.sh` **before** the first `docker compose up`. If
|
|
> `nginx/.htpasswd` does not exist, Docker creates an empty directory in its
|
|
> place and nginx rejects every login. The script removes that directory
|
|
> automatically; restart the container afterwards.
|
|
|
|
### Configuration
|
|
|
|
Set via environment or a `.env` file next to `docker-compose.yml`:
|
|
|
|
| Variable | Default | Meaning |
|
|
|-----------------|-----------------|-----------------------------------|
|
|
| `HEATPUMP_PORT` | `8080` | Host port |
|
|
| `HEATPUMP_CSV` | `./history.csv` | CSV file served as `history.csv` |
|
|
|
|
Change the password with `./set-password.sh <user>` followed by
|
|
`docker compose restart`. Replacing the CSV needs no restart; reload the page.
|
|
|
|
## CSV format
|
|
|
|
Home Assistant's history download (*History → ⋮ → Download data*):
|
|
|
|
```csv
|
|
entity_id,state,last_changed
|
|
binary_sensor.heatpump_compressor,on,2026-09-17T03:00:19.068Z
|
|
binary_sensor.heatpump_compressor,off,2026-09-17T03:08:01.062Z
|
|
```
|
|
|
|
Only the `state` and `last_changed` columns are used; column order doesn't
|
|
matter. A different CSV can also be opened ad hoc with **Load CSV…**. It is
|
|
parsed in the browser and not uploaded.
|
|
|
|
### How phases are computed
|
|
|
|
- An on-phase starts at the first `on` and ends at the next state that is not
|
|
`on` (`off`, `unknown`, `unavailable`, …). Repeated `on` rows are merged.
|
|
- A phase still running at the end of the data is not counted.
|
|
- Statistics include the phases that **start** inside the visible range. Total
|
|
on-time and duty cycle are clipped to the range.
|
|
- `unknown` / `unavailable` periods are shown as gaps in the timeline.
|
|
- All times are shown in the browser's local time zone.
|
|
|
|
## Project layout
|
|
|
|
```
|
|
docker-compose.yml nginx container, volumes, healthcheck
|
|
set-password.sh writes nginx/.htpasswd (apr1 hash via openssl)
|
|
nginx/default.conf basic auth, gzip, CSV alias, /healthz
|
|
nginx/security-headers.conf CSP and other headers, included per location
|
|
html/index.html markup
|
|
html/style.css styles (light/dark tokens)
|
|
html/i18n.js English / German strings
|
|
html/app.js CSV parsing, statistics, chart, UI wiring
|
|
html/vendor/ Plotly 2.35.2 + German locale
|
|
```
|
|
|
|
`nginx/.htpasswd` and `history.csv` are git-ignored.
|
|
|
|
## Operations
|
|
|
|
- **Health:** `GET /healthz` (no auth) returns `ok`; used by the Docker
|
|
healthcheck.
|
|
- **TLS:** the container serves plain HTTP. Basic auth sends credentials in
|
|
clear text, so outside a trusted LAN put it behind a TLS-terminating reverse
|
|
proxy (Traefik, Caddy, nginx, …).
|
|
- **Updating Plotly:**
|
|
```sh
|
|
v=2.35.2
|
|
curl -fLo html/vendor/plotly.min.js https://cdn.jsdelivr.net/npm/plotly.js-dist-min@$v/plotly.min.js
|
|
curl -fLo html/vendor/plotly-locale-de.js https://cdn.jsdelivr.net/npm/plotly.js@$v/dist/plotly-locale-de.js
|
|
```
|
|
|
|
## Adding a language
|
|
|
|
Add an entry to `I18N` in `html/i18n.js` (copy `en`, translate, set `locale`),
|
|
add a `<button data-lang="xx">` in `index.html`, and, for translated chart
|
|
controls, vendor `plotly-locale-xx.js` and include it in `index.html`.
|