Architecture Overview
System diagram
┌─────────────┐ callService ┌──────────────────┐
│ Lovelace │ ──────────────────▶ │ Home Assistant │
│ Cards (JS) │ │ │
│ │ ◀── sensor state ── │ helpers, scripts │
└─────────────┘ │ automations │
└────────┬──────────┘
│
HASS plugin API
(listen_state, call_service,
fire_event, get_state)
│
┌────────▼──────────┐
│ AppDaemon │
│ │
│ apps/ │
│ ├─ door_notify │
│ ├─ detection_* │
│ ├─ photo_frame_* │
│ ├─ dashboard_notify│
│ ├─ calendar_from_*│
│ ├─ immich_fetcher │
│ ├─ health_checks │
│ ├─ media_dashboard│
│ ├─ countdown_app │
│ ├─ school_lunch_* │
│ └─ zigbee_ota │
│ │
│ providers/ │
│ ├─ ai_providers │
│ ├─ ha_provisioner │
│ ├─ photo_providers│
│ ├─ media_providers│
│ ├─ school_menu │
│ ├─ alertmanager │
│ └─ metrics │
└───────────────────-┘
│
External APIs
(OpenAI, Gemini, Ollama,
ComfyUI, Immich,
School Nutrition and Fitness,
Tautulli, TMDb, SerpApi,
Alertmanager)
Data flow paths
| Path | Direction | Mechanism |
|---|---|---|
| Commands (card → app) | Card → HA → AppDaemon | callService("script", "relay") → HA event → listen_event() |
| State (app → card) | AppDaemon → HA → Card | set_state() on sensor → card reads attributes |
| External APIs | AppDaemon → Internet | Provider adapters in providers/ make HTTP calls |
| Provisioning | AppDaemon → HA REST API | ha_provisioner creates helpers/scripts on startup |
| Static asset check | AppDaemon → HA HTTP | Unauthenticated HEAD /local/... to confirm HA is really serving a file an app asked it to stage |
| Checker self-heal | AppDaemon → AppDaemon (admin namespace) | The health-check controller reads the app.<name> entities to learn which checker apps should exist and what state they are in, and calls app/restart on one that never registered |
| Metrics exposition | Prometheus → AppDaemon | The cluster's Prometheus scrapes :9100/metrics, served by the metrics provider for the health-check controller. Apart from AppDaemon's own admin UI and API on port 5050, it is the only inbound HTTP path |
| Entity classification | AppDaemon → HA | render_template() over the device registry — zigbee_ota asks each tick which update.* entities are really Zigbee2MQTT devices, which is what makes a fleet-wide glob safe |
Key concepts
Self-provisioning
Apps create their own HA entities (helpers, scripts) on startup via ha_provisioner. No manual entity setup is needed.
Relay script pattern
Lovelace cards communicate with AppDaemon through relay scripts:
- Card calls
hass.callService("script", "<app>_relay", { command, payload }) - HA script fires an
<app>_commandevent - AppDaemon listens for the event and processes it
This pattern works for non-admin users (unlike fire_event which requires admin).
Health monitoring
The health check system uses the event bus as a decoupling layer between checker apps and a central controller. Checker apps register themselves and report status via HA events; the controller aggregates everything into a single sensor that custom Lovelace cards read. This pattern allows new checkers to be added — often config-only — without modifying the controller. Repair-capable checkers handle their own recovery logic (e.g., smart switch power cycling, config-entry reloads) while the controller only routes those commands. The one recovery the controller does itself is for checkers that never announce themselves. It reads AppDaemon's own list of apps to know which checkers should be present, restarts one that is missing (a few times, with growing waits), and reports it as a critical checker if it stays missing, so a dead checker can't silently hide the checks that depend on it (details). The controller exports every checker's status, repair outcomes and checker-supplied values as Prometheus metrics through the metrics provider, which the cluster scrapes for Grafana history (details). It also mirrors checker status into the cluster's Alertmanager via the alertmanager provider — critical findings page the phone, and recovery resolves the alert once it holds, so a flapping condition pages once per incident rather than once per swing.
Container separation
HA and AppDaemon run in separate Kubernetes pods. They share a /media NFS mount for file exchange. AppDaemon cannot access /config/www/ directly — it uses HA shell_command services for file operations in that directory. Because a shell command's result says little about whether the files actually landed, apps confirm the outcome by requesting the same /local/... URL the dashboard card will load, and only publish it once Home Assistant answers.