--- okf: "0.1" type: runbook visibility: private status: active updated: 2026-07-22 links: - ../services/home-assistant.md --- # home-assistant — import, deploy, testy ## Import ```bash scripts/ha/import.sh ken scripts/ha/import.sh ken-legacy ``` Read-only, and idempotent for both adapters — re-running against an unchanged instance produces no diff. **`docker-exec` adapter** (`ken-legacy`): pulls the whole `/config` tree over `ssh ... docker exec ... tar`, filters it through `.gitignore`, normalizes and splits it into `config//`, exports curated `.storage/*` into `storage-export//`. **`api` adapter** (`ken`, `chelsty-ha`): for instances with no filesystem/SSH access (HAOS has none). Pulls automations/scripts/scenes via one REST GET per object (`/api/config//config/`) and dashboards/area+entity registries/`input_*` helpers via the WebSocket API (`/api/websocket`) — read-only commands only, nothing that mutates the live instance. Full `/config` import is out of scope for this adapter; see DESIGN.md, "Deploy path: adapter per instance". Both adapters also write a dated `/api/states` fixture snapshot to `fixtures/` if a deploy token exists at `~/.config/ha-deploy/.token`. For the `api` adapter, that token is not optional — see "Tokens" below. ### Tokens The `api` adapter has no filesystem fallback, so a missing or empty token at `token_path` (see `instances.yaml`) is a hard error, not a soft-skip — `import.sh` aborts immediately with a clear message rather than silently producing an empty import. See `DESIGN.md`, "Tokens", for how the `deploy_agent` account and its long-lived access token are provisioned. The token itself never touches a subprocess argv or a log line: REST calls go through `scripts/ha/lib/ha_api.py`, which reads the token file itself and sets the `Authorization` header in-process via `requests` (never `curl -H`, which would put the token in that process's argv, visible to any local user via `ps`). `requests` (`python3-requests`) is a very common preinstalled/ transitive package on Debian-based nodes; if it's ever missing, `import.sh` fails with Python's own `ModuleNotFoundError` — install it the same way as `python3-websocket` below (`sudo apt install python3-requests` or `pip install --user requests`). ### WebSocket dependency The dashboard/registry/helper export (`scripts/ha/lib/ha_ws.py`) depends on the `websocket-client` PyPI package (import name `websocket`) — not in the stdlib, not installed by default. Install one of: ```bash sudo apt install python3-websocket # Debian/Ubuntu package name # or pip install --user websocket-client ``` If it's missing, `import.sh ken` still imports automations/scripts/scenes (REST-only, no extra dependency needed) and then reports the dashboards/registries/helpers step as skipped with an actionable message naming the package to install — it does not crash with a raw traceback, and it does not silently produce an incomplete `storage-export/` without saying so. ## Deploy ```bash scripts/ha/deploy.sh ken --dry-run # plan only, read-only scripts/ha/deploy.sh ken --dry-run automations/111.yaml # plan for one object scripts/ha/deploy.sh ken # deploy everything in scope scripts/ha/deploy.sh ken scripts/notify_email_ntfy.yaml # deploy one object ``` Repo -> instance, `api` adapter only, automations/scripts/scenes only (dashboards/helpers are WebSocket-only — `ha_ws.py` has no mutating command, deliberately, and that write path doesn't exist yet). Refuses any instance with `status != active` (see `instances.yaml`) or an adapter other than `api` — no override flag. Hard sequence, per `DESIGN.md`'s "Sync model" and "Validation gate", any failure aborts before the next step: 1. **drift-check** — re-imports the instance's current state and diffs it against the last commit (`HEAD`) for every object *not* being deployed this run. Any difference aborts with a diff printed; drift is never silently overwritten. 2. **validate** — local sanity (YAML parses, required keys present per domain) plus a live `check_config` gate on the instance. 3. **write** — one `POST /api/config//config/` per object. 4. **verify** — GETs the same object back and compares it to what was written. A mismatch is reported (which object, what differs); there is no auto-rollback. `--dry-run` runs steps 1-2 (both read-only in effect) and stops before any write, printing the same plan a live run would act on. ## Tests ```bash scripts/ha/tests/test_split_normalize.sh scripts/ha/tests/test_normalize_tags.sh scripts/ha/tests/test_import_api_offline.sh scripts/ha/tests/test_deploy_api_offline.sh ``` All offline — no network, no HA instance required. `test_import_api_offline.sh` covers the `api` adapter: normalization of real-shaped API responses (saved as fixtures under `tests/fixtures/`), idempotency, stale-file cleanup, and the missing-`websocket-client` error path (via `sys.modules` injection, not an actual network call). `test_deploy_api_offline.sh` covers the deploy write path: a clean deploy, drift-abort, local validation rejecting broken/ unparseable YAML, and verify detecting a post-write mismatch — using a real temporary git repo for the `HEAD` comparison and a fake in-memory client for `get`/`post_config`/`check_config` (no network).