144 lines
7.4 KiB
Markdown
144 lines
7.4 KiB
Markdown
|
|
# Home Assistant configs-as-code — design decisions
|
||
|
|
|
||
|
|
Status: **skeleton only**. No deploy path is implemented yet — this document
|
||
|
|
and the accompanying structure/import tooling are phase 0/1 scaffolding.
|
||
|
|
See `docs/backlog.md` for the tracking entry.
|
||
|
|
|
||
|
|
## Phasing
|
||
|
|
|
||
|
|
| Phase | Scope |
|
||
|
|
|---|---|
|
||
|
|
| **0 — Snapshot** | Import-only tooling (this skeleton). Pull `/config` + `.storage` from each instance into the repo, read-only. No deploy, no write path back to HA. |
|
||
|
|
| **1 — Repo + CC** | Repo is the reviewable source of truth. Changes are authored in the repo (by a human or Claude Code) and pushed manually via the docker-exec / api adapters described below. Deploy has a hard drift-abort (see Sync model). |
|
||
|
|
| **2 — MCP read-only** | Expose HA state (entities, areas, config) to agents via an MCP server in read-only mode — either a self-hosted MCP or `hass-mcp` — to let agents reason about the live instance without touching import/deploy paths. Undecided which (see Open questions). |
|
||
|
|
| **3 — Agents** | Agents propose changes (automations, scripts, scenes) through the same reviewable repo path used by humans; the human-in-the-loop approval flow from `services/control-plane/` (pending → approved → executed) governs anything destructive. Telegram becomes a first-class interface alongside CC. |
|
||
|
|
|
||
|
|
Each phase is a hard gate: no phase-N tooling depends on phase-(N+1) existing.
|
||
|
|
|
||
|
|
## Scope
|
||
|
|
|
||
|
|
Full `/config` tree per instance, plus a curated export of `.storage/*` JSON
|
||
|
|
files that are meaningfully version-controllable (registries, dashboards,
|
||
|
|
helpers) — not secrets, tokens, or runtime databases. See `.gitignore` below
|
||
|
|
for the exact exclusion list.
|
||
|
|
|
||
|
|
## Canonical format
|
||
|
|
|
||
|
|
All YAML committed to the repo passes through one normalization function
|
||
|
|
(`scripts/ha/lib/normalize.py`), used by every import path with no
|
||
|
|
per-caller variation:
|
||
|
|
|
||
|
|
- block style (no flow/inline collections)
|
||
|
|
- fixed indent width 2
|
||
|
|
- keys sorted (stable diffs)
|
||
|
|
- unlimited line width (no wrapping)
|
||
|
|
- UTF-8, no BOM
|
||
|
|
|
||
|
|
`.storage/*` files are JSON on disk in HA; on import they are parsed as JSON
|
||
|
|
and re-emitted through the same YAML normalizer so the whole repo — config
|
||
|
|
and storage-export alike — has one diff format.
|
||
|
|
|
||
|
|
## Deploy path: adapter per instance
|
||
|
|
|
||
|
|
There is no single deploy mechanism — each HA instance gets an adapter
|
||
|
|
behind a common interface (`import.sh`/eventual `deploy.sh <instance>`):
|
||
|
|
|
||
|
|
| Instance | Adapter | Why |
|
||
|
|
|---|---|---|
|
||
|
|
| `ken` (piha, container `homeassistant5`) | **docker-exec over SSH** | No HA API port reachable from where imports run today; container filesystem is reachable via `ssh oskar@piha "docker exec homeassistant5 ..."`. See `hosts/piha/README.md`. |
|
||
|
|
| `chelsty-ha` | **api** | Reachable over Tailscale at `100.70.180.90:8123` (confirmed working path — `services/ha-diag-agent/DEPLOY.md` already curls this for health checks). Config-as-code deploy will reuse the same reachability, calling the HA REST/websocket API rather than shelling into the container. |
|
||
|
|
|
||
|
|
**Open**: a `file` adapter (direct bind-mount / SSH `rsync` to the config
|
||
|
|
directory, bypassing `docker exec`) is worth revisiting once SSH access to
|
||
|
|
the `chelsty-ha` VM itself is verified — see Open questions.
|
||
|
|
|
||
|
|
## Sync model: bidirectional, asymmetric safety
|
||
|
|
|
||
|
|
- **Deploy** (repo → instance): hard **drift-abort**. Before writing, the
|
||
|
|
adapter re-imports the instance's current state and diffs it against the
|
||
|
|
last-known-imported snapshot committed in the repo. Any unexpected
|
||
|
|
difference aborts the deploy with a non-zero exit and a diff printed —
|
||
|
|
never silently overwrites live drift. This applies unconditionally on
|
||
|
|
PIHA (`ken`); untested/undecided whether it should be relaxed for
|
||
|
|
`chelsty-ha` given its intermittent LTE uplink (see Open questions).
|
||
|
|
- **Import** (instance → repo): the reverse direction is intentionally
|
||
|
|
permissive. Running `import.sh <instance>` on a dev workstation pulls
|
||
|
|
current state and, if it differs from the last commit, the operator
|
||
|
|
commits it under a conventional message: `drift(<instance>): <summary>`.
|
||
|
|
This is how out-of-band UI changes (made directly in the HA web UI) get
|
||
|
|
captured back into git history instead of being silently overwritten by
|
||
|
|
the next deploy.
|
||
|
|
|
||
|
|
## Validation gate
|
||
|
|
|
||
|
|
`check_config` (HA's built-in config validator, invoked via the running
|
||
|
|
container/instance — `docker exec homeassistant5 python -m homeassistant
|
||
|
|
--script check_config -c /config` or the equivalent over the api adapter)
|
||
|
|
is a mandatory gate before any deploy. A failing `check_config` blocks the
|
||
|
|
deploy entirely; it is not a warning.
|
||
|
|
|
||
|
|
## Change classification: reload vs. restart
|
||
|
|
|
||
|
|
Deploys default to the least disruptive mechanism:
|
||
|
|
|
||
|
|
- **reload** — default for anything HA exposes a reload service for
|
||
|
|
(automations, scripts, scenes, input_* helpers, template entities, etc.)
|
||
|
|
via `homeassistant.reload_config_entry` / domain-specific `*.reload`
|
||
|
|
services.
|
||
|
|
- **restart** — only when the changed file requires it (e.g.
|
||
|
|
`configuration.yaml` core changes, new integrations, `.storage`
|
||
|
|
registry edits) **and** only when the deploy is invoked with an explicit
|
||
|
|
`--restart` flag. No implicit restarts, ever — an unattended restart on
|
||
|
|
`chelsty-ha` during a period of LTE unavailability would leave the site
|
||
|
|
without automation until someone is physically present.
|
||
|
|
|
||
|
|
## Split + normalization
|
||
|
|
|
||
|
|
On import, list-of-object YAML files are split one-object-per-file so diffs
|
||
|
|
stay scoped to what actually changed:
|
||
|
|
|
||
|
|
- `automations.yaml` → `config/<instance>/automations/<id>.yaml`
|
||
|
|
- `scripts.yaml` → `config/<instance>/scripts/<key>.yaml`
|
||
|
|
- `scenes.yaml` → `config/<instance>/scenes/<id>.yaml`
|
||
|
|
|
||
|
|
Every other `*.yaml` file under `/config` is copied through the normalizer
|
||
|
|
1:1 (same relative path, same filename). Non-YAML files under `/config`
|
||
|
|
(binaries, databases, `secrets.yaml`, logs — see `.gitignore`) are never
|
||
|
|
copied into the repo.
|
||
|
|
|
||
|
|
## Tokens
|
||
|
|
|
||
|
|
- A dedicated `deploy_agent` HA user account (admin rights, **local-only**
|
||
|
|
— never exposed through the public API/ingress) is created per instance,
|
||
|
|
mirroring the existing `diag_agent` account pattern documented in
|
||
|
|
`services/ha-diag-agent/DEPLOY.md`. Reusing `diag_agent` is explicitly
|
||
|
|
rejected — deploy tooling and the diagnostic agent must be revocable
|
||
|
|
independently.
|
||
|
|
- Long-lived access tokens for `deploy_agent` live at
|
||
|
|
`~/.config/ha-deploy/<instance>.token` **on PIHA** (the control node
|
||
|
|
where import/deploy tooling runs), `chmod 600`.
|
||
|
|
- Tokens are never committed to the repo, never templated into
|
||
|
|
`env.example`-style files, and never logged. Import scripts that need a
|
||
|
|
token to hit `/api/states` fail soft (skip the fixtures step with a
|
||
|
|
message) if the token file is absent, rather than aborting the whole
|
||
|
|
import — see `scripts/ha/import.sh`.
|
||
|
|
|
||
|
|
## Interface
|
||
|
|
|
||
|
|
- Phase 0/1: Claude Code is the interface, with the operator's mobile CC
|
||
|
|
client acting as the bridge for approvals made away from a desk.
|
||
|
|
- Phase 3 (agents): Telegram joins CC as a first-class interface, reusing
|
||
|
|
the existing Telegram bot / approval-queue pattern from
|
||
|
|
`services/control-plane/`.
|
||
|
|
|
||
|
|
## Open questions
|
||
|
|
|
||
|
|
- What actually drives the phase-3 operational agent (a new agent process
|
||
|
|
vs. extending an existing one in `services/`)?
|
||
|
|
- Own minimal MCP server vs. adopting `hass-mcp` for phase 2 read-only
|
||
|
|
access — tradeoffs not yet evaluated.
|
||
|
|
- Whether SSH access to `chelsty-ha` itself (not just its HA API) is
|
||
|
|
available/reliable enough to justify a `file` adapter there, which would
|
||
|
|
let phase-1 tooling treat `chelsty-ha` more like `ken` for drift-checking
|
||
|
|
purposes.
|