feat(smarthome): add smart home features and Home Assistant integration (WebSocket protocol, discovery, factories, interfaces)
This commit is contained in:
214
ts/features/feature.camera.ts
Normal file
214
ts/features/feature.camera.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* 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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user