feat(unifi): implement comprehensive UniFi API client with controllers, protect, access, account, managers, resources, HTTP client, interfaces, logging, plugins, and tests

This commit is contained in:
2026-02-02 15:46:41 +00:00
parent aaa9e67835
commit 740b70cd83
38 changed files with 6275 additions and 15 deletions

8
ts/00_commitinfo_data.ts Normal file
View File

@@ -0,0 +1,8 @@
/**
* autocreated commitinfo by @push.rocks/commitinfo
*/
export const commitinfo = {
name: '@apiclient.xyz/unifi',
version: '1.1.0',
description: 'an unofficial unifi api package'
}

275
ts/classes.camera.ts Normal file
View File

@@ -0,0 +1,275 @@
import type { UnifiProtect } from './classes.unifi-protect.js';
import type {
IProtectCamera,
IProtectCameraChannel,
IProtectRecordingSettings,
IProtectSmartDetectSettings,
IProtectIspSettings,
IProtectFeatureFlags,
IProtectCameraStats,
} from './interfaces/index.js';
/**
* Represents a UniFi Protect camera
*/
export class UnifiCamera implements IProtectCamera {
/** Reference to parent protect instance */
private protect?: UnifiProtect;
// IProtectCamera properties
public id: string;
public mac: string;
public host: string;
public name: string;
public type: string;
public modelKey?: string;
public state: 'CONNECTED' | 'DISCONNECTED' | 'CONNECTING' | 'ADOPTING' | 'MANAGED';
public hardwareRevision?: string;
public firmwareVersion?: string;
public firmwareBuild?: string;
public isUpdating?: boolean;
public isAdopting?: boolean;
public isManaged?: boolean;
public isConnected?: boolean;
public isRecording?: boolean;
public isMotionDetected?: boolean;
public isDark?: boolean;
public recordingSettings?: IProtectRecordingSettings;
public smartDetectSettings?: IProtectSmartDetectSettings;
public ispSettings?: IProtectIspSettings;
public micVolume?: number;
public speakerVolume?: number;
public lastMotion?: number;
public lastRing?: number;
public uptime?: number;
public connectedSince?: number;
public upSince?: number;
public lastSeen?: number;
public channels?: IProtectCameraChannel[];
public featureFlags?: IProtectFeatureFlags;
public stats?: IProtectCameraStats;
constructor() {
this.id = '';
this.mac = '';
this.host = '';
this.name = '';
this.type = '';
this.state = 'DISCONNECTED';
}
/**
* Create a camera instance from API response object
*/
public static createFromApiObject(
apiObject: IProtectCamera,
protect?: UnifiProtect
): UnifiCamera {
const camera = new UnifiCamera();
Object.assign(camera, apiObject);
camera.protect = protect;
return camera;
}
/**
* Get the raw API object representation
*/
public toApiObject(): IProtectCamera {
return {
id: this.id,
mac: this.mac,
host: this.host,
name: this.name,
type: this.type,
modelKey: this.modelKey,
state: this.state,
hardwareRevision: this.hardwareRevision,
firmwareVersion: this.firmwareVersion,
firmwareBuild: this.firmwareBuild,
isUpdating: this.isUpdating,
isAdopting: this.isAdopting,
isManaged: this.isManaged,
isConnected: this.isConnected,
isRecording: this.isRecording,
isMotionDetected: this.isMotionDetected,
isDark: this.isDark,
recordingSettings: this.recordingSettings,
smartDetectSettings: this.smartDetectSettings,
ispSettings: this.ispSettings,
micVolume: this.micVolume,
speakerVolume: this.speakerVolume,
lastMotion: this.lastMotion,
lastRing: this.lastRing,
uptime: this.uptime,
connectedSince: this.connectedSince,
upSince: this.upSince,
lastSeen: this.lastSeen,
channels: this.channels,
featureFlags: this.featureFlags,
stats: this.stats,
};
}
/**
* Check if camera is online
*/
public isOnline(): boolean {
return this.state === 'CONNECTED' && this.isConnected === true;
}
/**
* Check if camera is a doorbell
*/
public isDoorbell(): boolean {
return this.type.toLowerCase().includes('doorbell');
}
/**
* Check if camera has smart detection capability
*/
public hasSmartDetect(): boolean {
return this.featureFlags?.hasSmartDetect === true;
}
/**
* Get RTSP stream URL for a channel
*/
public getRtspUrl(channelId: number = 0): string | null {
const channel = this.channels?.find((c) => c.id === channelId);
if (!channel || !channel.isRtspEnabled || !channel.rtspAlias) {
return null;
}
// RTSP URL format: rtsp://{host}:7447/{rtspAlias}
return `rtsp://${this.host}:7447/${channel.rtspAlias}`;
}
/**
* Get the high quality channel
*/
public getHighQualityChannel(): IProtectCameraChannel | null {
// Channel 0 is typically the highest quality
return this.channels?.find((c) => c.id === 0) || null;
}
/**
* Get the medium quality channel
*/
public getMediumQualityChannel(): IProtectCameraChannel | null {
// Channel 1 is typically medium quality
return this.channels?.find((c) => c.id === 1) || null;
}
/**
* Get the low quality channel
*/
public getLowQualityChannel(): IProtectCameraChannel | null {
// Channel 2 is typically low quality
return this.channels?.find((c) => c.id === 2) || null;
}
/**
* Update camera settings
*/
public async updateSettings(settings: Partial<IProtectCamera>): Promise<void> {
if (!this.protect) {
throw new Error('Cannot update camera: no protect reference');
}
await this.protect.request('PATCH', `/cameras/${this.id}`, settings);
// Update local state
Object.assign(this, settings);
}
/**
* Set recording mode
*/
public async setRecordingMode(
mode: 'always' | 'detections' | 'never' | 'schedule'
): Promise<void> {
await this.updateSettings({
recordingSettings: {
...this.recordingSettings,
mode,
},
});
}
/**
* Enable/disable smart detection types
*/
public async setSmartDetectTypes(types: string[]): Promise<void> {
await this.updateSettings({
smartDetectSettings: {
...this.smartDetectSettings,
objectTypes: types,
},
});
}
/**
* Set microphone volume
*/
public async setMicVolume(volume: number): Promise<void> {
const clampedVolume = Math.max(0, Math.min(100, volume));
await this.updateSettings({ micVolume: clampedVolume });
}
/**
* Set speaker volume
*/
public async setSpeakerVolume(volume: number): Promise<void> {
const clampedVolume = Math.max(0, Math.min(100, volume));
await this.updateSettings({ speakerVolume: clampedVolume });
}
/**
* Rename the camera
*/
public async rename(newName: string): Promise<void> {
await this.updateSettings({ name: newName });
}
/**
* Restart the camera
*/
public async restart(): Promise<void> {
if (!this.protect) {
throw new Error('Cannot restart camera: no protect reference');
}
await this.protect.request('POST', `/cameras/${this.id}/reboot`);
}
/**
* Get snapshot URL
*/
public getSnapshotUrl(): string {
if (!this.protect) {
throw new Error('Cannot get snapshot URL: no protect reference');
}
return `/proxy/protect/api/cameras/${this.id}/snapshot`;
}
/**
* Check if camera has motion in the last N seconds
*/
public hasRecentMotion(seconds: number = 60): boolean {
if (!this.lastMotion) return false;
const now = Date.now();
const motionAge = (now - this.lastMotion) / 1000;
return motionAge <= seconds;
}
/**
* Get time since last motion in seconds
*/
public getTimeSinceLastMotion(): number | null {
if (!this.lastMotion) return null;
return Math.floor((Date.now() - this.lastMotion) / 1000);
}
}

227
ts/classes.cameramanager.ts Normal file
View File

@@ -0,0 +1,227 @@
import type { UnifiProtect } from './classes.unifi-protect.js';
import { UnifiCamera } from './classes.camera.js';
import type { IProtectCamera, IProtectMotionEvent } from './interfaces/index.js';
import { logger } from './unifi.logger.js';
/**
* Manager for UniFi Protect cameras
*/
export class CameraManager {
private protect: UnifiProtect;
constructor(protect: UnifiProtect) {
this.protect = protect;
}
/**
* List all cameras
*/
public async listCameras(): Promise<UnifiCamera[]> {
logger.log('debug', 'Fetching cameras from Protect');
const cameras = this.protect.getCamerasFromBootstrap();
return cameras.map((cameraData) =>
UnifiCamera.createFromApiObject(cameraData, this.protect)
);
}
/**
* Get a camera by ID
*/
public async getCameraById(cameraId: string): Promise<UnifiCamera | null> {
const cameras = await this.listCameras();
return cameras.find((camera) => camera.id === cameraId) || null;
}
/**
* Get a camera by MAC address
*/
public async getCameraByMac(mac: string): Promise<UnifiCamera | null> {
const normalizedMac = mac.toLowerCase().replace(/[:-]/g, ':');
const cameras = await this.listCameras();
return cameras.find((camera) => camera.mac.toLowerCase() === normalizedMac) || null;
}
/**
* Find a camera by name
*/
public async getCameraByName(name: string): Promise<UnifiCamera | null> {
const cameras = await this.listCameras();
return cameras.find(
(camera) => camera.name.toLowerCase() === name.toLowerCase()
) || null;
}
/**
* Get online cameras only
*/
public async getOnlineCameras(): Promise<UnifiCamera[]> {
const cameras = await this.listCameras();
return cameras.filter((camera) => camera.isOnline());
}
/**
* Get offline cameras only
*/
public async getOfflineCameras(): Promise<UnifiCamera[]> {
const cameras = await this.listCameras();
return cameras.filter((camera) => !camera.isOnline());
}
/**
* Get doorbells only
*/
public async getDoorbells(): Promise<UnifiCamera[]> {
const cameras = await this.listCameras();
return cameras.filter((camera) => camera.isDoorbell());
}
/**
* Get cameras with smart detection
*/
public async getSmartDetectCameras(): Promise<UnifiCamera[]> {
const cameras = await this.listCameras();
return cameras.filter((camera) => camera.hasSmartDetect());
}
/**
* Get cameras currently recording
*/
public async getRecordingCameras(): Promise<UnifiCamera[]> {
const cameras = await this.listCameras();
return cameras.filter((camera) => camera.isRecording === true);
}
/**
* Get cameras with recent motion
*/
public async getCamerasWithRecentMotion(seconds: number = 60): Promise<UnifiCamera[]> {
const cameras = await this.listCameras();
return cameras.filter((camera) => camera.hasRecentMotion(seconds));
}
/**
* Update camera settings
*/
public async updateCamera(
cameraId: string,
settings: Partial<IProtectCamera>
): Promise<void> {
logger.log('info', `Updating camera: ${cameraId}`);
await this.protect.request('PATCH', `/cameras/${cameraId}`, settings);
}
/**
* Get motion events for a camera
*/
public async getMotionEvents(
cameraId: string,
options: { start?: number; end?: number; limit?: number } = {}
): Promise<IProtectMotionEvent[]> {
logger.log('debug', `Fetching motion events for camera: ${cameraId}`);
const params = new URLSearchParams();
params.append('cameras', cameraId);
if (options.start) params.append('start', options.start.toString());
if (options.end) params.append('end', options.end.toString());
if (options.limit) params.append('limit', options.limit.toString());
const response = await this.protect.request<IProtectMotionEvent[] | { data: IProtectMotionEvent[] }>(
'GET',
`/events?${params.toString()}`
);
// Handle both array and {data: [...]} response formats
return Array.isArray(response) ? response : (response?.data || []);
}
/**
* Get all recent motion events
*/
public async getAllMotionEvents(
options: { start?: number; end?: number; limit?: number } = {}
): Promise<IProtectMotionEvent[]> {
logger.log('debug', 'Fetching all motion events');
const params = new URLSearchParams();
if (options.start) params.append('start', options.start.toString());
if (options.end) params.append('end', options.end.toString());
if (options.limit) params.append('limit', options.limit.toString());
const queryString = params.toString() ? `?${params.toString()}` : '';
const response = await this.protect.request<IProtectMotionEvent[] | { data: IProtectMotionEvent[] }>(
'GET',
`/events${queryString}`
);
// Handle both array and {data: [...]} response formats
return Array.isArray(response) ? response : (response?.data || []);
}
/**
* Get snapshot for a camera
*/
public async getSnapshot(
cameraId: string,
options: { width?: number; height?: number; ts?: number } = {}
): Promise<ArrayBuffer> {
logger.log('debug', `Fetching snapshot for camera: ${cameraId}`);
const params = new URLSearchParams();
if (options.width) params.append('w', options.width.toString());
if (options.height) params.append('h', options.height.toString());
if (options.ts) params.append('ts', options.ts.toString());
const queryString = params.toString() ? `?${params.toString()}` : '';
// Note: This returns binary data, may need different handling
const response = await this.protect.request<ArrayBuffer>(
'GET',
`/cameras/${cameraId}/snapshot${queryString}`
);
return response;
}
/**
* Set recording mode for all cameras
*/
public async setAllRecordingMode(
mode: 'always' | 'detections' | 'never' | 'schedule'
): Promise<void> {
logger.log('info', `Setting all cameras to recording mode: ${mode}`);
const cameras = await this.listCameras();
const updatePromises = cameras.map((camera) =>
this.updateCamera(camera.id, {
recordingSettings: {
...camera.recordingSettings,
mode,
},
})
);
await Promise.all(updatePromises);
}
/**
* Adopt a camera
*/
public async adoptCamera(mac: string): Promise<void> {
logger.log('info', `Adopting camera: ${mac}`);
await this.protect.request('POST', '/cameras/adopt', {
mac: mac.toLowerCase(),
});
}
/**
* Remove a camera
*/
public async removeCamera(cameraId: string): Promise<void> {
logger.log('info', `Removing camera: ${cameraId}`);
await this.protect.request('DELETE', `/cameras/${cameraId}`);
}
}

276
ts/classes.client.ts Normal file
View File

