@komponent/unifi-protect-lib
Version:
Node library for connecting to Ubiquiti Unifi Protect controllers and listen for events
167 lines • 7.4 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const UnifiApiUpdateUtil_1 = require("./UnifiApiUpdateUtil");
const Utils_1 = __importDefault(require("./Utils"));
class UnifiCameraHandler {
constructor(log, apiClient, config = { refreshInterval: 5 }) {
this.log = log;
this.apiClient = apiClient;
this.lastMotion = {};
this.lastRing = {};
this.eventTimers = {};
this.config = config;
}
/**
* Get the list of UniFi Protect cameras associated with the NVR.
*
* @return {*} {Promise<Array<Device>>}
* @memberof UnifiApiClient
*/
async getCameras() {
if (!this.apiClient.bootstrap) {
return;
}
if (this.cameras) {
return this.cameras;
}
const newDeviceList = this.apiClient.bootstrap?.cameras;
// Notify the user about any new devices that we've discovered.
if (newDeviceList) {
for (const newDevice of newDeviceList) {
// We only want to discover managed devices.
if (!newDevice.isManaged) {
continue;
}
const deviceName = Utils_1.default.getDeviceName(newDevice, newDevice.name, true);
// We've discovered a new device.
this.log.info("%s: Discovered %s: %s.", this.apiClient.nvrName, newDevice.modelKey, deviceName);
}
}
this.cameras = newDeviceList;
return newDeviceList;
}
async handleUpdate(event) {
const updatePacket = UnifiApiUpdateUtil_1.ProtectApiUpdates.decodeUpdatePacket(this.log, event);
if (!updatePacket) {
this.log.error("%s: Unable to process message from the realtime update events API.", this.apiClient.nvrName);
return;
}
const cameras = await this.getCameras();
// The update actions that we care about (doorbell rings, motion detection) look like this:
//
// action: "update"
// id: "someCameraId"
// modelKey: "camera"
// newUpdateId: "ignorethis"
//
// The payloads are what differentiate them - one updates lastMotion and the other lastRing.
switch (updatePacket.action.modelKey) {
case "camera": {
// We listen for the following camera update actions:
// doorbell rings
// motion detection
// We're only interested in update actions.
if (updatePacket.action.action !== "update") {
return;
}
// Grab the right payload type, camera update payloads.
const payload = updatePacket.payload;
// Now filter out payloads we aren't interested in. We only want motion detection and doorbell rings for now.
if (!payload.isMotionDetected && !payload.lastRing) {
return;
}
// Get the camera that the event belongs to
const camera = cameras?.find((x) => x.id == updatePacket.action.id);
if (!camera) {
return;
}
// It's a motion event - process it accordingly,
// but only if we're not configured for smart motion events - we handle those elsewhere.
if (payload.isMotionDetected) {
if (payload.lastMotion) {
this.log.debug(`Camera ${camera.name} detected a motion`);
this.handleMotionEvent(camera, payload.lastMotion);
}
}
// It's a ring event - process it accordingly.
if (payload.lastRing) {
this.log.debug(`Doorbell ${camera.name} rang`);
this.handleDoorbellEvent(camera, payload.lastRing);
}
break;
}
case "event": {
// We listen for the following event actions:
// smart motion detection
// We're only interested in add events.
if (updatePacket.action.action !== "add") {
return;
}
// Grab the right payload type, for event add payloads.
const payload = updatePacket.payload;
// We're only interested in smart motion detection events.
if (payload.type !== "smartDetectZone") {
return;
}
const camera = cameras?.find((x) => x.mac == updatePacket.action.id);
if (!camera) {
return;
}
// Handle smart motion events
this.log.info(`Camera ${camera.name} detected a smart motion`);
this.handleMotionEvent(camera, payload.start, payload.smartDetectTypes);
return;
}
default:
// It's not a modelKey we're interested in. We're done.
return;
}
}
handleMotionEvent(camera, lastMotion, detectedObjects = []) {
// Have we seen this event before? If so...move along.
if (this.lastMotion[camera.mac] >= lastMotion) {
this.log.debug("%s: Skipping duplicate motion event.", camera.name);
return;
}
// We only consider events that have happened within the last two refresh intervals. Otherwise, we assume
// it's stale data and don't inform the user.
if (Date.now() - lastMotion > this.config.refreshInterval * 2 * 1000) {
this.log.debug("%s: Skipping motion event due to stale data.", camera.name);
return;
}
// Remember this event.
this.lastMotion[camera.mac] = lastMotion;
// If we already have a motion event inflight, allow it to complete so we don't spam users.
if (this.eventTimers[camera.mac]) {
return;
}
// Call the external motion handler if specified
if (this.config.onMotionEvent) {
this.config.onMotionEvent(camera);
}
}
handleDoorbellEvent(camera, lastRing) {
// Have we seen this event before? If so...move along. It's unlikely we hit this in a doorbell scenario, but just in case.
if (this.lastRing[camera.mac] >= lastRing) {
this.log.debug("%s: Skipping duplicate doorbell ring.", camera.name);
return;
}
// We only consider events that have happened within the last two refresh intervals. Otherwise, we assume it's stale
// data and don't inform the user.
if (Date.now() - lastRing > this.config.refreshInterval * 2 * 1000) {
this.log.debug("%s: Skipping doorbell ring due to stale data.", camera.name);
return;
}
// Remember this event.
this.lastRing[camera.mac] = lastRing;
// Call the external doorbell handler if specified
if (this.config.onDoorbellEvent) {
this.config.onDoorbellEvent(camera);
}
}
}
exports.default = UnifiCameraHandler;
//# sourceMappingURL=UnifiCameraHandler.js.map