@ubreu/homebridge-eufy-security
Version:
Control Eufy Security from homebridge.
386 lines • 17.8 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SnapshotManager = void 0;
const stream_1 = require("stream");
const eufy_security_client_1 = require("eufy-security-client");
const ffmpeg_for_homebridge_1 = __importDefault(require("ffmpeg-for-homebridge"));
const utils_1 = require("../utils/utils");
const ffmpeg_1 = require("../utils/ffmpeg");
const fs = __importStar(require("fs"));
const SnapshotBlackPath = require.resolve('../../media/Snapshot-black.png');
const SnapshotUnavailable = require.resolve('../../media/Snapshot-Unavailable.png');
let MINUTES_TO_WAIT_FOR_AUTOMATIC_REFRESH_TO_BEGIN = 1; // should be incremented by 1 for every device
/**
* possible performance settings:
* 1. snapshots as current as possible (weak homebridge performance) -> forceRefreshSnapshot
* - always get a new image from cloud or cam
* 2. balanced
* - start snapshot refresh but return snapshot as fast as possible
* if request takes too long old snapshot will be returned
* 3. get an old snapshot immediately -> !forceRefreshSnapshot
* - wait on cloud snapshot with new events
*
* extra options:
* - force refresh snapshots with interval
* - force immediate snapshot-reject when ringing
*
* Drawbacks: elapsed time in homekit might be wrong
*/
class SnapshotManager extends stream_1.EventEmitter {
// eslint-disable-next-line max-len
constructor(platform, device, cameraConfig, livestreamManager, log) {
super();
this.videoProcessor = ffmpeg_for_homebridge_1.default || 'ffmpeg';
this.refreshProcessRunning = false;
this.lastEvent = 0;
this.lastRingEvent = 0;
this.log = log;
this.platform = platform;
this.device = device;
this.cameraConfig = cameraConfig;
this.livestreamManager = livestreamManager;
this.device.on('property changed', (device, name, value) => this.onPropertyValueChanged(device, name, value));
this.device.on('crying detected', (device, state) => this.onEvent(device, state));
this.device.on('motion detected', (device, state) => this.onEvent(device, state));
this.device.on('person detected', (device, state) => this.onEvent(device, state));
this.device.on('pet detected', (device, state) => this.onEvent(device, state));
this.device.on('sound detected', (device, state) => this.onEvent(device, state));
this.device.on('rings', (device, state) => this.onRingEvent(device, state));
if (this.cameraConfig.refreshSnapshotIntervalMinutes) {
if (this.cameraConfig.refreshSnapshotIntervalMinutes < 5) {
this.log.warn(this.device.getName(), 'The interval to automatically refresh snapshots is set too low. Minimum is one minute.');
this.cameraConfig.refreshSnapshotIntervalMinutes = 5;
}
// eslint-disable-next-line max-len
this.log.info(this.device.getName(), 'Setting up automatic snapshot refresh every ' + this.cameraConfig.refreshSnapshotIntervalMinutes + ' minutes. This may decrease battery life dramatically. The refresh process for ' + this.device.getName() + ' should begin in ' + MINUTES_TO_WAIT_FOR_AUTOMATIC_REFRESH_TO_BEGIN + ' minutes.');
setTimeout(() => {
this.automaticSnapshotRefresh();
}, MINUTES_TO_WAIT_FOR_AUTOMATIC_REFRESH_TO_BEGIN * 60 * 1000);
MINUTES_TO_WAIT_FOR_AUTOMATIC_REFRESH_TO_BEGIN++;
}
if (this.cameraConfig.snapshotHandlingMethod === 1) {
// eslint-disable-next-line max-len
this.log.info(this.device.getName(), 'is set to generate new snapshots on events every time. This might reduce homebridge performance and increase power consumption.');
if (this.cameraConfig.refreshSnapshotIntervalMinutes) {
// eslint-disable-next-line max-len
this.log.warn(this.device.getName(), 'You have enabled automatic snapshot refreshing. It is recommened not to use this setting with forced snapshot refreshing.');
}
}
else if (this.cameraConfig.snapshotHandlingMethod === 2) {
this.log.info(this.device.getName(), 'is set to balanced snapshot handling.');
}
else if (this.cameraConfig.snapshotHandlingMethod === 3) {
this.log.info(this.device.getName(), 'is set to handle snapshots with cloud images. Snapshots might be older than they appear.');
}
else {
this.log.warn(this.device.getName(), 'unknown snapshot handling method. SNapshots will not be generated.');
}
try {
this.blackSnapshot = fs.readFileSync(SnapshotBlackPath);
if (this.cameraConfig.immediateRingNotificationWithoutSnapshot) {
this.log.info(this.device.getName(), 'Empty snapshot will be sent on ring events immediately to speed up homekit notifications.');
}
}
catch (err) {
this.log.error(this.device.getName(), 'could not cache black snapshot file for further use: ' + err);
}
}
onRingEvent(device, state) {
if (state) {
this.log.debug(this.device.getName(), 'Snapshot handler detected ring event.');
this.lastRingEvent = Date.now();
}
}
onEvent(device, state) {
if (state) {
this.log.debug(this.device.getName(), 'Snapshot handler detected event.');
this.lastEvent = Date.now();
}
}
async getSnapshotBuffer(request) {
// return a new snapshot if it is recent enough (not more than 15 seconds)
if (this.currentSnapshot) {
const diff = Math.abs((Date.now() - this.currentSnapshot.timestamp) / 1000);
if (diff <= 15) {
return this.resizeSnapshot(this.currentSnapshot.image, request);
}
}
const diff = (Date.now() - this.lastRingEvent) / 1000;
if (this.cameraConfig.immediateRingNotificationWithoutSnapshot && diff < 5) {
this.log.debug(this.device.getName(), 'Sending empty snapshot to speed up homekit notification for ring event.');
if (this.blackSnapshot) {
return this.resizeSnapshot(this.blackSnapshot, request);
}
else {
return Promise.reject('Prioritize ring notification over snapshot request. But could not supply empty snapshot.');
}
}
let snapshot = Buffer.from([]);
try {
if (this.cameraConfig.snapshotHandlingMethod === 1) {
// return a preferablly most recent snapshot every time
snapshot = await this.getNewestSnapshotBuffer();
}
else if (this.cameraConfig.snapshotHandlingMethod === 2) {
// balanced method
snapshot = await this.getBalancedSnapshot();
}
else if (this.cameraConfig.snapshotHandlingMethod === 3) {
// fastest method with potentially old snapshots
snapshot = await this.getNewestCloudSnapshot();
}
else {
return Promise.reject('No suitable handling method for snapshots defined');
}
return this.resizeSnapshot(snapshot, request);
}
catch (err) {
return Promise.reject(err);
}
}
async getNewestSnapshotBuffer() {
return new Promise((resolve, reject) => {
this.fetchCurrentCameraSnapshot().catch((err) => reject(err));
const requestTimeout = setTimeout(() => {
reject('snapshot request timed out');
}, 15000);
this.once('new snapshot', () => {
if (requestTimeout) {
clearTimeout(requestTimeout);
}
if (this.currentSnapshot) {
resolve(this.currentSnapshot.image);
}
else {
reject('Unknown snapshot request error');
}
});
});
}
async getBalancedSnapshot() {
return new Promise((resolve, reject) => {
let snapshotTimeout = setTimeout(() => {
if (this.currentSnapshot) {
resolve(this.currentSnapshot.image);
}
else {
resolve(fs.readFileSync(SnapshotUnavailable));
}
}, 1000);
this.fetchCurrentCameraSnapshot().catch((err) => this.log.warn(this.device.getName(), err));
const newestEvent = (this.lastRingEvent > this.lastEvent) ? this.lastRingEvent : this.lastEvent;
const diff = (Date.now() - newestEvent) / 1000;
if (diff < 15) { // wait for cloud or camera snapshot
this.log.debug(this.device.getName(), 'Waiting on cloud snapshot...');
if (snapshotTimeout) {
clearTimeout(snapshotTimeout);
}
snapshotTimeout = setTimeout(() => {
if (this.currentSnapshot) {
resolve(this.currentSnapshot.image);
}
else {
resolve(fs.readFileSync(SnapshotUnavailable));
}
}, 15000);
}
this.once('new snapshot', () => {
if (snapshotTimeout) {
clearTimeout(snapshotTimeout);
}
if (this.currentSnapshot) {
resolve(this.currentSnapshot.image);
}
else {
resolve(fs.readFileSync(SnapshotUnavailable));
}
});
});
}
async getNewestCloudSnapshot() {
return new Promise((resolve, reject) => {
const newestEvent = (this.lastRingEvent > this.lastEvent) ? this.lastRingEvent : this.lastEvent;
const diff = (Date.now() - newestEvent) / 1000;
if (diff < 15) { // wait for cloud snapshot
this.log.debug(this.device.getName(), 'Waiting on cloud snapshot...');
const snapshotTimeout = setTimeout(() => {
reject('No snapshot has been retrieved in time from eufy cloud.');
}, 15000);
this.once('new snapshot', () => {
if (snapshotTimeout) {
clearTimeout(snapshotTimeout);
}
if (this.currentSnapshot) {
resolve(this.currentSnapshot.image);
}
else {
resolve(fs.readFileSync(SnapshotUnavailable));
}
});
}
else {
if (this.currentSnapshot) {
resolve(this.currentSnapshot.image);
}
else {
resolve(fs.readFileSync(SnapshotUnavailable));
}
}
});
}
automaticSnapshotRefresh() {
this.log.debug(this.device.getName(), 'Automatic snapshot refresh triggered.');
this.fetchCurrentCameraSnapshot().catch((err) => this.log.warn(this.device.getName(), err));
if (this.snapshotRefreshTimer) {
clearTimeout(this.snapshotRefreshTimer);
}
if (this.cameraConfig.refreshSnapshotIntervalMinutes) {
this.snapshotRefreshTimer = setTimeout(() => {
this.automaticSnapshotRefresh();
}, this.cameraConfig.refreshSnapshotIntervalMinutes * 60 * 1000);
}
}
storeImage(file, image) {
const filePath = `${this.platform.eufyPath}/${file}`;
try {
fs.writeFileSync(filePath, image);
this.platform.log.debug(`${this.device.getName()} Stored Image: ${filePath}`);
}
catch (error) {
this.platform.log.debug(`${this.device.getName()} Error: ${filePath} - ${error}`);
}
}
async onPropertyValueChanged(device, name, value) {
if (name === 'picture') {
const picture = device.getPropertyValue(eufy_security_client_1.PropertyName.DevicePicture);
if (picture && picture.type) {
this.storeImage(`${device.getSerial()}.${picture.type.ext}`, picture.data);
this.currentSnapshot = { timestamp: Date.now(), image: picture.data };
this.emit('new snapshot');
}
}
}
async fetchCurrentCameraSnapshot() {
if (this.refreshProcessRunning) {
return Promise.resolve();
}
this.refreshProcessRunning = true;
this.log.debug(this.device.getName(), 'Locked refresh process.');
this.log.debug(this.device.getName(), 'Fetching new snapshot from camera.');
const timestamp = Date.now();
try {
const snapshotBuffer = await this.getCurrentCameraSnapshot();
this.refreshProcessRunning = false;
this.log.debug(this.device.getName(), 'Unlocked refresh process.');
this.log.debug(this.device.getName(), 'store new snapshot from camera in memory. Using this for future use.');
this.currentSnapshot = {
timestamp: timestamp,
image: snapshotBuffer,
};
this.emit('new snapshot');
return Promise.resolve();
}
catch (err) {
this.refreshProcessRunning = false;
this.log.debug(this.device.getName(), 'Unlocked refresh process.');
return Promise.reject(err);
}
}
async getCurrentCameraSnapshot() {
var _a;
const source = await this.getCameraSource();
if (!source) {
return Promise.reject('No camera source detected.');
}
const parameters = await ffmpeg_1.FFmpegParameters.forSnapshot((_a = this.cameraConfig.videoConfig) === null || _a === void 0 ? void 0 : _a.debug);
if (source.url) {
parameters.setInputSource(source.url);
}
else if (source.stream && source.livestreamId) {
await parameters.setInputStream(source.stream);
}
else {
return Promise.reject('No valid camera source detected.');
}
if (this.cameraConfig.delayCameraSnapshot) {
parameters.setDelayedSnapshot();
}
try {
const ffmpeg = new ffmpeg_1.FFmpeg(`[${this.device.getName()}] [Snapshot Process]`, parameters, this.platform.ffmpegLogger);
const buffer = await ffmpeg.getResult();
if (source.livestreamId) {
this.livestreamManager.stopProxyStream(source.livestreamId);
}
return Promise.resolve(buffer);
}
catch (err) {
if (source.livestreamId) {
this.livestreamManager.stopProxyStream(source.livestreamId);
}
return Promise.reject(err);
}
}
async getCameraSource() {
if ((0, utils_1.is_rtsp_ready)(this.device, this.cameraConfig, this.log)) {
try {
const url = this.device.getPropertyValue(eufy_security_client_1.PropertyName.DeviceRTSPStreamUrl);
this.log.debug(this.device.getName(), 'RTSP URL: ' + url);
return {
url: url,
};
}
catch (err) {
this.log.warn(this.device.getName(), 'Could not get snapshot from rtsp stream!');
return null;
}
}
else {
try {
const streamData = await this.livestreamManager.getLocalLivestream();
return {
stream: streamData.videostream,
livestreamId: streamData.id,
};
}
catch (err) {
this.log.warn(this.device.getName(), 'Could not get snapshot from livestream!');
return null;
}
}
}
async resizeSnapshot(snapshot, request) {
var _a;
const parameters = await ffmpeg_1.FFmpegParameters.forSnapshot((_a = this.cameraConfig.videoConfig) === null || _a === void 0 ? void 0 : _a.debug);
parameters.setup(this.cameraConfig, request);
const ffmpeg = new ffmpeg_1.FFmpeg(`[${this.device.getName()}] [Snapshot Resize Process]`, parameters, this.platform.ffmpegLogger);
return ffmpeg.getResult(snapshot);
}
}
exports.SnapshotManager = SnapshotManager;
//# sourceMappingURL=SnapshotManager.js.map