Skip to content

Commit eb8d9d3

Browse files
committed
fix: no MQTT discovery when the camera list is empty
An empty cameras list means 'all cameras' for event processing, but discovery iterated over that same empty set and announced nothing - so a default install got no Home Assistant entities at all. Ask Frigate for the list when none is configured, and announce a camera on first sight as a fallback. Also makes the Frigate MQTT topic configurable (a custom topic_prefix meant FaceID silently heard nothing) and logs whether Frigate answers at startup.
1 parent 8718322 commit eb8d9d3

8 files changed

Lines changed: 168 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,21 @@
33
All notable changes to FaceID. The Home Assistant app shows this file in the
44
update dialog; standalone users can watch GitHub releases.
55

6+
## 0.6.9 — 2026-07-26
7+
8+
- **Fixed: no Home Assistant entities unless you listed your cameras.** An empty
9+
`cameras` list means "process every camera" — but MQTT discovery looped over exactly
10+
that empty list, so it announced nothing. Anyone running the default configuration got
11+
no sensors at all. FaceID now asks Frigate for the camera list when none is configured,
12+
and additionally announces a camera the first time it sees an event from it. Reported
13+
in the community thread; it never showed up here because our own config lists cameras
14+
explicitly.
15+
- **New: `frigate_topic_prefix`** (default `frigate`). The subscription was hard-coded,
16+
so a Frigate instance with a custom `mqtt.topic_prefix` was silently never heard.
17+
- **Startup now says whether Frigate is reachable**, lists its cameras, and warns about
18+
configured cameras that do not exist there. "It recognises nothing" and "nothing ever
19+
arrives" were indistinguishable in the log before.
20+
621
## 0.6.8 — 2026-07-25
722

823
- **Hotfix: the web UI stayed blank after 0.6.6.** The new "photos averaged per match"

app/mqtt_listener.py

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@ def __init__(self, cfg: dict, engine, gallery, frigate):
4646
# Ereignisse, die Frigate nicht per MQTT meldet (z. B. per API angelegte
4747
# Kamera-Meldungen als Zuverlaessigkeits-Bruecke), per Abfrage nachziehen.
4848
self.poll_interval = float(f.get("poll_interval", 0))
49+
# Frigate darf sein MQTT-Topic umbenennen (topic_prefix in dessen config.yml).
50+
self.frigate_topic = str(f.get("frigate_topic_prefix", "frigate")).strip("/") or "frigate"
4951
self._polled: deque = deque(maxlen=500) # schon gesehene IDs
52+
self._announced: set = set() # Kameras mit angemeldetem Sensor
5053
self.prefix = str(f.get("mqtt_prefix", "faceid")).strip("/") or "faceid"
5154
self.present: dict[str, dict[str, float]] = {} # camera -> {person: zuletzt gesehen}
5255
self._last_presence: dict[str, list] = {} # zuletzt publizierter Stand je Kamera
@@ -64,14 +67,38 @@ def start(self):
6467
c.connect(m["host"], int(m.get("port", 1883)), keepalive=60)
6568
c.loop_start()
6669
self.client = c
70+
self._check_frigate()
6771
threading.Thread(target=self._worker, daemon=True, name="faceid-worker").start()
6872
threading.Thread(target=self._finalizer, daemon=True, name="faceid-finalizer").start()
6973
if self.poll_interval > 0:
7074
threading.Thread(target=self._poller, daemon=True, name="faceid-poller").start()
7175

