UNPKG

@komponent/unifi-protect-lib

Version:

Node library for connecting to Ubiquiti Unifi Protect controllers and listen for events

175 lines 9.21 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ProtectApiUpdates = void 0; const zlib_1 = __importDefault(require("zlib")); /* * The UniFi Protect realtime updates API is largely undocumented and has been reverse engineered mostly through * trial and error, as well as observing the Protect controller in action. * * Here's how to get started with the UniFi Protect Updates API: * * 1. Login to the UniFi Protect controller, obtain the bootstrap JSON. * 2. Open the websocket to the updates URL (see protect-api.ts). * * Then you're ready to listen to messages. You can see an example of this in protect-nvr.ts. * * Those are the basics and gets us up and running. Now, to explain how the updates API works... * * UniFi OS update data packets are used to provide a realtime stream of updates to Protect. It differs from * the system events API in that the system events API appears to be shared across other applications (Network, Access, etc.) * while the updates events API appears to only be utilized by Protect and not shared by other applications, although the protocol * is shared. * * So how does it all work? Cameras continuously stream updates to the UniFi Protect controller containing things like camera * health, statistics, and - crucially for us - events such as motion and doorbell ring. A complete update packet is composed of four * frames: * * Header Frame (8 bytes) * Action Frame * Header Frame (8 bytes) * Data Frame * * The header frame is required overhead since websockets provide only a transport medium. It's purpose is to tell us what's * coming in the frame that follows. * * The action frame identifies what the action and category that the update contains: * * Property Description * -------- ----------- * action What action is being taken. Known actions are "add" and "update". * id The identifier for the device we're updating. * modelKey The device model category that we're updating. * newUpdateId A new UUID generated on a per-update basis. This can be safely ignored it seems. * * The final part of the update packet is the data frame. The data frame can be three different types of data - although in * practice, I've only seen JSONs come across. Those types are: * * Payload Type Description * 1 JSON. For update actions that are not events, this is always a subset of the configuration bootstrap JSON. * 2 A UTF8-encoded string * 3 Node Buffer * * Some tips: * * - "update" actions are always tied to the following modelKeys: camera, event, nvr, and user. * * - "add" actions are always tied to the "event" modelKey and indicate the beginning of an event item in the Protect events list. * A subsequent "update" action is sent signaling the end of the event capture, and it's confidence score for motion detection. * * - The above is NOT the same thing as motion detection. If you want to detect motion, you should watch the "update" action for "camera" * modelKeys, and look for a JSON that updates lastMotion. For doorbell rings, lastRing. The Protect events list is useful for the * Protect app, but it's of limited utility to HomeKit, and it's slow - relative to looking for lastMotion that is. If you want true * realtime updates, you want to look at the "update" action. * * - JSONs are only payload type that seems to be sent, although the protocol is designed to accept all three. * * - With the exception of update actions with a modelKey of event, JSONs are always a subset of the bootstrap JSON, indexed off * of modelKey. So for a modelKey of camera, the data payload is always a subset of ProtectCameraConfigInterface (see protect-types.ts). */ // Update realtime API packet header size, in bytes. const UPDATE_PACKET_HEADER_SIZE = 8; // Update realtime API packet types. var UpdatePacketType; (function (UpdatePacketType) { UpdatePacketType[UpdatePacketType["ACTION"] = 1] = "ACTION"; UpdatePacketType[UpdatePacketType["PAYLOAD"] = 2] = "PAYLOAD"; })(UpdatePacketType || (UpdatePacketType = {})); // Update realtime API payload types. var UpdatePayloadType; (function (UpdatePayloadType) { UpdatePayloadType[UpdatePayloadType["JSON"] = 1] = "JSON"; UpdatePayloadType[UpdatePayloadType["STRING"] = 2] = "STRING"; UpdatePayloadType[UpdatePayloadType["BUFFER"] = 3] = "BUFFER"; })(UpdatePayloadType || (UpdatePayloadType = {})); /* A packet header is composed of 8 bytes in this order: * * Byte Offset Description Bits Values * 0 Packet Type 8 1 - action frame, 2 - payload frame. * 1 Payload Format 8 1 - JSON object, 2 - UTF8-encoded string, 3 - Node Buffer. * 2 Deflated 8 0 - uncompressed, 1 - compressed / deflated (zlib-based compression). * 3 Unknown 8 Always 0. Possibly reserved for future use by Ubiquiti? * 4-7 Payload Size: 32 Size of payload in network-byte order (big endian). */ var UpdatePacketHeader; (function (UpdatePacketHeader) { UpdatePacketHeader[UpdatePacketHeader["TYPE"] = 0] = "TYPE"; UpdatePacketHeader[UpdatePacketHeader["PAYLOAD_FORMAT"] = 1] = "PAYLOAD_FORMAT"; UpdatePacketHeader[UpdatePacketHeader["DEFLATED"] = 2] = "DEFLATED"; UpdatePacketHeader[UpdatePacketHeader["UNKNOWN"] = 3] = "UNKNOWN"; UpdatePacketHeader[UpdatePacketHeader["PAYLOAD_SIZE"] = 4] = "PAYLOAD_SIZE"; })(UpdatePacketHeader || (UpdatePacketHeader = {})); class ProtectApiUpdates { // Process an update data packet and return the action and payload. static decodeUpdatePacket(log, packet) { // What we need to do here is to split this packet into the header and payload, and decode them. let dataOffset; try { // The fourth byte holds our payload size. When you add the payload size to our header frame size, you get the location of the // data header frame. dataOffset = packet.readUInt32BE(UpdatePacketHeader.PAYLOAD_SIZE) + UPDATE_PACKET_HEADER_SIZE; // Validate our packet size, just in case we have more or less data than we expect. If we do, we're done for now. if (packet.length !== dataOffset + UPDATE_PACKET_HEADER_SIZE + packet.readUInt32BE(dataOffset + UpdatePacketHeader.PAYLOAD_SIZE)) { throw new Error("Packet length doesn't match header information."); } } catch (error) { log.error("Realtime update API: error decoding update packet: %s.", error); return null; } // Decode the action and payload frames now that we know where everything is. const actionFrame = this.decodeUpdateFrame(log, packet.slice(0, dataOffset), UpdatePacketType.ACTION); const payloadFrame = this.decodeUpdateFrame(log, packet.slice(dataOffset), UpdatePacketType.PAYLOAD); if (!actionFrame || !payloadFrame) { return null; } return { action: actionFrame, payload: payloadFrame }; } // Decode a frame, composed of a header and payload, received through the update events API. static decodeUpdateFrame(log, packet, packetType) { // Read the packet frame type. const frameType = packet.readUInt8(UpdatePacketHeader.TYPE); // This isn't the frame type we were expecting - we're done. if (packetType !== frameType) { return null; } // Read the payload format. const payloadFormat = packet.readUInt8(UpdatePacketHeader.PAYLOAD_FORMAT); // Check to see if we're compressed or not, and inflate if needed after skipping past the 8-byte header. const payload = packet.readUInt8(UpdatePacketHeader.DEFLATED) ? zlib_1.default.inflateSync(packet.slice(UPDATE_PACKET_HEADER_SIZE)) : packet.slice(UPDATE_PACKET_HEADER_SIZE); // If it's an action, it can only have one format. if (frameType === UpdatePacketType.ACTION) { return payloadFormat === UpdatePayloadType.JSON ? JSON.parse(payload.toString()) : null; } // Process the payload format accordingly. switch (payloadFormat) { case UpdatePayloadType.JSON: // If it's data payload, it can be anything. return JSON.parse(payload.toString()); break; case UpdatePayloadType.STRING: return payload.toString("utf8"); break; case UpdatePayloadType.BUFFER: return payload; break; default: log.error("Unknown payload packet type received in the realtime update events API: %s.", payloadFormat); return null; break; } } } exports.ProtectApiUpdates = ProtectApiUpdates; //# sourceMappingURL=UnifiApiUpdateUtil.js.map