unifi-protect
Version:
A complete implementation of the UniFi Protect API.
1,164 lines (1,163 loc) • 42.8 kB
TypeScript
/**
* 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;
}