Skip to content

Commit 9530750

Browse files
committed
optionally poll Frigate's event API for events MQTT never announces
Events created through Frigate's API are not tracked objects, so frigate/events stays silent about them. A camera's own person detection wired up as a bridge therefore went unused - measured at one front door that was ~22 events a day with a usable face in 12 of 20, roughly doubling what FaceID could learn from. Off by default since it costs an API request per interval.
1 parent ac2bedc commit 9530750

7 files changed

Lines changed: 145 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@
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.7 — 2026-07-25
7+
8+
- **New: `poll_interval`** — optionally also poll Frigate's event API instead of relying
9+
on MQTT alone. Events created through Frigate's API are not tracked objects and never
10+
appear on `frigate/events`, so FaceID never saw them. A common case is a camera's own
11+
person detection wired up as a reliability bridge: on one installation that was ~22
12+
events per day at the front door, of which 12 in 20 held a usable face — roughly
13+
doubling the events FaceID could learn from. Off by default; 30 seconds is sensible.
14+
- Polled events run through the same pipeline. They carry no bounding box, so their
15+
snapshot is the full frame rather than a person crop — face detection copes, and the
16+
clip path (sharper reference photos) applies as usual.
17+
618
## 0.6.6 — 2026-07-25
719

820
- **`match_top_k` is now adjustable in Settings** ("Photos averaged per match"). It was

app/mqtt_listener.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from collections import deque
1414

1515
import paho.mqtt.client as mqtt
16+
import requests
1617

1718
from .engine import FaceEngine, crop_face
1819
from .hires import upgrade_face
@@ -42,6 +43,10 @@ def __init__(self, cfg: dict, engine, gallery, frigate):
4243
self.ignore_thr = float(f.get("ignore_threshold", f.get("match_threshold", 0.5)))
4344
self.ignore_learning = bool(f.get("ignore_learning", True))
4445
self.hires_enroll = bool(f.get("hires_enroll", True))
46+
# Ereignisse, die Frigate nicht per MQTT meldet (z. B. per API angelegte
47+
# Kamera-Meldungen als Zuverlaessigkeits-Bruecke), per Abfrage nachziehen.
48+
self.poll_interval = float(f.get("poll_interval", 0))
49+
self._polled: deque = deque(maxlen=500) # schon gesehene IDs
4550
self.prefix = str(f.get("mqtt_prefix", "faceid")).strip("/") or "faceid"
4651
self.present: dict[str, dict[str, float]] = {} # camera -> {person: zuletzt gesehen}
4752
self._last_presence: dict[str, list] = {} # zuletzt publizierter Stand je Kamera
@@ -61,6 +66,8 @@ def start(self):
6166
self.client = c
6267
threading.Thread(target=self._worker, daemon=True, name="faceid-worker").start()
6368
threading.Thread(target=self._finalizer, daemon=True, name="faceid-finalizer").start()
69+
if self.poll_interval > 0:
70+
threading.Thread(target=self._poller, daemon=True, name="faceid-poller").start()
6471

6572
def _on_connect(self, client, userdata, flags, reason_code, properties):
6673
log.info("MQTT verbunden (%s)", reason_code)
@@ -157,6 +164,55 @@ def _process(self, eid: str):
157164
st["best_unknown"] = {"crop": crop, "emb": emb, "det_score": float(face.det_score),
158165
"guess": name, "guess_score": float(score), "full": img}
159166