@@ -0,0 +1,276 @@
import type { UnifiController } from './classes.unifi-controller.js';
import type { INetworkClient } from './interfaces/index.js';
/**
* Represents a connected network client
*/
export class UnifiClient implements INetworkClient {
/** Reference to parent controller */
private controller?: UnifiController;
/** Site ID for API calls */
private siteId?: string;
// INetworkClient properties
public _id: string;
public mac: string;
public site_id: string;
public is_guest?: boolean;
public is_wired: boolean;
public first_seen?: number;
public last_seen?: number;
public hostname?: string;
public name?: string;
public ip?: string;
public network_id?: string;
public uplink_mac?: string;
public ap_name?: string;
public essid?: string;
public bssid?: string;
public channel?: number;
public radio_proto?: string;
public signal?: number;
public tx_rate?: number;
public rx_rate?: number;
public tx_bytes?: number;
public rx_bytes?: number;
public tx_packets?: number;
public rx_packets?: number;
public sw_port?: number;
public usergroup_id?: string;
public oui?: string;
public noted?: boolean;
public user_id?: string;
public fingerprint_source?: number;
public dev_cat?: number;
public dev_family?: number;
public dev_vendor?: number;
public dev_id?: number;
public os_name?: number;
public satisfaction?: number;
public anomalies?: number;
constructor() {
this._id = '';
this.mac = '';
this.site_id = '';
this.is_wired = false;
}
/**
* Create a client instance from API response object
*/
public static createFromApiObject(
apiObject: INetworkClient,
controller?: UnifiController,
siteId?: string
): UnifiClient {
const client = new UnifiClient();
Object.assign(client, apiObject);
client.controller = controller;
client.siteId = siteId || apiObject.site_id;
return client;
}
/**
* Get the raw API object representation
*/
public toApiObject(): INetworkClient {
return {
_id: this._id,
mac: this.mac,
site_id: this.site_id,
is_guest: this.is_guest,
is_wired: this.is_wired,
first_seen: this.first_seen,
last_seen: this.last_seen,
hostname: this.hostname,
name: this.name,
ip: this.ip,
network_id: this.network_id,
uplink_mac: this.uplink_mac,
ap_name: this.ap_name,
essid: this.essid,
bssid: this.bssid,
channel: this.channel,
radio_proto: this.radio_proto,
signal: this.signal,
tx_rate: this.tx_rate,
rx_rate: this.rx_rate,
tx_bytes: this.tx_bytes,
rx_bytes: this.rx_bytes,
tx_packets: this.tx_packets,
rx_packets: this.rx_packets,
sw_port: this.sw_port,
usergroup_id: this.usergroup_id,
oui: this.oui,
noted: this.noted,
user_id: this.user_id,
fingerprint_source: this.fingerprint_source,
dev_cat: this.dev_cat,
dev_family: this.dev_family,
dev_vendor: this.dev_vendor,
dev_id: this.dev_id,
os_name: this.os_name,
satisfaction: this.satisfaction,
anomalies: this.anomalies,
};
}
/**
* Get display name (name, hostname, or MAC)
*/
public getDisplayName(): string {
return this.name || this.hostname || this.mac;
}
/**
* Check if client is wireless
*/
public isWireless(): boolean {
return !this.is_wired;
}
/**
* Check if client is a guest
*/
public isGuest(): boolean {
return this.is_guest === true;
}
/**
* Get connection type string
*/
public getConnectionType(): string {
if (this.is_wired) {
return `Wired (Port ${this.sw_port || 'unknown'})`;
}
return `Wireless (${this.essid || 'unknown'})`;
}
/**
* Get signal strength description
*/
public getSignalQuality(): string {
if (this.is_wired) return 'N/A';
if (!this.signal) return 'Unknown';
// Signal is typically in dBm, with higher (less negative) being better
const signal = this.signal;
if (signal >= -50) return 'Excellent';
if (signal >= -60) return 'Good';
if (signal >= -70) return 'Fair';
if (signal >= -80) return 'Poor';
return 'Very Poor';
}
/**
* Block this client from the network
*/
public async block(): Promise<void> {
if (!this.controller || !this.siteId) {
throw new Error('Cannot block client: no controller reference');
}
await this.controller.request(
'POST',
`/api/s/${this.siteId}/cmd/stamgr`,
{
cmd: 'block-sta',
mac: this.mac,
}
);
}
/**
* Unblock this client
*/
public async unblock(): Promise<void> {
if (!this.controller || !this.siteId) {
throw new Error('Cannot unblock client: no controller reference');
}
await this.controller.request(
'POST',
`/api/s/${this.siteId}/cmd/stamgr`,
{
cmd: 'unblock-sta',
mac: this.mac,
}
);
}
/**
* Reconnect (kick) this client
*/
public async reconnect(): Promise<void> {
if (!this.controller || !this.siteId) {
throw new Error('Cannot reconnect client: no controller reference');
}
await this.controller.request(
'POST',
`/api/s/${this.siteId}/cmd/stamgr`,
{
cmd: 'kick-sta',
mac: this.mac,
}
);
}
/**
* Rename this client
*/
public async rename(newName: string): Promise<void> {
if (!this.controller || !this.siteId) {
throw new Error('Cannot rename client: no controller reference');
}
// Check if user exists (has fixed IP/config)
if (this.user_id) {
await this.controller.request(
'PUT',
`/api/s/${this.siteId}/rest/user/${this.user_id}`,
{
name: newName,
}
);
} else {
// Create user entry for this client
await this.controller.request(
'POST',
`/api/s/${this.siteId}/rest/user`,
{
mac: this.mac,
name: newName,
}
);
}
this.name = newName;
}
/**
* Get data usage (combined TX and RX bytes)
*/
public getDataUsage(): number {
return (this.tx_bytes || 0) + (this.rx_bytes || 0);
}
/**
* Format data usage as human-readable string
*/
public getDataUsageFormatted(): string {
const bytes = this.getDataUsage();
if (bytes >= 1024 * 1024 * 1024) {
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
}
if (bytes >= 1024 * 1024) {
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
}
if (bytes >= 1024) {
return `${(bytes / 1024).toFixed(2)} KB`;
}
return `${bytes} B`;
}
}

262
ts/classes.clientmanager.ts Normal file
View File

@@ -0,0 +1,262 @@
import type { UnifiController } from './classes.unifi-controller.js';
import { UnifiClient } from './classes.client.js';
import type { INetworkClient, IUnifiApiResponse } from './interfaces/index.js';
import { logger } from './unifi.logger.js';
/**
* Manager for UniFi network clients
*/
export class ClientManager {
private controller: UnifiController;
constructor(controller: UnifiController) {
this.controller = controller;
}
/**
* List all active (connected) clients for a site
*/
public async listActiveClients(siteId: string = 'default'): Promise<UnifiClient[]> {
logger.log('debug', `Fetching active clients for site: ${siteId}`);
const response = await this.controller.request<IUnifiApiResponse<INetworkClient>>(
'GET',
`/api/s/${siteId}/stat/sta`
);
const clients: UnifiClient[] = [];
for (const clientData of response.data || []) {
clients.push(UnifiClient.createFromApiObject(clientData, this.controller, siteId));
}
logger.log('info', `Found ${clients.length} active clients`);
return clients;
}
/**
* List all known clients (including historical)
*/
public async listAllClients(siteId: string = 'default'): Promise<UnifiClient[]> {
logger.log('debug', `Fetching all known clients for site: ${siteId}`);
const response = await this.controller.request<IUnifiApiResponse<INetworkClient>>(
'GET',
`/api/s/${siteId}/rest/user`
);
const clients: UnifiClient[] = [];
for (const clientData of response.data || []) {
clients.push(UnifiClient.createFromApiObject(clientData, this.controller, siteId));
}
logger.log('info', `Found ${clients.length} known clients`);
return clients;
}
/**
* Get a client by MAC address
*/
public async getClientByMac(mac: string, siteId: string = 'default'): Promise<UnifiClient | null> {
const normalizedMac = mac.toLowerCase().replace(/[:-]/g, ':');
const clients = await this.listActiveClients(siteId);
return clients.find((client) => client.mac.toLowerCase() === normalizedMac) || null;
}
/**
* Get a client by IP address
*/
public async getClientByIp(ip: string, siteId: string = 'default'): Promise<UnifiClient | null> {
const clients = await this.listActiveClients(siteId);
return clients.find((client) => client.ip === ip) || null;
}
/**
* Get a client by hostname
*/
public async getClientByHostname(hostname: string, siteId: string = 'default'): Promise<UnifiClient | null> {
const clients = await this.listActiveClients(siteId);
return clients.find(
(client) => client.hostname?.toLowerCase() === hostname.toLowerCase()
) || null;
}
/**
* Get wired clients only
*/
public async getWiredClients(siteId: string = 'default'): Promise<UnifiClient[]> {
const clients = await this.listActiveClients(siteId);
return clients.filter((client) => client.is_wired);
}
/**
* Get wireless clients only
*/
public async getWirelessClients(siteId: string = 'default'): Promise<UnifiClient[]> {
const clients = await this.listActiveClients(siteId);
return clients.filter((client) => client.isWireless());
}
/**
* Get guest clients only
*/
public async getGuestClients(siteId: string = 'default'): Promise<UnifiClient[]> {
const clients = await this.listActiveClients(siteId);
return clients.filter((client) => client.isGuest());
}
/**
* Get clients connected to a specific AP
*/
public async getClientsByAP(apMac: string, siteId: string = 'default'): Promise<UnifiClient[]> {
const normalizedMac = apMac.toLowerCase();
const clients = await this.listActiveClients(siteId);
return clients.filter((client) => client.uplink_mac?.toLowerCase() === normalizedMac);
}
/**
* Get clients on a specific SSID
*/
public async getClientsBySSID(ssid: string, siteId: string = 'default'): Promise<UnifiClient[]> {
const clients = await this.listActiveClients(siteId);
return clients.filter((client) => client.essid === ssid);
}
/**
* Block a client by MAC address
*/
public async blockClient(mac: string, siteId: string = 'default'): Promise<void> {
logger.log('info', `Blocking client: ${mac}`);
await this.controller.request(
'POST',
`/api/s/${siteId}/cmd/stamgr`,
{
cmd: 'block-sta',
mac: mac.toLowerCase(),
}
);
}
/**
* Unblock a client by MAC address
*/
public async unblockClient(mac: string, siteId: string = 'default'): Promise<void> {
logger.log('info', `Unblocking client: ${mac}`);
await this.controller.request(
'POST',
`/api/s/${siteId}/cmd/stamgr`,
{
cmd: 'unblock-sta',
mac: mac.toLowerCase(),
}
);
}
/**
* Reconnect (kick) a client by MAC address
*/
public async reconnectClient(mac: string, siteId: string = 'default'): Promise<void> {
logger.log('info', `Reconnecting client: ${mac}`);
await this.controller.request(
'POST',
`/api/s/${siteId}/cmd/stamgr`,
{
cmd: 'kick-sta',
mac: mac.toLowerCase(),
}
);
}
/**
* Authorize a guest client
*/
public async authorizeGuest(
mac: string,
minutes: number = 60,
options: {
up?: number; // Upload bandwidth limit in Kbps
down?: number; // Download bandwidth limit in Kbps
bytes?: number; // Data quota in MB
apMac?: string; // Specific AP MAC
} = {},
siteId: string = 'default'
): Promise<void> {
logger.log('info', `Authorizing guest: ${mac} for ${minutes} minutes`);
await this.controller.request(
'POST',
`/api/s/${siteId}/cmd/stamgr`,
{
cmd: 'authorize-guest',
mac: mac.toLowerCase(),
minutes,
up: options.up,
down: options.down,
bytes: options.bytes,
ap_mac: options.apMac?.toLowerCase(),
}
);
}
/**
* Unauthorize a guest client
*/
public async unauthorizeGuest(mac: string, siteId: string = 'default'): Promise<void> {
logger.log('info', `Unauthorizing guest: ${mac}`);
await this.controller.request(
'POST',
`/api/s/${siteId}/cmd/stamgr`,
{
cmd: 'unauthorize-guest',
mac: mac.toLowerCase(),
}
);
}
/**
* Set or update client name/alias
*/
public async setClientName(mac: string, name: string, siteId: string = 'default'): Promise<void> {
logger.log('info', `Setting client name: ${mac} -> ${name}`);
// First try to find existing user
const allClients = await this.listAllClients(siteId);
const existingClient = allClients.find(
(c) => c.mac.toLowerCase() === mac.toLowerCase()
);
if (existingClient?.user_id) {
// Update existing user
await this.controller.request(
'PUT',
`/api/s/${siteId}/rest/user/${existingClient.user_id}`,
{ name }
);
} else {
// Create new user entry
await this.controller.request(
'POST',
`/api/s/${siteId}/rest/user`,
{
mac: mac.toLowerCase(),
name,
}
);
}
}
/**
* Get blocked clients
*/
public async getBlockedClients(siteId: string = 'default'): Promise<UnifiClient[]> {
logger.log('debug', `Fetching blocked clients for site: ${siteId}`);
// Blocked clients are in the user endpoint with blocked=true
const allClients = await this.listAllClients(siteId);
// Note: This filtering may need adjustment based on actual API response
return allClients.filter((client) => (client as any).blocked === true);
}
}

255
ts/classes.device.ts Normal file
View File

