homelab-codex-ws/services/home-assistant
oskar f09dcbf10d fix(ha/ken): fix-pack 1 po audycie — alerty wodne, respekt manual, 3am punktowo, klima OFF za auto
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>
2026-07-23 17:17:38 +02:00
..
config fix(ha/ken): fix-pack 1 po audycie — alerty wodne, respekt manual, 3am punktowo, klima OFF za auto 2026-07-23 17:17:38 +02:00
docs docs(ha): audyt automatyzacji ken 2026-07-23 2026-07-23 16:16:58 +02:00
fixtures drift(ken): helpers klimy salonowej (input_* utworzone w UI) + swieze fixtures 2026-07-23 16:12:52 +02:00
storage-export drift(ken): helpers klimy salonowej (input_* utworzone w UI) + swieze fixtures 2026-07-23 16:12:52 +02:00
.gitignore feat(ha): skeleton for Home Assistant configs-as-code subproject 2026-07-21 15:29:39 +02:00
DESIGN.md fix(ha/ken): fix-pack 1 po audycie — alerty wodne, respekt manual, 3am punktowo, klima OFF za auto 2026-07-23 17:17:38 +02:00
instances.yaml feat(ha): cutover ken to 31.7 HAOS, archive legacy container instance 2026-07-22 16:17:18 +02:00
README.md feat(ha): deploy.sh — api write path with drift-abort and verify 2026-07-22 18:31:22 +02:00

home-assistant (configs-as-code)

Status: phase 1 (partial). Read-only import tooling, plus a deploy (repo -> instance) write path for the api adapter's automations/scripts/ scenes scope only (scripts/ha/deploy.sh) — see "Deploy" below. Dashboards, helpers, and the docker-exec adapter have no write path yet. See DESIGN.md for the full phasing, adapter, sync, and validation model, and for the open questions still blocking phase 2/3.

Layout

services/home-assistant/
├── DESIGN.md              # decision registry — read this first
├── instances.yaml         # per-instance adapter/host/token config
├── .gitignore             # excludes secrets/db/log/token paths from every import
├── config/<instance>/     # canonical, normalized /config mirror per instance
├── storage-export/<instance>/  # curated .storage/* export (registries, dashboards)
└── fixtures/              # dated /api/states snapshots

Instances: ken (RPi4/HAOS, LAN 192.168.31.7, api adapter — canonical home instance since the 2026-07-22 cutover), ken-legacy (PIHA, container homeassistant5, docker-exec adapter, archived — pre-migration instance, import only, never deploy), chelsty-ha (Tailscale, api adapter — see instances.yaml and DESIGN.md "Incident log").

Import

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/<instance>/, exports curated .storage/* into storage-export/<instance>/.

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/<domain>/config/<id>) 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/<instance>.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:

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

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/<domain>/config/<id> 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

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).