UNPKG

unifi-protect

Version:

A complete implementation of the UniFi Protect API.

1,164 lines (1,163 loc) 42.8 kB
/** * A complete implementation of the UniFi Protect API, providing comprehensive access to UniFi Protect controllers. * * ## Overview * * This module provides a high-performance, event-driven interface to the UniFi Protect API, enabling full access to * Protect's rich ecosystem of security devices and capabilities. The API has been reverse-engineered through careful * analysis of the Protect web interface and extensive testing, as Ubiquiti does not provide official documentation. * * ## Key Features * * - **Complete Device Support**: Cameras, lights, sensors, chimes, viewers, and the NVR itself * - **Real-time Events**: WebSocket-based event streaming for instant notifications * - **Livestream Access**: Direct H.264 fMP4 stream access, not just RTSP * - **Robust Error Handling**: Automatic retry logic with exponential backoff * - **Type Safety**: Full TypeScript support with comprehensive type definitions * * ## Quick Start * * ```typescript * import { ProtectApi } from "unifi-protect"; * * // Create an API instance. * const protect = new ProtectApi(); * * // Login to your Protect controller. * await protect.login("192.168.1.1", "username", "password"); * * // Bootstrap to get the current state. * await protect.getBootstrap(); * * // Access your devices. * const cameras = protect.bootstrap?.cameras ?? []; * console.log("Found " + cameras.length.toString() + " cameras."); * * // Listen for real-time events. * protect.on("message", (packet) => { * console.log("Event received:", packet); * }); * ``` * * ## Architecture * * The API is built on modern Node.js technologies: * - **Undici**: High-performance HTTP/1.1 and HTTP/2 client * - **WebSockets**: Real-time bidirectional communication * - **EventEmitter**: Node.js event-driven architecture * * ## Authentication * * The API uses cookie-based authentication with CSRF token protection, mimicking the Protect web interface. * Administrative privileges are required for configuration changes, while read-only access is available to all users. * * @module ProtectApi */ import { Agent, type Dispatcher } from "undici"; import type { DeepPartial, Nullable, ProtectCameraConfig, ProtectCameraConfigInterface, ProtectChimeConfig, ProtectLightConfig, ProtectNvrBootstrap, ProtectNvrConfig, ProtectSensorConfig, ProtectViewerConfig } from "./protect-types.js"; import { EventEmitter } from "node:events"; import { ProtectLivestream } from "./protect-api-livestream.js"; import type { ProtectLogging } from "./protect-logging.js"; /** * The Protect device types we know about and are available to us. */ export type ProtectKnownDeviceTypes = ProtectCameraConfig | ProtectChimeConfig | ProtectLightConfig | ProtectNvrConfig | ProtectSensorConfig | ProtectViewerConfig; /** * The model key identifiers for known Protect device categories, derived from the device type interfaces. */ export type ProtectKnownDeviceModelKey = ProtectKnownDeviceTypes["modelKey"]; /** * Known Protect API endpoint identifiers accepted by {@link ProtectApi.getApiEndpoint}. Device endpoints correspond to {@link ProtectKnownDeviceModelKey} values. */ export type ProtectApiEndpoint = ProtectKnownDeviceModelKey | "bootstrap" | "login" | "self" | "websocket"; /** * The Protect device payload types we know about and are available to us. */ export type ProtectKnownDevicePayloads = DeepPartial<ProtectCameraConfig> | DeepPartial<ProtectChimeConfig> | DeepPartial<ProtectLightConfig> | DeepPartial<ProtectNvrConfig> | DeepPartial<ProtectSensorConfig> | DeepPartial<ProtectViewerConfig>; /** * The Protect NVR bootstrap data type used by the {@link ProtectApi.bootstrap | bootstrap} getter. Device interfaces include index signatures for accessing untyped API * fields without casting. */ export type ProtectNvrBootstrapData = Nullable<ProtectNvrBootstrap>; /** * Configuration options for HTTP requests executed by `retrieve()`. * * @remarks Extends Undici’s [`Dispatcher.RequestOptions`](https://undici.nodejs.org/#/docs/api/Dispatcher.md?id=parameter-requestoptions), but omits the `origin` and * `path` properties, since those are derived from the `url` argument passed to `retrieve()`. You can optionally supply a custom `Dispatcher` instance to control * connection pooling, timeouts, etc. */ export type RequestOptions = { /** * Optional custom Undici `Dispatcher` instance to use for this request. If omitted, the native `unifi-protect` dispatcher is used, which should be suitable for most * use cases. */ dispatcher?: Dispatcher; } & Omit<Dispatcher.RequestOptions, "origin" | "path">; /** * Options to tailor the behavior of {@link ProtectApi.retrieve}. * * @property {boolean} [logErrors=true] - Log errors. Defaults to `true`. * @property {number} [timeout=3500] - Amount of time, in milliseconds, to wait for the Protect controller to respond before timing out. Defaults to `3500`. */ export interface RetrieveOptions { logErrors?: boolean; timeout?: number; } /** * This class provides an event-driven API to access the UniFi Protect API. * * ## Getting Started * * To begin using the API, follow these three essential steps: * * 1. **Login**: Authenticate with the Protect controller using {@link login} * 2. **Bootstrap**: Retrieve the controller configuration with {@link getBootstrap} * 3. **Listen**: Subscribe to real-time events via the `message` event * * ## Events * * The API emits several events during its lifecycle: * * | Event | Payload | Description | * |-------|---------|-------------| * | `login` | `boolean` | Emitted after each login attempt with success status | * | `bootstrap` | {@link ProtectNvrBootstrap} | Emitted when bootstrap data is retrieved | * | `message` | {@link ProtectEventPacket} | Real-time event packets from the controller | * * ## Connection Management * * The API automatically manages connection pooling and implements intelligent retry logic: * - Up to 5 concurrent connections * - Automatic retry with exponential backoff * - Throttling after repeated failures * - Graceful WebSocket reconnection * * ## Error Handling * * All API methods implement comprehensive error handling: * - Network errors are logged and retried * - Authentication failures trigger re-login attempts * - Server errors are handled gracefully * - Detailed logging for debugging * * @event login - Emitted after each login attempt with the success status as a boolean value. This event fires whether the login succeeds or fails, allowing * applications to respond appropriately to authentication state changes. * @event bootstrap - Emitted when bootstrap data is successfully retrieved from the controller. The event includes the complete {@link ProtectNvrBootstrap} * configuration object containing all device states and system settings. * @event message - Emitted for each real-time event packet received from the controller's WebSocket connection. The event includes a * {@link ProtectEventPacket} containing device updates, motion events, and other system notifications. * * @example * Complete example with error handling and event processing: * * ```typescript * import { ProtectApi, ProtectCameraConfig } from "unifi-protect"; * * class ProtectManager { * private api: ProtectApi; * * constructor() { * this.api = new ProtectApi(); * this.setupEventHandlers(); * } * * private setupEventHandlers(): void { * // Handle login events. * this.api.on("login", (success: boolean) => { * console.log(success ? "Login successful." : "Login failed."); * }); * * // Process real-time events. * this.api.on("message", (packet) => { * if(packet.header.modelKey === "camera") { * console.log("Camera event:", packet); * } * }); * } * * async connect(host: string, username: string, password: string): Promise<boolean> { * try { * // Login to the controller. * if(!await this.api.login(host, username, password)) { * throw new Error("Authentication failed."); * } * * // Bootstrap the configuration. * if(!await this.api.getBootstrap()) { * throw new Error("Bootstrap failed."); * } * * console.log("Connected to " + this.api.name + "."); * return true; * * } catch(error) { * console.error("Connection failed:", error); * return false; * } * } * * async enableAllRtspStreams(): Promise<void> { * const cameras = this.api.bootstrap?.cameras ?? []; * * for(const camera of cameras) { * const updated = await this.api.enableRtsp(camera); * if(updated) { * console.log("RTSP enabled for " + this.api.getDeviceName(camera) + "."); * } * } * } * } * ``` */ export declare class ProtectApi extends EventEmitter { private _bootstrap; private _eventsWs; private apiErrorCount; private apiThrottleStart; private dispatcher?; private headers; private _isAdminUser; private _isThrottled; private log; private nvrAddress; private password; private username; private wsAgent; /** * Create an instance of the UniFi Protect API. * * @param log - Custom logging implementation. * * @defaultValue Console logging to stdout/stderr * * @remarks * The logging interface allows you to integrate the API with your application's logging system. * By default, errors and warnings are logged to the console, while debug messages are suppressed. * * @example * Using a custom logger: * * ```typescript * import { ProtectApi, ProtectLogging } from "unifi-protect"; * import winston from "winston"; * * const logger = winston.createLogger({ * level: "info", * format: winston.format.simple(), * transports: [new winston.transports.Console()] * }); * * const customLog: ProtectLogging = { * debug: (message: string, ...args: unknown[]) => logger.debug(message, args), * error: (message: string, ...args: unknown[]) => logger.error(message, args), * info: (message: string, ...args: unknown[]) => logger.info(message, args), * warn: (message: string, ...args: unknown[]) => logger.warn(message, args) * }; * * const protect = new ProtectApi(customLog); * ``` * * @category Constructor */ constructor(log?: ProtectLogging); /** * Execute a login attempt to the UniFi Protect API. * * @param nvrAddress - Address of the UniFi Protect controller (FQDN or IP address) * @param username - Username for authentication * @param password - Password for authentication * * @returns Promise resolving to `true` on success, `false` on failure. * * @event login - Emitted with `true` if authentication succeeds, `false` if it fails. The event fires after every login attempt, regardless of outcome. * * @remarks * This method performs the following actions: * * - Terminates any existing sessions * - Acquires CSRF tokens for API security * - Establishes cookie-based authentication * - Emits a `login` event with the result * * The method automatically handles UniFi OS CSRF protection and maintains session state for subsequent API calls. Administrative privileges are determined during * login and cached for the session duration. * * @example * Multiple authentication patterns: * * ```typescript * import { ProtectApi } from "unifi-protect"; * * const protect = new ProtectApi(); * * // Pattern 1: Using async/await. * async function connectWithAwait() { * const success = await protect.login("192.168.1.1", "admin", "password"); * if(success) { * console.log("Connected successfully."); * } * } * * // Pattern 2: Using event listeners. * function connectWithEvents() { * protect.once("login", (success: boolean) => { * if(success) { * console.log("Connected successfully."); * // Continue with bootstrap. * protect.getBootstrap(); * } * }); * * protect.login("192.168.1.1", "admin", "password"); * } * * // Pattern 3: With retry logic. * async function connectWithRetry(maxAttempts = 3) { * for(let i = 0; i < maxAttempts; i++) { * if(await protect.login("192.168.1.1", "admin", "password")) { * return true; * } * console.log("Login attempt " + (i + 1).toString() + " failed, retrying..."); * await new Promise(resolve => setTimeout(resolve, 2000)); * } * return false; * } * ``` * * @category Authentication */ login(nvrAddress: string, username: string, password: string): Promise<boolean>; private loginController; private bootstrapController; /** * Connect to the realtime update events API. * * @event message - Emitted for each WebSocket message received from the controller after successful decoding. Each message contains a * {@link ProtectEventPacket} with real-time device updates and system events. * * @internal */ private launchEventsWs; /** * Retrieve the bootstrap JSON from a UniFi Protect controller. * * @returns Promise resolving to `true` on success, `false` on failure. * * @event bootstrap - Emitted with the complete {@link ProtectNvrBootstrap} configuration when successfully retrieved. The bootstrap contains all device configurations, * user accounts, system settings, and current device states. * @event message - Once the bootstrap is retrieved, the WebSocket connection is established and this event will be emitted for each real-time update packet received * from the controller. * * @remarks * The bootstrap contains the complete state of the Protect controller, including: * * - All device configurations (cameras, lights, sensors, etc.) * - User accounts and permissions * - System settings and capabilities * - Current device states and health * * This method automatically: * * - Reconnects if the session has expired * - Establishes WebSocket connections for real-time events * - Determines administrative privileges * - Emits a `bootstrap` event with the configuration * * @example * Working with bootstrap data: * * ```typescript * import { ProtectApi, ProtectCameraConfig } from "unifi-protect"; * * const protect = new ProtectApi(); * * async function analyzeSystem() { * // Login and bootstrap. * await protect.login("192.168.1.1", "admin", "password"); * await protect.getBootstrap(); * * // Access the bootstrap data. * const bootstrap = protect.bootstrap; * if(!bootstrap) return; * * // System information. * console.log("NVR: " + bootstrap.nvr.name); * console.log("Version: " + bootstrap.nvr.version); * console.log("Up since: " + new Date(bootstrap.nvr.upSince).toString()); * * // Device inventory. * console.log("Cameras: " + bootstrap.cameras.length.toString()); * console.log("Lights: " + bootstrap.lights.length.toString()); * console.log("Sensors: " + bootstrap.sensors.length.toString()); * * // Find specific devices. * const doorbells = bootstrap.cameras.filter((cam) => cam.featureFlags.isDoorbell); * const motionSensors = bootstrap.sensors.filter((sensor) => sensor.type === "motion"); * * // Check recording status. * const recording = bootstrap.cameras.filter((cam) => cam.isRecording && cam.isConnected); * * console.log(recording.length.toString() + " cameras actively recording."); * } * ``` * * @category API Access */ getBootstrap(): Promise<boolean>; private checkAdminUserStatus; /** * Retrieve a snapshot image from a Protect camera. * * @param device - Protect camera device * @param options - Snapshot configuration options * * @returns Promise resolving to a Buffer containing the JPEG image, or `null` on failure. * * @remarks * Snapshots are generated on-demand by the Protect controller. The image quality and resolution depend on the camera's capabilities and current settings. Package * camera snapshots are only available on devices with dual cameras (e.g., G4 Doorbell Pro). * * The `options` parameter accepts: * * | Property | Type | Description | Default | * |----------|------|-------------|---------| * | `width` | `number` | Requested image width in pixels | Camera default | * | `height` | `number` | Requested image height in pixels | Camera default | * | `usePackageCamera` | `boolean` | Use package camera if available | `false` | * * @example * Capturing and saving snapshots: * * ```typescript * import { ProtectApi } from "unifi-protect"; * import { writeFile } from "fs/promises"; * * const protect = new ProtectApi(); * * async function captureSnapshots() { * await protect.login("192.168.1.1", "admin", "password"); * await protect.getBootstrap(); * * const cameras = protect.bootstrap?.cameras ?? []; * * for(const camera of cameras) { * // Full resolution snapshot. * const fullRes = await protect.getSnapshot(camera); * * // Thumbnail snapshot. * const thumbnail = await protect.getSnapshot(camera, { * height: 360, * width: 640 * }); * * // Package camera snapshot (if available). * if(camera.featureFlags.hasPackageCamera) { * const packageSnap = await protect.getSnapshot(camera, { * usePackageCamera: true * }); * * if(packageSnap) { * await writeFile(camera.name + "-package.jpg", packageSnap); * } * } * * if(fullRes && thumbnail) { * await writeFile(camera.name + "-full.jpg", fullRes); * await writeFile(camera.name + "-thumb.jpg", thumbnail); * console.log("Saved snapshots for " + camera.name + "."); * } * } * } * ``` * * @category API Access */ getSnapshot(device: ProtectCameraConfig, options?: Partial<{ width: number; height: number; usePackageCamera: boolean; }>): Promise<Nullable<Buffer>>; /** * Update a Protect device's configuration on the UniFi Protect controller. * * @typeParam DeviceType - Generic for any known Protect device type * * @param device - Protect device to update * @param payload - Configuration changes to apply * * @returns Promise resolving to the updated device configuration, or `null` on failure. * * @remarks * This method applies configuration changes to any Protect device. Common modifications include: * * - Camera settings (name, recording modes, motion zones) * - Light settings (brightness, motion activation) * - Sensor settings (sensitivity, mount type) * - Chime settings (volume, ringtones) * * **Important**: Most configuration changes require administrative privileges. The user account must have the Super Admin role assigned in UniFi Protect. * * Changes are applied immediately and persist across device reboots. The method returns the complete updated device configuration, reflecting any server-side * adjustments. * * @example * Common device configuration scenarios: * * ```typescript * import { ProtectApi, ProtectCameraConfig } from "unifi-protect"; * * const protect = new ProtectApi(); * * async function configureDevices() { * await protect.login("192.168.1.1", "admin", "password"); * await protect.getBootstrap(); * * const camera = protect.bootstrap?.cameras[0]; * if(!camera) return; * * // Update camera name and recording settings. * const updatedCamera = await protect.updateDevice(camera, { * name: "Front Door Camera", * recordingSettings: { * mode: "always", * postPaddingSecs: 3, * prePaddingSecs: 3, * retentionDurationMs: 7 * 24 * 60 * 60 * 1000 * } * }); * * // Configure smart detection. * await protect.updateDevice(camera, { * smartDetectSettings: { * autoTrackingObjectTypes: ["person"], * objectTypes: [ "person", "vehicle" ] * } * }); * * // Update light device. * const light = protect.bootstrap?.lights[0]; * if(light) { * await protect.updateDevice(light, { * lightDeviceSettings: { * ledLevel: 6, * pirDuration: 15000, * pirSensitivity: 50 * }, * lightModeSettings: { * mode: "motion" * } * }); * } * * // Configure doorbell chime volume. * const chime = protect.bootstrap?.chimes[0]; * if(chime) { * await protect.updateDevice(chime, { * volume: 75 * }); * } * } * ``` * * @category API Access */ updateDevice<DeviceType extends ProtectKnownDeviceTypes>(device: DeviceType, payload: ProtectKnownDevicePayloads): Promise<Nullable<DeviceType>>; private updateCameraChannels; /** * Utility method that enables all RTSP channels on a given Protect camera. * * @param device - Protect camera to modify * * @returns Promise resolving to the updated camera configuration, or `null` on failure. * * @remarks * RTSP (Real Time Streaming Protocol) streams allow third-party applications to access camera feeds directly. This method enables RTSP on all available * channels (resolutions) for a camera, making them accessible at: * * - High: `rtsp://[NVR_IP]:7447/[CAMERA_GUID]_0` * - Medium: `rtsp://[NVR_IP]:7447/[CAMERA_GUID]_1` * - Low: `rtsp://[NVR_IP]:7447/[CAMERA_GUID]_2` * * **Note**: Enabling RTSP requires Super Admin privileges in UniFi Protect. * * @example * Enabling RTSP streams for integration: * * ```typescript * import { ProtectApi } from "unifi-protect"; * * const protect = new ProtectApi(); * * async function setupRtspStreams() { * await protect.login("192.168.1.1", "admin", "password"); * await protect.getBootstrap(); * * const cameras = protect.bootstrap?.cameras ?? []; * * for(const camera of cameras) { * const updated = await protect.enableRtsp(camera); * * if(updated) { * console.log("RTSP enabled for " + camera.name + "."); * * // Display RTSP URLs. * for(const [index, channel] of updated.channels.entries()) { * if(channel.isRtspEnabled) { * const rtspUrl = "rtsp://192.168.1.1:7447/" + channel.rtspAlias; * console.log(" Channel " + index.toString() + ": " + rtspUrl); * } * } * } else { * console.log("Failed to enable RTSP for " + camera.name + "."); * } * } * } * ``` * * @category Utilities */ enableRtsp(device: ProtectCameraConfigInterface): Promise<Nullable<ProtectCameraConfig>>; /** * Utility method that generates a nicely formatted device information string. * * @param device - Protect device * @param name - Custom name to use (defaults to device name) * @param deviceInfo - Include IP and MAC address information * * @returns Formatted device string. * * @remarks * Returns device information in a consistent, readable format: * * - Basic: `Device Name [Device Type]` * - With info: `Device Name [Device Type] (address: IP mac: MAC)` * * This method handles all Protect device types and gracefully handles missing information. * * @example * Formatting device information: * * ```typescript * import { ProtectApi } from "unifi-protect"; * * const protect = new ProtectApi(); * * async function listDevices() { * await protect.login("192.168.1.1", "admin", "password"); * await protect.getBootstrap(); * * // List all devices with full information. * const allDevices = [ * ...(protect.bootstrap?.cameras ?? []), * ...(protect.bootstrap?.lights ?? []), * ...(protect.bootstrap?.sensors ?? []), * ...(protect.bootstrap?.chimes ?? []), * ...(protect.bootstrap?.viewers ?? []) * ]; * * for(const device of allDevices) { * // Basic format. * console.log(protect.getDeviceName(device)); * // Output: "Front Door [G4 Doorbell Pro]" * * // With network info. * console.log(protect.getDeviceName(device, device.name, true)); * // Output: "Front Door [G4 Doorbell Pro] (address: 192.168.1.50 mac: 00:00:00:00:00:00)" * * // Custom name. * console.log(protect.getDeviceName(device, "Custom Name")); * // Output: "Custom Name [G4 Doorbell Pro]" * } * } * ``` * * @category Utilities */ getDeviceName(device: ProtectKnownDeviceTypes, name?: string | undefined, deviceInfo?: boolean): string; /** * Utility method that generates a combined device and controller information string. * * @param device - Protect device * * @returns Formatted string including both controller and device information. * * @remarks * Combines controller and device information for complete context: `Controller Name [Controller Type] Device Name [Device Type]` * * Useful for logging and multi-controller environments where device context is important. * * @example * Logging with full context: * * ```typescript * import { ProtectApi } from "unifi-protect"; * * const protect = new ProtectApi(); * * async function monitorDevices() { * await protect.login("192.168.1.1", "admin", "password"); * await protect.getBootstrap(); * * protect.on("message", (packet) => { * const device = protect.bootstrap?.cameras.find((c) => c.id === packet.header.id); * * if(device) { * // Logs: "Dream Machine Pro [UDMP] Front Door [G4 Doorbell Pro]" * console.log(protect.getFullName(device) + ": " + packet.header.action); * } * }); * } * ``` * * @category Utilities */ getFullName(device: ProtectKnownDeviceTypes): string; /** * Terminate any open connection to the UniFi Protect API. * * @remarks * Performs a clean shutdown of all API connections: * * - Closes WebSocket connections * - Destroys the HTTP connection pool * - Clears cached bootstrap data * - Resets authentication state * * Call this method when shutting down your application or switching controllers. The API can be reused after reset by calling {@link login} again. * * @example * Proper cleanup: * * ```typescript * import { ProtectApi } from "unifi-protect"; * * const protect = new ProtectApi(); * * async function main() { * try { * await protect.login("192.168.1.1", "admin", "password"); * await protect.getBootstrap(); * * // Do work... * * } finally { * // Always clean up connections. * protect.reset(); * } * } * * // Handle process termination. * process.on("SIGINT", () => { * protect.reset(); * process.exit(0); * }); * ``` * * @category Utilities */ reset(): void; /** * Clear login credentials and terminate all API connections. * * @remarks * Performs a complete logout: * * - Clears authentication tokens and cookies * - Terminates all active connections * - Resets user privilege status * - Preserves CSRF token for future logins * * After logout, a new {@link login} call is required to use the API again. * * @example * Switching between controllers: * * ```typescript * import { ProtectApi } from "unifi-protect"; * * const protect = new ProtectApi(); * * async function switchControllers() { * // Connect to first controller. * await protect.login("192.168.1.1", "admin", "password1"); * await protect.getBootstrap(); * console.log("Connected to " + protect.name + "."); * * // Switch to second controller. * protect.logout(); * * await protect.login("192.168.2.1", "admin", "password2"); * await protect.getBootstrap(); * console.log("Connected to " + protect.name + "."); * } * ``` * * @category Authentication */ logout(): void; private canModifyCamera; /** * Return a websocket API endpoint for the requested endpoint type. * * @param endpoint - Endpoint type (`livestream` or `talkback`) * @param params - URL parameters for the endpoint * * @returns Promise resolving to the WebSocket URL, or `null` on failure. * * @remarks * This method provides access to real-time WebSocket endpoints: * * ### Livestream Endpoint * Returns a WebSocket URL for H.264 fMP4 video streams. **Do not use directly** - use {@link createLivestream} instead for proper stream handling. * * ### Talkback Endpoint * Creates a two-way audio connection to cameras with speakers (doorbells, two-way audio cameras). The WebSocket accepts AAC-encoded ADTS audio streams. * * Required parameter: * * - `camera`: The camera ID to connect to * * @example * Setting up two-way audio: * * ```typescript * import { ProtectApi } from "unifi-protect"; * import { WebSocket } from "ws"; * * const protect = new ProtectApi(); * * async function setupTalkback(cameraId: string) { * await protect.login("192.168.1.1", "admin", "password"); * * // Get the talkback endpoint. * const params = new URLSearchParams({ camera: cameraId }); * const wsUrl = await protect.getWsEndpoint("talkback", params); * * if(!wsUrl) { * console.error("Failed to get talkback endpoint."); * return; * } * * // Connect to the WebSocket. * const ws = new WebSocket(wsUrl); * * ws.on("open", () => { * console.log("Talkback connection established."); * // Send AAC-encoded audio data. * // ws.send(aacAudioBuffer); * }); * * ws.on("error", (error) => { * console.error("Talkback error:", error); * }); * } * ``` * * @category API Access */ getWsEndpoint(endpoint: "livestream" | "talkback", params?: URLSearchParams): Promise<Nullable<string>>; /** * Execute an HTTP request to the Protect controller. * * @param url - Full URL to request (e.g., `https://192.168.1.1/proxy/protect/api/cameras`) * @param options - Undici-compatible request options * @param retrieveOptions - Additional options for error handling and timeouts * * @returns Promise resolving to the Response object, or `null` on failure. * * @remarks * This method provides direct access to the Protect controller API for advanced use cases not covered by the built-in methods. It handles: * * - Authentication and session management * - Automatic retry with exponential backoff * - Error logging and throttling * - CSRF token management * * The `options` parameter extends [Undici's RequestOptions](https://undici.nodejs.org/#/docs/api/Dispatcher.md?id=parameter-requestoptions), providing full control * over the HTTP request. * * @example * Making custom API calls: * * ```typescript * import { ProtectApi } from "unifi-protect"; * * const protect = new ProtectApi(); * * async function customApiCalls() { * await protect.login("192.168.1.1", "admin", "password"); * * // Get events from the last hour. * const end = Date.now(); * const start = end - (60 * 60 * 1000); * * const response = await protect.retrieve( * "https://192.168.1.1/proxy/protect/api/events?start=" + start.toString() + "&end=" + end.toString(), * { method: "GET" } * ); * * if(response) { * const events = await response.body.json(); * console.log("Found " + events.length.toString() + " events."); * } * * // Download a video clip. * const videoResponse = await protect.retrieve( * "https://192.168.1.1/proxy/protect/api/video/export", * { * body: JSON.stringify({ * camera: "camera-id", * end: end, * start: start, * type: "timelapse" * }), * method: "POST" * }, * { * timeout: 30000 * } * ); * * if(videoResponse) { * const videoBuffer = Buffer.from(await videoResponse.body.arrayBuffer()); * // Save or process the video. * } * } * ``` * * @category API Access */ retrieve(url: string, options?: RequestOptions, retrieveOptions?: RetrieveOptions): Promise<Nullable<Dispatcher.ResponseData<unknown>>>; private _retrieve; private logRetry; /** * Determines whether an HTTP status code represents a successful response. * * @param code - HTTP status code to check * * @returns `true` if code is 2xx, `false` otherwise. * * @remarks * Validates HTTP response codes according to standard conventions: * * - 2xx codes (200-299) indicate success * - All other codes indicate failure * - `undefined` is treated as failure * * @example * Response validation: * * ```typescript * import { ProtectApi } from "unifi-protect"; * * const protect = new ProtectApi(); * * async function validateResponses() { * const response = await protect.retrieve( * "https://192.168.1.1/proxy/protect/api/cameras" * ); * * if(response && protect.responseOk(response.statusCode)) { * console.log("Request successful."); * const data = await response.body.json(); * // Process data... * } else { * console.error("Request failed: " + (response?.statusCode?.toString() ?? "unknown") + "."); * } * } * ``` * * @category Utilities */ responseOk(code?: number): boolean; /** * Return a new instance of the Protect livestream API. * * @returns New livestream API instance. * * @remarks * The livestream API provides direct access to camera H.264 fMP4 streams, enabling: * * - Real-time video streaming * - Stream recording and processing * - Integration with video processing pipelines * - Low-latency video access * * Unlike RTSP streams, livestreams are delivered over WebSockets with minimal latency and don't require additional authentication. * * @example * Recording a livestream to a file using the Readable stream interface: * * ```typescript * import { ProtectApi } from "unifi-protect"; * import { createWriteStream } from "fs"; * * const protect = new ProtectApi(); * * async function recordLivestream(cameraId: string, durationMs: number) { * await protect.login("192.168.1.1", "admin", "password"); * await protect.getBootstrap(); * * const camera = protect.bootstrap?.cameras.find((c) => c.id === cameraId); * if(!camera) return; * * // Create a livestream instance and start streaming on channel 0 (highest quality) with the Readable stream interface enabled. * const livestream = protect.createLivestream(); * * if(!await livestream.start(camera.id, 0, { useStream: true })) { * console.error("Failed to start livestream."); * return; * } * * // Pipe the fMP4 stream to a file. * const output = createWriteStream("recording-" + Date.now().toString() + ".mp4"); * livestream.stream?.pipe(output); * * // Stop after the requested duration. * setTimeout(() => { * livestream.stop(); * output.end(); * console.log("Recording complete."); * }, durationMs); * } * ``` * * @category API Access */ createLivestream(): ProtectLivestream; /** * Return an API endpoint URL for the requested endpoint type. * * @param endpoint - Endpoint type to retrieve * * @returns Full URL to the requested endpoint. * * @remarks * Generates properly formatted URLs for Protect API endpoints: * * | Endpoint | Path | Description | * |----------|------|-------------| * | `bootstrap` | `/proxy/protect/api/bootstrap` | Complete system configuration | * | `camera` | `/proxy/protect/api/cameras` | Camera management | * | `chime` | `/proxy/protect/api/chimes` | Chime device management | * | `light` | `/proxy/protect/api/lights` | Light device management | * | `login` | `/api/auth/login` | Authentication endpoint | * | `nvr` | `/proxy/protect/api/nvr` | NVR configuration | * | `self` | `/api/users/self` | Current user information | * | `sensor` | `/proxy/protect/api/sensors` | Sensor device management | * | `websocket` | `/proxy/protect/api/ws` | WebSocket endpoints | * | `viewer` | `/proxy/protect/api/viewers` | Viewport device management | * * @example * Building custom API URLs: * * ```typescript * import { ProtectApi } from "unifi-protect"; * * const protect = new ProtectApi(); * * async function customEndpoints() { * await protect.login("192.168.1.1", "admin", "password"); * * // Get base endpoints. * const cameraEndpoint = protect.getApiEndpoint("camera"); * // Returns: "https://192.168.1.1/proxy/protect/api/cameras" * * // Build specific camera URL. * const cameraId = "abc123"; * const specificCamera = cameraEndpoint + "/" + cameraId; * * // Make custom request. * const response = await protect.retrieve(specificCamera); * if(response) { * const camera = await response.body.json(); * console.log("Camera: " + camera.name); * } * } * ``` * * @category API Access */ getApiEndpoint(endpoint: ProtectApiEndpoint): string; /** * Access the Protect controller bootstrap JSON. * * @returns Bootstrap configuration if available, `null` otherwise. * * @remarks * The bootstrap must be retrieved via {@link getBootstrap} before accessing this property. The bootstrap contains the complete system state and is automatically * updated when configuration changes occur. * * @see {@link getBootstrap} to retrieve the bootstrap configuration * @see {@link ProtectNvrBootstrap} for the complete data structure * * @category API Access */ get bootstrap(): ProtectNvrBootstrapData; /** * Check if the current user has administrative privileges. * * @returns `true` if the user has Super Admin role, `false` otherwise. * * @remarks * Administrative privileges are required for: * * - Modifying device configurations * - Enabling/disabling RTSP streams * - Changing system settings * - Managing user accounts * * The privilege level is determined during login and updated on each bootstrap. * * @category Utilities */ get isAdminUser(): boolean; /** * Check if API calls are currently throttled due to errors. * * @returns `true` if throttled, `false` otherwise. * * @remarks * The API implements automatic throttling after repeated errors to prevent overwhelming the controller. During throttling: * * - API calls return `null` immediately * - No network requests are made * - Throttling automatically clears after the retry interval * * Default throttling occurs after 10 consecutive errors for 5 minutes. * * @category Utilities */ get isThrottled(): boolean; /** * Access the shared WebSocket agent used for all WebSocket connections to the Protect controller. * * @returns The shared Agent instance if available, `null` otherwise. * * @remarks * This agent is shared across the events WebSocket and all livestream instances. It uses HTTP/1.1 (required for WebSocket upgrades) with TCP keepalive enabled for * TLS session reuse and dead connection detection. * * @internal */ get wsDispatcher(): Nullable<Agent>; /** * Get a formatted name for the Protect controller. * * @returns Controller name in format: `Name [Type]` or just the address if not bootstrapped. * * @remarks * Returns a human-readable controller identifier. After bootstrap, includes the controller's configured name and model type. Before bootstrap, returns the hostname or * IP address used for connection. * * @example * ```typescript * // Before bootstrap: "192.168.1.1" * // After bootstrap: "Dream Machine Pro [UDMP]" * console.log(protect.name); * ``` * * @category Utilities */ get name(): string; }