@@ -0,0 +1,255 @@
import type { UnifiController } from './classes.unifi-controller.js';
import type { INetworkDevice, IPortConfig } from './interfaces/index.js';
/**
* Represents a UniFi network device (AP, switch, gateway, etc.)
*/
export class UnifiDevice implements INetworkDevice {
/** Reference to parent controller */
private controller?: UnifiController;
/** Site ID for API calls */
private siteId?: string;
// INetworkDevice properties
public _id: string;
public mac: string;
public model: string;
public type: string;
public name?: string;
public site_id: string;
public adopted: boolean;
public ip: string;
public state: number;
public serial?: string;
public version?: string;
public uptime?: number;
public last_seen?: number;
public upgradable?: boolean;
public upgrade_to_firmware?: string;
public config_network?: {
type?: string;
ip?: string;
};
public ethernet_table?: Array<{
name: string;
mac: string;
num_port?: number;
}>;
public port_overrides?: Array<{
port_idx: number;
name?: string;
poe_mode?: string;
}>;
public sys_stats?: {
loadavg_1?: number;
loadavg_5?: number;
loadavg_15?: number;
mem_total?: number;
mem_used?: number;
};
public led_override?: string;
public led_override_color?: string;
public led_override_color_brightness?: number;
constructor() {
this._id = '';
this.mac = '';
this.model = '';
this.type = '';
this.site_id = '';
this.adopted = false;
this.ip = '';
this.state = 0;
}
/**
* Create a device instance from API response object
*/
public static createFromApiObject(
apiObject: INetworkDevice,
controller?: UnifiController,
siteId?: string
): UnifiDevice {
const device = new UnifiDevice();
Object.assign(device, apiObject);
device.controller = controller;
device.siteId = siteId || apiObject.site_id;
return device;
}
/**
* Get the raw API object representation
*/
public toApiObject(): INetworkDevice {
return {
_id: this._id,
mac: this.mac,
model: this.model,
type: this.type,
name: this.name,
site_id: this.site_id,
adopted: this.adopted,
ip: this.ip,
state: this.state,
serial: this.serial,
version: this.version,
uptime: this.uptime,
last_seen: this.last_seen,
upgradable: this.upgradable,
upgrade_to_firmware: this.upgrade_to_firmware,
config_network: this.config_network,
ethernet_table: this.ethernet_table,
port_overrides: this.port_overrides,
sys_stats: this.sys_stats,
led_override: this.led_override,
led_override_color: this.led_override_color,
led_override_color_brightness: this.led_override_color_brightness,
};
}
/**
* Check if device is online (state 1 = connected)
*/
public isOnline(): boolean {
return this.state === 1;
}
/**
* Check if device is an access point
*/
public isAccessPoint(): boolean {
return this.type === 'uap';
}
/**
* Check if device is a switch
*/
public isSwitch(): boolean {
return this.type === 'usw';
}
/**
* Check if device is a gateway/router
*/
public isGateway(): boolean {
return this.type === 'ugw' || this.type === 'udm';
}
/**
* Check if device has available firmware upgrade
*/
public hasUpgrade(): boolean {
return this.upgradable === true && !!this.upgrade_to_firmware;
}
/**
* Get device display name (name or MAC if no name)
*/
public getDisplayName(): string {
return this.name || this.mac;
}
/**
* Restart the device
*/
public async restart(): Promise<void> {
if (!this.controller || !this.siteId) {
throw new Error('Cannot restart device: no controller reference');
}
await this.controller.request(
'POST',
`/api/s/${this.siteId}/cmd/devmgr`,
{
cmd: 'restart',
mac: this.mac,
}
);
}
/**
* Upgrade the device firmware
*/
public async upgrade(): Promise<void> {
if (!this.controller || !this.siteId) {
throw new Error('Cannot upgrade device: no controller reference');
}
await this.controller.request(
'POST',
`/api/s/${this.siteId}/cmd/devmgr`,
{
cmd: 'upgrade',
mac: this.mac,
}
);
}
/**
* Set LED override
*/
public async setLedOverride(mode: 'default' | 'on' | 'off'): Promise<void> {
if (!this.controller || !this.siteId) {
throw new Error('Cannot set LED: no controller reference');
}
await this.controller.request(
'PUT',
`/api/s/${this.siteId}/rest/device/${this._id}`,
{
led_override: mode,
}
);
}
/**
* Rename the device
*/
public async rename(newName: string): Promise<void> {
if (!this.controller || !this.siteId) {
throw new Error('Cannot rename device: no controller reference');
}
await this.controller.request(
'PUT',
`/api/s/${this.siteId}/rest/device/${this._id}`,
{
name: newName,
}
);
this.name = newName;
}
/**
* Set port configuration
*/
public async setPortConfig(portIdx: number, config: Partial<IPortConfig>): Promise<void> {
if (!this.controller || !this.siteId) {
throw new Error('Cannot set port config: no controller reference');
}
const portOverride = {
port_idx: portIdx,
...config,
};
// Get existing port overrides and update
const existingOverrides = this.port_overrides || [];
const overrideIndex = existingOverrides.findIndex((p) => p.port_idx === portIdx);
if (overrideIndex >= 0) {
existingOverrides[overrideIndex] = { ...existingOverrides[overrideIndex], ...portOverride };
} else {
existingOverrides.push(portOverride);
}
await this.controller.request(
'PUT',
`/api/s/${this.siteId}/rest/device/${this._id}`,
{
port_overrides: existingOverrides,
}
);
}
}

188
ts/classes.devicemanager.ts Normal file
View File

@@ -0,0 +1,188 @@
import type { UnifiController } from './classes.unifi-controller.js';
import { UnifiDevice } from './classes.device.js';
import type { INetworkDevice, IUnifiApiResponse } from './interfaces/index.js';
import { logger } from './unifi.logger.js';
/**
* Manager for UniFi network devices
*/
export class DeviceManager {
private controller: UnifiController;
constructor(controller: UnifiController) {
this.controller = controller;
}
/**
* List all devices for a site
*/
public async listDevices(siteId: string = 'default'): Promise<UnifiDevice[]> {
logger.log('debug', `Fetching devices for site: ${siteId}`);
const response = await this.controller.request<IUnifiApiResponse<INetworkDevice>>(
'GET',
`/api/s/${siteId}/stat/device`
);
const devices: UnifiDevice[] = [];
for (const deviceData of response.data || []) {
devices.push(UnifiDevice.createFromApiObject(deviceData, this.controller, siteId));
}
logger.log('info', `Found ${devices.length} devices`);
return devices;
}
/**
* Get a device by MAC address
*/
public async getDeviceByMac(mac: string, siteId: string = 'default'): Promise<UnifiDevice | null> {
const normalizedMac = mac.toLowerCase().replace(/[:-]/g, ':');
const devices = await this.listDevices(siteId);
return devices.find((device) => device.mac.toLowerCase() === normalizedMac) || null;
}
/**
* Get a device by ID
*/
public async getDeviceById(deviceId: string, siteId: string = 'default'): Promise<UnifiDevice | null> {
const devices = await this.listDevices(siteId);
return devices.find((device) => device._id === deviceId) || null;
}
/**
* Get access points only
*/
public async getAccessPoints(siteId: string = 'default'): Promise<UnifiDevice[]> {
const devices = await this.listDevices(siteId);
return devices.filter((device) => device.isAccessPoint());
}
/**
* Get switches only
*/
public async getSwitches(siteId: string = 'default'): Promise<UnifiDevice[]> {
const devices = await this.listDevices(siteId);
return devices.filter((device) => device.isSwitch());
}
/**
* Get gateways only
*/
public async getGateways(siteId: string = 'default'): Promise<UnifiDevice[]> {
const devices = await this.listDevices(siteId);
return devices.filter((device) => device.isGateway());
}
/**
* Get devices with available upgrades
*/
public async getUpgradableDevices(siteId: string = 'default'): Promise<UnifiDevice[]> {
const devices = await this.listDevices(siteId);
return devices.filter((device) => device.hasUpgrade());
}
/**
* Get offline devices
*/
public async getOfflineDevices(siteId: string = 'default'): Promise<UnifiDevice[]> {
const devices = await this.listDevices(siteId);
return devices.filter((device) => !device.isOnline());
}
/**
* Restart a device by MAC
*/
public async restartDevice(mac: string, siteId: string = 'default'): Promise<void> {
logger.log('info', `Restarting device: ${mac}`);
await this.controller.request(
'POST',
`/api/s/${siteId}/cmd/devmgr`,
{
cmd: 'restart',
mac: mac.toLowerCase(),
}
);
}
/**
* Upgrade a device's firmware
*/
public async upgradeDevice(mac: string, siteId: string = 'default'): Promise<void> {
logger.log('info', `Upgrading device: ${mac}`);
await this.controller.request(
'POST',
`/api/s/${siteId}/cmd/devmgr`,
{
cmd: 'upgrade',
mac: mac.toLowerCase(),
}
);
}
/**
* Adopt a device
*/
public async adoptDevice(mac: string, siteId: string = 'default'): Promise<void> {
logger.log('info', `Adopting device: ${mac}`);
await this.controller.request(
'POST',
`/api/s/${siteId}/cmd/devmgr`,
{
cmd: 'adopt',
mac: mac.toLowerCase(),
}
);
}
/**
* Forget a device (remove from controller)
*/
public async forgetDevice(mac: string, siteId: string = 'default'): Promise<void> {
logger.log('info', `Forgetting device: ${mac}`);
await this.controller.request(
'POST',
`/api/s/${siteId}/cmd/devmgr`,
{
cmd: 'delete-device',
mac: mac.toLowerCase(),
}
);
}
/**
* Locate device (flash LEDs)
*/
public async locateDevice(mac: string, enabled: boolean, siteId: string = 'default'): Promise<void> {
logger.log('info', `${enabled ? 'Locating' : 'Stop locating'} device: ${mac}`);
await this.controller.request(
'POST',
`/api/s/${siteId}/cmd/devmgr`,
{
cmd: enabled ? 'set-locate' : 'unset-locate',
mac: mac.toLowerCase(),
}
);
}
/**
* Force provision a device (push config)
*/
public async provisionDevice(mac: string, siteId: string = 'default'): Promise<void> {
logger.log('info', `Provisioning device: ${mac}`);
await this.controller.request(
'POST',
`/api/s/${siteId}/cmd/devmgr`,
{
cmd: 'force-provision',
mac: mac.toLowerCase(),
}
);
}
}

155
ts/classes.door.ts Normal file
View File

@@ -0,0 +1,155 @@
import type { UnifiAccess } from './classes.unifi-access.js';
import type { IAccessDoor, IAccessDoorRule, IAccessFloor } from './interfaces/index.js';
/**
* Represents a UniFi Access door
*/
export class UnifiDoor implements IAccessDoor {
/** Reference to parent access instance */
private access?: UnifiAccess;
// IAccessDoor properties
public unique_id: string;
public name: string;
public alias?: string;
public door_type?: string;
public door_lock_relay_status?: 'lock' | 'unlock';
public door_position_status?: 'open' | 'close';
public device_id?: string;
public camera_resource_id?: string;
public location_id?: string;
public full_name?: string;
public extra_type?: string;
public door_guard?: boolean;
public rules?: IAccessDoorRule[];
public level_id?: string;
public floor?: IAccessFloor;
constructor() {
this.unique_id = '';
this.name = '';
}
/**
* Create a door instance from API response object
*/
public static createFromApiObject(
apiObject: IAccessDoor,
access?: UnifiAccess
): UnifiDoor {
const door = new UnifiDoor();
Object.assign(door, apiObject);
door.access = access;
return door;
}
/**
* Get the raw API object representation
*/
public toApiObject(): IAccessDoor {
return {
unique_id: this.unique_id,
name: this.name,
alias: this.alias,
door_type: this.door_type,
door_lock_relay_status: this.door_lock_relay_status,
door_position_status: this.door_position_status,
device_id: this.device_id,
camera_resource_id: this.camera_resource_id,
location_id: this.location_id,
full_name: this.full_name,
extra_type: this.extra_type,
door_guard: this.door_guard,
rules: this.rules,
level_id: this.level_id,
floor: this.floor,
};
}
/**
* Get display name (alias or name)
*/
public getDisplayName(): string {
return this.alias || this.name;
}
/**
* Check if door is currently locked
*/
public isLocked(): boolean {
return this.door_lock_relay_status === 'lock';
}
/**
* Check if door is currently open (contact sensor)
*/
public isOpen(): boolean {
return this.door_position_status === 'open';
}
/**
* Check if door is currently closed
*/
public isClosed(): boolean {
return this.door_position_status === 'close';
}
/**
* Unlock the door
*/
public async unlock(): Promise<void> {
if (!this.access) {
throw new Error('Cannot unlock door: no access reference');
}
await this.access.unlockDoor(this.unique_id);
this.door_lock_relay_status = 'unlock';
}
/**
* Lock the door
*/
public async lock(): Promise<void> {
if (!this.access) {
throw new Error('Cannot lock door: no access reference');
}
await this.access.lockDoor(this.unique_id);
this.door_lock_relay_status = 'lock';
}
/**
* Update door settings
*/
public async updateSettings(settings: Partial<IAccessDoor>): Promise<void> {
if (!this.access) {
throw new Error('Cannot update door: no access reference');
}
await this.access.request('PUT', `/door/${this.unique_id}`, settings);
Object.assign(this, settings);
}
/**
* Rename the door
*/
public async rename(newName: string): Promise<void> {
await this.updateSettings({ name: newName });
}
/**
* Set door alias
*/
public async setAlias(alias: string): Promise<void> {
await this.updateSettings({ alias });
}
/**
* Get door status summary
*/
public getStatus(): string {
const lockStatus = this.isLocked() ? 'Locked' : 'Unlocked';
const positionStatus = this.isOpen() ? 'Open' : 'Closed';
return `${lockStatus}, ${positionStatus}`;
}
}

168
ts/classes.doormanager.ts Normal file
View File

@@ -0,0 +1,168 @@
import type { UnifiAccess } from './classes.unifi-access.js';
import { UnifiDoor } from './classes.door.js';
import type { IAccessDoor, IAccessApiResponse, IAccessEvent } from './interfaces/index.js';
import { logger } from './unifi.logger.js';
/**
* Manager for UniFi Access doors
*/
export class DoorManager {
private access: UnifiAccess;
constructor(access: UnifiAccess) {
this.access = access;
}
/**
* List all doors
*/
public async listDoors(): Promise<UnifiDoor[]> {
logger.log('debug', 'Fetching doors from Access');
const response = await this.access.request<IAccessApiResponse<IAccessDoor[]>>(
'GET',
'/door'
);
const doors: UnifiDoor[] = [];
for (const doorData of response.data || []) {
doors.push(UnifiDoor.createFromApiObject(doorData, this.access));
}
logger.log('info', `Found ${doors.length} doors`);
return doors;
}
/**
* Get a door by ID
*/
public async getDoorById(doorId: string): Promise<UnifiDoor | null> {
logger.log('debug', `Fetching door: ${doorId}`);
try {
const response = await this.access.request<IAccessApiResponse<IAccessDoor>>(
'GET',
`/door/${doorId}`
);
return UnifiDoor.createFromApiObject(response.data, this.access);
} catch (error) {
logger.log('warn', `Door not found: ${doorId}`);
return null;
}
}
/**
* Find a door by name
*/
public async getDoorByName(name: string): Promise<UnifiDoor | null> {
const doors = await this.listDoors();
return doors.find(
(door) =>
door.name.toLowerCase() === name.toLowerCase() ||
door.alias?.toLowerCase() === name.toLowerCase()
) || null;
}
/**
* Get locked doors only
*/
public async getLockedDoors(): Promise<UnifiDoor[]> {
const doors = await this.listDoors();
return doors.filter((door) => door.isLocked());
}
/**
* Get unlocked doors only
*/
public async getUnlockedDoors(): Promise<UnifiDoor[]> {
const doors = await this.listDoors();
return doors.filter((door) => !door.isLocked());
}
/**
* Get open doors only
*/
public async getOpenDoors(): Promise<UnifiDoor[]> {
const doors = await this.listDoors();
return doors.filter((door) => door.isOpen());
}
/**
* Get doors by location
*/
public async getDoorsByLocation(locationId: string): Promise<UnifiDoor[]> {
const doors = await this.listDoors();
return doors.filter((door) => door.location_id === locationId);
}
/**
* Unlock a door by ID
*/
public async unlockDoor(doorId: string): Promise<void> {
logger.log('info', `Unlocking door: ${doorId}`);
await this.access.request('PUT', `/door/${doorId}/unlock`);
}
/**
* Lock a door by ID
*/
public async lockDoor(doorId: string): Promise<void> {
logger.log('info', `Locking door: ${doorId}`);
await this.access.request('PUT', `/door/${doorId}/lock`);
}
/**
* Unlock all doors
*/
public async unlockAllDoors(): Promise<void> {
logger.log('info', 'Unlocking all doors');
const doors = await this.listDoors();
await Promise.all(doors.map((door) => this.unlockDoor(door.unique_id)));
}
/**
* Lock all doors
*/
public async lockAllDoors(): Promise<void> {
logger.log('info', 'Locking all doors');
const doors = await this.listDoors();
await Promise.all(doors.map((door) => this.lockDoor(door.unique_id)));
}
/**
* Get access events for a door
*/
public async getDoorEvents(
doorId: string,
options: { start?: number; end?: number; limit?: number } = {}
): Promise<IAccessEvent[]> {
logger.log('debug', `Fetching events for door: ${doorId}`);
const params = new URLSearchParams();
params.append('door_id', doorId);
if (options.start) params.append('start', options.start.toString());
if (options.end) params.append('end', options.end.toString());
if (options.limit) params.append('limit', options.limit.toString());
const response = await this.access.request<IAccessApiResponse<IAccessEvent[]>>(
'GET',
`/event?${params.toString()}`
);
return response.data || [];
}
/**
* Update door settings
*/
public async updateDoor(doorId: string, settings: Partial<IAccessDoor>): Promise<void> {
logger.log('info', `Updating door: ${doorId}`);
await this.access.request('PUT', `/door/${doorId}`, settings);
}
}

