215 lines
5.7 KiB
TypeScript
215 lines
5.7 KiB
TypeScript
/**
|
|
* 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<ICameraCapabilities>;
|
|
}
|
|
|
|
/**
|
|
* Camera Feature - snapshot and stream access
|
|
*
|
|
* Protocol-agnostic: works with Home Assistant, ONVIF, RTSP, etc.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* const camera = device.getFeature<CameraFeature>('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<void> {
|
|
try {
|
|
const state = await this.protocolClient.getState(this.entityId);
|
|
this.updateStateInternal(state);
|
|
} catch {
|
|
// Ignore errors fetching initial state
|
|
}
|
|
}
|
|
|
|
protected async doDisconnect(): Promise<void> {
|
|
// Nothing to disconnect
|
|
}
|
|
|
|
// ============================================================================
|
|
// Camera Access
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Get a snapshot image from the camera
|
|
* @returns Buffer containing image data
|
|
*/
|
|
public async getSnapshot(): Promise<Buffer> {
|
|
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<string> {
|
|
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<string> {
|
|
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<ICameraState> {
|
|
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(),
|
|
};
|
|
}
|
|
}
|