167+
def _poller(self):
168+
"""Frigate-Ereignisse abfragen, die per MQTT nie ankommen.
169+
170+
Manuell ueber die API angelegte Ereignisse sind fuer Frigate keine getrackten
171+
Objekte und loesen ``frigate/events`` nicht aus — eine Kamera-eigene
172+
Personenmeldung, die als Bruecke ein Ereignis anlegt, bliebe sonst ungenutzt.
173+
Sie haben keine Bounding-Box, der Snapshot ist also das Vollbild; die Erkennung
174+
laeuft ansonsten durch dieselbe Pipeline.
175+
"""
176+
url = self.cfg["frigate"]["url"].rstrip("/")
177+
# Beim Start nicht die halbe Historie aufrollen.
178+
since = time.time() - min(self.poll_interval * 4, 300)
179+
while True:
180+
time.sleep(self.poll_interval)
181+
try:
182+
r = requests.get(f"{url}/api/events",
183+
params={"label": "person", "has_snapshot": 1,
184+
"limit": 50, "after": since - 30},
185+
timeout=10)
186+
if r.status_code != 200:
187+
continue
188+
batch = r.json()
189+
except (requests.RequestException, ValueError) as e:
190+
log.debug("Poll fehlgeschlagen: %s", e)
191+
continue
192+
since = time.time()
193+
for ev in batch:
194+
eid = ev.get("id")
195+
if not eid or eid in self._polled or eid in self.events:
196+
continue
197+
cam = ev.get("camera", "")
198+
if self.cameras and cam not in self.cameras:
199+
continue
200+
self._polled.append(eid)
201+
# Nur abgeschlossene Ereignisse — laufende meldet MQTT ohnehin.
202+
if not ev.get("end_time"):
203+
continue
204+
self.events[eid] = {
205+
"camera": cam, "attempts": 0, "best_score": 0.0, "best_person": None,
206+
"best_unknown": None, "last_try": 0.0, "done": False, "ended": True,
207+
"created": time.time(), "polled": True,
208+
"start_time": ev.get("start_time") or time.time(),
209+
"end_time": ev.get("end_time"),
210+
}
211+
try:
212+
self.queue.put_nowait({"eid": eid})
213+
except queue.Full:
214+
log.warning("Queue voll — Poll-Ereignis %s verworfen", eid)
215+
160216
def _finalizer(self):
161217
"""Beendete Events abschließen: Unknown ablegen, 'unbekannt' melden, aufräumen."""
162218
while True:

docs/example-config.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ faceid:
2323
min_face_px: 48 # minimum face size in the snapshot (pixels)
2424
det_size: 640 # detection input size (higher = better far faces, slower)
2525
max_attempts: 6 # recognition attempts per Frigate event
26+
poll_interval: 0 # seconds; >0 also polls Frigate's event API.
27+
# Catches events MQTT never announces — notably
28+
# ones created through Frigate's API (e.g. a
29+
# camera's own person detection used as a
30+
# reliability bridge). 30 is a sensible value.
2631
retry_seconds: 2.5 # min. seconds between attempts on the same event
2732
cluster_eps: 0.55 # DBSCAN cosine distance for grouping unknowns in the UI
2833
# review UI: unknown faces with a best-match score >= this get grouped into a

faceid-addon/CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@
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.7 — 2026-07-25
7+
8+
- **New: `poll_interval`** — optionally also poll Frigate's event API instead of relying
9+
on MQTT alone. Events created through Frigate's API are not tracked objects and never
10+
appear on `frigate/events`, so FaceID never saw them. A common case is a camera's own
11+
person detection wired up as a reliability bridge: on one installation that was ~22
12+
events per day at the front door, of which 12 in 20 held a usable face — roughly
13+
doubling the events FaceID could learn from. Off by default; 30 seconds is sensible.
14+
- Polled events run through the same pipeline. They carry no bounding box, so their
15+
snapshot is the full frame rather than a person crop — face detection copes, and the
16+
clip path (sharper reference photos) applies as usual.
17+
618
## 0.6.6 — 2026-07-25
719

820
- **`match_top_k` is now adjustable in Settings** ("Photos averaged per match"). It was

faceid-addon/app/mqtt_listener.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from collections import deque
1414

1515
import paho.mqtt.client as mqtt
16+
import requests
1617

1718
from .engine import FaceEngine, crop_face
1819
from .hires import upgrade_face
@@ -42,6 +43,10 @@ def __init__(self, cfg: dict, engine, gallery, frigate):
4243
self.ignore_thr = float(f.get("ignore_threshold", f.get("match_threshold", 0.5)))
4344
self.ignore_learning = bool(f.get("ignore_learning", True))
4445
self.hires_enroll = bool(f.get("hires_enroll", True))
46+
# Ereignisse, die Frigate nicht per MQTT meldet (z. B. per API angelegte
47+
# Kamera-Meldungen als Zuverlaessigkeits-Bruecke), per Abfrage nachziehen.
48+
self.poll_interval = float(f.get("poll_interval", 0))
49+
self._polled: deque = deque(maxlen=500) # schon gesehene IDs
4550
self.prefix = str(f.get("mqtt_prefix", "faceid")).strip("/") or "faceid"
4651
self.present: dict[str, dict[str, float]] = {} # camera -> {person: zuletzt gesehen}
4752
self._last_presence: dict[str, list] = {} # zuletzt publizierter Stand je Kamera
@@ -61,6 +66,8 @@ def start(self):
6166
self.client = c
6267
threading.Thread(target=self._worker, daemon=True, name="faceid-worker").start()
6368
threading.Thread(target=self._finalizer, daemon=True, name="faceid-finalizer").start()
69+
if self.poll_interval > 0:
70+
threading.Thread(target=self._poller, daemon=True, name="faceid-poller").start()
6471

