- 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)
107 lines
3.4 KiB
TypeScript
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;
|
|
}
|
|
}
|