Zmiany wyłącznie w repo (config/ken/ automations), deploy LIVE robi operator. - 1752086407230 "dry in Kuchnia": trigger moist -> not_moist (kopiuj-wklej z "moist in Kuchnia", "sucho" nigdy nie przyjdzie). - 1752085965483 "dry in Lazienka": literówka mesaage -> message (skrypt notify_email_ntfy dostawał niezdefiniowane message, akcja się wykładała). - 1764190493305 "after 15 minutes anyway": dodany warunek stanu automation.turn_off_lights_in_kuchania == on, żeby respektować ręczne "Disable AUTO off" (double-click) zamiast unieważniać je po 15 minutach. - 1700832676138 "unconditionally at 3am": time_pattern hours: '3' (60 odpaleń/noc) -> punktowy trigger time: 03:00:00, zgodnie z intencją aliasu. - 1784804668795 "Klima salon: wyłącz...": dodany warunek input_boolean.klima_salon_auto == on dla gałęzi sunset/balkon (automat ubija tylko chłodzenie, którym sam zarządza — ręczne chłodzenie przy zgaszonym auto zostaje nietknięte). Trigger auto_off (zgaszenie klima_salon_auto) ma osobną gałąź choose bez tego warunku — graceful shutdown suszenia parownika działa nawet gdy przełącznik właśnie zgasł. Zweryfikowano na żywym ken: helpery klimy istnieją (input_boolean. klima_salon_auto=off, input_number.klima_salon_temp_docelowa=24.0, input_number.klima_salon_tolerancja=3.0). DESIGN.md: nowa sekcja "Decyzje operatora po audycie 2026-07-23" — co zaimplementowane, co świadomie odłożone (enforcer sleep mode + konsolidacja nocnych wyłączników -> projekt night_mode; OwnTracks / Leave batch 02 / alerty on-leave -> bez zmian). docs/backlog.md: cztery nowe wpisy w Aktywne — guard TRV przed sezonem grzewczym, przycisk graceful shutdown klimy na dashboard, diagnoza wspólnej awarii sprzętowej 2026-07-17 (czujniki ruchu + pilot 4button + xiaomi_miot), projekt architektura night_mode. Walidacja: round-trip scripts/ha/lib/normalize.py na wszystkich 5 zmienionych plików (stabilny), 4 zestawy testów offline scripts/ha/tests/*.sh (wszystkie PASS), dry-run scripts/ha/deploy.sh ken --dry-run per plik na żywym ken (wszystkie: check_config valid, brak driftu, brak abortu). Choinkowe automatyzacje (audyt 4.2) celowo nietknięte w plikach — disable to stan runtime, nie config; operator wyłączy je przez UI. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
12 KiB
Home Assistant configs-as-code — design decisions
Status: phase 1 (partial). scripts/ha/deploy.sh implements the write
path for the api adapter's automations/scripts/scenes scope (see "Deploy
path" and "Sync model" below); dashboards/helpers and the docker-exec
adapter have no write path yet. 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). Partially built: scripts/ha/deploy.sh covers the api adapter's automations/scripts/scenes scope; docker-exec deploy and dashboards/helpers writes are still open. |
| 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 (RPi4, HAOS, LAN 192.168.31.7:8123) |
api | Canonical home instance since the 2026-07-22 cutover (see Incident log). HAOS has no SSH access, so there is no docker exec/filesystem path — only the HA REST/websocket API is reachable. Full /config import is deferred until an alternative access path exists; for now the api adapter's import scope is limited to what the API exposes: automations, scripts, scenes, dashboards. |
ken-legacy (piha, container homeassistant5) |
docker-exec over SSH (archive-only) | Pre-migration container instance, superseded by ken at 31.7 (see Incident log) — same container filesystem access as the old ken entry (ssh oskar@piha "docker exec homeassistant5 ..."). Import only, for historical reference; never a deploy target. |
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 forchelsty-hagiven 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*.reloadservices. - restart — only when the changed file requires it (e.g.
configuration.yamlcore changes, new integrations,.storageregistry edits) and only when the deploy is invoked with an explicit--restartflag. No implicit restarts, ever — an unattended restart onchelsty-haduring 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>.yamlscripts.yaml→config/<instance>/scripts/<key>.yamlscenes.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_agentHA user account (admin rights, local-only — never exposed through the public API/ingress) is created per instance, mirroring the existingdiag_agentaccount pattern documented inservices/ha-diag-agent/DEPLOY.md. Reusingdiag_agentis explicitly rejected — deploy tooling and the diagnostic agent must be revocable independently. - Long-lived access tokens for
deploy_agentlive at~/.config/ha-deploy/<instance>.tokenon 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/statesfail soft (skip the fixtures step with a message) if the token file is absent, rather than aborting the whole import — seescripts/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/.
Incident log
2026-07-22 — two HA instances controlling the house in parallel
Symptom: automations firing twice from a single physical trigger — e.g.
turn_on_led_nad_blatem_1 firing the same day from the same button press,
mirror_on at 04:30 and gniazdka_w_lazience_on at 05:00 all firing on
both instances.
How detected: comparing last_triggered from restore_state across the
two instances showed identical automation IDs firing at the same times on
both — the container on piha (homeassistant5, HA 2026.4.3, location_name
KEN, mounted at /home/pi/homeassistant/config) never actually stopped
running after the migration to the RPi4/HAOS instance at 192.168.31.7; it
stayed alive and MQTT-connected, so both were independently reacting to the
same physical events.
Root cause: instances.yaml had ken pointed at the piha container —
that was the pre-migration instance, not the real one. The actual home
instance had already moved to Home Assistant OS on a dedicated RPi4
(192.168.31.7:8123, ingress ha.kapala.org via NPM, confirmed HAOS via
observer :4357, HACS installed, 118 automations), but the repo never
followed the move.
Decision: 192.168.31.7 (HAOS/RPi4) is canonical ken. The piha
container is renamed ken-legacy in instances.yaml, status: archived.
Plan: archival import for historical reference → docker stop (not rm)
→ one week of observation → decide on docker rm. See docs/backlog.md
for the ha-diag-agent re-pointing and wind-down follow-ups this incident
generated.
Decyzje operatora po audycie 2026-07-23
Zobacz docs/audyt-automatyzacji-2026-07-23.md (sekcja "Do decyzji operatora",
17 punktów) — poniżej wyłącznie decyzje, które doprowadziły do zmian w
fix-pack 1 (task/ha-fix-pack-1) albo świadomie do braku zmian. Reszta
checklisty (baterie/re-pairing czujników, kalibracje TRV, xiaomi_miot,
konsolidacja aliasów, higiena 4.x) zostaje otwarta w docs/backlog.md.
- Pkt 6 (klima salon: sunset ubija też ręczne chłodzenie?) — decyzja:
NIE. „Klima salon: wyłącz…" (
1784804668795) ma teraz respektowaćinput_boolean.klima_salon_auto— gałąź sunset/balkon działa tylko gdyklima_salon_auto = on(automat wyłącza tylko chłodzenie, którym sam zarządza). Wyjątek świadomy: trigger „klima_salon_autoprzechodzi woff" nadal odpala suszenie parownika bez tego warunku (choosez osobną gałęzią) — to jest graceful shutdown samego automatu, nie efekt trybu auto, więc warunekauto = onby go zawsze ucinał (przełącznik właśnie zgasł). Zweryfikowano na żywymken: helpery klimy (klima_salon_auto,_temp_docelowa,_tolerancja) istnieją (pkt 5 checklisty — obawa o martwe helpery z 1.1 nieaktualna). - Pkt 7 (enforcer sleep mode gasi światła cyklicznie całą noc) i pkt 9
(konsolidacja czterech nocnych wyłączników) — bez zmian w tym fix-packu.
Oba wchłania przyszły projekt „architektura night_mode" (patrz
docs/backlog.md) — punktowa łatka tu tylko dodałaby kolejny wariant do już przegęszczonego zestawu nakładających się automatyzacji (audyt 2.2). - Pkt 11 (OwnTracks: przywrócić czy skasować) i pkt 12 (Leave auto on: batch 02 — włączyć z powrotem?) — świadomie bez zmian; obie wymagają decyzji operatora spoza zakresu tego fix-packu (integracja/urlop), nie jednoliniowej poprawki.
- On-leave alerty ruchu (1.2a) — świadomie bez zmian: martwe przez
sprzęt (czujniki
unavailable, pkt 1 checklisty), nie przez logikę automatyzacji; poprawka logiki niczego by nie naprawiła bez wymiany baterii/re-pairingu.
Zaimplementowane w tym fix-packu (bez decyzji operatora — jednoliniowe poprawki oczywistych bugów, audyt 2.6 i 2.3):
1752086407230„dry in Kuchnia": triggermoist→not_moist.1752085965483„dry in Lazienka": literówkamesaage→message.1700832676138„…at 3am":time_pattern hours: '3'→ punktowy triggertime: 03:00:00(było 60 odpaleń/noc, teraz 1).- Pkt 8 (
1764190493305„after 15 minutes anyway" ma respektować ręczne „Disable AUTO off"?) — TAK: dodany warunekstate: automation. turn_off_lights_in_kuchania = on(double-click wyłącza tę automatyzację; „anyway" teraz cofa się przed tym stanem zamiast go unieważniać po 15 min).
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-mcpfor phase 2 read-only access — tradeoffs not yet evaluated. - Whether SSH access to
chelsty-haitself (not just its HA API) is available/reliable enough to justify afileadapter there, which would let phase-1 tooling treatchelsty-hamore likekenfor drift-checking purposes.