85
ts/classes.host.ts Normal file
View File

@@ -0,0 +1,85 @@
import type { UnifiAccount } from './classes.unifi-account.js';
import type { IUnifiHost } from './interfaces/index.js';
/**
* Represents a UniFi host device from Site Manager
*/
export class UnifiHost implements IUnifiHost {
/** Reference to parent account */
private unifiAccount?: UnifiAccount;
// IUnifiHost properties
public id: string;
public hardwareId?: string;
public name?: string;
public type?: string;
public firmwareVersion?: string;
public isOnline?: boolean;
public ipAddress?: string;
public macAddress?: string;
public siteId?: string;
public status?: {
state?: string;
lastSeen?: string;
};
public features?: string[];
public reportedState?: Record<string, unknown>;
constructor() {
this.id = '';
}
/**
* Create a host instance from API response object
*/
public static createFromApiObject(
apiObject: IUnifiHost,
unifiAccount?: UnifiAccount
): UnifiHost {
const host = new UnifiHost();
Object.assign(host, apiObject);
host.unifiAccount = unifiAccount;
return host;
}
/**
* Get the raw API object representation
*/
public toApiObject(): IUnifiHost {
return {
id: this.id,
hardwareId: this.hardwareId,
name: this.name,
type: this.type,
firmwareVersion: this.firmwareVersion,
isOnline: this.isOnline,
ipAddress: this.ipAddress,
macAddress: this.macAddress,
siteId: this.siteId,
status: this.status,
features: this.features,
reportedState: this.reportedState,
};
}
/**
* Check if host is online
*/
public checkOnline(): boolean {
return this.isOnline === true;
}
/**
* Get host type (e.g., 'udm-pro', 'cloud-key')
*/
public getType(): string {
return this.type || 'unknown';
}
/**
* Check if host supports a specific feature
*/
public hasFeature(feature: string): boolean {
return this.features?.includes(feature) ?? false;
}
}

78
ts/classes.hostmanager.ts Normal file
View File

@@ -0,0 +1,78 @@
import type { UnifiAccount } from './classes.unifi-account.js';
import { UnifiHost } from './classes.host.js';
import type { IUnifiHost, ISiteManagerListResponse } from './interfaces/index.js';
import { logger } from './unifi.logger.js';
/**
* Manager for UniFi hosts via Site Manager API
*/
export class HostManager {
private unifiAccount: UnifiAccount;
constructor(unifiAccount: UnifiAccount) {
this.unifiAccount = unifiAccount;
}
/**
* List all hosts
*/
public async listHosts(): Promise<UnifiHost[]> {
logger.log('debug', 'Fetching all hosts from Site Manager');
const response = await this.unifiAccount.request<ISiteManagerListResponse<IUnifiHost>>(
'GET',
'/ea/hosts'
);
const hosts: UnifiHost[] = [];
for (const hostData of response.data || []) {
hosts.push(UnifiHost.createFromApiObject(hostData, this.unifiAccount));
}
logger.log('info', `Found ${hosts.length} hosts`);
return hosts;
}
/**
* Get a host by ID
*/
public async getHostById(hostId: string): Promise<UnifiHost | null> {
logger.log('debug', `Fetching host: ${hostId}`);
try {
const response = await this.unifiAccount.request<IUnifiHost>(
'GET',
`/ea/hosts/${hostId}`
);
return UnifiHost.createFromApiObject(response, this.unifiAccount);
} catch (error) {
logger.log('warn', `Host not found: ${hostId}`);
return null;
}
}
/**
* Find a host by name
*/
public async findHostByName(name: string): Promise<UnifiHost | null> {
const hosts = await this.listHosts();
return hosts.find((host) => host.name === name) || null;
}
/**
* Get hosts by site ID
*/
public async getHostsBySiteId(siteId: string): Promise<UnifiHost[]> {
const hosts = await this.listHosts();
return hosts.filter((host) => host.siteId === siteId);
}
/**
* Get online hosts only
*/
public async getOnlineHosts(): Promise<UnifiHost[]> {
const hosts = await this.listHosts();
return hosts.filter((host) => host.checkOnline());
}
}

57
ts/classes.site.ts Normal file
View File

@@ -0,0 +1,57 @@
import type { UnifiAccount } from './classes.unifi-account.js';
import type { IUnifiSite } from './interfaces/index.js';
/**
* Represents a UniFi site from Site Manager
*/
export class UnifiSite implements IUnifiSite {
/** Reference to parent account */
private unifiAccount?: UnifiAccount;
// IUnifiSite properties
public siteId: string;
public name: string;
public description?: string;
public isDefault?: boolean;
public timezone?: string;
public meta?: {
type?: string;
address?: string;
};
public createdAt?: string;
public updatedAt?: string;
constructor() {
this.siteId = '';
this.name = '';
}
/**
* Create a site instance from API response object
*/
public static createFromApiObject(
apiObject: IUnifiSite,
unifiAccount?: UnifiAccount
): UnifiSite {
const site = new UnifiSite();
Object.assign(site, apiObject);
site.unifiAccount = unifiAccount;
return site;
}
/**
* Get the raw API object representation
*/
public toApiObject(): IUnifiSite {
return {
siteId: this.siteId,
name: this.name,
description: this.description,
isDefault: this.isDefault,
timezone: this.timezone,
meta: this.meta,
createdAt: this.createdAt,
updatedAt: this.updatedAt,
};
}
}

62
ts/classes.sitemanager.ts Normal file
View File

@@ -0,0 +1,62 @@
import type { UnifiAccount } from './classes.unifi-account.js';
import { UnifiSite } from './classes.site.js';
import type { IUnifiSite, ISiteManagerListResponse } from './interfaces/index.js';
import { logger } from './unifi.logger.js';
/**
* Manager for UniFi sites via Site Manager API
*/
export class SiteManager {
private unifiAccount: UnifiAccount;
constructor(unifiAccount: UnifiAccount) {
this.unifiAccount = unifiAccount;
}
/**
* List all sites
*/
public async listSites(): Promise<UnifiSite[]> {
logger.log('debug', 'Fetching all sites from Site Manager');
const response = await this.unifiAccount.request<ISiteManagerListResponse<IUnifiSite>>(
'GET',
'/ea/sites'
);
const sites: UnifiSite[] = [];
for (const siteData of response.data || []) {
sites.push(UnifiSite.createFromApiObject(siteData, this.unifiAccount));
}
logger.log('info', `Found ${sites.length} sites`);
return sites;
}
/**
* Get a site by ID
*/
public async getSiteById(siteId: string): Promise<UnifiSite | null> {
logger.log('debug', `Fetching site: ${siteId}`);
try {
const response = await this.unifiAccount.request<IUnifiSite>(
'GET',
`/ea/sites/${siteId}`
);
return UnifiSite.createFromApiObject(response, this.unifiAccount);
} catch (error) {
logger.log('warn', `Site not found: ${siteId}`);
return null;
}
}
/**
* Find a site by name
*/
public async findSiteByName(name: string): Promise<UnifiSite | null> {
const sites = await this.listSites();
return sites.find((site) => site.name === name) || null;
}
}

339
ts/classes.unifi-access.ts Normal file
View File

@@ -0,0 +1,339 @@
import * as plugins from './plugins.js';
import { logger } from './unifi.logger.js';
import { UnifiHttp } from './classes.unifihttp.js';
import { DoorManager } from './classes.doormanager.js';
import type {
IUnifiAccessOptions,
IAccessDevice,
IAccessUser,
IAccessPolicy,
IAccessLocation,
IAccessEvent,
IAccessApiResponse,
THttpMethod,
} from './interfaces/index.js';
/**
* UniFi Access - Entry point for Access Controller API
*
* This class provides access to the UniFi Access API for managing doors,
* users, credentials, and access events. It uses bearer token authentication.
*
* @example
* ```typescript
* const access = new UnifiAccess({
* host: '192.168.1.1',
* token: 'your-bearer-token',
* });
*
* const doors = await access.doorManager.listDoors();
* const users = await access.getUsers();
*
* // Unlock a door
* await access.unlockDoor('door-id');
* ```
*/
export class UnifiAccess {
/** Access API port */
private static readonly API_PORT = 12445;
/** Access host */
private host: string;
/** Bearer token for authentication */
private token: string;
/** Whether to verify SSL certificates */
private verifySsl: boolean;
/** HTTP client */
private http: UnifiHttp;
/** Door manager instance */
public doorManager: DoorManager;
constructor(options: IUnifiAccessOptions) {
this.host = options.host.replace(/\/$/, '');
this.token = options.token;
this.verifySsl = options.verifySsl ?? false;
// Build base URL with Access API port
const baseHost = this.host.startsWith('http') ? this.host : `https://${this.host}`;
// Access API is typically on port 12445 at /api/v1/developer
const baseUrl = `${baseHost}:${UnifiAccess.API_PORT}/api/v1/developer`;
this.http = new UnifiHttp(baseUrl, this.verifySsl);
this.http.setHeader('Authorization', `Bearer ${this.token}`);
// Initialize managers
this.doorManager = new DoorManager(this);
logger.log('info', `UnifiAccess initialized for ${this.host}`);
}
/**
* Make a request to the Access API
*/
public async request<T>(
method: THttpMethod,
endpoint: string,
data?: unknown
): Promise<T> {
return this.http.request<T>(method, endpoint, data);
}
/**
* Unlock a door by ID
*/
public async unlockDoor(doorId: string): Promise<void> {
logger.log('info', `Unlocking door: ${doorId}`);
await this.request('PUT', `/door/${doorId}/unlock`);
}
/**
* Lock a door by ID
*/
public async lockDoor(doorId: string): Promise<void> {
logger.log('info', `Locking door: ${doorId}`);
await this.request('PUT', `/door/${doorId}/lock`);
}
/**
* Get all doors (convenience method)
*/
public async getDoors() {
return this.doorManager.listDoors();
}
/**
* Get all devices (hubs, readers, etc.)
*/
public async getDevices(): Promise<IAccessDevice[]> {
logger.log('debug', 'Fetching Access devices');
const response = await this.request<IAccessApiResponse<IAccessDevice[]>>(
'GET',
'/device'
);
return response.data || [];
}
/**
* Get a device by ID
*/
public async getDeviceById(deviceId: string): Promise<IAccessDevice | null> {
try {
const response = await this.request<IAccessApiResponse<IAccessDevice>>(
'GET',
`/device/${deviceId}`
);
return response.data;
} catch {
return null;
}
}
/**
* Get all users/credential holders
*/
public async getUsers(): Promise<IAccessUser[]> {
logger.log('debug', 'Fetching Access users');
const response = await this.request<IAccessApiResponse<IAccessUser[]>>(
'GET',
'/user'
);
return response.data || [];
}
/**
* Get a user by ID
*/
public async getUserById(userId: string): Promise<IAccessUser | null> {
try {
const response = await this.request<IAccessApiResponse<IAccessUser>>(
'GET',
`/user/${userId}`
);
return response.data;
} catch {
return null;
}
}
/**
* Create a new user
*/
public async createUser(user: Partial<IAccessUser>): Promise<IAccessUser> {
logger.log('info', `Creating user: ${user.first_name} ${user.last_name}`);
const response = await this.request<IAccessApiResponse<IAccessUser>>(
'POST',
'/user',
user
);
return response.data;
}
/**
* Update a user
*/
public async updateUser(userId: string, user: Partial<IAccessUser>): Promise<IAccessUser> {
logger.log('info', `Updating user: ${userId}`);
const response = await this.request<IAccessApiResponse<IAccessUser>>(
'PUT',
`/user/${userId}`,
user
);
return response.data;
}
/**
* Delete a user
*/
public async deleteUser(userId: string): Promise<void> {
logger.log('info', `Deleting user: ${userId}`);
await this.request('DELETE', `/user/${userId}`);
}
/**
* Get access policies/groups
*/
public async getPolicies(): Promise<IAccessPolicy[]> {
logger.log('debug', 'Fetching Access policies');
const response = await this.request<IAccessApiResponse<IAccessPolicy[]>>(
'GET',
'/policy'
);
return response.data || [];
}
/**
* Get a policy by ID
*/
public async getPolicyById(policyId: string): Promise<IAccessPolicy | null> {
try {
const response = await this.request<IAccessApiResponse<IAccessPolicy>>(
'GET',
`/policy/${policyId}`
);
return response.data;
} catch {
return null;
}
}
/**
* Get locations
*/
public async getLocations(): Promise<IAccessLocation[]> {
logger.log('debug', 'Fetching Access locations');
const response = await this.request<IAccessApiResponse<IAccessLocation[]>>(
'GET',
'/location'
);
return response.data || [];
}
/**
* Get access events (entry log)
*/
public async getEvents(
options: { start?: number; end?: number; limit?: number; doorId?: string; userId?: string } = {}
): Promise<IAccessEvent[]> {
logger.log('debug', 'Fetching Access events');
const params = new URLSearchParams();
if (options.start) params.append('start', options.start.toString());
if (options.end) params.append('end', options.end.toString());
if (options.limit) params.append('limit', options.limit.toString());
if (options.doorId) params.append('door_id', options.doorId);
if (options.userId) params.append('user_id', options.userId);
const queryString = params.toString() ? `?${params.toString()}` : '';
const response = await this.request<IAccessApiResponse<IAccessEvent[]>>(
'GET',
`/event${queryString}`
);
return response.data || [];
}
/**
* Get recent access events
*/
public async getRecentEvents(limit: number = 100): Promise<IAccessEvent[]> {
return this.getEvents({ limit });
}
/**
* Grant user access to a door
*/
public async grantAccess(userId: string, doorId: string): Promise<void> {
logger.log('info', `Granting user ${userId} access to door ${doorId}`);
await this.request('POST', `/user/${userId}/access`, {
door_id: doorId,
});
}
/**
* Revoke user access from a door
*/
public async revokeAccess(userId: string, doorId: string): Promise<void> {
logger.log('info', `Revoking user ${userId} access from door ${doorId}`);
await this.request('DELETE', `/user/${userId}/access/${doorId}`);
}
/**
* Assign NFC card to user
*/
public async assignNfcCard(
userId: string,
cardToken: string,
alias?: string
): Promise<void> {
logger.log('info', `Assigning NFC card to user ${userId}`);
await this.request('POST', `/user/${userId}/nfc_card`, {
token: cardToken,
alias: alias,
});
}
/**
* Remove NFC card from user
*/
public async removeNfcCard(userId: string, cardId: string): Promise<void> {
logger.log('info', `Removing NFC card ${cardId} from user ${userId}`);
await this.request('DELETE', `/user/${userId}/nfc_card/${cardId}`);
}
/**
* Set user PIN code
*/
public async setUserPin(userId: string, pinCode: string): Promise<void> {
logger.log('info', `Setting PIN for user ${userId}`);
await this.request('PUT', `/user/${userId}`, {
pin_code: pinCode,
});
}
}

