feat(smarthome): add smart home features and Home Assistant integration (WebSocket protocol, discovery, factories, interfaces)

This commit is contained in:
2026-01-09 16:20:54 +00:00
parent 7bcec69658
commit 38a6e5c250
23 changed files with 4786 additions and 5 deletions

View 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(),
};
}
}