Agent skill

hass-declarative

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

Stars 53
Forks 5

Install this agent skill to your Project

npx add-skill https://github.com/edmundmiller/dotfiles/tree/main/.pi/skills/hass-declarative

SKILL.md

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[].namescene.<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

Expand your agent's capabilities with these related and highly-rated skills.

edmundmiller/dotfiles

zbench

Benchmark interactive zsh performance with zsh-bench and track regressions. Use when benchmarking shell startup, comparing zsh latency after config changes, investigating slow shell, or running git bisect on performance. Trigger phrases: "benchmark zsh", "shell is slow", "zbench", "zsh-bench", "shell startup time", "profile zsh", "zsh performance".

53 5
Explore
edmundmiller/dotfiles

nix-rebuild

Rebuild nix-darwin/NixOS system after dotfiles changes. Use when config files managed by Nix (lazygit, ghostty, etc.) need to be regenerated, or after editing any .nix file in the dotfiles repo.

53 5
Explore
edmundmiller/dotfiles

hass-config-flow

Interact with Home Assistant via the REST API on a NixOS host. Use when adding integrations, querying entities, managing config flows, creating API tokens, or automating HA setup programmatically. Also covers identifying device protocols (Matter, Zigbee, Thread, HomeKit) from the device registry. Trigger phrases: "add HA integration", "configure home assistant", "query HA entities", "create HA token", "HA REST API", "pair homekit", "set up matter in HA", "add spotify to HA", "is this device zigbee or thread", "what protocol is this device", "move devices to ZHA", "identify matter devices".

53 5
Explore
edmundmiller/dotfiles

agenix-secrets

Create, edit, and wire up agenix-encrypted secrets in this dotfiles repo. Use when adding API keys, tokens, credentials, passwords, or any sensitive values to NixOS host configs. Trigger phrases: "add a secret", "encrypt with agenix", "new age secret", "hide this value", "agenix secret".

53 5
Explore
edmundmiller/dotfiles

linear

Read-only Linear issue access via the Linear GraphQL API.

53 5
Explore
edmundmiller/dotfiles

jut

Jujutsu version control through jut, a human and agentic framework around jj. Use for: check status, view changes, commit work, create branches, push, pull, create PRs, squash commits, reword messages, absorb changes, undo operations, view history. Complements jj — use jut for opinionated workflows, drop into raw jj for everything else.

53 5
Explore

Didn't find tool you were looking for?

Be as detailed as possible for better results