View File

@@ -0,0 +1,82 @@
import * as plugins from './plugins.js';
import { logger } from './unifi.logger.js';
import { UnifiHttp } from './classes.unifihttp.js';
import { SiteManager } from './classes.sitemanager.js';
import { HostManager } from './classes.hostmanager.js';
import type { IUnifiAccountOptions, THttpMethod } from './interfaces/index.js';
/**
* UniFi Account - Entry point for Site Manager (cloud) API
*
* This class provides access to the UniFi Site Manager API using API key authentication.
* It's used to manage sites and hosts across your UniFi deployment via ui.com.
*
* @example
* ```typescript
* const account = new UnifiAccount({ apiKey: 'your-api-key' });
* const sites = await account.siteManager.listSites();
* const hosts = await account.hostManager.listHosts();
* ```
*/
export class UnifiAccount {
/** Site Manager API base URL */
private static readonly BASE_URL = 'https://api.ui.com/v1';
/** API key for authentication */
private apiKey: string;
/** HTTP client */
private http: UnifiHttp;
/** Site manager instance */
public siteManager: SiteManager;
/** Host manager instance */
public hostManager: HostManager;
constructor(options: IUnifiAccountOptions) {
this.apiKey = options.apiKey;
// Initialize HTTP client
this.http = new UnifiHttp(UnifiAccount.BASE_URL, true); // Cloud API uses valid SSL
this.http.setHeader('X-API-Key', this.apiKey);
// Initialize managers
this.siteManager = new SiteManager(this);
this.hostManager = new HostManager(this);
logger.log('info', 'UnifiAccount initialized for Site Manager API');
}
/**
* Make a request to the Site Manager API
*/
public async request<T>(
method: THttpMethod,
endpoint: string,
data?: unknown
): Promise<T> {
return this.http.request<T>(method, endpoint, data);
}
/**
* Get account info
*/
public async getAccountInfo(): Promise<unknown> {
return this.request('GET', '/ea/account');
}
/**
* Get all sites (convenience method)
*/
public async getSites() {
return this.siteManager.listSites();
}
/**
* Get all hosts (convenience method)
*/
public async getHosts() {
return this.hostManager.listHosts();
}
}

View File

@@ -0,0 +1,377 @@
import * as plugins from './plugins.js';
import { logger } from './unifi.logger.js';
import { UnifiHttp } from './classes.unifihttp.js';
import { DeviceManager } from './classes.devicemanager.js';
import { ClientManager } from './classes.clientmanager.js';
import type {
IUnifiControllerOptions,
INetworkAuthResponse,
INetworkSite,
IUnifiApiResponse,
THttpMethod,
} from './interfaces/index.js';
/**
* UniFi Controller - Entry point for Network Controller API
*
* This class provides access to the UniFi Network Controller API.
* Supports two authentication methods:
* 1. API Key (preferred) - Set X-API-Key header, no login required
* 2. Session auth - Username/password login with session cookies
*
* @example
* ```typescript
* // Using API key (no login required)
* const controller = new UnifiController({
* host: '192.168.1.1',
* apiKey: 'your-api-key',
* });
* const devices = await controller.deviceManager.listDevices();
*
* // Using session auth (requires login)
* const controller = new UnifiController({
* host: '192.168.1.1',
* username: 'admin',
* password: 'password',
* });
* await controller.login();
* const devices = await controller.deviceManager.listDevices();
* await controller.logout();
* ```
*/
export class UnifiController {
/** Controller host */
private host: string;
/** API key for authentication */
private apiKey?: string;
/** Username for session authentication */
private username?: string;
/** Password for session authentication */
private password?: string;
/** Controller type determines API paths */
private controllerType: 'unifi-os' | 'udm-pro' | 'standalone';
/** Whether to verify SSL certificates */
private verifySsl: boolean;
/** HTTP client */
private http: UnifiHttp;
/** Whether currently authenticated */
private authenticated: boolean = false;
/** CSRF token (for UniFi OS session auth) */
private csrfToken?: string;
/** Device manager instance */
public deviceManager: DeviceManager;
/** Client manager instance */
public clientManager: ClientManager;
constructor(options: IUnifiControllerOptions) {
this.host = options.host.replace(/\/$/, '');
this.apiKey = options.apiKey;
this.username = options.username;
this.password = options.password;
this.controllerType = options.controllerType || 'unifi-os';
this.verifySsl = options.verifySsl ?? false;
// Build base URL based on controller type
const baseUrl = this.getBaseUrl();
this.http = new UnifiHttp(baseUrl, this.verifySsl);
// If API key provided, set it and mark as authenticated
if (this.apiKey) {
this.http.setHeader('X-API-Key', this.apiKey);
this.authenticated = true;
}
// Initialize managers
this.deviceManager = new DeviceManager(this);
this.clientManager = new ClientManager(this);
logger.log('info', `UnifiController initialized for ${this.host} (${this.controllerType})`);
}
/**
* Get the base URL for API requests based on controller type
*/
private getBaseUrl(): string {
// Add https:// if not present
const host = this.host.startsWith('http') ? this.host : `https://${this.host}`;
switch (this.controllerType) {
case 'unifi-os':
case 'udm-pro':
// UniFi OS consoles (UDM, UDM Pro, Cloud Key Gen2+) use /proxy/network prefix
return `${host}/proxy/network`;
case 'standalone':
// Standalone controllers (software controller)
return host;
default:
return host;
}
}
/**
* Get the login endpoint based on controller type
*/
private getLoginEndpoint(): string {
switch (this.controllerType) {
case 'unifi-os':
case 'udm-pro':
// UniFi OS uses the root /api/auth/login
return '/api/auth/login';
case 'standalone':
return '/api/login';
default:
return '/api/login';
}
}
/**
* Get the logout endpoint based on controller type
*/
private getLogoutEndpoint(): string {
switch (this.controllerType) {
case 'unifi-os':
case 'udm-pro':
return '/api/auth/logout';
case 'standalone':
return '/api/logout';
default:
return '/api/logout';
}
}
/**
* Login to the controller (only needed for session auth, not API key)
*/
public async login(): Promise<void> {
// If using API key, already authenticated
if (this.apiKey) {
logger.log('info', 'Using API key authentication, no login required');
this.authenticated = true;
return;
}
if (!this.username || !this.password) {
throw new Error('Username and password required for session authentication');
}
logger.log('info', `Logging in to UniFi Controller at ${this.host}`);
// For UniFi OS, login happens at the console level (not through /proxy/network)
let loginUrl: string;
if (this.controllerType === 'unifi-os' || this.controllerType === 'udm-pro') {
const host = this.host.startsWith('http') ? this.host : `https://${this.host}`;
loginUrl = `${host}${this.getLoginEndpoint()}`;
} else {
loginUrl = `${this.getBaseUrl()}${this.getLoginEndpoint()}`;
}
// Create a separate HTTP client for login (different base URL for UniFi OS)
const loginHttp = new UnifiHttp(
loginUrl.replace(this.getLoginEndpoint(), ''),
this.verifySsl
);
const response = await loginHttp.rawRequest('POST', this.getLoginEndpoint(), {
username: this.username,
password: this.password,
});
if (!response.ok) {
const errorText = await response.text().catch(() => 'Unknown error');
throw new Error(`Login failed: HTTP ${response.status} - ${errorText}`);
}
// Store cookies from response
const setCookieHeaders = response.headers?.['set-cookie'];
if (setCookieHeaders && Array.isArray(setCookieHeaders)) {
this.http.setCookies(setCookieHeaders);
}
// Extract CSRF token from response (UniFi OS)
const csrfHeader = response.headers?.['x-csrf-token'];
if (csrfHeader) {
this.csrfToken = Array.isArray(csrfHeader) ? csrfHeader[0] : csrfHeader;
this.http.setHeader('X-CSRF-Token', this.csrfToken);
}
this.authenticated = true;
logger.log('info', 'Successfully logged in to UniFi Controller');
}
/**
* Logout from the controller (only for session auth)
*/
public async logout(): Promise<void> {
// If using API key, nothing to logout
if (this.apiKey) {
return;
}
if (!this.authenticated) {
return;
}
logger.log('info', 'Logging out from UniFi Controller');
try {
// For UniFi OS, logout happens at the console level
if (this.controllerType === 'unifi-os' || this.controllerType === 'udm-pro') {
const host = this.host.startsWith('http') ? this.host : `https://${this.host}`;
const logoutHttp = new UnifiHttp(host, this.verifySsl);
logoutHttp.setCookies([this.http.getCookieHeader()]);
if (this.csrfToken) {
logoutHttp.setHeader('X-CSRF-Token', this.csrfToken);
}
await logoutHttp.rawRequest('POST', this.getLogoutEndpoint());
} else {
await this.http.rawRequest('POST', this.getLogoutEndpoint());
}
} catch (error) {
logger.log('warn', `Logout error (may be expected): ${error}`);
}
this.authenticated = false;
this.csrfToken = undefined;
}
/**
* Check if logged in
*/
public isAuthenticated(): boolean {
return this.authenticated;
}
/**
* Make a request to the controller API
*/
public async request<T>(
method: THttpMethod,
endpoint: string,
data?: unknown
): Promise<T> {
if (!this.authenticated) {
throw new Error('Not authenticated. Call login() first or provide API key.');
}
return this.http.request<T>(method, endpoint, data);
}
/**
* List all sites on this controller
*/
public async listSites(): Promise<INetworkSite[]> {
logger.log('debug', 'Fetching sites');
const response = await this.request<IUnifiApiResponse<INetworkSite>>(
'GET',
'/api/self/sites'
);
return response.data || [];
}
/**
* Get system info
*/
public async getSystemInfo(): Promise<unknown> {
return this.request('GET', '/api/s/default/stat/sysinfo');
}
/**
* Get controller health
*/
public async getHealth(siteId: string = 'default'): Promise<unknown> {
return this.request('GET', `/api/s/${siteId}/stat/health`);
}
/**
* Get active alerts
*/
public async getAlerts(siteId: string = 'default'): Promise<unknown> {
return this.request('GET', `/api/s/${siteId}/stat/alarm`);
}
/**
* Get events
*/
public async getEvents(
siteId: string = 'default',
options: { start?: number; end?: number; limit?: number } = {}
): Promise<unknown> {
const params = new URLSearchParams();
if (options.start) params.append('start', options.start.toString());
if (options.end) params.append('end', options.end.toString());
if (options.limit) params.append('_limit', options.limit.toString());
const queryString = params.toString() ? `?${params.toString()}` : '';
return this.request('GET', `/api/s/${siteId}/stat/event${queryString}`);
}
/**
* Get WLAN configurations
*/
public async getWlans(siteId: string = 'default'): Promise<unknown> {
return this.request('GET', `/api/s/${siteId}/rest/wlanconf`);
}
/**
* Get network configurations
*/
public async getNetworks(siteId: string = 'default'): Promise<unknown> {
return this.request('GET', `/api/s/${siteId}/rest/networkconf`);
}
/**
* Get port forward rules
*/
public async getPortForwards(siteId: string = 'default'): Promise<unknown> {
return this.request('GET', `/api/s/${siteId}/rest/portforward`);
}
/**
* Get firewall rules
*/
public async getFirewallRules(siteId: string = 'default'): Promise<unknown> {
return this.request('GET', `/api/s/${siteId}/rest/firewallrule`);
}
/**
* Get DPI stats
*/
public async getDpiStats(siteId: string = 'default'): Promise<unknown> {
return this.request('GET', `/api/s/${siteId}/stat/dpi`);
}
/**
* Backup the controller configuration
*/
public async createBackup(siteId: string = 'default'): Promise<unknown> {
return this.request('POST', `/api/s/${siteId}/cmd/backup`, {
cmd: 'backup',
});
}
/**
* Get devices (convenience method)
*/
public async getDevices(siteId: string = 'default') {
return this.deviceManager.listDevices(siteId);
}
/**
* Get active clients (convenience method)
*/
public async getClients(siteId: string = 'default') {
return this.clientManager.listActiveClients(siteId);
}
}

343
ts/classes.unifi-protect.ts Normal file
View File

