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

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

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

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