Files
imperial-command-center/electron/services/FrigateDetector.ts
root 3b38a78295 Person detection via direct MQTT instead of HA entities
- Electron main process subscribes to Frigate's MQTT topics
  (frigate/<camera>/person and frigate/events) directly via mqtt.js,
  bypassing the broken HA MQTT integration
- Watched cameras: Front_Porch, FPE, Porch_Downstairs, Driveway_door
- On person detection, exits photo-frame idle and shows full-screen
  camera feed for 30 seconds
- Removed HA entity-based person detection code (entityToCameraName,
  personDetectionEntities config dependency)
- Deleted unused useFrigateDetection HTTP polling hook (superseded)
2026-04-16 21:46:28 -05:00

107 lines
3.4 KiB
TypeScript

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<string>();
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/<camera>/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;
}
}