
UniFi Events in Home Assistant
A custom AppDaemon app and Lovelace card that brings UniFi Protect detection thumbnails into Home Assistant — with instant updates and minimal overhead.
Justin Wyne / March 23, 2026
I run UniFi Protect cameras around my house. The UniFi app has a clean home screen that shows a live grid of the most recent AI detection thumbnails — persons, vehicles, animals, packages — with fuzzy timestamps. It's the first thing I check when I hear something outside.
My Home Assistant dashboard runs on a Google Nest Hub in the kitchen. I wanted that same at-a-glance detection feed there, without opening the UniFi app. This is what I built.
How It Works
There are two pieces:
- AppDaemon app — a Python script that connects directly to the UniFi Protect API, fetches recent detection thumbnails, and writes them to disk along with a JSON manifest.
- Custom Lovelace card — a vanilla JS web component that reads that manifest and renders the detection grid. It watches a Home Assistant sensor for state changes to trigger instant refreshes.
The card never talks directly to UniFi Protect. It only reads a static JSON file and serves images that AppDaemon already downloaded. This keeps the card simple and fast.
Event-Driven Updates
The naive approach would be to poll UniFi Protect on a timer. The problem: thumbnails take a few seconds to process after a detection fires. If you poll too frequently you burn resources; too infrequently and you miss the moment.
Instead, the AppDaemon app listens to binary sensors in Home Assistant — the ones that fire when Protect detects a person, vehicle, etc. When a sensor triggers:
- A placeholder entry is injected into the JSON feed immediately, so the card shows an icon within seconds.
- After a configurable delay (trigger_delay, default 10s), the app fetches from the Protect API, where the thumbnail should now be ready.
- If it still isn't ready, it continues fast-polling every few seconds for up to a minute.
The result: a new thumbnail usually appears on the card within 15–20 seconds of the actual detection, without hammering the API.
Performance on Low-End Hardware
The Google Nest Hub is not a powerful device. A few design decisions keep the card running smoothly on it:
- No framework. The card is plain vanilla JS — no React, no Vue, no build step. It's a single file you drop in your www folder.
- DOM patching, not re-rendering. The grid cells are created once at build time. On each update, _patch() updates only the src attribute or placeholder visibility. No nodes are destroyed or recreated.
- Cache-busting on fetch. The JSON manifest is fetched with ?_t=<timestamp> to bypass the browser cache, so stale data never sits around.
- Entity-triggered refresh. The card listens to a single HA sensor (sensor.unifi_detections_updated). When its state changes, the card fetches. No polling loop inside the card.
The card is fast enough that I run it on the Nest Hub full-time with no performance issues.
The Card
Tap the card to open a lightbox with a larger grid of recent detections. Tap anywhere or press Escape to close.
Each cell shows the detection thumbnail with a fuzzy age label ("now", "3 m", "2 h"). If a thumbnail isn't ready yet — because the detection just fired — it shows an icon for the detection type instead.
Card configuration is minimal:
1type: custom:unifi-events-card2url: /local/unifi_events/recent.json3entity: sensor.unifi_detections_updated4count: 35lightbox_count: 96cols: 37refresh_interval: 300
| Option | Default | Description |
|---|---|---|
| url | required | Path to recent.json served by AppDaemon |
| entity | — | HA entity to watch for instant refresh triggers |
| count | 3 | Thumbnails shown in the main grid |
| lightbox_count | 6 | Thumbnails shown in the lightbox |
| cols | 3 | Columns per row |
| refresh_interval | 300 | Fallback polling interval in seconds |
The AppDaemon App
AppDaemon runs as an add-on inside Home Assistant. The app connects to the UniFi Protect API using the uiprotect Python library, fetches recent smart detection events, downloads each thumbnail, and writes a recent.json manifest to the HA www directory (so the card can read it over HTTP).
It also maintains the sensor.unifi_detections_updated entity in HA, updating its state timestamp whenever a fresh fetch completes. That state change is what triggers the card to reload.
1recent_detections:2module: recent_detections3class: RecentDetections45host: !secret unifi_protect_host port: 443 username: !secret6unifi_protect_username password: !secret unifi_protect_password verify_ssl:7false89hours: 2 # how far back to search each run count: 6 # max thumbnails in the feed10interval: 300 # seconds between scheduled runs1112trigger_delay: 10 # seconds after sensor fires before fetching13trigger_poll_interval: 5 # seconds between fast polls trigger_poll_count: 12 #14max fast polls (12×5s = 60s window)1516# trigger_sensors:1718# - binary_sensor.cam_person_detected1920# - binary_sensor.cam_animal_detected2122output_dir: /homeassistant/www/unifi_events web_root: /local/unifi_events23
The trigger_sensors list should match the binary sensors that your UniFi Protect integration exposes — one per camera and detection type. When any of them flips to on, the fast-trigger flow kicks in.
All configuration options:
| Option | Default | Description |
|---|---|---|
| host | required | UniFi Protect host IP |
| port | 443 | HTTPS port |
| username / password | required | Local admin credentials |
| verify_ssl | false | SSL verification |
| hours | 2 | How far back to look for events |
| count | none | Max thumbnails to keep in feed |
| types | all | Filter to person, animal, vehicle, package |
| interval | 300 | Seconds between scheduled fetches |
| trigger_delay | 120 | Seconds to wait after a sensor trigger |
| trigger_poll_interval | 5 | Seconds between fast polls after trigger |
| trigger_poll_count | 12 | Max fast polls before giving up |
| trigger_sensors | [] | HA binary sensors to watch |
| output_dir | — | Where to write thumbnails and JSON |
| web_root | — | URL prefix for thumbnail paths in JSON |
Installation
Prerequisites
- HACS installed in Home Assistant
- AppDaemon add-on installed via Settings → Add-ons → Add-on Store
- AppDaemon app discovery enabled in HACS: Settings → Devices & Services → HACS → Configure → enable AppDaemon apps discovery
Step 1 — Point AppDaemon at the HACS app directory
Edit the AppDaemon add-on configuration file:
12/mnt/data/supervisor/addon_configs/a0d7b954_appdaemon/appdaemon.yaml3
Set app_dir to:
1app_dir: /homeassistant/appdaemon/apps
This only needs to be done once.
Step 2 — Add Python dependencies
In the AppDaemon add-on configuration, add:
1python_packages:2- uiprotect3- aiofiles
Step 3 — Install via HACS
- HACS → three-dot menu → Custom repositories
- Paste https://github.com/wyne/ha-unifi-events, category AppDaemon, click Add
- Find UniFi Recent Detections in HACS and click Download
HACS places the app at /homeassistant/appdaemon/apps/recent_detections/.
Step 4 — Install the custom card
- Download unifi-events-card.js from the repo and copy it to /homeassistant/www/
- Settings → Dashboards → three-dot menu → Resources
- Add resource → URL: /local/unifi-events-card.js → type: JavaScript module
Step 5 — Add credentials to secrets.yaml
1unifi_protect_host: 192.168.1.12unifi_protect_username: localadmin3unifi_protect_password: your_password_here
Step 6 — Configure apps.yaml
Paste the recent_detections block into /homeassistant/appdaemon/apps/apps.yaml. Adjust trigger_sensors to match your camera binary sensor entity IDs.
Step 7 — Restart AppDaemon
After restart, check the AppDaemon log. You should see:
1Starting apps: ['recent_detections', ...]2Connected. Fetching events from the last 2h...3Event feed saved -> /homeassistant/www/unifi_events/recent.json (6 entries)
Step 8 — Add the card to your dashboard
Switch your dashboard to edit mode, add a Manual card, and paste the YAML config from above.
Local Testing
The AppDaemon app also runs as a standalone CLI script — useful for validating your credentials and checking what events come back before setting everything up in HA.
1pip install -r requirements.txt2cp local_config.example.py local_config.py3# edit local_config.py with your credentials45cd apps/recent_detections6python3 recent_detections.py --count 678# then serve the output and open the test card:9cd ../..10python3 -m http.server 808011# open http://localhost:8080/test_card.html
Supported flags: --hours, --count, --web-root, --types.
Source
The full source is on GitHub: wyne/ha-unifi-events. Issues and PRs welcome.