@@ -0,0 +1,343 @@
import * as plugins from './plugins.js';
import { logger } from './unifi.logger.js';
import { UnifiHttp } from './classes.unifihttp.js';
import { CameraManager } from './classes.cameramanager.js';
import type {
IUnifiProtectOptions,
IProtectBootstrap,
IProtectCamera,
IProtectNvr,
THttpMethod,
} from './interfaces/index.js';
/**
* UniFi Protect - Entry point for Protect NVR API
*
* This class provides access to the UniFi Protect API for managing cameras,
* recordings, and motion events. Supports two authentication methods:
* 1. API Key (preferred) - Set X-API-Key header, no login required
* 2. Session auth - Username/password login with session cookies + CSRF
*
* @example
* ```typescript
* // Using API key (no login required)
* const protect = new UnifiProtect({
* host: '192.168.1.1',
* apiKey: 'your-api-key',
* });
* await protect.refreshBootstrap(); // Load camera data
* const cameras = await protect.cameraManager.listCameras();
*
* // Using session auth
* const protect = new UnifiProtect({
* host: '192.168.1.1',
* username: 'admin',
* password: 'password',
* });
* await protect.login();
* const cameras = await protect.cameraManager.listCameras();
* await protect.logout();
* ```
*/
export class UnifiProtect {
/** Protect host */
private host: string;
/** API key for authentication */
private apiKey?: string;
/** Username for session authentication */
private username?: string;
/** Password for session authentication */
private password?: string;
/** Whether to verify SSL certificates */
private verifySsl: boolean;
/** HTTP client for Protect API */
private http: UnifiHttp;
/** HTTP client for auth (console level) */
private authHttp: UnifiHttp;
/** Whether currently authenticated */
private authenticated: boolean = false;
/** CSRF token */
private csrfToken?: string;
/** Bootstrap data (contains all cameras, NVR info, etc.) */
private bootstrap?: IProtectBootstrap;
/** Camera manager instance */
public cameraManager: CameraManager;
constructor(options: IUnifiProtectOptions) {
this.host = options.host.replace(/\/$/, '');
this.apiKey = options.apiKey;
this.username = options.username;
this.password = options.password;
this.verifySsl = options.verifySsl ?? false;
// Build base URLs
const baseHost = this.host.startsWith('http') ? this.host : `https://${this.host}`;
// Auth happens at console level
this.authHttp = new UnifiHttp(baseHost, this.verifySsl);
// Protect API is behind /proxy/protect/api
this.http = new UnifiHttp(`${baseHost}/proxy/protect/api`, this.verifySsl);
// If API key provided, set it and mark as authenticated
if (this.apiKey) {
this.http.setHeader('X-API-Key', this.apiKey);
this.authenticated = true;
}
// Initialize managers
this.cameraManager = new CameraManager(this);
logger.log('info', `UnifiProtect initialized for ${this.host}`);
}
/**
* Login to Protect (only needed for session auth, not API key)
*/
public async login(): Promise<void> {
// If using API key, already authenticated
if (this.apiKey) {
logger.log('info', 'Using API key authentication, no login required');
this.authenticated = true;
await this.fetchBootstrap();
return;
}
if (!this.username || !this.password) {
throw new Error('Username and password required for session authentication');
}
logger.log('info', `Logging in to UniFi Protect at ${this.host}`);
// Login at console level
const response = await this.authHttp.rawRequest('POST', '/api/auth/login', {
username: this.username,
password: this.password,
});
if (!response.ok) {
const errorText = await response.text().catch(() => 'Unknown error');
throw new Error(`Protect login failed: HTTP ${response.status} - ${errorText}`);
}
// Store cookies
const setCookieHeaders = response.headers?.['set-cookie'];
if (setCookieHeaders && Array.isArray(setCookieHeaders)) {
this.authHttp.setCookies(setCookieHeaders);
this.http.setCookies(setCookieHeaders);
}
// Extract CSRF token
const csrfHeader = response.headers?.['x-csrf-token'];
if (csrfHeader) {
this.csrfToken = Array.isArray(csrfHeader) ? csrfHeader[0] : csrfHeader;
this.http.setHeader('X-CSRF-Token', this.csrfToken);
}
this.authenticated = true;
// Fetch bootstrap to get cameras and NVR info
await this.fetchBootstrap();
logger.log('info', 'Successfully logged in to UniFi Protect');
}
/**
* Logout from Protect (only for session auth)
*/
public async logout(): Promise<void> {
// If using API key, nothing to logout
if (this.apiKey) {
return;
}
if (!this.authenticated) {
return;
}
logger.log('info', 'Logging out from UniFi Protect');
try {
await this.authHttp.rawRequest('POST', '/api/auth/logout');
} catch (error) {
logger.log('warn', `Protect logout error (may be expected): ${error}`);
}
this.authenticated = false;
this.csrfToken = undefined;
this.bootstrap = undefined;
}
/**
* Check if logged in
*/
public isAuthenticated(): boolean {
return this.authenticated;
}
/**
* Fetch bootstrap data (cameras, NVR info, etc.)
*/
private async fetchBootstrap(): Promise<void> {
logger.log('debug', 'Fetching Protect bootstrap');
this.bootstrap = await this.http.request<IProtectBootstrap>('GET', '/bootstrap');
logger.log('info', `Bootstrap loaded: ${this.bootstrap.cameras?.length || 0} cameras`);
}
/**
* Refresh bootstrap data
*/
public async refreshBootstrap(): Promise<void> {
await this.fetchBootstrap();
}
/**
* Get NVR info from bootstrap
*/
public getNvrInfo(): IProtectNvr | undefined {
return this.bootstrap?.nvr;
}
/**
* Get cameras from bootstrap (cached)
*/
public getCamerasFromBootstrap(): IProtectCamera[] {
return this.bootstrap?.cameras || [];
}
/**
* Make a request to the Protect API
*/
public async request<T>(
method: THttpMethod,
endpoint: string,
data?: unknown
): Promise<T> {
if (!this.authenticated) {
throw new Error('Not authenticated. Call login() first or provide API key.');
}
return this.http.request<T>(method, endpoint, data);
}
/**
* Get all cameras (convenience method)
*/
public async getCameras() {
return this.cameraManager.listCameras();
}
/**
* Get storage info from NVR
*/
public getStorageInfo(): { totalSize?: number; usedSpace?: number; freeSpace?: number } | undefined {
const nvr = this.getNvrInfo();
if (!nvr?.storageInfo) return undefined;
const totalSize = nvr.storageInfo.totalSize;
const usedSpace = nvr.storageInfo.totalSpaceUsed;
const freeSpace = totalSize && usedSpace ? totalSize - usedSpace : undefined;
return {
totalSize,
usedSpace,
freeSpace,
};
}
/**
* Get NVR uptime
*/
public getNvrUptime(): number | undefined {
return this.getNvrInfo()?.uptime;
}
/**
* Check if NVR is connected to cloud
*/
public isCloudConnected(): boolean {
return this.getNvrInfo()?.isConnectedToCloud === true;
}
/**
* Get system events
*/
public async getSystemEvents(limit: number = 100): Promise<unknown> {
return this.request('GET', `/events?limit=${limit}`);
}
/**
* Get liveviews
*/
public async getLiveviews(): Promise<unknown> {
return this.request('GET', '/liveviews');
}
/**
* Get users
*/
public async getUsers(): Promise<unknown> {
return this.request('GET', '/users');
}
/**
* Get groups
*/
public async getGroups(): Promise<unknown> {
return this.request('GET', '/groups');
}
/**
* Get lights (if any)
*/
public getLights() {
return this.bootstrap?.lights || [];
}
/**
* Get sensors (if any)
*/
public getSensors() {
return this.bootstrap?.sensors || [];
}
/**
* Get doorbells
*/
public getDoorbells() {
return this.bootstrap?.doorbells || [];
}
/**
* Get chimes
*/
public getChimes() {
return this.bootstrap?.chimes || [];
}
/**
* Get bridges
*/
public getBridges() {
return this.bootstrap?.bridges || [];
}
/**
* Get viewers
*/
public getViewers() {
return this.bootstrap?.viewers || [];
}
}

265
ts/classes.unifihttp.ts Normal file
View File

@@ -0,0 +1,265 @@
import * as plugins from './plugins.js';
import { logger } from './unifi.logger.js';
import type { THttpMethod } from './interfaces/index.js';
/**
* Extended response type
*/
export interface IUnifiHttpResponse<T = any> {
ok: boolean;
status: number;
statusText?: string;
headers?: Record<string, string | string[]>;
body: T;
text: () => Promise<string>;
json: () => Promise<T>;
}
/**
* Base HTTP client for UniFi APIs with SSL handling and authentication support
*/
export class UnifiHttp {
private baseUrl: string;
private headers: Record<string, string> = {};
private verifySsl: boolean;
private cookies: string[] = [];
constructor(baseUrl: string, verifySsl: boolean = false) {
this.baseUrl = baseUrl.replace(/\/$/, ''); // Remove trailing slash
this.verifySsl = verifySsl;
}
/**
* Set a header value
*/
public setHeader(name: string, value: string): void {
this.headers[name] = value;
}
/**
* Remove a header
*/
public removeHeader(name: string): void {
delete this.headers[name];
}
/**
* Set cookies from Set-Cookie headers
*/
public setCookies(cookies: string[]): void {
this.cookies = cookies;
}
/**
* Get the cookie header string
*/
public getCookieHeader(): string {
return this.cookies
.map((cookie) => cookie.split(';')[0])
.join('; ');
}
/**
* Build request with common options
*/
private buildRequest(url: string, data?: unknown): plugins.smartrequest.SmartRequestClient {
let requestBuilder = plugins.smartrequest.SmartRequestClient.create()
.url(url)
.header('Content-Type', 'application/json');
// Add stored headers
for (const [name, value] of Object.entries(this.headers)) {
requestBuilder = requestBuilder.header(name, value);
}
// Add cookies
const cookieHeader = this.getCookieHeader();
if (cookieHeader) {
requestBuilder = requestBuilder.header('Cookie', cookieHeader);
}
// Add JSON data
if (data) {
requestBuilder = requestBuilder.json(data);
}
return requestBuilder;
}
/**
* Make an HTTP request
*/
public async request<T>(
method: THttpMethod,
endpoint: string,
data?: unknown
): Promise<T> {
const url = `${this.baseUrl}${endpoint}`;
logger.log('debug', `UniFi HTTP ${method} ${url}`);
const requestBuilder = this.buildRequest(url, data);
let response: plugins.smartrequest.IExtendedIncomingMessage;
// Note: smartrequest v2 doesn't have built-in SSL bypass, but Node's default is fine for most cases
// For self-signed certs, we need to handle at the environment level or use NODE_TLS_REJECT_UNAUTHORIZED
const originalRejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED;
if (!this.verifySsl) {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
}
try {
switch (method) {
case 'GET':
response = await requestBuilder.get();
break;
case 'POST':
response = await requestBuilder.post();
break;
case 'PUT':
response = await requestBuilder.put();
break;
case 'DELETE':
response = await requestBuilder.delete();
break;
case 'PATCH':
response = await requestBuilder.patch();
break;
default:
throw new Error(`Unsupported HTTP method: ${method}`);
}
} finally {
if (!this.verifySsl) {
if (originalRejectUnauthorized !== undefined) {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = originalRejectUnauthorized;
} else {
delete process.env.NODE_TLS_REJECT_UNAUTHORIZED;
}
}
}
// Store cookies from response
const setCookieHeaders = response.headers?.['set-cookie'];
if (setCookieHeaders && Array.isArray(setCookieHeaders)) {
this.setCookies(setCookieHeaders);
}
// Check for HTTP errors
const statusCode = response.statusCode || 0;
if (statusCode >= 400) {
const errorBody = typeof response.body === 'string' ? response.body : JSON.stringify(response.body);
logger.log('error', `UniFi HTTP error: ${statusCode} - ${errorBody}`);
throw new Error(`HTTP ${statusCode}: ${response.statusMessage || 'Unknown'} - ${errorBody}`);
}
// Return body directly
return response.body as T;
}
/**
* Make a raw request returning the full response (for handling cookies/headers)
*/
public async rawRequest(
method: THttpMethod,
endpoint: string,
data?: unknown
): Promise<IUnifiHttpResponse> {
const url = `${this.baseUrl}${endpoint}`;
logger.log('debug', `UniFi HTTP raw ${method} ${url}`);
const requestBuilder = this.buildRequest(url, data);
let response: plugins.smartrequest.IExtendedIncomingMessage;
const originalRejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED;
if (!this.verifySsl) {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
}
try {
switch (method) {
case 'GET':
response = await requestBuilder.get();
break;
case 'POST':
response = await requestBuilder.post();
break;
case 'PUT':
response = await requestBuilder.put();
break;
case 'DELETE':
response = await requestBuilder.delete();
break;
case 'PATCH':
response = await requestBuilder.patch();
break;
default:
throw new Error(`Unsupported HTTP method: ${method}`);
}
} finally {
if (!this.verifySsl) {
if (originalRejectUnauthorized !== undefined) {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = originalRejectUnauthorized;
} else {
delete process.env.NODE_TLS_REJECT_UNAUTHORIZED;
}
}
}
// Store cookies from response
const setCookieHeaders = response.headers?.['set-cookie'];
if (setCookieHeaders && Array.isArray(setCookieHeaders)) {
this.setCookies(setCookieHeaders);
}
const statusCode = response.statusCode || 0;
const body = response.body;
return {
ok: statusCode >= 200 && statusCode < 300,
status: statusCode,
statusText: response.statusMessage,
headers: response.headers as Record<string, string | string[]>,
body,
text: async () => typeof body === 'string' ? body : JSON.stringify(body),
json: async () => typeof body === 'object' ? body : JSON.parse(body as string),
};
}
/**
* Shorthand for GET requests
*/
public async get<T>(endpoint: string): Promise<T> {
return this.request<T>('GET', endpoint);
}
/**
* Shorthand for POST requests
*/
public async post<T>(endpoint: string, data?: unknown): Promise<T> {
return this.request<T>('POST', endpoint, data);
}
/**
* Shorthand for PUT requests
*/
public async put<T>(endpoint: string, data?: unknown): Promise<T> {
return this.request<T>('PUT', endpoint, data);
}
/**
* Shorthand for DELETE requests
*/
public async delete<T>(endpoint: string): Promise<T> {
return this.request<T>('DELETE', endpoint);
}
/**
* Shorthand for PATCH requests
*/
public async patch<T>(endpoint: string, data?: unknown): Promise<T> {
return this.request<T>('PATCH', endpoint, data);
}
}

View File