76+
def _check_frigate(self):
77+
"""Beim Start einmal nachsehen, ob Frigate ueberhaupt antwortet.
78+
79+
Ohne diese Zeile im Log ist "es erkennt nichts" kaum von "es kommt nichts an"
80+
zu unterscheiden."""
81+
url = self.cfg["frigate"]["url"].rstrip("/")
82+
try:
83+
r = requests.get(f"{url}/api/config", timeout=8)
84+
if r.status_code != 200:
85+
log.error("Frigate unter %s antwortet mit HTTP %s — ohne Snapshots kann "
86+
"nicht erkannt werden", url, r.status_code)
87+
return
88+
cams = list((r.json().get("cameras") or {}).keys())
89+
log.info("Frigate erreichbar (%s), Kameras: %s", url, ", ".join(cams) or "keine")
90+
if self.cameras:
91+
unknown = self.cameras - set(cams)
92+
if unknown:
93+
log.warning("Konfigurierte Kamera(s) %s gibt es in Frigate nicht — "
94+
"von diesen wird nie etwas verarbeitet", ", ".join(sorted(unknown)))
95+
except (requests.RequestException, ValueError) as e:
96+
log.error("Frigate unter %s nicht erreichbar: %s — Snapshots und damit die "
97+
"Erkennung werden fehlschlagen", url, e)
98+
7299
def _on_connect(self, client, userdata, flags, reason_code, properties):
73-
log.info("MQTT verbunden (%s)", reason_code)
74-
client.subscribe("frigate/events")
100+
log.info("MQTT verbunden (%s), abonniere %s/events", reason_code, self.frigate_topic)
101+
client.subscribe(f"{self.frigate_topic}/events")
75102
client.publish(f"{self.prefix}/status", "online", retain=True)
76103
self._publish_discovery()
77104

