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:
8
ts/00_commitinfo_data.ts
Normal file
8
ts/00_commitinfo_data.ts
Normal 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
275
ts/classes.camera.ts
Normal 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
227
ts/classes.cameramanager.ts
Normal 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
276
ts/classes.client.ts
Normal 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
262
ts/classes.clientmanager.ts
Normal 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
255
ts/classes.device.ts
Normal 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
188
ts/classes.devicemanager.ts
Normal 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
155
ts/classes.door.ts
Normal 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
168
ts/classes.doormanager.ts
Normal 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
85
ts/classes.host.ts
Normal 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
78
ts/classes.hostmanager.ts
Normal 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
57
ts/classes.site.ts
Normal 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
62
ts/classes.sitemanager.ts
Normal 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
339
ts/classes.unifi-access.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
82
ts/classes.unifi-account.ts
Normal file
82
ts/classes.unifi-account.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
377
ts/classes.unifi-controller.ts
Normal file
377
ts/classes.unifi-controller.ts
Normal 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
343
ts/classes.unifi-protect.ts
Normal 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
265
ts/classes.unifihttp.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
38
ts/index.ts
38
ts/index.ts
@@ -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
284
ts/interfaces/access.ts
Normal 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
75
ts/interfaces/common.ts
Normal 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
9
ts/interfaces/index.ts
Normal 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
272
ts/interfaces/network.ts
Normal 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
564
ts/interfaces/protect.ts
Normal 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;
|
||||
}
|
||||
78
ts/interfaces/sitemanager.ts
Normal file
78
ts/interfaces/sitemanager.ts
Normal 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;
|
||||
};
|
||||
}
|
||||
@@ -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
12
ts/unifi.logger.ts
Normal 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',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user