Skip to content

Commit a649e24

Browse files
web-flowclaude
andcommitted
docs: add blog post on camera IP auto-sync to Frigate
Documents the full automation pipeline: - nmap discovery via MAC address - HA sensor with ip_state for change detection - K8s webhook for ConfigMap updates - Reloader for automatic pod restarts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 27e0b80 commit a649e24

1 file changed

Lines changed: 237 additions & 0 deletions

File tree

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
# Auto-Syncing Camera IPs to Frigate: A GitOps Approach
2+
3+
**Date**: January 26, 2026
4+
**Author**: Claude + Human collaboration
5+
**Tags**: frigate, home-assistant, gitops, kubernetes, automation, reolink, dhcp, camera
6+
7+
---
8+
9+
## The Problem: Sunday Morning Feed Outages
10+
11+
Every Sunday morning, my Reolink cameras reboot for scheduled maintenance. When they come back up, DHCP sometimes assigns them new IPs. Frigate keeps trying to connect to the old IPs, and I wake up to dead camera feeds.
12+
13+
The "fix" was always the same:
14+
1. Open the Reolink app to find the new IPs
15+
2. SSH into the cluster
16+
3. Edit the Frigate ConfigMap
17+
4. Restart Frigate
18+
5. Wait for streams to reconnect
19+
20+
Manual, annoying, and exactly the kind of thing that should be automated.
21+
22+
## Why Not Just Use DHCP Reservations?
23+
24+
Great question. My AT&T router has DHCP reservations, but they don't persist across router reboots. Every firmware update wipes them. I've re-added them at least 5 times.
25+
26+
The cameras do support static IPs, but that creates a different problem: if I ever change my network configuration, I have to physically access each camera to reconfigure it.
27+
28+
Dynamic IPs with automatic discovery is actually more resilient.
29+
30+
## The Solution: Event-Driven IP Sync
31+
32+
```
33+
┌─────────────────────────────────────────────────────────────────────┐
34+
│ Camera IP Auto-Sync Flow │
35+
├─────────────────────────────────────────────────────────────────────┤
36+
│ │
37+
│ ┌──────────────┐ ┌───────────────┐ ┌──────────────────────┐ │
38+
│ │ Reolink │ │ Home │ │ K8s Cluster │ │
39+
│ │ Cameras │ │ Assistant │ │ │ │
40+
│ └──────┬───────┘ └───────┬───────┘ │ ┌────────────────┐ │ │
41+
│ │ │ │ │ frigate-webhook│ │ │
42+
│ │ DHCP assigns │ │ └────────┬───────┘ │ │
43+
│ │ new IP │ │ │ │ │
44+
│ ▼ │ │ ▼ │ │
45+
│ ┌──────────────┐ │ │ ┌────────────────┐ │ │
46+
│ │ 192.168.1.x │◄──nmap────┤ │ │ ConfigMap │ │ │
47+
│ │ (by MAC) │ scan │ │ │ frigate-config │ │ │
48+
│ └──────────────┘ │ │ └────────┬───────┘ │ │
49+
│ │ │ │ │ │
50+
│ ▼ │ ▼ │ │
51+
│ ┌───────────────┐ │ ┌────────────────┐ │ │
52+
│ │sensor.camera_ │ │ │ Reloader │ │ │
53+
│ │ips (state │ │ │ (watches CM) │ │ │
54+
│ │ change) │ │ └────────┬───────┘ │ │
55+
│ └───────┬───────┘ │ │ │ │
56+
│ │ │ ▼ │ │
57+
│ │ │ ┌────────────────┐ │ │
58+
│ ┌───────▼───────┐ │ │ Frigate │ │ │
59+
│ │ Automation │──POST─┼─►│ (restarted) │ │ │
60+
│ │ (REST cmd) │ │ └────────────────┘ │ │
61+
│ └───────────────┘ │ │ │
62+
│ └──────────────────────┘ │
63+
└─────────────────────────────────────────────────────────────────────┘
64+
```
65+
66+
### Component 1: Camera Discovery via MAC Address
67+
68+
Reolink cameras don't support mDNS/Bonjour. They use a proprietary UDP discovery protocol and ONVIF WS-Discovery. Both are flaky for automated discovery.
69+
70+
What does work reliably? Good old nmap. The cameras have fixed MAC addresses, and nmap can find them:
71+
72+
```python
73+
# camera-ip-sync.py (runs in HA container)
74+
CAMERAS = {
75+
"hall": "0C:79:55:4B:D4:2A",
76+
"living_room": "14:EA:63:A9:04:08",
77+
}
78+
79+
def scan_network(scan_range):
80+
result = subprocess.run(
81+
["nmap", "-sn", scan_range],
82+
capture_output=True, text=True, timeout=120
83+
)
84+
# Parse MAC -> IP mapping from output
85+
...
86+
```
87+
88+
This runs every 5 minutes via HA's `command_line` sensor. The output includes an `ip_state` string that changes when IPs change:
89+
90+
```json
91+
{
92+
"cameras": {"hall": "192.168.1.137", "living_room": "192.168.1.138"},
93+
"ip_state": "hall:192.168.1.137,living_room:192.168.1.138",
94+
"all_found": true
95+
}
96+
```
97+
98+
### Component 2: State-Based Automation Trigger
99+
100+
The key insight: use `ip_state` as the sensor's state value, not just an attribute. This means HA's state change trigger fires exactly when IPs change:
101+
102+
```yaml
103+
# configuration.yaml
104+
command_line:
105+
- sensor:
106+
name: Camera IPs
107+
unique_id: camera_ips_sensor
108+
command: "cat /config/camera_ips.json"
109+
value_template: "{{ value_json.ip_state }}" # State = IP string
110+
json_attributes:
111+
- cameras
112+
- all_found
113+
scan_interval: 300
114+
```
115+
116+
The automation only fires on actual IP changes, not every scan:
117+
118+
```yaml
119+
# automations.yaml
120+
- id: camera_ip_sync_to_frigate
121+
alias: Camera IP Sync to Frigate
122+
trigger:
123+
- platform: state
124+
entity_id: sensor.camera_ips
125+
condition:
126+
# Ignore startup/unavailable states
127+
- condition: template
128+
value_template: "{{ trigger.from_state.state not in ['unavailable', 'unknown', ''] }}"
129+
- condition: template
130+
value_template: "{{ trigger.to_state.state not in ['unavailable', 'unknown', ''] }}"
131+
# Only if state actually changed
132+
- condition: template
133+
value_template: "{{ trigger.from_state.state != trigger.to_state.state }}"
134+
# Only if all cameras found
135+
- condition: template
136+
value_template: "{{ state_attr('sensor.camera_ips', 'all_found') == true }}"
137+
action:
138+
- service: rest_command.update_frigate_camera_ips
139+
```
140+
141+
### Component 3: K8s Webhook for ConfigMap Updates
142+
143+
HA calls a webhook running in the K8s cluster. The webhook patches the Frigate ConfigMap:
144+
145+
```python
146+
# webhook.py (Flask app in K8s)
147+
@app.route("/update", methods=["POST"])
148+
def update_ips():
149+
camera_ips = request.get_json()
150+
# {"living_room": "192.168.1.138", "hall": "192.168.1.137"}
151+
152+
# Patch ConfigMap with regex replacements
153+
# - go2rtc stream URLs: @OLD_IP:554 -> @NEW_IP:554
154+
# - ONVIF hosts: host: OLD_IP -> host: NEW_IP
155+
156+
# Trigger restart via deployment annotation
157+
patch = {"spec": {"template": {"metadata": {"annotations": {
158+
"camera-ips-updated": datetime.utcnow().isoformat()
159+
}}}}}
160+
apps_v1.patch_namespaced_deployment("frigate", "frigate", patch)
161+
```
162+
163+
The webhook has proper RBAC to patch ConfigMaps and Deployments in the frigate namespace.
164+
165+
### Component 4: Reloader for Automatic Restarts
166+
167+
Instead of the webhook triggering restarts, I use [Stakater Reloader](https://github.com/stakater/Reloader). It watches ConfigMaps and automatically restarts pods when they change:
168+
169+
```yaml
170+
# deployment.yaml
171+
metadata:
172+
annotations:
173+
reloader.stakater.com/auto: "true"
174+
```
175+
176+
This is cleaner than manual restart logic and works for any ConfigMap change, not just IP updates.
177+
178+
## The GitOps Angle
179+
180+
Everything is managed through GitOps:
181+
182+
1. **Webhook container** - Built via GitHub Actions, pushed to ghcr.io
183+
2. **K8s manifests** - Deployment, Service, IngressRoute in git
184+
3. **Flux reconciliation** - Changes auto-deploy on push
185+
186+
```yaml
187+
# .github/workflows/build-frigate-webhook.yaml
188+
on:
189+
push:
190+
paths:
191+
- 'gitops/clusters/homelab/apps/frigate-ip-webhook/**'
192+
```
193+
194+
## What I Learned
195+
196+
### 1. State vs Attributes Matter
197+
198+
Initially I had `all_found` as the sensor state. This only triggered when cameras appeared/disappeared, not when IPs changed. Moving the IP string to state fixed it.
199+
200+
### 2. nmap > Proprietary Discovery
201+
202+
I spent hours trying to get Reolink's UDP discovery working from K8s. Broadcast packets don't cross subnets. nmap from the HA container (same L2 as cameras) just works.
203+
204+
### 3. ConfigMap Changes Don't Restart Pods
205+
206+
This surprised me. Kubernetes applies ConfigMap updates, but pods keep running with old values. You need either:
207+
- Reloader (recommended)
208+
- Checksum annotation in pod template
209+
- Manual restart logic
210+
211+
### 4. Webhook > Direct K8s from HA
212+
213+
I considered giving HA direct K8s access via a ServiceAccount token. But:
214+
- More attack surface
215+
- HA doesn't need K8s knowledge
216+
- Webhook is simpler to test and debug
217+
218+
## Future Improvements
219+
220+
1. **Notification on IP change** - Add a mobile push when automation fires
221+
2. **Frigate API validation** - Check streams are actually working after restart
222+
3. **Retry logic** - If webhook fails, retry with backoff
223+
224+
## Files Reference
225+
226+
| File | Purpose |
227+
|------|---------|
228+
| `scripts/haos/camera-ip-sync.py` | nmap scanner, writes JSON |
229+
| `scripts/haos/ha-config-camera-ip-sync.yaml` | HA config template |
230+
| `scripts/haos/automations/camera-ip-sync-to-frigate.yaml` | Automation |
231+
| `gitops/.../frigate-ip-webhook/webhook.py` | K8s webhook |
232+
| `gitops/.../frigate-ip-webhook/deployment.yaml` | RBAC + Deployment |
233+
| `.github/workflows/build-frigate-webhook.yaml` | Container build |
234+
235+
---
236+
237+
*The real test comes next Sunday. I'll update this post with results.*

0 commit comments

Comments
 (0)