.pi/skills/hass-declarative/SKILL.md
Manage Home Assistant automations, scenes, and scripts declaratively via NixOS modules. Covers adding/editing/removing entities in the domain-based Nix structure, the ensureEnabled wrapper (initial_state enforcement), the sweep service that cleans orphaned entities, entity identity (IDs, slugs, unique_ids), the eval test assertions, and the build-time manifest. Trigger phrases: "add HA automation", "new scene", "new script", "remove automation", "declarative HA", "sweep unmanaged", "entity drift", "ghost entity", "orphaned automation", "HA domain file", "eval-automations test", "hass assertion", "ensureEnabled", "initial_state".
npx skillsauth add edmundmiller/dotfiles hass-declarativeInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
All HA automations, scenes, and scripts are defined in Nix under
modules/services/hass/_domains/. A post-deploy sweep service removes
anything not in the declared set.
_lib.nix ← shared helpers (ensureEnabled)
_domains/ ← domain files (Nix modules)
ambient.nix ← lighting schedules, plant lights
aranet.nix ← CO2 monitoring
conversation.nix ← voice intents
lighting.nix ← adaptive lighting, AL sleep mode
modes.nix ← DND, guest mode, everything_off script
sleep/default.nix ← bedtime flow, wake detection, 8Sleep sync
tv.nix ← TV idle timer, sleep timer
vacation.nix ← presence-based vacation mode
sweep-unmanaged.nix ← extracts declared IDs at build time
sweep-unmanaged.py ← runtime: removes orphans via WS API
_tests/eval-automations.nix ← structural assertions (flake check)
How HA maps Nix config → entity registry:
| Domain | Nix declaration | Entity ID | Unique ID (registry) |
| ---------- | -------------------------------- | ------------------------- | --------------------- |
| automation | id = "winding_down"; | automation.winding_down | Same as id field |
| scene | name = "Good Morning"; | scene.good_morning | HA-generated UUID |
| script | script.everything_off = {...}; | script.everything_off | Same as attribute key |
Scene slugs: HA lowercases the name and replaces spaces/hyphens with underscores. Keep names ASCII-alphanumeric + spaces to avoid slug surprises.
Add to the automation = lib.mkAfter (ensureEnabled [...]) list in the
appropriate domain file. Every automation must have a unique id field
— the sweep service uses it.
Always wrap with ensureEnabled (from _lib.nix) — it injects
initial_state = true so automations re-enable on HA restart. Without it,
HA silently persists "off" state from the entity registry and automations
stay disabled forever. The eval test catches missing wrappers at build time.
{ lib, ... }:
let
inherit (import ../_lib.nix) ensureEnabled;
in {
services.home-assistant.config.automation = lib.mkAfter (ensureEnabled [
{
alias = "Human-Readable Name";
id = "unique_snake_case_id";
description = "What it does";
trigger = { platform = "time"; at = "22:00:00"; };
condition = [];
action = [{ action = "scene.turn_on"; target.entity_id = "scene.foo"; }];
}
]);
}
Individual automations can override with initial_state = false if ever needed
(the // merge gives right-hand precedence), but we never want this in practice.
Add to scene = lib.mkAfter [...]. Scenes use name as their identity.
{
name = "My Scene";
icon = "mdi:icon-name";
entities = {
"light.some_light" = "on";
"switch.some_switch" = "off";
};
}
Add to script = lib.mkAfter {...} (attrset, not list). The attribute
key becomes the entity_id.
my_script_key = {
alias = "Human Name";
icon = "mdi:icon";
sequence = [{ action = "light.turn_off"; target.entity_id = "light.foo"; }];
};
Or directly on config: script.my_key = {...}; (as in modes.nix).
.nix filehey nuc)hass-sweep-unmanaged service auto-removes the orphan from HA's entity registryNo manual cleanup needed. Check sweep results:
hey nuc-service hass-sweep-unmanaged
ssh nuc "sudo journalctl -u hass-sweep-unmanaged --no-pager -n 30"
sweep-unmanaged.nix creates a systemd oneshot that runs after HA starts.
Build time: Evaluates NixOS config to extract:
automation_ids — from haConfig.automation[].idscene_entity_ids — from haConfig.scene[].name → scene.<slug>script_entity_ids — from haConfig.script keys → script.<key>Writes these to a JSON manifest in the Nix store.
Runtime (sweep-unmanaged.py):
agent-automation JWTautomation.* / scene.* / script.* not in the manifest:
platform to avoid removing integration-created entitiesconfig/entity_registry/removeautomations.yaml, scenes.yaml, scripts.yaml)Platform filtering (safety):
platform = "automation" (YAML-sourced)platform = "homeassistant" (YAML-sourced)platform = "script" (YAML-sourced)_tests/eval-automations.nix runs as nix flake check and pre-commit hook.
Tests structural properties:
initial_state = true — catches missing ensureEnabled wrappersAdd assertions when adding automations with critical invariants.
The initial_state assertion is global — it iterates all automations in the
final merged config. No per-automation opt-in needed. If you add an automation
anywhere without ensureEnabled, the build fails:
automation 'My Thing' missing initial_state = true (use ensureEnabled from _lib.nix)
Why this matters: HA's initial_state defaults to "restore from entity
registry." If an automation was ever toggled off in the UI (or the registry
entry drifts), it stays off across restarts — silently. With
configWritable = false, the UI toggle is especially dangerous since there's
no way to re-enable it without redeploying. ensureEnabled + the eval
assertion make this class of bug impossible.
# Check what the sweep would do (dry-run)
ssh nuc "sudo systemctl cat hass-sweep-unmanaged" # see ExecStart paths
ssh nuc "sudo /path/to/python3 /path/to/sweep-unmanaged.py /path/to/manifest.json --dry-run"
# View the build-time manifest
nix eval --json '.#nixosConfigurations.nuc.config.services.home-assistant.config.automation' 2>/dev/null | python3 -c "import json,sys; print([a['id'] for a in json.load(sys.stdin) if a.get('id')])"
# Run eval assertions locally
nix build '.#checks.x86_64-linux.ha-automation-assertions' --dry-run
| File | Contents |
| -------------------------------- | ------------------------------------------------------------------------------------ |
| references/entity-lifecycle.md | How HA stores entities internally, the .storage files, and what "ghost" entities are |
development
Read-only Linear issue access via the Linear GraphQL API.
data-ai
## <!-- Purpose: Teach agents fast day-to-day memory browse/search/read/sync workflows in pi-context-repo. --> name: searching-memory description: > Search, browse, and inspect memory quickly in pi-context-repo. Use when asked to find prior notes, inspect memory files, locate preferences, or sync recent memory updates. Trigger phrases: "search memory", "list memory files", "find in memory", "read memory file", "memory status", "sync memory". --- # Searching Memory Use this workflow for fast
development
Comprehensive guide for initializing or reorganizing agent memory into a deeply hierarchical file structure. Use when running /init, when user asks to set up memory, or when memory needs a major reorganization. Trigger phrases: "initialize memory", "set up memory", "populate memory", "build my memory", "memory init".
data-ai
Decomposes and reorganizes agent memory files into focused, single-purpose components. Use when memory has large multi-topic blocks, redundancy, or poor organization. Trigger phrases: "defrag memory", "reorganize memory", "clean up memory files", "split memory blocks".