import { EventEmitter } from 'events'; import mqtt from 'mqtt'; interface FrigateDetectorConfig { mqttUrl: string; mqttUser?: string; mqttPassword?: string; topicPrefix?: string; cameras: string[]; } /** * Subscribes to Frigate's MQTT events directly and emits 'personDetected' * with the camera name when a new person event starts on a watched camera. */ export class FrigateDetector extends EventEmitter { private client: mqtt.MqttClient | null = null; private config: FrigateDetectorConfig; private seenEvents = new Set(); constructor(config: FrigateDetectorConfig) { super(); this.config = config; } start(): void { const { mqttUrl, mqttUser, mqttPassword, topicPrefix = 'frigate' } = this.config; const opts: mqtt.IClientOptions = { clientId: `icc-frigate-${Date.now()}`, reconnectPeriod: 5000, connectTimeout: 10000, }; if (mqttUser) opts.username = mqttUser; if (mqttPassword) opts.password = mqttPassword; this.client = mqtt.connect(mqttUrl, opts); this.client.on('connect', () => { console.log('FrigateDetector: MQTT connected'); // Subscribe to per-camera person topic for each watched camera for (const cam of this.config.cameras) { this.client!.subscribe(`${topicPrefix}/${cam}/person`, { qos: 0 }); } // Also subscribe to events topic for richer event data this.client!.subscribe(`${topicPrefix}/events`, { qos: 0 }); }); this.client.on('message', (topic: string, payload: Buffer) => { try { const prefix = this.config.topicPrefix || 'frigate'; // frigate//person → payload is a count (0 or 1+) const personMatch = topic.match(new RegExp(`^${prefix}/(.+)/person$`)); if (personMatch) { const cam = personMatch[1]; const count = parseInt(payload.toString(), 10); if (count > 0) { const key = `person-${cam}`; if (!this.seenEvents.has(key)) { this.seenEvents.add(key); console.log(`FrigateDetector: person detected on ${cam}`); this.emit('personDetected', cam); // Clear after 60s so a new detection can trigger again setTimeout(() => this.seenEvents.delete(key), 60000); } } else { // Person left — allow re-trigger this.seenEvents.delete(`person-${cam}`); } return; } // frigate/events → JSON with event details if (topic === `${prefix}/events`) { const data = JSON.parse(payload.toString()); const after = data?.after || data; if (after?.label === 'person' && data?.type === 'new') { const cam = after.camera; if (this.config.cameras.some((c) => c.toLowerCase() === cam?.toLowerCase())) { const key = `event-${after.id}`; if (!this.seenEvents.has(key)) { this.seenEvents.add(key); this.emit('personDetected', cam); } } } } } catch (err) { console.error('FrigateDetector: parse error', err); } }); this.client.on('error', (err: Error) => { console.error('FrigateDetector: MQTT error', err.message); }); this.client.on('reconnect', () => { console.log('FrigateDetector: MQTT reconnecting...'); }); } stop(): void { this.client?.end(true); this.client = null; } }