iobroker.frigate
Version:
1,050 lines (1,013 loc) • 37.2 kB
JavaScript
'use strict';
/*
* Created with @iobroker/create-adapter v2.6.1
*/
// The adapter-core module gives you access to the core ioBroker functions
// you need to create an adapter
const utils = require('@iobroker/adapter-core');
const json2iob = require('json2iob');
const fs = require('fs');
const { v4: uuidv4 } = require('uuid');
const { sep } = require('path');
const { tmpdir } = require('os');
// @ts-ignore
const axios = require('axios').default;
// @ts-ignore
const aedes = require('aedes')();
const server = require('net').createServer(aedes.handle);
class Frigate extends utils.Adapter {
/**
* @param {Partial<utils.AdapterOptions>} [options={}]
*/
constructor(options) {
super({
...options,
name: 'frigate',
});
this.on('ready', this.onReady.bind(this));
this.on('stateChange', this.onStateChange.bind(this));
this.on('unload', this.onUnload.bind(this));
this.requestClient = axios.create({
withCredentials: true,
headers: {
'User-Agent': 'ioBroker.frigate',
accept: '*/*',
},
timeout: 3 * 60 * 1000, //3min client timeout
});
this.clientId = 'frigate';
this.json2iob = new json2iob(this);
this.tmpDir = tmpdir();
this.notificationMinScore = null;
this.firstStart = true;
this.deviceArray = [''];
this.notificationsLog = {};
}
/**
* Is called when databases are connected and adapter received configuration.
*/
async onReady() {
this.setState('info.connection', false, true);
this.subscribeStates('*_state');
this.subscribeStates('*.remote.*');
this.subscribeStates('remote.*');
if (!this.config.friurl) {
this.log.warn('No Frigate url set');
}
if (this.config.friurl.includes(':8971')) {
this.log.warn('You are using the UI port 8971. Please use the API port 5000');
}
try {
if (this.config.notificationMinScore) {
this.notificationMinScore = parseFloat(this.config.notificationMinScore);
if (this.notificationMinScore > 1) {
this.notificationMinScore = this.notificationMinScore / 100;
this.log.info('Notification min score is higher than 1. Recalculated to ' + this.notificationMinScore);
}
}
} catch (error) {
this.log.error(error);
}
if (this.config.notificationEventClipWaitTime < 1) {
this.log.warn('Notification clip wait time is lower than 1. Set to 1');
this.config.notificationEventClipWaitTime = 1;
}
if (this.config.notificationExcludeList) {
this.notificationExcludeArray = this.config.notificationExcludeList.replace(/ /g, '').split(',');
}
if (this.config.notificationActive) {
this.log.debug('Clean old images and clips');
let count = 0;
try {
fs.readdirSync(this.tmpDir).forEach((file) => {
if (file.endsWith('.jpg') || file.endsWith('.mp4')) {
this.log.debug('Try to delete ' + file);
fs.unlinkSync(this.tmpDir + sep + file);
count++;
this.log.debug('Deleted ' + file);
}
});
count && this.log.info('Deleted ' + count + ' old images and clips in tmp folder');
} catch (error) {
this.log.warn('Cannot delete old images and clips');
this.log.warn(error);
}
}
await this.cleanOldObjects();
await this.extendObjectAsync('events', {
type: 'channel',
common: {
name: 'Events current and history',
},
native: {},
});
await this.extendObjectAsync('events.history.json', {
type: 'state',
common: {
name: 'Events history',
type: 'string',
role: 'json',
read: true,
write: false,
},
native: {},
});
await this.extendObjectAsync('remote', {
type: 'channel',
common: {
name: 'Control adapter',
},
native: {},
});
await this.extendObjectAsync('remote.restart', {
type: 'state',
common: {
name: 'Restart Frigate',
type: 'boolean',
role: 'button',
def: false,
read: true,
write: true,
},
native: {},
});
await this.extendObjectAsync('remote.pauseNotifications', {
type: 'state',
common: {
name: 'Pause All notifications',
type: 'boolean',
role: 'switch',
def: false,
read: true,
write: true,
},
native: {},
});
await this.extendObjectAsync('remote.pauseNotificationsForTime', {
type: 'state',
common: {
name: 'Pause All notifications for time in minutes',
type: 'number',
role: 'value',
def: 10,
read: true,
write: true,
},
native: {},
});
await this.initMqtt();
}
async cleanOldObjects() {
await this.delObjectAsync('reviews.before.data.detections', { recursive: true });
await this.delObjectAsync('reviews.after.data.detections', { recursive: true });
const remoteState = await this.getObjectAsync('lastidurl');
if (remoteState) {
this.log.info('clean old states ');
await this.delObjectAsync('', { recursive: true });
}
await this.setObjectNotExistsAsync('info.connection', {
type: 'state',
common: {
name: 'connection',
type: 'boolean',
role: 'indicator.connected',
read: true,
write: false,
},
native: {},
});
}
async initMqtt() {
server
.listen(this.config.mqttPort, () => {
this.log.info('MQTT server started and listening on port ' + this.config.mqttPort);
this.log.info("Please enter host: '" + this.host + "' and port: '" + this.config.mqttPort + "' in frigate config");
this.log.info("If you don't see a new client connected, please restart frigate and adapter.");
})
.once('error', (err) => {
this.log.error('MQTT server error: ' + err);
this.log.error(
'Please check if port ' +
this.config.mqttPort +
' is already in use. Use a different port in instance and frigate settings or restart ioBroker.',
);
});
aedes.on('client', (client) => {
this.log.info('New client: ' + client.id);
this.log.info('Filter for message from client: ' + client.id);
this.clientId = client.id;
this.setState('info.connection', true, true);
this.fetchEventHistory();
});
aedes.on('clientDisconnect', (client) => {
this.log.info('client disconnected ' + client.id);
this.setState('info.connection', false, true);
this.setState('available', 'offline', true);
});
aedes.on('publish', async (packet, client) => {
if (packet.payload) {
if (packet.topic === 'frigate/stats' || packet.topic.endsWith('snapshot')) {
this.log.silly('publish' + ' ' + packet.topic + ' ' + packet.payload.toString());
} else {
this.log.debug('publish' + ' ' + packet.topic + ' ' + packet.payload.toString());
}
} else {
this.log.debug(JSON.stringify(packet));
}
if (client) {
try {
let pathArray = packet.topic.split('/');
let data = packet.payload.toString();
let write = false;
try {
data = JSON.parse(data);
} catch (error) {
this.log.debug('Cannot parse ' + data + ' ' + error);
//do nothing
}
if (pathArray[0] === 'frigate') {
//remove first element
pathArray.shift();
//convert snapshot jpg to base64 with data url
if (pathArray[pathArray.length - 1] === 'snapshot') {
data = 'data:image/jpeg;base64,' + packet.payload.toString('base64');
if (this.config.notificationCamera) {
const uuid = uuidv4();
const fileName = `${this.tmpDir}${sep}${uuid}.jpg`;
this.log.debug('Save ' + pathArray[pathArray.length - 1] + ' image to ' + fileName);
fs.writeFileSync(fileName, packet.payload);
await this.sendNotification({
source: pathArray[0],
type: pathArray[1],
state: pathArray[pathArray.length - 1],
image: fileName,
});
try {
if (fileName) {
this.log.debug('Try to delete ' + fileName);
fs.unlinkSync(fileName);
this.log.debug('Deleted ' + fileName);
}
} catch (error) {
this.log.error(error);
}
}
}
//if last path state then make it writable
if (pathArray[pathArray.length - 1] === 'state') {
write = true;
}
// events topic trigger history fetching
if (pathArray[pathArray.length - 1] === 'events') {
this.prepareEventNotification(data);
this.fetchEventHistory();
// if (data.before && data.before.start_time) {
// data.before.start_time = data.before.start_time.split('.')[0];
// data.before.end_time = data.before.end_time.split('.')[0];
// }
// if (data.after && data.after.start_time) {
// data.after.start_time = data.after.start_time.split('.')[0];
// data.after.end_time = data.after.end_time.split('.')[0];
// }
}
// join every path item except the first one to create a flat hierarchy
if (
pathArray[0] !== 'stats' &&
pathArray[0] !== 'events' &&
pathArray[0] !== 'available' &&
pathArray[0] !== 'reviews' &&
pathArray[0] !== 'camera_activity' &&
pathArray.length > 1
) {
const cameraId = pathArray.shift();
pathArray = [cameraId, pathArray.join('_')];
}
if (pathArray[0] === 'reviews') {
delete data.after.data.detections;
delete data.before.data.detections;
}
if (pathArray[0] === 'events') {
delete data.after.path_data;
delete data.before.path_data;
if (data.after.snapshot) {
delete data.after.snapshot.path_data;
}
if (data.before.snapshot) {
delete data.before.snapshot.path_data;
}
}
//create devices state for cameras
if (pathArray[0] === 'stats') {
delete data['cpu_usages'];
this.createCameraDevices();
}
}
//parse json to iobroker states
await this.json2iob.parse(pathArray.join('.'), data, { write: write });
} catch (error) {
this.log.warn(error);
}
}
});
aedes.on('subscribe', (subscriptions, client) => {
this.log.info(
'MQTT client \x1b[32m' +
(client ? client.id : client) +
'\x1b[0m subscribed to topics: ' +
subscriptions.map((s) => s.topic).join('\n') +
' ' +
'from broker' +
' ' +
aedes.id,
);
});
aedes.on('unsubscribe', (subscriptions, client) => {
this.log.info(
'MQTT client \x1b[32m' +
(client ? client.id : client) +
'\x1b[0m unsubscribed to topics: ' +
subscriptions.join('\n') +
' ' +
'from broker' +
' ' +
aedes.id,
);
});
aedes.on('clientError', (client, err) => {
this.log.warn('client error: ' + client.id + ' ' + err.message + ' ' + err.stack);
});
aedes.on('connectionError', (client, err) => {
this.log.warn('client error: ' + client + ' ' + err.message + ' ' + err.stack);
});
}
async createCameraDevices() {
if (this.firstStart) {
this.log.info('Create Device information and fetch Event History');
const data = await this.requestClient({
url: 'http://' + this.config.friurl + '/api/config',
method: 'get',
})
.then((response) => {
this.log.debug(JSON.stringify(response.data));
return response.data;
})
.catch((error) => {
this.log.warn('createCameraDevices error from http://' + this.config.friurl + '/api/config');
this.log.error(error);
error.response && this.log.error(JSON.stringify(error.response.data));
return;
});
if (!data) {
return;
}
if (data.cameras) {
for (const key in data.cameras) {
this.deviceArray.push(key);
this.log.info('Create device information for: ' + key);
await this.extendObjectAsync(key, {
type: 'device',
common: {
name: 'Camera ' + key,
},
native: {},
});
await this.extendObjectAsync(key + '.history', {
type: 'channel',
common: {
name: 'Event History',
},
native: {},
});
await this.extendObjectAsync(key + '.remote', {
type: 'channel',
common: {
name: 'Control camera',
},
native: {},
});
await this.extendObjectAsync(key + '.remote.createEvent', {
type: 'state',
common: {
name: 'Create Event with label',
type: 'string',
role: 'text',
def: 'Label',
read: true,
write: true,
},
native: {},
});
await this.extendObjectAsync(key + '.remote.createEventBody', {
type: 'state',
common: {
name: 'Body for create Event',
type: 'string',
role: 'json',
def: `{}`,
read: true,
write: true,
},
native: {},
});
await this.extendObjectAsync(key + '.remote.pauseNotifications', {
type: 'state',
common: {
name: 'Pause Camera notifications',
type: 'boolean',
role: 'switch',
def: false,
read: true,
write: true,
},
native: {},
});
await this.extendObjectAsync(key + '.remote.pauseNotificationsForTime', {
type: 'state',
common: {
name: 'Pause All notifications for time in minutes',
type: 'number',
role: 'value',
def: 10,
read: true,
write: true,
},
native: {},
});
await this.extendObjectAsync(key + '.remote.notificationText', {
type: 'state',
common: {
name: 'Overwrite the notification text',
type: 'string',
role: 'text',
def: '',
read: true,
write: true,
},
native: {},
});
await this.extendObjectAsync(key + '.remote.notificationMinScore', {
type: 'state',
common: {
name: 'Overwrite notification min score',
type: 'number',
role: 'value',
def: 0,
read: true,
write: true,
},
native: {},
});
await this.extendObjectAsync(key + '.remote.ptz', {
type: 'state',
common: {
name: 'Send PTZ commands preset_preset1, MOVE_LEFT, ZOOM_IN, STOP etc See docu',
desc: 'https://docs.frigate.video/integrations/mqtt/#frigatecamera_nameptz',
type: 'string',
role: 'text',
def: 'preset_preset1',
read: true,
write: true,
},
native: {},
});
}
} else {
this.log.warn('No cameras found');
this.log.info(JSON.stringify(data));
}
this.log.info('Fetch event history for ' + (this.deviceArray.length - 1) + ' cameras');
this.fetchEventHistory();
this.firstStart = false;
this.log.info('Device information created');
}
}
async prepareEventNotification(data) {
let state = 'Event Before';
let camera = data.before.camera;
let label = data.before.label;
let score = data.before.top_score;
let zones = data.before.entered_zones;
const status = data.type;
//check if only end events should be notified or start and update events
if (
(this.config.notificationEventSnapshot && status === 'end') ||
(this.config.notificationEventSnapshotStart && status === 'new') ||
(this.config.notificationEventSnapshotUpdate && status === 'update') ||
(this.config.notificationEventSnapshotUpdateOnce && status === 'update' && !this.notificationsLog[data.before.id])
) {
let imageUrl = '';
let fileName = '';
if (data.before.has_snapshot) {
imageUrl = `http://${this.config.friurl}/api/events/${data.before.id}/snapshot.jpg`;
}
if (data.after) {
// image = data.after.snapshot;
state = 'Event After';
camera = data.after.camera;
label = data.after.label;
score = data.after.top_score;
zones = data.after.entered_zones;
if (data.after.has_snapshot) {
imageUrl = `http://${this.config.friurl}/api/events/${data.after.id}/snapshot.jpg`;
}
}
if (imageUrl) {
const uuid = uuidv4();
fileName = `${this.tmpDir}${sep}${uuid}.jpg`;
this.log.debug('create uuid image to ' + fileName);
await this.requestClient({
url: imageUrl,
method: 'get',
responseType: 'stream',
})
.then(async (response) => {
if (response.data) {
this.log.debug('new writer for ' + fileName);
const writer = fs.createWriteStream(fileName);
response.data.pipe(writer);
await new Promise((resolve, reject) => {
writer.on('finish', resolve);
writer.on('error', reject);
}).catch((error) => {
this.log.error(error);
});
this.log.debug('prepareEventNotification saved image to ' + fileName);
return;
}
this.log.debug('prepareEventNotification no data from ' + imageUrl);
return;
})
.catch((error) => {
this.log.warn('prepareEventNotification error from ' + imageUrl);
if (error.response && error.response.status >= 500) {
this.log.warn('Cannot reach server. You can ignore this after restarting the frigate server.');
}
this.log.warn(error);
return;
});
} else {
this.log.info(`Notification sending active but no image available for type ${label} state ${state}`);
}
if (fileName) {
await this.sendNotification({
source: camera,
type: label,
state: state,
status: status,
image: fileName,
score: score,
zones: zones,
id: data.before.id,
});
}
try {
if (fileName) {
this.log.debug('Try to delete ' + fileName);
fs.unlinkSync(fileName);
this.log.debug('Deleted ' + fileName);
}
} catch (error) {
this.log.error(error);
}
}
//check if clip should be notified and event is end
if (this.config.notificationEventClip || this.config.notificationEventClipLink) {
if (data.type === 'end') {
if (data.before && data.before.has_clip) {
let fileName = '';
let state = 'Event Before';
score = data.before.top_score;
zones = data.before.entered_zones;
let clipUrl = `http://${this.config.friurl}/api/events/${data.before.id}/clip.mp4`;
let clipm3u8 = `http://${this.config.friurl}/vod/event/${data.before.id}/master.m3u8`;
if (data.after && data.after.has_clip) {
state = 'Event After';
score = data.after.top_score;
zones = data.after.entered_zones;
clipUrl = `http://${this.config.friurl}/api/events/${data.after.id}/clip.mp4`;
clipm3u8 = `http://${this.config.friurl}/vod/event/${data.after.id}/master.m3u8`;
}
if (this.config.notificationEventClipLink) {
this.sendNotification({
source: camera,
type: label,
state: state,
status: status,
clipUrl: clipUrl,
clipm3u8: clipm3u8,
score: score,
zones: zones,
});
}
if (this.config.notificationEventClip) {
const uuid = uuidv4();
fileName = `${this.tmpDir}${sep}${uuid}.mp4`;
this.log.debug(`Wait ${this.config.notificationEventClipWaitTime} seconds for clip`);
await this.sleep(this.config.notificationEventClipWaitTime * 1000);
await this.requestClient({
url: clipUrl,
method: 'get',
responseType: 'stream',
})
.then(async (response) => {
if (response.data) {
const writer = fs.createWriteStream(fileName);
response.data.pipe(writer);
await new Promise((resolve, reject) => {
writer.on('finish', resolve);
writer.on('error', reject);
}).catch((error) => {
this.log.error(error);
});
this.log.debug('prepareEventNotification saved clip to ' + fileName);
return;
}
this.log.debug('prepareEventNotification no data from ' + clipUrl);
})
.catch((error) => {
this.log.warn('prepareEventNotification error from ' + clipUrl);
if (error.response && error.response.status >= 500) {
this.log.warn('Cannot reach server. You can ignore this after restarting the frigate server.');
}
this.log.warn(error);
});
await this.sendNotification({
source: camera,
type: label,
state: state,
status: status,
clip: fileName,
score: score,
zones: zones,
});
try {
if (fileName) {
this.log.debug('Try to delete ' + fileName);
fs.unlinkSync(fileName);
this.log.debug('Deleted ' + fileName);
}
} catch (error) {
this.log.error(error);
}
}
} else {
this.log.info(`Clip sending active but no clip available `);
}
}
}
}
async sleep(ms) {
return new Promise((resolve) => this.setTimeout(resolve, ms));
}
async fetchEventHistory() {
for (const device of this.deviceArray) {
const params = { limit: this.config.webnum };
if (device) {
params.cameras = device;
}
await this.requestClient({
url: 'http://' + this.config.friurl + '/api/events',
method: 'get',
params: params,
})
.then(async (response) => {
if (response.data) {
this.log.debug('fetchEventHistory succesfull ' + device);
for (const event of response.data) {
event.websnap = 'http://' + this.config.friurl + '/api/events/' + event.id + '/snapshot.jpg';
event.webclip = 'http://' + this.config.friurl + '/api/events/' + event.id + '/clip.mp4';
event.webm3u8 = 'http://' + this.config.friurl + '/vod/event/' + event.id + '/master.m3u8';
event.thumbnail = 'data:image/jpeg;base64,' + event.thumbnail;
}
let path = 'events.history';
if (device) {
path = device + '.history';
}
this.json2iob.parse(path, response.data, { forceIndex: true, channelName: 'Events history' });
this.setStateAsync('events.history.json', JSON.stringify(response.data), true);
}
})
.catch((error) => {
this.log.warn('fetchEventHistory error from http://' + this.config.friurl + '/api/events');
if (error.response && error.response.status >= 500) {
this.log.warn('Cannot reach server. You can ignore this after restarting the frigate server.');
}
this.log.warn(error);
});
}
}
async sendNotification(message) {
const pauseState = await this.getStateAsync('remote.pauseNotifications');
if (pauseState && pauseState.val) {
this.log.debug('Notifications paused');
return;
}
const cameraPauseState = await this.getStateAsync(message.source + '.remote.pauseNotifications');
if (cameraPauseState && cameraPauseState.val) {
this.log.debug('Notifications paused for camera ' + message.source);
return;
}
if (this.notificationExcludeArray && this.notificationExcludeArray.includes(message.source)) {
this.log.debug('Notification for ' + message.source + ' is excluded');
return;
}
if (this.config.notificationExcludeZoneList) {
const excludeZones = this.config.notificationExcludeZoneList.replace(/ /g, '').split(',');
if (message.zones && message.zones.length > 0) {
//check if all zones are excluded
let allExcluded = true;
this.log.debug(`Check if all zones are excluded ${message.zones} from ${excludeZones}`);
for (const zone of message.zones) {
if (!excludeZones.includes(zone)) {
allExcluded = false;
}
}
if (allExcluded) {
this.log.debug('Notification for ' + message.source + ' is excluded because all zones are excluded');
return;
}
}
}
if (this.config.notificationExcludeEmptyZoneList) {
const cameras = this.config.notificationExcludeEmptyZoneList.replace(/ /g, '').split(',');
if (cameras.includes(message.source)) {
if (!message.zones || message.zones.length == 0) {
this.log.debug('Notification for ' + message.source + ' is excluded because no zones are entered');
return;
}
}
}
if (this.config.notificationActive) {
let fileName = message.image;
let type = 'photo';
if (message.clip != null) {
fileName = message.clip;
type = 'video';
}
this.log.debug(
`Notification score ${message.score} type ${message.type} state ${message.state} ${message.status} image/clip file: ${fileName} format ${type}`,
);
const notificationMinScoreState = await this.getStateAsync(message.source + '.remote.notificationMinScore');
if (notificationMinScoreState && notificationMinScoreState.val) {
if (notificationMinScoreState.val != null && notificationMinScoreState.val > 0 && message.score < notificationMinScoreState.val) {
this.log.info(
`Notification skipped score ${message.score} is lower than ${notificationMinScoreState.val} state ${message.state} type ${message.type}`,
);
return;
}
} else if (message.score != null && this.notificationMinScore > 0 && message.score < this.config.notificationMinScore) {
this.log.info(
`Notification skipped score ${message.score} is lower than ${this.config.notificationMinScore} state ${message.state} type ${message.type}`,
);
return;
}
this.log.debug(`Notification score ${message.score} is higher than ${this.config.notificationMinScore} type ${message.type}`);
const sendInstances = this.config.notificationInstances.replace(/ /g, '').split(',');
let sendUser = [];
if (this.config.notificationUsers) {
sendUser = this.config.notificationUsers.replace(/ /g, '').split(',');
}
let messageTextTemplate = this.config.notificationTextTemplate;
const notificationTextState = await this.getStateAsync(message.source + '.remote.notificationText');
if (notificationTextState && notificationTextState.val) {
if (notificationTextState.val != null) {
messageTextTemplate = notificationTextState.val.toString();
}
}
let messageText = messageTextTemplate
.replace(/{{source}}/g, message.source || '')
.replace(/{{type}}/g, message.type || '')
.replace(/{{state}}/g, message.state || '')
.replace(/{{score}}/g, message.score || '')
.replace(/{{status}}/g, message.status || '')
.replace(/{{zones}}/g, message.zones || '');
if (message.clipm3u8) {
messageText = message.source + ': ' + message.clipm3u8;
fileName = '';
type = 'typing';
}
this.log.debug('Notification message ' + messageText + ' file ' + fileName + ' type ' + type);
this.notificationsLog[message.id] = true;
for (const sendInstance of sendInstances) {
if (!sendInstance) {
this.log.warn('No notification instance set');
continue;
}
if (sendUser.length > 0) {
for (const user of sendUser) {
if (sendInstance.includes('pushover')) {
if (type === 'video') {
this.log.info('Pushover does not support video.');
return;
}
await this.sendToAsync(sendInstance, {
device: user,
file: fileName,
message: messageText,
});
} else if (sendInstance.includes('signal-cmb')) {
await this.sendToAsync(sendInstance, 'send', {
text: messageText,
phone: user,
});
} else if (sendInstance.includes('mail')) {
await this.sendToAsync(sendInstance, 'send', {
subject: messageText,
to: user,
text: messageText,
attachments: fileName ? [{ path: fileName }] : [],
});
} else {
await this.sendToAsync(sendInstance, {
user: user,
message: fileName || messageText,
text: fileName || messageText,
type: type,
caption: messageText,
title: messageText,
});
}
}
} else {
if (sendInstance.includes('pushover')) {
if (type === 'video') {
this.log.info('Pushover does not support video.');
return;
}
await this.sendToAsync(sendInstance, {
file: fileName,
message: messageText,
});
} else if (sendInstance.includes('signal-cmb')) {
await this.sendToAsync(sendInstance, 'send', {
text: messageText,
});
} else if (sendInstance.includes('mail')) {
await this.sendToAsync(sendInstance, 'send', {
subject: messageText,
text: messageText,
attachments: fileName ? [{ path: fileName }] : [],
});
} else {
await this.sendToAsync(sendInstance, {
message: fileName || messageText,
text: fileName || messageText,
type: type,
caption: messageText,
title: messageText,
});
}
}
}
}
}
/**
* Is called when adapter shuts down - callback has to be called under any circumstances!
* @param {() => void} callback
*/
onUnload(callback) {
try {
server.close();
callback();
} catch (e) {
this.log.error('Error onUnload: ' + e);
callback();
}
}
/**
* Is called if a subscribed state changes
* @param {string} id
* @param {ioBroker.State | null | undefined} state
*/
async onStateChange(id, state) {
if (state) {
if (!state.ack) {
this.log.debug('state ' + id + ' changed: ' + state.val + ' (ack = ' + state.ack + ')');
if (id.endsWith('_state')) {
//remove adapter name and instance from id
id = id.replace(this.name + '.' + this.instance + '.', '');
id = id.replace('_state', '');
const idArray = id.split('.');
const pathArray = ['frigate', ...idArray, 'set'];
const topic = pathArray.join('/');
this.log.debug('publish sending to ' + ' ' + topic + ' ' + state.val);
aedes.publish(
{
cmd: 'publish',
qos: 0,
topic: topic,
payload: state.val,
retain: false,
},
(err) => {
if (err) {
this.log.error(err);
} else {
this.log.info('published ' + topic + ' ' + state.val);
}
},
);
}
if (id.endsWith('remote.createEvent')) {
//remove adapter name and instance from id
const cameraId = id.split('.')[2];
const label = state.val;
let body = '';
const createEventBodyState = await this.getStateAsync(id.replace('createEvent', 'createEventBody'));
if (createEventBodyState && createEventBodyState.val) {
try {
body = JSON.parse(createEventBodyState.val);
} catch (error) {
this.log.error(
'Cannot parse createEventBody. Please use valid JSON https://docs.frigate.video/integrations/api/#post-apieventscamera_namelabelcreate',
);
this.log.error(error);
}
}
this.requestClient({
url: 'http://' + this.config.friurl + '/api/events/' + cameraId + '/' + label + '/create',
method: 'post',
data: body,
})
.then((response) => {
this.log.info('Create event for ' + cameraId + ' with label ' + label);
this.log.info(JSON.stringify(response.data));
})
.catch((error) => {
this.log.warn('createEvent error from http://' + this.config.friurl + '/api/events');
this.log.error(error);
});
}
if (id.endsWith('remote.restart') && state.val) {
//remove adapter name and instance from id
aedes.publish(
{
cmd: 'publish',
qos: 0,
topic: `frigate/restart`,
retain: false,
},
(err) => {
if (err) {
this.log.error(err);
} else {
this.log.info('published ' + `frigate/restart`);
}
},
);
}
if (id.endsWith('remote.ptz')) {
//remove adapter name and instance from id
const cameraId = id.split('.')[2];
const command = state.val;
aedes.publish(
{
cmd: 'publish',
qos: 0,
topic: `frigate/${cameraId}/ptz`,
payload: command,
retain: false,
},
(err) => {
if (err) {
this.log.error(err);
} else {
this.log.info('published ' + `frigate/${cameraId}/ptz` + ' ' + command);
}
},
);
}
if (id.endsWith('remote.pauseNotificationsForTime')) {
const pauseTime = state.val || 10;
const pauseId = id.replace('pauseNotificationsForTime', 'pauseNotifications').replace(this.name + '.' + this.instance + '.', '');
this.setState(pauseId, true, true);
let deviceId = id.split('.')[0];
if (deviceId === 'remote') {
deviceId = 'all';
}
this.log.info('Pause ' + deviceId + ' notifications for ' + pauseTime + ' minutes');
this.setTimeout(
() => {
this.setState(pauseId, false, true);
this.log.info('Pause All notifications ended');
},
pauseTime * 60 * 1000,
);
}
}
} else {
// The state was deleted
// this.log.info(`state ${id} deleted`);
}
}
}
if (require.main !== module) {
// Export the constructor in compact mode
/**
* @param {Partial<utils.AdapterOptions>} [options={}]
*/
module.exports = (options) => new Frigate(options);
} else {
// otherwise start the instance directly
new Frigate();
}