@@ -90,6 +117,7 @@ def _on_message(self, client, userdata, msg):
90117
eid = after.get("id")
91118
if not eid:
92119
return
120+
self._ensure_discovery(cam)
93121
st = self.events.setdefault(
94122
eid,
95123
{"camera": cam, "attempts": 0, "best_score": 0.0, "best_person": None,
@@ -198,6 +226,7 @@ def _poller(self):
198226
if self.cameras and cam not in self.cameras:
199227
continue
200228
self._polled.append(eid)
229+
self._ensure_discovery(cam)
201230
# Nur abgeschlossene Ereignisse — laufende meldet MQTT ohnehin.
202231
if not ev.get("end_time"):
203232
continue
@@ -293,9 +322,41 @@ def _publish_presence(self, cam: str, last: dict | None = None):
293322
self.client.publish(f"{self.prefix}/{cam}/person", ", ".join(names) or "nobody", retain=True)
294323
self.client.publish(f"{self.prefix}/{cam}/attributes", json.dumps(attrs, ensure_ascii=False), retain=True)
295324

296-
def _publish_discovery(self):
297-
"""HA MQTT-Discovery: ein Sensor je Kamera (zuletzt erkannte Person)."""
298-
cams = self.cameras or set(self.cfg["faceid"].get("discovery_cameras") or [])
325+
def _frigate_cameras(self) -> set:
326+
"""Kameranamen von Frigate holen — fuer den Fall, dass keine konfiguriert sind."""
327+
try:
328+
r = requests.get(f"{self.cfg['frigate']['url'].rstrip('/')}/api/config", timeout=8)
329+
if r.status_code == 200:
330+
return set((r.json().get("cameras") or {}).keys())
331+
except (requests.RequestException, ValueError) as e:
332+
log.warning("Kameraliste von Frigate nicht abrufbar (%s) — Sensoren entstehen "
333+
"dann erst, sobald die erste Person erkannt wird", e)
334+
return set()
335+
336+
def _ensure_discovery(self, cam: str):
337+
"""Sensor fuer eine Kamera anlegen, falls noch nicht geschehen."""
338+
if not cam or cam in self._announced:
339+
return
340+
self._announced.add(cam)
341+
self._publish_discovery([cam])
342+
343+
def _publish_discovery(self, only: list | None = None):
344+
"""HA MQTT-Discovery: ein Sensor je Kamera (zuletzt erkannte Person).
345+
346+
Eine leere ``cameras``-Liste bedeutet "alle Kameras verarbeiten" — frueher
347+
entstanden dann gar keine Sensoren, weil hier ueber eine leere Menge gelaufen
348+
wurde. Ohne Konfiguration fragen wir deshalb Frigate; klappt auch das nicht,
349+
legt ``_ensure_discovery`` den Sensor an, sobald die Kamera das erste Mal
350+
auftaucht."""
351+
if only is not None:
352+
cams = set(only)
353+
else:
354+
cams = (self.cameras
355+
or set(self.cfg["faceid"].get("discovery_cameras") or [])
356+
or self._frigate_cameras())
357+
self._announced |= cams
358+
log.info("MQTT-Discovery: %d Sensor(en) angemeldet%s", len(cams),
359+
"" if cams else " — Kameras unbekannt, folgen bei der ersten Erkennung")
299360
device = {"identifiers": [self.prefix], "name": self.prefix.replace("-", " ").title() if self.prefix != "faceid" else "FaceID",
300361
"manufacturer": "Eigenbau", "model": "InsightFace/ArcFace"}
301362
for cam in cams:

docs/example-config.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ faceid:
3737
min_face_px: 48 # minimum face size in the snapshot (pixels)
3838
det_size: 640 # detection input size (higher = better far faces, slower)
3939
max_attempts: 6 # recognition attempts per Frigate event
40+
frigate_topic_prefix: frigate # must match `mqtt.topic_prefix` in Frigate's config
4041
poll_interval: 0 # seconds; >0 also polls Frigate's event API.
4142
# Catches events MQTT never announces — notably
4243
# ones created through Frigate's API (e.g. a

faceid-addon/CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,21 @@
33
All notable changes to FaceID. The Home Assistant app shows this file in the
44
update dialog; standalone users can watch GitHub releases.
55

6+
## 0.6.9 — 2026-07-26
7+
8+
- **Fixed: no Home Assistant entities unless you listed your cameras.** An empty
9+
`cameras` list means "process every camera" — but MQTT discovery looped over exactly
10+
that empty list, so it announced nothing. Anyone running the default configuration got
11+
no sensors at all. FaceID now asks Frigate for the camera list when none is configured,
12+
and additionally announces a camera the first time it sees an event from it. Reported
13+
in the community thread; it never showed up here because our own config lists cameras
14+
explicitly.
15+
- **New: `frigate_topic_prefix`** (default `frigate`). The subscription was hard-coded,
16+
so a Frigate instance with a custom `mqtt.topic_prefix` was silently never heard.
17+
- **Startup now says whether Frigate is reachable**, lists its cameras, and warns about
18+
configured cameras that do not exist there. "It recognises nothing" and "nothing ever
19+
arrives" were indistinguishable in the log before.
20+
621
## 0.6.8 — 2026-07-25
722

823
- **Hotfix: the web UI stayed blank after 0.6.6.** The new "photos averaged per match"

faceid-addon/DOCS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Full documentation: https://github.com/SkyTechNerds/faceid
3636
| `trimmed_keep` | how many set-aside photos to keep per person (0 = delete immediately) |
3737
| `dedupe_threshold` | default sensitivity for the Settings "Remove duplicates" action |
3838
| `hires_enroll` | fetch new review-queue faces from the recording instead of the detect snapshot (sharper references) |
39+
| `frigate_topic_prefix` | must match `mqtt.topic_prefix` in Frigate's own config (default `frigate`). Wrong value = FaceID hears nothing at all |
3940
| `poll_interval` | seconds; >0 also polls Frigate's event API for events MQTT never announces (e.g. events created by an automation from a camera's own detection). 0 = off |
4041
| `backup_enabled` / `backup_hour` / `backup_keep` | optional built-in daily gallery backup |
4142

faceid-addon/app/mqtt_listener.py

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@ def __init__(self, cfg: dict, engine, gallery, frigate):
4646
# Ereignisse, die Frigate nicht per MQTT meldet (z. B. per API angelegte
4747
# Kamera-Meldungen als Zuverlaessigkeits-Bruecke), per Abfrage nachziehen.
4848
self.poll_interval = float(f.get("poll_interval", 0))
49+
# Frigate darf sein MQTT-Topic umbenennen (topic_prefix in dessen config.yml).
50+
self.frigate_topic = str(f.get("frigate_topic_prefix", "frigate")).strip("/") or "frigate"
4951
self._polled: deque = deque(maxlen=500) # schon gesehene IDs
52+
self._announced: set = set() # Kameras mit angemeldetem Sensor
5053
self.prefix = str(f.get("mqtt_prefix", "faceid")).strip("/") or "faceid"
5154
self.present: dict[str, dict[str, float]] = {} # camera -> {person: zuletzt gesehen}
5255
self._last_presence: dict[str, list] = {} # zuletzt publizierter Stand je Kamera
@@ -64,14 +67,38 @@ def start(self):
6467
c.connect(m["host"], int(m.get("port", 1883)), keepalive=60)
6568
c.loop_start()
6669
self.client = c
70+
self._check_frigate()
6771
threading.Thread(target=self._worker, daemon=True, name="faceid-worker").start()
6872
threading.Thread(target=self._finalizer, daemon=True, name="faceid-finalizer").start()
6973
if self.poll_interval > 0:
7074
threading.Thread(target=self._poller, daemon=True, name="faceid-poller").start()
7175

76+
def _check_frigate(self):
77+
"""Beim Start einmal nachsehen, ob Frigate ueberhaupt antwortet.
78+
79+
Ohne diese Zeile im Log ist "es erkennt nichts" kaum von "es kommt nichts an"
80+
zu unterscheiden."""
81+
url = self.cfg["frigate"]["url"].rstrip("/")
82+
try:
83+
r = requests.get(f"{url}/api/config", timeout=8)
84+
if r.status_code != 200:
85+
log.error("Frigate unter %s antwortet mit HTTP %s — ohne Snapshots kann "
86+
"nicht erkannt werden", url, r.status_code)
87+
return
88+
cams = list((r.json().get("cameras") or {}).keys())
89+
log.info("Frigate erreichbar (%s), Kameras: %s", url, ", ".join(cams) or "keine")
90+
if self.cameras:
91+
unknown = self.cameras - set(cams)
92+
if unknown:
93+
log.warning("Konfigurierte Kamera(s) %s gibt es in Frigate nicht — "
94+
"von diesen wird nie etwas verarbeitet", ", ".join(sorted(unknown)))
95+
except (requests.RequestException, ValueError) as e:
96+
log.error("Frigate unter %s nicht erreichbar: %s — Snapshots und damit die "
97+
"Erkennung werden fehlschlagen", url, e)
98+
7299
def _on_connect(self, client, userdata, flags, reason_code, properties):
73-
log.info("MQTT verbunden (%s)", reason_code)
74-
client.subscribe("frigate/events")
100+
log.info("MQTT verbunden (%s), abonniere %s/events", reason_code, self.frigate_topic)
101+
client.subscribe(f"{self.frigate_topic}/events")
75102
client.publish(f"{self.prefix}/status", "online", retain=True)
76103
self._publish_discovery()
77104

@@ -90,6 +117,7 @@ def _on_message(self, client, userdata, msg):
90117
eid = after.get("id")
91118
if not eid:
92119
return
120+
self._ensure_discovery(cam)
93121
st = self.events.setdefault(
94122
eid,
95123
{"camera": cam, "attempts": 0, "best_score": 0.0, "best_person": None,
@@ -198,6 +226,7 @@ def _poller(self):
198226
if self.cameras and cam not in self.cameras:
199227
continue
200228
self._polled.append(eid)
229+
self._ensure_discovery(cam)
201230
# Nur abgeschlossene Ereignisse — laufende meldet MQTT ohnehin.
202231
if not ev.get("end_time"):
203232
continue
@@ -293,9 +322,41 @@ def _publish_presence(self, cam: str, last: dict | None = None):
293322
self.client.publish(f"{self.prefix}/{cam}/person", ", ".join(names) or "nobody", retain=True)
294323
self.client.publish(f"{self.prefix}/{cam}/attributes", json.dumps(attrs, ensure_ascii=False), retain=True)
295324

296-
def _publish_discovery(self):
297-
"""HA MQTT-Discovery: ein Sensor je Kamera (zuletzt erkannte Person)."""
298-
cams = self.cameras or set(self.cfg["faceid"].get("discovery_cameras") or [])
325+
def _frigate_cameras(self) -> set:
326+
"""Kameranamen von Frigate holen — fuer den Fall, dass keine konfiguriert sind."""
327+
try:
328+
r = requests.get(f"{self.cfg['frigate']['url'].rstrip('/')}/api/config", timeout=8)
329+
if r.status_code == 200:
330+
return set((r.json().get("cameras") or {}).keys())
331+
except (requests.RequestException, ValueError) as e:
332+
log.warning("Kameraliste von Frigate nicht abrufbar (%s) — Sensoren entstehen "
333+
"dann erst, sobald die erste Person erkannt wird", e)
334+
return set()
335+
336+
def _ensure_discovery(self, cam: str):
337+
"""Sensor fuer eine Kamera anlegen, falls noch nicht geschehen."""
338+
if not cam or cam in self._announced:
339+
return
340+
self._announced.add(cam)
341+
self._publish_discovery([cam])
342+
343+
def _publish_discovery(self, only: list | None = None):
344+
"""HA MQTT-Discovery: ein Sensor je Kamera (zuletzt erkannte Person).
345+
346+
Eine leere ``cameras``-Liste bedeutet "alle Kameras verarbeiten" — frueher
347+
entstanden dann gar keine Sensoren, weil hier ueber eine leere Menge gelaufen
348+
wurde. Ohne Konfiguration fragen wir deshalb Frigate; klappt auch das nicht,
349+
legt ``_ensure_discovery`` den Sensor an, sobald die Kamera das erste Mal
350+
auftaucht."""
351+
if only is not None:
352+
cams = set(only)
353+
else:
354+
cams = (self.cameras
355+
or set(self.cfg["faceid"].get("discovery_cameras") or [])
356+
or self._frigate_cameras())
357+
self._announced |= cams
358+
log.info("MQTT-Discovery: %d Sensor(en) angemeldet%s", len(cams),
359+
"" if cams else " — Kameras unbekannt, folgen bei der ersten Erkennung")
299360
device = {"identifiers": [self.prefix], "name": self.prefix.replace("-", " ").title() if self.prefix != "faceid" else "FaceID",
300361
"manufacturer": "Eigenbau", "model": "InsightFace/ArcFace"}
301362
for cam in cams:

faceid-addon/config.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: FaceID
2-
version: "0.6.8"
2+
version: "0.6.9"
33
slug: faceid
44
description: Face recognition for Frigate — trainable gallery, clustered unknown review, HA sensors
55
url: https://github.com/SkyTechNerds/faceid
@@ -35,6 +35,7 @@ options:
3535
trimmed_keep: 10
3636
dedupe_threshold: 0.65
3737
hires_enroll: true
38+
frigate_topic_prefix: frigate
3839
poll_interval: 0
3940
backup_enabled: false
4041
backup_hour: 3
@@ -58,6 +59,7 @@ schema:
5859
trimmed_keep: int(0,100)
5960
dedupe_threshold: float(0.50,0.95)
6061
hires_enroll: bool
62+
frigate_topic_prefix: str?
6163
poll_interval: int(0,3600)
6264
backup_enabled: bool
6365
backup_hour: int(0,23)

faceid-addon/run.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ faceid:
6464
trimmed_keep: $(cfg '.trimmed_keep')
6565
dedupe_threshold: $(cfg '.dedupe_threshold')
6666
hires_enroll: $(cfg '.hires_enroll')
67+
frigate_topic_prefix: $(cfg '.frigate_topic_prefix')
6768
poll_interval: $(cfg '.poll_interval')
6869
backup_enabled: $(cfg '.backup_enabled')
6970
backup_hour: $(cfg '.backup_hour')

0 commit comments

Comments
 (0)