@@ -1,3 +1,37 @@
import * as plugins from './plugins.js';
/**
* @apiclient.xyz/unifi
*
* A comprehensive UniFi API client supporting multiple UniFi applications:
* - Site Manager API (cloud) - API key authentication
* - Network Controller API (local) - Session cookie authentication
* - Protect API (local) - Session + CSRF token authentication
* - Access API (local) - Bearer token authentication
*/
export let demoExport = 'Hi there! :) This is an exported string';
// Re-export all interfaces
export * from './interfaces/index.js';
// Export entry point classes
export { UnifiAccount } from './classes.unifi-account.js';
export { UnifiController } from './classes.unifi-controller.js';
export { UnifiProtect } from './classes.unifi-protect.js';
export { UnifiAccess } from './classes.unifi-access.js';
// Export manager classes
export { SiteManager } from './classes.sitemanager.js';
export { HostManager } from './classes.hostmanager.js';
export { DeviceManager } from './classes.devicemanager.js';
export { ClientManager } from './classes.clientmanager.js';
export { CameraManager } from './classes.cameramanager.js';
export { DoorManager } from './classes.doormanager.js';
// Export resource classes
export { UnifiSite } from './classes.site.js';
export { UnifiHost } from './classes.host.js';
export { UnifiDevice } from './classes.device.js';
export { UnifiClient } from './classes.client.js';
export { UnifiCamera } from './classes.camera.js';
export { UnifiDoor } from './classes.door.js';
// Export HTTP client for advanced usage
export { UnifiHttp } from './classes.unifihttp.js';

284
ts/interfaces/access.ts Normal file
View File

@@ -0,0 +1,284 @@
/**
* Access API interfaces
* Base URL: https://{host}:12445/api/v1/developer
*/
/**
* Access device (door controller, hub, reader)
*/
export interface IAccessDevice {
/** Device unique ID */
unique_id: string;
/** Device name */
name: string;
/** Device alias */
alias?: string;
/** Device model */
device_type: string;
/** Hardware revision */
revision?: number;
/** Firmware version */
version?: string;
/** Firmware version */
version_update_to?: string;
/** Adopted */
adopted?: boolean;
/** Connected */
connected?: boolean;
/** IP address */
ip?: string;
/** MAC address */
mac?: string;
/** Start timestamp */
start_time?: number;
/** Security level */
security_check?: boolean;
/** Location */
location?: IAccessLocation;
/** Capabilities */
capabilities?: string[];
/** Device configuration */
configs?: IAccessDeviceConfig[];
/** Connected devices (for hub) */
connected_devices?: string[];
}
/**
* Access door
*/
export interface IAccessDoor {
/** Door unique ID */
unique_id: string;
/** Door name */
name: string;
/** Door alias */
alias?: string;
/** Door type */
door_type?: string;
/** Whether door is locked */
door_lock_relay_status?: 'lock' | 'unlock';
/** Whether door is open (contact sensor) */
door_position_status?: 'open' | 'close';
/** Door controller device ID */
device_id?: string;
/** Associated camera ID */
camera_resource_id?: string;
/** Location */
location_id?: string;
/** Full */
full_name?: string;
/** Extra type */
extra_type?: string;
/** Door guard */
door_guard?: boolean;
/** Rules */
rules?: IAccessDoorRule[];
/** Level ID */
level_id?: string;
/** Floor info */
floor?: IAccessFloor;
}
/**
* Door rule
*/
export interface IAccessDoorRule {
/** Rule unique ID */
unique_id: string;
/** Rule name */
name?: string;
/** Rule type */
type?: string;
/** Enabled */
enabled?: boolean;
/** Interval start */
interval_start?: string;
/** Interval end */
interval_end?: string;
/** Days of week */
days?: string[];
}
/**
* Access user/credential holder
*/
export interface IAccessUser {
/** User unique ID */
unique_id: string;
/** User ID */
id?: string;
/** First name */
first_name: string;
/** Last name */
last_name: string;
/** Full name */
full_name?: string;
/** Email */
email?: string;
/** Phone */
phone?: string;
/** Employee number */
employee_number?: string;
/** Status */
status?: 'active' | 'inactive' | 'pending';
/** Avatar */
avatar?: string;
/** PIN code (hashed) */
pin_code?: string;
/** NFC cards */
nfc_cards?: IAccessNfcCard[];
/** Access groups */
access_groups?: string[];
/** Notes */
notes?: string;
/** Start date */
start_date?: string;
/** End date */
end_date?: string;
/** Onboarding timestamp */
onboarding_timestamp?: number;
}
/**
* NFC card
*/
export interface IAccessNfcCard {
/** Card unique ID */
unique_id?: string;
/** Token (card number) */
token: string;
/** Card type */
card_type?: string;
/** Card alias */
alias?: string;
/** Status */
status?: 'active' | 'inactive' | 'pending';
/** Is lost */
is_lost?: boolean;
}
/**
* Access policy/group
*/
export interface IAccessPolicy {
/** Policy unique ID */
unique_id: string;
/** Policy name */
name: string;
/** Description */
description?: string;
/** Resources (doors) */
resources?: IAccessPolicyResource[];
/** Schedules */
schedules?: IAccessSchedule[];
}
/**
* Policy resource
*/
export interface IAccessPolicyResource {
/** Resource unique ID */
unique_id: string;
/** Resource type */
type?: string;
}
/**
* Schedule
*/
export interface IAccessSchedule {
/** Schedule unique ID */
unique_id: string;
/** Name */
name?: string;
/** Type */
type?: string;
/** Days */
days?: string[];
/** Start time */
start_time?: string;
/** End time */
end_time?: string;
}
/**
* Location (building/area)
*/
export interface IAccessLocation {
/** Location unique ID */
unique_id: string;
/** Location name */
name: string;
/** Address */
address?: string;
/** Timezone */
timezone?: string;
/** Latitude */
latitude?: number;
/** Longitude */
longitude?: number;
}
/**
* Floor
*/
export interface IAccessFloor {
/** Floor unique ID */
unique_id: string;
/** Floor name */
name: string;
/** Floor number */
number?: number;
/** Floor plan image */
floor_plan_id?: string;
}
/**
* Device configuration
*/
export interface IAccessDeviceConfig {
/** Config key */
key: string;
/** Config value */
value: string | number | boolean;
}
/**
* Access event (entry/exit log)
*/
export interface IAccessEvent {
/** Event unique ID */
unique_id: string;
/** Event type */
type: 'access.door.unlock' | 'access.door.lock' | 'access.door.open' | 'access.door.close' | 'access.entry.granted' | 'access.entry.denied';
/** Timestamp */
timestamp: number;
/** Door ID */
door_id?: string;
/** User ID */
user_id?: string;
/** Device ID */
device_id?: string;
/** Reason */
reason?: string;
/** Extra data */
extra?: Record<string, unknown>;
}
/**
* Access API response wrapper
*/
export interface IAccessApiResponse<T> {
/** Code (SUCCESS, etc.) */
code: string;
/** Message */
msg?: string;
/** Data payload */
data: T;
/** Pagination info */
pagination?: {
page?: number;
page_size?: number;
total?: number;
};
}

75
ts/interfaces/common.ts Normal file
View File

@@ -0,0 +1,75 @@
/**
* Common interfaces shared across all UniFi APIs
*/
/**
* Standard UniFi API response wrapper
*/
export interface IUnifiApiResponse<T> {
meta: {
rc: 'ok' | 'error';
msg?: string;
};
data: T[];
}
/**
* Options for UniFi Account (Site Manager cloud API)
*/
export interface IUnifiAccountOptions {
/** API key from ui.com */
apiKey: string;
}
/**
* Options for UniFi Controller (Network Controller local API)
* Supports either API key auth OR username/password session auth
*/
export interface IUnifiControllerOptions {
/** Controller host (IP or hostname) */
host: string;
/** API key for authentication (preferred) */
apiKey?: string;
/** Username for session authentication */
username?: string;
/** Password for session authentication */
password?: string;
/** Controller type - affects API paths */
controllerType?: 'unifi-os' | 'udm-pro' | 'standalone';
/** Whether to verify SSL certificates (default: false for self-signed) */
verifySsl?: boolean;
}
/**
* Options for UniFi Protect (NVR local API)
* Supports either API key auth OR username/password session auth
*/
export interface IUnifiProtectOptions {
/** Protect host (IP or hostname) */
host: string;
/** API key for authentication (preferred) */
apiKey?: string;
/** Username for session authentication */
username?: string;
/** Password for session authentication */
password?: string;
/** Whether to verify SSL certificates (default: false for self-signed) */
verifySsl?: boolean;
}
/**
* Options for UniFi Access (Access Controller local API)
*/
export interface IUnifiAccessOptions {
/** Access host (IP or hostname) */
host: string;
/** Bearer token for authentication */
token: string;
/** Whether to verify SSL certificates (default: false for self-signed) */
verifySsl?: boolean;
}
/**
* HTTP request method types
*/
export type THttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';

9
ts/interfaces/index.ts Normal file
View File

@@ -0,0 +1,9 @@
/**
* Re-export all interfaces
*/
export * from './common.js';
export * from './sitemanager.js';
export * from './network.js';
export * from './protect.js';
export * from './access.js';

272
ts/interfaces/network.ts Normal file
View File

@@ -0,0 +1,272 @@
/**
* Network Controller API interfaces
* Base URL: https://{host}/api or https://{host}/proxy/network/api
*/
/**
* Network site
*/
export interface INetworkSite {
/** Site ID (e.g., 'default') */
_id: string;
/** Site name */
name: string;
/** Site description */
desc?: string;
/** Whether anonymous ID is enabled */
anonymous_id?: string;
/** Role for the site */
role?: string;
/** Attribute for hidden ID */
attr_hidden_id?: string;
/** Attribute for hidden and no delete */
attr_no_delete?: boolean;
}
/**
* Network device (switch, AP, gateway, etc.)
*/
export interface INetworkDevice {
/** Device ID */
_id: string;
/** Device MAC address */
mac: string;
/** Device model */
model: string;
/** Device type (ugw, usw, uap, etc.) */
type: string;
/** Device name */
name?: string;
/** Site ID */
site_id: string;
/** Whether device is adopted */
adopted: boolean;
/** Device IP address */
ip: string;
/** Device state (0=offline, 1=connected, etc.) */
state: number;
/** Serial number */
serial?: string;
/** Firmware version */
version?: string;
/** Uptime in seconds */
uptime?: number;
/** Last seen timestamp */
last_seen?: number;
/** Whether device is upgradable */
upgradable?: boolean;
/** Available upgrade version */
upgrade_to_firmware?: string;
/** Device configuration */
config_network?: {
type?: string;
ip?: string;
};
/** Device ethernet table */
ethernet_table?: Array<{
name: string;
mac: string;
num_port?: number;
}>;
/** Port overrides configuration */
port_overrides?: Array<{
port_idx: number;
name?: string;
poe_mode?: string;
}>;
/** System stats */
sys_stats?: {
loadavg_1?: number;
loadavg_5?: number;
loadavg_15?: number;
mem_total?: number;
mem_used?: number;
};
/** LED override */
led_override?: string;
/** LED override color */
led_override_color?: string;
/** LED override brightness */
led_override_color_brightness?: number;
}
/**
* Network client (connected device)
*/
export interface INetworkClient {
/** Client ID */
_id: string;
/** MAC address */
mac: string;
/** Site ID */
site_id: string;
/** Whether client is authorized (for guest network) */
is_guest?: boolean;
/** Whether client is wired */
is_wired: boolean;
/** First seen timestamp */
first_seen?: number;
/** Last seen timestamp */
last_seen?: number;
/** Hostname */
hostname?: string;
/** Client name (user-assigned) */
name?: string;
/** Client IP address */
ip?: string;
/** Network ID */
network_id?: string;
/** Uplink MAC (AP or switch) */
uplink_mac?: string;
/** Connected AP name */
ap_name?: string;
/** SSID if wireless */
essid?: string;
/** BSSID if wireless */
bssid?: string;
/** Channel if wireless */
channel?: number;
/** Radio protocol (ng, na, ac, ax) */
radio_proto?: string;
/** Signal strength */
signal?: number;
/** TX rate */
tx_rate?: number;
/** RX rate */
rx_rate?: number;
/** TX bytes */
tx_bytes?: number;
/** RX bytes */
rx_bytes?: number;
/** TX packets */
tx_packets?: number;
/** RX packets */
rx_packets?: number;
/** Connected switch port */
sw_port?: number;
/** User group ID */
usergroup_id?: string;
/** OUI (device manufacturer) */
oui?: string;
/** Noted status */
noted?: boolean;
/** User ID if fixed IP */
user_id?: string;
/** Fingerprint data */
fingerprint_source?: number;
/** Device fingerprint */
dev_cat?: number;
dev_family?: number;
dev_vendor?: number;
dev_id?: number;
/** OS name */
os_name?: number;
/** Satisfaction score */
satisfaction?: number;
/** Anomalies count */
anomalies?: number;
}
/**
* Network WLAN configuration
*/
export interface INetworkWlan {
/** WLAN ID */
_id: string;
/** WLAN name */
name: string;
/** Site ID */
site_id: string;
/** SSID */
x_passphrase?: string;
/** Whether WLAN is enabled */
enabled: boolean;
/** Security mode */
security?: string;
/** WPA mode */
wpa_mode?: string;
/** WPA encryption */
wpa_enc?: string;
/** VLAN ID */
networkconf_id?: string;
/** User group ID */
usergroup_id?: string;
/** Whether hidden */
hide_ssid?: boolean;
/** Whether PMF is enabled */
pmf_mode?: string;
/** Group rekey interval */
group_rekey?: number;
}
/**
* Network configuration (VLAN/subnet)
*/
export interface INetworkConfig {
/** Config ID */
_id: string;
/** Name */
name: string;
/** Site ID */
site_id: string;
/** Purpose (corporate, guest, wan, etc.) */
purpose: string;
/** VLAN ID */
vlan?: number;
/** VLAN enabled */
vlan_enabled?: boolean;
/** Subnet */
ip_subnet?: string;
/** DHCP enabled */
dhcpd_enabled?: boolean;
/** DHCP start */
dhcpd_start?: string;
/** DHCP stop */
dhcpd_stop?: string;
/** Domain name */
domain_name?: string;
/** Whether this is the default network */
is_nat?: boolean;
/** Network group */
networkgroup?: string;
/** IGMP snooping */
igmp_snooping?: boolean;
}
/**
* Device port configuration
*/
export interface IPortConfig {
port_idx: number;
name?: string;
poe_mode?: string;
port_poe?: boolean;
portconf_id?: string;
speed_caps?: number;
op_mode?: string;
autoneg?: boolean;
}
/**
* Auth response from controller
*/
export interface INetworkAuthResponse {
/** Response code */
rc: string;
/** Session token/unique ID */
unique_id?: string;
/** First name */
first_name?: string;
/** Last name */
last_name?: string;
/** Full name */
full_name?: string;
/** Email */
email?: string;
/** Is super admin */
is_super?: boolean;
/** Device ID */
device_id?: string;
/** UI settings */
ui_settings?: Record<string, unknown>;
}

564
ts/interfaces/protect.ts Normal file
View File