6572
def _on_connect(self, client, userdata, flags, reason_code, properties):
6673
log.info("MQTT verbunden (%s)", reason_code)
@@ -157,6 +164,55 @@ def _process(self, eid: str):
157164
st["best_unknown"] = {"crop": crop, "emb": emb, "det_score": float(face.det_score),
158165
"guess": name, "guess_score": float(score), "full": img}
159166

167+
def _poller(self):
168+
"""Frigate-Ereignisse abfragen, die per MQTT nie ankommen.
169+
170+
Manuell ueber die API angelegte Ereignisse sind fuer Frigate keine getrackten
171+
Objekte und loesen ``frigate/events`` nicht aus — eine Kamera-eigene
172+
Personenmeldung, die als Bruecke ein Ereignis anlegt, bliebe sonst ungenutzt.
173+
Sie haben keine Bounding-Box, der Snapshot ist also das Vollbild; die Erkennung
174+
laeuft ansonsten durch dieselbe Pipeline.
175+
"""
176+
url = self.cfg["frigate"]["url"].rstrip("/")
177+
# Beim Start nicht die halbe Historie aufrollen.
178+
since = time.time() - min(self.poll_interval * 4, 300)
179+
while True:
180+
time.sleep(self.poll_interval)
181+
try:
182+
r = requests.get(f"{url}/api/events",
183+
params={"label": "person", "has_snapshot": 1,
184+
"limit": 50, "after": since - 30},
185+
timeout=10)
186+
if r.status_code != 200:
187+
continue
188+
batch = r.json()
189+
except (requests.RequestException, ValueError) as e:
190+
log.debug("Poll fehlgeschlagen: %s", e)
191+
continue
192+
since = time.time()
193+
for ev in batch:
194+
eid = ev.get("id")
195+
if not eid or eid in self._polled or eid in self.events:
196+
continue
197+
cam = ev.get("camera", "")
198+
if self.cameras and cam not in self.cameras:
199+
continue
200+
self._polled.append(eid)
201+
# Nur abgeschlossene Ereignisse — laufende meldet MQTT ohnehin.
202+
if not ev.get("end_time"):
203+
continue
204+
self.events[eid] = {
205+
"camera": cam, "attempts": 0, "best_score": 0.0, "best_person": None,
206+
"best_unknown": None, "last_try": 0.0, "done": False, "ended": True,
207+
"created": time.time(), "polled": True,
208+
"start_time": ev.get("start_time") or time.time(),
209+
"end_time": ev.get("end_time"),
210+
}
211+
try:
212+
self.queue.put_nowait({"eid": eid})
213+
except queue.Full:
214+
log.warning("Queue voll — Poll-Ereignis %s verworfen", eid)
215+
160216
def _finalizer(self):
161217
"""Beendete Events abschließen: Unknown ablegen, 'unbekannt' melden, aufräumen."""
162218
while True:

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.6"
2+
version: "0.6.7"
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+
poll_interval: 0
3839
backup_enabled: false
3940
backup_hour: 3
4041
backup_keep: 7
@@ -57,6 +58,7 @@ schema:
5758
trimmed_keep: int(0,100)
5859
dedupe_threshold: float(0.50,0.95)
5960
hires_enroll: bool
61+
poll_interval: int(0,3600)
6062
backup_enabled: bool
6163
backup_hour: int(0,23)
6264
backup_keep: int(1,90)

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+
poll_interval: $(cfg '.poll_interval')
6768
backup_enabled: $(cfg '.backup_enabled')
6869
backup_hour: $(cfg '.backup_hour')
6970
backup_keep: $(cfg '.backup_keep')

0 commit comments

Comments
 (0)