# 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 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 ` 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 `