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 { 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 { 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 { 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 { const cameras = await this.listCameras(); return cameras.find( (camera) => camera.name.toLowerCase() === name.toLowerCase() ) || null; } /** * Get online cameras only */ public async getOnlineCameras(): Promise { const cameras = await this.listCameras(); return cameras.filter((camera) => camera.isOnline()); } /** * Get offline cameras only */ public async getOfflineCameras(): Promise { const cameras = await this.listCameras(); return cameras.filter((camera) => !camera.isOnline()); } /** * Get doorbells only */ public async getDoorbells(): Promise { const cameras = await this.listCameras(); return cameras.filter((camera) => camera.isDoorbell()); } /** * Get cameras with smart detection */ public async getSmartDetectCameras(): Promise { const cameras = await this.listCameras(); return cameras.filter((camera) => camera.hasSmartDetect()); } /** * Get cameras currently recording */ public async getRecordingCameras(): Promise { const cameras = await this.listCameras(); return cameras.filter((camera) => camera.isRecording === true); } /** * Get cameras with recent motion */ public async getCamerasWithRecentMotion(seconds: number = 60): Promise { const cameras = await this.listCameras(); return cameras.filter((camera) => camera.hasRecentMotion(seconds)); } /** * Update camera settings */ public async updateCamera( cameraId: string, settings: Partial ): Promise { 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 { 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( '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 { 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( '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 { 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( 'GET', `/cameras/${cameraId}/snapshot${queryString}` ); return response; } /** * Set recording mode for all cameras */ public async setAllRecordingMode( mode: 'always' | 'detections' | 'never' | 'schedule' ): Promise { 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 { 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 { logger.log('info', `Removing camera: ${cameraId}`); await this.protect.request('DELETE', `/cameras/${cameraId}`); } }