@@ -0,0 +1,564 @@
/**
* Protect API interfaces
* Base URL: https://{host}/proxy/protect/api
*/
/**
* Protect bootstrap response containing system configuration
*/
export interface IProtectBootstrap {
/** Auth user info */
authUser?: IProtectUser;
/** Access key */
accessKey?: string;
/** Cameras list */
cameras: IProtectCamera[];
/** Users list */
users?: IProtectUser[];
/** Groups list */
groups?: IProtectGroup[];
/** Liveviews */
liveviews?: IProtectLiveview[];
/** Viewers */
viewers?: IProtectViewer[];
/** Lights */
lights?: IProtectLight[];
/** Bridges */
bridges?: IProtectBridge[];
/** Sensors */
sensors?: IProtectSensor[];
/** Doorbells */
doorbells?: IProtectDoorbell[];
/** Chimes */
chimes?: IProtectChime[];
/** NVR info */
nvr: IProtectNvr;
/** Last update ID */
lastUpdateId?: string;
}
/**
* Protect camera
*/
export interface IProtectCamera {
/** Camera ID */
id: string;
/** MAC address */
mac: string;
/** Host address */
host: string;
/** Camera name */
name: string;
/** Camera type/model */
type: string;
/** Model key */
modelKey?: string;
/** Camera state */
state: 'CONNECTED' | 'DISCONNECTED' | 'CONNECTING' | 'ADOPTING' | 'MANAGED';
/** Hardware revision */
hardwareRevision?: string;
/** Firmware version */
firmwareVersion?: string;
/** Firmware build */
firmwareBuild?: string;
/** Whether camera is updating */
isUpdating?: boolean;
/** Whether camera is adopting */
isAdopting?: boolean;
/** Whether camera is managed */
isManaged?: boolean;
/** Whether camera is connected */
isConnected?: boolean;
/** Whether recording is enabled */
isRecording?: boolean;
/** Whether motion detection is enabled */
isMotionDetected?: boolean;
/** Whether camera is dark (IR mode) */
isDark?: boolean;
/** Recording settings */
recordingSettings?: IProtectRecordingSettings;
/** Smart detect settings */
smartDetectSettings?: IProtectSmartDetectSettings;
/** ISP settings (image settings) */
ispSettings?: IProtectIspSettings;
/** Microphone settings */
micVolume?: number;
/** Speaker settings */
speakerVolume?: number;
/** Last motion timestamp */
lastMotion?: number;
/** Last ring timestamp (for doorbells) */
lastRing?: number;
/** Uptime */
uptime?: number;
/** Connected since */
connectedSince?: number;
/** Up since */
upSince?: number;
/** Last seen */
lastSeen?: number;
/** Channels info */
channels?: IProtectCameraChannel[];
/** Feature flags */
featureFlags?: IProtectFeatureFlags;
/** Stats */
stats?: IProtectCameraStats;
}
/**
* Camera channel configuration
*/
export interface IProtectCameraChannel {
/** Channel ID */
id: number;
/** Video mode */
videoMode?: string;
/** Enabled */
enabled: boolean;
/** FPS mode */
fpsValues?: number[];
/** Is RTSP enabled */
isRtspEnabled?: boolean;
/** RTSP alias */
rtspAlias?: string;
/** Width */
width?: number;
/** Height */
height?: number;
/** FPS */
fps?: number;
/** Bitrate */
bitrate?: number;
/** Min bitrate */
minBitrate?: number;
/** Max bitrate */
maxBitrate?: number;
}
/**
* Recording settings
*/
export interface IProtectRecordingSettings {
/** Pre-padding seconds */
prePaddingSecs?: number;
/** Post-padding seconds */
postPaddingSecs?: number;
/** Min motion event trigger */
minMotionEventTrigger?: number;
/** End motion event delay */
endMotionEventDelay?: number;
/** Suppress illumination surge */
suppressIlluminationSurge?: boolean;
/** Mode */
mode?: 'always' | 'detections' | 'never' | 'schedule';
/** Enable PIR timelapse */
enablePirTimelapse?: boolean;
/** Use new motion algorithm */
useNewMotionAlgorithm?: boolean;
/** In schedule mode */
inScheduleMode?: string;
/** Out schedule mode */
outScheduleMode?: string;
/** Geofencing */
geofencing?: string;
}
/**
* Smart detect settings
*/
export interface IProtectSmartDetectSettings {
/** Object types to detect */
objectTypes?: string[];
/** Audio types to detect */
audioTypes?: string[];
/** Auto tracking object types */
autoTrackingObjectTypes?: string[];
}
/**
* ISP (Image Signal Processor) settings
*/
export interface IProtectIspSettings {
/** AE mode */
aeMode?: string;
/** IR LED mode */
irLedMode?: string;
/** IR LED level */
irLedLevel?: number;
/** WDR */
wdr?: number;
/** ICR sensitivity */
icrSensitivity?: number;
/** Brightness */
brightness?: number;
/** Contrast */
contrast?: number;
/** Hue */
hue?: number;
/** Saturation */
saturation?: number;
/** Sharpness */
sharpness?: number;
/** Denoise */
denoise?: number;
/** Is flip enabled */
isFlippedVertical?: boolean;
/** Is mirror enabled */
isFlippedHorizontal?: boolean;
/** Is auto rotate enabled */
isAutoRotateEnabled?: boolean;
/** HDR mode */
hdrMode?: string;
/** Is color night vision enabled */
isColorNightVisionEnabled?: boolean;
/** Spotlight duration */
spotlightDuration?: number;
/** Focus mode */
focusMode?: string;
/** Focus position */
focusPosition?: number;
/** Zoom position */
zoomPosition?: number;
/** Touch focus X */
touchFocusX?: number;
/** Touch focus Y */
touchFocusY?: number;
}
/**
* Camera feature flags
*/
export interface IProtectFeatureFlags {
/** Can adjust IR LED level */
canAdjustIrLedLevel?: boolean;
/** Has chime */
hasChime?: boolean;
/** Has flash */
hasFlash?: boolean;
/** Has HDR */
hasHdr?: boolean;
/** Has IR LED */
hasIrLed?: boolean;
/** Has LCD screen */
hasLcdScreen?: boolean;
/** Has LED status */
hasLedStatus?: boolean;
/** Has line in */
hasLineIn?: boolean;
/** Has mic */
hasMic?: boolean;
/** Has privacy mask */
hasPrivacyMask?: boolean;
/** Has RTSP */
hasRtsp?: boolean;
/** Has SD card */
hasSdCard?: boolean;
/** Has smart detect */
hasSmartDetect?: boolean;
/** Has speaker */
hasSpeaker?: boolean;
/** Has WiFi */
hasWifi?: boolean;
/** Video modes */
videoModes?: string[];
/** Privacy mask capability */
privacyMaskCapability?: {
maxMasks?: number;
rectangleOnly?: boolean;
};
/** Smart detect types */
smartDetectTypes?: string[];
/** Smart detect audio types */
smartDetectAudioTypes?: string[];
/** Motion algorithms */
motionAlgorithms?: string[];
}
/**
* Camera stats
*/
export interface IProtectCameraStats {
/** RX bytes */
rxBytes?: number;
/** TX bytes */
txBytes?: number;
/** WiFi quality */
wifiQuality?: number;
/** WiFi strength */
wifiStrength?: number;
}
/**
* Protect user
*/
export interface IProtectUser {
/** User ID */
id: string;
/** Is owner */
isOwner?: boolean;
/** Name */
name?: string;
/** Email */
email?: string;
/** Local username */
localUsername?: string;
/** Has accepted invite */
hasAcceptedInvite?: boolean;
/** All permissions */
allPermissions?: string[];
/** Model key */
modelKey?: string;
}
/**
* Protect group
*/
export interface IProtectGroup {
/** Group ID */
id: string;
/** Group name */
name: string;
/** Group type */
type?: string;
/** Model key */
modelKey?: string;
}
/**
* Protect liveview
*/
export interface IProtectLiveview {
/** Liveview ID */
id: string;
/** Name */
name: string;
/** Is default */
isDefault?: boolean;
/** Layout */
layout?: number;
/** Model key */
modelKey?: string;
/** Slots */
slots?: IProtectLiveviewSlot[];
}
/**
* Protect liveview slot
*/
export interface IProtectLiveviewSlot {
/** Camera IDs */
cameras?: string[];
/** Cycle mode */
cycleMode?: string;
/** Cycle interval */
cycleInterval?: number;
}
/**
* Protect viewer
*/
export interface IProtectViewer {
/** Viewer ID */
id: string;
/** Name */
name?: string;
/** Model key */
modelKey?: string;
}
/**
* Protect light
*/
export interface IProtectLight {
/** Light ID */
id: string;
/** MAC */
mac: string;
/** Name */
name: string;
/** Type */
type: string;
/** State */
state: string;
/** Is light on */
isLightOn?: boolean;
/** Light device settings */
lightDeviceSettings?: {
ledLevel?: number;
luxSensitivity?: string;
pirDuration?: number;
pirSensitivity?: number;
};
}
/**
* Protect bridge
*/
export interface IProtectBridge {
/** Bridge ID */
id: string;
/** MAC */
mac: string;
/** Name */
name: string;
/** Type */
type: string;
/** State */
state: string;
}
/**
* Protect sensor
*/
export interface IProtectSensor {
/** Sensor ID */
id: string;
/** MAC */
mac: string;
/** Name */
name: string;
/** Type */
type: string;
/** State */
state: string;
/** Battery status */
batteryStatus?: {
percentage?: number;
isLow?: boolean;
};
/** Mount type */
mountType?: string;
/** Is motion detected */
isMotionDetected?: boolean;
/** Is opened */
isOpened?: boolean;
/** Humidity */
humidity?: number;
/** Temperature */
temperature?: number;
/** Light */
light?: number;
/** Alarm triggered type */
alarmTriggeredType?: string;
}
/**
* Protect doorbell (extends camera)
*/
export interface IProtectDoorbell extends IProtectCamera {
/** LCD message */
lcdMessage?: {
text?: string;
resetAt?: number;
type?: string;
};
/** Chime duration */
chimeDuration?: number;
}
/**
* Protect chime
*/
export interface IProtectChime {
/** Chime ID */
id: string;
/** MAC */
mac: string;
/** Name */
name: string;
/** Type */
type: string;
/** State */
state: string;
/** Is paired with doorbell */
isPaired?: boolean;
/** Volume */
volume?: number;
}
/**
* Protect NVR info
*/
export interface IProtectNvr {
/** NVR ID */
id: string;
/** MAC */
mac: string;
/** Host */
host: string;
/** Name */
name: string;
/** Type */
type: string;
/** Is connected to cloud */
isConnectedToCloud?: boolean;
/** Firmware version */
firmwareVersion?: string;
/** Hardware */
hardware?: {
shortname?: string;
name?: string;
};
/** Uptime */
uptime?: number;
/** Last seen */
lastSeen?: number;
/** Is recording disabled */
isRecordingDisabled?: boolean;
/** Is recording motion only */
isRecordingMotionOnly?: boolean;
/** Storage info */
storageInfo?: {
totalSize?: number;
totalSpaceUsed?: number;
devices?: Array<{
model?: string;
size?: number;
healthy?: boolean;
}>;
};
/** Timezone */
timezone?: string;
/** Version */
version?: string;
}
/**
* Auth response from Protect
*/
export interface IProtectAuthResponse {
/** CSRF token in response */
csrfToken?: string;
/** User info */
user?: IProtectUser;
}
/**
* Motion event from Protect
*/
export interface IProtectMotionEvent {
/** Event ID */
id: string;
/** Event type */
type: 'motion' | 'ring' | 'smartDetectZone' | 'smartAudioDetect';
/** Start timestamp */
start: number;
/** End timestamp */
end?: number;
/** Score (confidence) */
score?: number;
/** Smart detect types */
smartDetectTypes?: string[];
/** Smart detect events */
smartDetectEvents?: string[];
/** Camera ID */
camera?: string;
/** Partition (for storage) */
partition?: string;
/** Model key */
modelKey?: string;
/** Thumbnail ID */
thumbnail?: string;
/** Has heatmap */
heatmap?: string;
}

View File

@@ -0,0 +1,78 @@
/**
* Site Manager (Cloud) API interfaces
* Base URL: https://api.ui.com/v1
*/
/**
* Site from Site Manager API
*/
export interface IUnifiSite {
/** Unique site ID */
siteId: string;
/** Site name */
name: string;
/** Site description */
description?: string;
/** Whether this is the default site */
isDefault?: boolean;
/** Timezone for the site */
timezone?: string;
/** Site meta info */
meta?: {
/** Site type */
type?: string;
/** Site address */
address?: string;
};
/** Creation timestamp */
createdAt?: string;
/** Last modified timestamp */
updatedAt?: string;
}
/**
* Host device from Site Manager API
*/
export interface IUnifiHost {
/** Unique host ID */
id: string;
/** Hardware UUID */
hardwareId?: string;
/** Host name */
name?: string;
/** Host type (e.g., 'udm-pro', 'cloud-key-gen2-plus') */
type?: string;
/** Firmware version */
firmwareVersion?: string;
/** Whether the host is online */
isOnline?: boolean;
/** IP address */
ipAddress?: string;
/** MAC address */
macAddress?: string;
/** Associated site ID */
siteId?: string;
/** Host status info */
status?: {
/** Connection state */
state?: string;
/** Last seen timestamp */
lastSeen?: string;
};
/** Features/applications running */
features?: string[];
/** Reported state from host */
reportedState?: Record<string, unknown>;
}
/**
* Site Manager API list response
*/
export interface ISiteManagerListResponse<T> {
data: T[];
meta?: {
total?: number;
page?: number;
pageSize?: number;
};
}

View File

@@ -4,6 +4,14 @@ import * as path from 'path';
export { path };
// @push.rocks scope
import * as smartlog from '@push.rocks/smartlog';
import * as smartpath from '@push.rocks/smartpath';
import * as smartpromise from '@push.rocks/smartpromise';
import * as smartrequest from '@push.rocks/smartrequest';
import * as smartstring from '@push.rocks/smartstring';
export { smartpath };
export { smartlog, smartpath, smartpromise, smartrequest, smartstring };
// Re-export smartrequest types
export type { IExtendedIncomingMessage } from '@push.rocks/smartrequest';
export { SmartRequestClient } from '@push.rocks/smartrequest';

12
ts/unifi.logger.ts Normal file
View File

@@ -0,0 +1,12 @@
import * as plugins from './plugins.js';
export const logger = new plugins.smartlog.Smartlog({
logContext: {
company: 'apiclient.xyz',
companyunit: 'unifi',
containerName: 'unifi-client',
environment: 'production',
runtime: 'node',
zone: 'api',
},
});