Skip to content
Back to skills

Hass Declarative

ASecurity

Manages Nix-owned HA automations, scenes, scripts, and entity identity. Use for domain edits, ensureEnabled assertions, or orphan-sweep debugging.

  • 81 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 8, 2026
toolspythongobashdebuggingapi

Works with

  • api

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned September 8, 2026

npx -y skills add edmundmiller/dotfiles --skill hass-declarative --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Hass Declarative?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Hass Declarative
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/edmundmiller-hass-declarative/badge)](https://www.skillsdirectory.com/skills/edmundmiller-hass-declarative)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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 |

Files in this skill

  • SKILL.md7.2 KB
  • references/entity-lifecycle.md3.3 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.