Manages Nix-owned HA automations, scenes, scripts, and entity identity. Use for domain edits, ensureEnabled assertions, or orphan-sweep debugging.
Scanned 9/8/2026
Install to Claude Code
npx -y skills add edmundmiller/dotfiles --skill hass-declarative --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Hass Declarative?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/edmundmiller-hass-declarative)More formats (shields.io, HTML) on the badges page.
---
name: hass-declarative
description: Manages Nix-owned HA automations, scenes, scripts, and entity identity. Use for domain edits, ensureEnabled assertions, or orphan-sweep debugging.
---
# HA Declarative Entity Management
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.
## Architecture
```
_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)
```
## Entity Identity
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.
## Adding Entities
### Automation
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.
```nix
{ 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.
### Scene
Add to `scene = lib.mkAfter [...]`. Scenes use `name` as their identity.
```nix
{
name = "My Scene";
icon = "mdi:icon-name";
entities = {
"light.some_light" = "on";
"switch.some_switch" = "off";
};
}
```
### Script
Add to `script = lib.mkAfter {...}` (attrset, not list). The attribute
key becomes the entity_id.
```nix
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`).
## Removing Entities
1. Delete from the domain `.nix` file
2. Deploy (`hey nuc`)
3. `hass-sweep-unmanaged` service auto-removes the orphan from HA's entity registry
No manual cleanup needed. Check sweep results:
```bash
hey nuc-service hass-sweep-unmanaged
ssh nuc "sudo journalctl -u hass-sweep-unmanaged --no-pager -n 30"
```
## Sweep Service
`sweep-unmanaged.nix` creates a systemd oneshot that runs after HA starts.
**Build time:** Evaluates NixOS config to extract:
- `automation_ids` — from `haConfig.automation[].id`
- `scene_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`):
1. Waits for HA to be ready (120s timeout)
2. Connects via WebSocket, authenticates with `agent-automation` JWT
3. Lists all entity registry entries
4. For each `automation.*` / `scene.*` / `script.*` not in the manifest:
- Checks `platform` to avoid removing integration-created entities
- Removes from entity registry via `config/entity_registry/remove`
5. Wipes UI YAML files (`automations.yaml`, `scenes.yaml`, `scripts.yaml`)
**Platform filtering** (safety):
- Automations: only removes `platform = "automation"` (YAML-sourced)
- Scenes: only removes `platform = "homeassistant"` (YAML-sourced)
- Scripts: only removes `platform = "script"` (YAML-sourced)
- Integration-created entities (Ecobee scenes, etc.) are never touched
## Eval Test Assertions
`_tests/eval-automations.nix` runs as `nix flake check` and pre-commit hook.
Tests structural properties:
- **Every automation has `initial_state = true`** — catches missing `ensureEnabled` wrappers
- Required automations/scenes exist
- Time guards present on wake detection (the "4:47 AM fix")
- Good Morning has presence-aware conditions
- Winding Down resets awake booleans
Add assertions when adding automations with critical invariants.
### initial_state enforcement
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.
## Debugging
```bash
# 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
```
## References
| File | Contents |
| -------------------------------- | ------------------------------------------------------------------------------------ |
| `references/entity-lifecycle.md` | How HA stores entities internally, the .storage files, and what "ghost" entities are |
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!