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): Promise { 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 { await this.updateSettings({ recordingSettings: { ...this.recordingSettings, mode, }, }); } /** * Enable/disable smart detection types */ public async setSmartDetectTypes(types: string[]): Promise { await this.updateSettings({ smartDetectSettings: { ...this.smartDetectSettings, objectTypes: types, }, }); } /** * Set microphone volume */ public async setMicVolume(volume: number): Promise { const clampedVolume = Math.max(0, Math.min(100, volume)); await this.updateSettings({ micVolume: clampedVolume }); } /** * Set speaker volume */ public async setSpeakerVolume(volume: number): Promise { const clampedVolume = Math.max(0, Math.min(100, volume)); await this.updateSettings({ speakerVolume: clampedVolume }); } /** * Rename the camera */ public async rename(newName: string): Promise { await this.updateSettings({ name: newName }); } /** * Restart the camera */ public async restart(): Promise { 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); } }