1313from collections import deque
1414
1515import paho .mqtt .client as mqtt
16+ import requests
1617
1718from .engine import FaceEngine , crop_face
1819from .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 :
0 commit comments