unifi-protect
Version:
A complete implementation of the UniFi Protect API.
116 lines • 6.43 kB
JavaScript
/* Copyright(C) 2019-2025, HJD (https://github.com/hjdhjd). All rights reserved.
*
* protect-api-events.ts: Our UniFi Protect realtime events API packet utilities.
*/
import zlib from "node:zlib";
// UniFi Protect events API packet header size, in bytes.
const EVENT_PACKET_HEADER_SIZE = 8;
// Update realtime API packet types.
var ProtectEventPacketType;
(function (ProtectEventPacketType) {
ProtectEventPacketType[ProtectEventPacketType["HEADER"] = 1] = "HEADER";
ProtectEventPacketType[ProtectEventPacketType["PAYLOAD"] = 2] = "PAYLOAD";
})(ProtectEventPacketType || (ProtectEventPacketType = {}));
// Update realtime API payload types.
var EventPayloadType;
(function (EventPayloadType) {
EventPayloadType[EventPayloadType["JSON"] = 1] = "JSON";
EventPayloadType[EventPayloadType["STRING"] = 2] = "STRING";
EventPayloadType[EventPayloadType["BUFFER"] = 3] = "BUFFER";
})(EventPayloadType || (EventPayloadType = {}));
/**
* @internal
*
* A packet header is composed of 8 bytes in this order:
*
* | Byte Offset | Description | Bits | Values |
* |-------------|----------------|------|-----------------------------------------------------------------------|
* | 0 | Packet Type | 8 | 1 - header 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 ProtectEventPacketHeader;
(function (ProtectEventPacketHeader) {
ProtectEventPacketHeader[ProtectEventPacketHeader["TYPE"] = 0] = "TYPE";
ProtectEventPacketHeader[ProtectEventPacketHeader["PAYLOAD_FORMAT"] = 1] = "PAYLOAD_FORMAT";
ProtectEventPacketHeader[ProtectEventPacketHeader["DEFLATED"] = 2] = "DEFLATED";
ProtectEventPacketHeader[ProtectEventPacketHeader["UNKNOWN"] = 3] = "UNKNOWN";
ProtectEventPacketHeader[ProtectEventPacketHeader["PAYLOAD_SIZE"] = 4] = "PAYLOAD_SIZE";
})(ProtectEventPacketHeader || (ProtectEventPacketHeader = {}));
/**
* UniFi Protect event utility class that provides functions for decoding realtime event API packet frames.
*/
export class ProtectApiEvents {
/** @internal */
constructor() {
}
/**
* Decode a UniFi Protect event packet.
*
* @param log - Logging functions to use.
* @param packet - Input packet to decode.
*
* @remarks A UniFi Protect event packet is an encoded representation of state updates that occur in a UniFi Protect controller. This utility function takes an
* encoded packet as an input, and decodes it into an event header and payload that can be acted upon. An example of it's use is in {@link ProtectApi} where, once
* successfully logged into the Protect controller, events are generated automatically and can be accessed by listening to `message` events emitted by
* {@link ProtectApi}.
*/
static decodePacket(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(ProtectEventPacketHeader.PAYLOAD_SIZE) + EVENT_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 + EVENT_PACKET_HEADER_SIZE + packet.readUInt32BE(dataOffset + ProtectEventPacketHeader.PAYLOAD_SIZE))) {
throw new Error("Packet length doesn't match header information.");
}
}
catch (error) {
log.error("Realtime events API: error decoding update packet: %s.", error);
return null;
}
// Decode the action and payload frames now that we know where everything is.
const headerFrame = this.decodeFrame(log, packet.slice(0, dataOffset), ProtectEventPacketType.HEADER);
const payloadFrame = this.decodeFrame(log, packet.slice(dataOffset), ProtectEventPacketType.PAYLOAD);
if (!headerFrame || !payloadFrame) {
return null;
}
return ({ header: headerFrame, payload: payloadFrame });
}
// Decode a frame, composed of a header and payload, received through the update events API.
static decodeFrame(log, packet, packetType) {
// Read the packet frame type.
const frameType = packet.readUInt8(ProtectEventPacketHeader.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(ProtectEventPacketHeader.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(ProtectEventPacketHeader.DEFLATED) ?
zlib.inflateSync(packet.slice(EVENT_PACKET_HEADER_SIZE)) : packet.slice(EVENT_PACKET_HEADER_SIZE);
// If it's a header, it can only have one format.
if (frameType === ProtectEventPacketType.HEADER) {
return (payloadFormat === EventPayloadType.JSON) ? JSON.parse(payload.toString()) : null;
}
// Process the payload format accordingly.
switch (payloadFormat) {
case EventPayloadType.JSON:
// If it's data payload, it can be anything.
return JSON.parse(payload.toString());
case EventPayloadType.STRING:
return payload.toString("utf8");
case EventPayloadType.BUFFER:
return payload;
default:
log.error("Unknown payload packet type received in the realtime events API: %s.", payloadFormat);
return null;
}
}
}
//# sourceMappingURL=protect-api-events.js.map