/** * Camera Feature * Provides control for smart cameras (snapshots, streams) */ import { Feature, type TDeviceReference } from './feature.abstract.js'; import type { IFeatureOptions } from '../interfaces/feature.interfaces.js'; import type { TCameraProtocol, ICameraCapabilities, ICameraState, ICameraFeatureInfo, ICameraProtocolClient, } from '../interfaces/smarthome.interfaces.js'; /** * Options for creating a CameraFeature */ export interface ICameraFeatureOptions extends IFeatureOptions { /** Protocol type */ protocol: TCameraProtocol; /** Entity ID (for Home Assistant) */ entityId: string; /** Protocol client for the camera */ protocolClient: ICameraProtocolClient; /** Camera capabilities */ capabilities?: Partial; } /** * Camera Feature - snapshot and stream access * * Protocol-agnostic: works with Home Assistant, ONVIF, RTSP, etc. * * @example * ```typescript * const camera = device.getFeature('camera'); * if (camera) { * const snapshot = await camera.getSnapshot(); * const streamUrl = await camera.getStreamUrl(); * } * ``` */ export class CameraFeature extends Feature { public readonly type = 'camera' as const; public readonly protocol: TCameraProtocol; /** Entity ID (e.g., "camera.front_door") */ public readonly entityId: string; /** Capabilities */ public readonly capabilities: ICameraCapabilities; /** Current state */ protected _isRecording: boolean = false; protected _isStreaming: boolean = false; protected _motionDetected: boolean = false; /** Protocol client for the camera */ private protocolClient: ICameraProtocolClient; constructor( device: TDeviceReference, port: number, options: ICameraFeatureOptions ) { super(device, port, options); this.protocol = options.protocol; this.entityId = options.entityId; this.protocolClient = options.protocolClient; this.capabilities = { supportsStream: options.capabilities?.supportsStream ?? true, supportsPtz: options.capabilities?.supportsPtz ?? false, supportsSnapshot: options.capabilities?.supportsSnapshot ?? true, supportsMotionDetection: options.capabilities?.supportsMotionDetection ?? false, frontendStreamType: options.capabilities?.frontendStreamType, streamUrl: options.capabilities?.streamUrl, }; } // ============================================================================ // Properties // ============================================================================ /** * Check if recording (cached) */ public get isRecording(): boolean { return this._isRecording; } /** * Check if streaming (cached) */ public get isStreaming(): boolean { return this._isStreaming; } /** * Check if motion detected (cached) */ public get motionDetected(): boolean { return this._motionDetected; } // ============================================================================ // Connection // ============================================================================ protected async doConnect(): Promise { try { const state = await this.protocolClient.getState(this.entityId); this.updateStateInternal(state); } catch { // Ignore errors fetching initial state } } protected async doDisconnect(): Promise { // Nothing to disconnect } // ============================================================================ // Camera Access // ============================================================================ /** * Get a snapshot image from the camera * @returns Buffer containing image data */ public async getSnapshot(): Promise { if (!this.capabilities.supportsSnapshot) { throw new Error('Camera does not support snapshots'); } return this.protocolClient.getSnapshot(this.entityId); } /** * Get snapshot URL * @returns URL for the snapshot image */ public async getSnapshotUrl(): Promise { if (!this.capabilities.supportsSnapshot) { throw new Error('Camera does not support snapshots'); } return this.protocolClient.getSnapshotUrl(this.entityId); } /** * Get stream URL * @returns URL for the video stream */ public async getStreamUrl(): Promise { if (!this.capabilities.supportsStream) { throw new Error('Camera does not support streaming'); } return this.protocolClient.getStreamUrl(this.entityId); } /** * Get current state as object */ public getState(): ICameraState { return { isRecording: this._isRecording, isStreaming: this._isStreaming, motionDetected: this._motionDetected, }; } /** * Refresh state from the device */ public async refreshState(): Promise { const state = await this.protocolClient.getState(this.entityId); this.updateStateInternal(state); return state; } /** * Update state from external source */ public updateState(state: ICameraState): void { this.updateStateInternal(state); this.emit('state:changed', state); } /** * Internal state update */ private updateStateInternal(state: ICameraState): void { this._isRecording = state.isRecording; this._isStreaming = state.isStreaming; this._motionDetected = state.motionDetected; } // ============================================================================ // Feature Info // ============================================================================ public getFeatureInfo(): ICameraFeatureInfo { return { ...this.getBaseFeatureInfo(), type: 'camera', protocol: this.protocol, capabilities: this.capabilities, currentState: this.getState(), }; } }