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

View File

@@ -0,0 +1,407 @@
/**
* Climate Feature
* Provides control for thermostats and HVAC systems
*/
import { Feature, type TDeviceReference } from './feature.abstract.js';
import type { IFeatureOptions } from '../interfaces/feature.interfaces.js';
import type {
TClimateProtocol,
THvacMode,
THvacAction,
IClimateCapabilities,
IClimateState,
IClimateFeatureInfo,
IClimateProtocolClient,
} from '../interfaces/smarthome.interfaces.js';
/**
* Options for creating a ClimateFeature
*/
export interface IClimateFeatureOptions extends IFeatureOptions {
/** Protocol type */
protocol: TClimateProtocol;
/** Entity ID (for Home Assistant) */
entityId: string;
/** Protocol client for controlling the climate device */
protocolClient: IClimateProtocolClient;
/** Climate capabilities */
capabilities?: Partial<IClimateCapabilities>;
}
/**
* Climate Feature - thermostat and HVAC control
*
* Protocol-agnostic: works with Home Assistant, Nest, Ecobee, MQTT, etc.
*
* @example
* ```typescript
* const climate = device.getFeature<ClimateFeature>('climate');
* if (climate) {
* await climate.setHvacMode('heat');
* await climate.setTargetTemp(21);
* console.log(`Current: ${climate.currentTemp}°C, Target: ${climate.targetTemp}°C`);
* }
* ```
*/
export class ClimateFeature extends Feature {
public readonly type = 'climate' as const;
public readonly protocol: TClimateProtocol;
/** Entity ID (e.g., "climate.living_room") */
public readonly entityId: string;
/** Capabilities */
public readonly capabilities: IClimateCapabilities;
/** Current state */
protected _currentTemp?: number;
protected _targetTemp?: number;
protected _targetTempHigh?: number;
protected _targetTempLow?: number;
protected _hvacMode: THvacMode = 'off';
protected _hvacAction?: THvacAction;
protected _presetMode?: string;
protected _fanMode?: string;
protected _swingMode?: string;
protected _humidity?: number;
protected _targetHumidity?: number;
protected _auxHeat?: boolean;
/** Protocol client for controlling the climate device */
private protocolClient: IClimateProtocolClient;
constructor(
device: TDeviceReference,
port: number,
options: IClimateFeatureOptions
) {
super(device, port, options);
this.protocol = options.protocol;
this.entityId = options.entityId;
this.protocolClient = options.protocolClient;
this.capabilities = {
hvacModes: options.capabilities?.hvacModes ?? ['off', 'heat', 'cool', 'auto'],
presetModes: options.capabilities?.presetModes,
fanModes: options.capabilities?.fanModes,
swingModes: options.capabilities?.swingModes,
supportsTargetTemp: options.capabilities?.supportsTargetTemp ?? true,
supportsTargetTempRange: options.capabilities?.supportsTargetTempRange ?? false,
supportsHumidity: options.capabilities?.supportsHumidity ?? false,
supportsAuxHeat: options.capabilities?.supportsAuxHeat ?? false,
minTemp: options.capabilities?.minTemp ?? 7,
maxTemp: options.capabilities?.maxTemp ?? 35,
tempStep: options.capabilities?.tempStep ?? 0.5,
minHumidity: options.capabilities?.minHumidity,
maxHumidity: options.capabilities?.maxHumidity,
};
}
// ============================================================================
// Properties
// ============================================================================
/**
* Get current temperature (cached)
*/
public get currentTemp(): number | undefined {
return this._currentTemp;
}
/**
* Get target temperature (cached)
*/
public get targetTemp(): number | undefined {
return this._targetTemp;
}
/**
* Get target temperature high (for heat_cool mode, cached)
*/
public get targetTempHigh(): number | undefined {
return this._targetTempHigh;
}
/**
* Get target temperature low (for heat_cool mode, cached)
*/
public get targetTempLow(): number | undefined {
return this._targetTempLow;
}
/**
* Get current HVAC mode (cached)
*/
public get hvacMode(): THvacMode {
return this._hvacMode;
}
/**
* Get current HVAC action (cached)
*/
public get hvacAction(): THvacAction | undefined {
return this._hvacAction;
}
/**
* Get current preset mode (cached)
*/
public get presetMode(): string | undefined {
return this._presetMode;
}
/**
* Get current fan mode (cached)
*/
public get fanMode(): string | undefined {
return this._fanMode;
}
/**
* Get current swing mode (cached)
*/
public get swingMode(): string | undefined {
return this._swingMode;
}
/**
* Get current humidity (cached)
*/
public get humidity(): number | undefined {
return this._humidity;
}
/**
* Get target humidity (cached)
*/
public get targetHumidity(): number | undefined {
return this._targetHumidity;
}
/**
* Get aux heat state (cached)
*/
public get auxHeat(): boolean | undefined {
return this._auxHeat;
}
/**
* Get available HVAC modes
*/
public get hvacModes(): THvacMode[] {
return this.capabilities.hvacModes;
}
/**
* Get available preset modes
*/
public get presetModes(): string[] | undefined {
return this.capabilities.presetModes;
}
/**
* Get available fan modes
*/
public get fanModes(): string[] | undefined {
return this.capabilities.fanModes;
}
// ============================================================================
// 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
}
// ============================================================================
// Climate Control
// ============================================================================
/**
* Set HVAC mode
* @param mode HVAC mode (off, heat, cool, etc.)
*/
public async setHvacMode(mode: THvacMode): Promise<void> {
if (!this.capabilities.hvacModes.includes(mode)) {
throw new Error(`HVAC mode ${mode} not supported`);
}
await this.protocolClient.setHvacMode(this.entityId, mode);
this._hvacMode = mode;
this.emit('state:changed', this.getState());
}
/**
* Set target temperature
* @param temp Target temperature
*/
public async setTargetTemp(temp: number): Promise<void> {
if (!this.capabilities.supportsTargetTemp) {
throw new Error('Climate device does not support target temperature');
}
const clamped = Math.max(
this.capabilities.minTemp,
Math.min(this.capabilities.maxTemp, temp)
);
await this.protocolClient.setTargetTemp(this.entityId, clamped);
this._targetTemp = clamped;
this.emit('state:changed', this.getState());
}
/**
* Set target temperature range (for heat_cool mode)
* @param low Low temperature
* @param high High temperature
*/
public async setTargetTempRange(low: number, high: number): Promise<void> {
if (!this.capabilities.supportsTargetTempRange) {
throw new Error('Climate device does not support temperature range');
}
const clampedLow = Math.max(this.capabilities.minTemp, Math.min(this.capabilities.maxTemp, low));
const clampedHigh = Math.max(this.capabilities.minTemp, Math.min(this.capabilities.maxTemp, high));
await this.protocolClient.setTargetTempRange(this.entityId, clampedLow, clampedHigh);
this._targetTempLow = clampedLow;
this._targetTempHigh = clampedHigh;
this.emit('state:changed', this.getState());
}
/**
* Set preset mode
* @param preset Preset mode name
*/
public async setPresetMode(preset: string): Promise<void> {
if (!this.capabilities.presetModes?.includes(preset)) {
throw new Error(`Preset mode ${preset} not supported`);
}
await this.protocolClient.setPresetMode(this.entityId, preset);
this._presetMode = preset;
this.emit('state:changed', this.getState());
}
/**
* Set fan mode
* @param mode Fan mode name
*/
public async setFanMode(mode: string): Promise<void> {
if (!this.capabilities.fanModes?.includes(mode)) {
throw new Error(`Fan mode ${mode} not supported`);
}
await this.protocolClient.setFanMode(this.entityId, mode);
this._fanMode = mode;
this.emit('state:changed', this.getState());
}
/**
* Set swing mode
* @param mode Swing mode name
*/
public async setSwingMode(mode: string): Promise<void> {
if (!this.capabilities.swingModes?.includes(mode)) {
throw new Error(`Swing mode ${mode} not supported`);
}
await this.protocolClient.setSwingMode(this.entityId, mode);
this._swingMode = mode;
this.emit('state:changed', this.getState());
}
/**
* Set aux heat
* @param enabled Whether aux heat is enabled
*/
public async setAuxHeat(enabled: boolean): Promise<void> {
if (!this.capabilities.supportsAuxHeat) {
throw new Error('Climate device does not support aux heat');
}
await this.protocolClient.setAuxHeat(this.entityId, enabled);
this._auxHeat = enabled;
this.emit('state:changed', this.getState());
}
/**
* Get current state as object
*/
public getState(): IClimateState {
return {
currentTemp: this._currentTemp,
targetTemp: this._targetTemp,
targetTempHigh: this._targetTempHigh,
targetTempLow: this._targetTempLow,
hvacMode: this._hvacMode,
hvacAction: this._hvacAction,
presetMode: this._presetMode,
fanMode: this._fanMode,
swingMode: this._swingMode,
humidity: this._humidity,
targetHumidity: this._targetHumidity,
auxHeat: this._auxHeat,
};
}
/**
* Refresh state from the device
*/
public async refreshState(): Promise<IClimateState> {
const state = await this.protocolClient.getState(this.entityId);
this.updateStateInternal(state);
return state;
}
/**
* Update state from external source
*/
public updateState(state: IClimateState): void {
this.updateStateInternal(state);
this.emit('state:changed', state);
}
/**
* Internal state update
*/
private updateStateInternal(state: IClimateState): void {
this._currentTemp = state.currentTemp;
this._targetTemp = state.targetTemp;
this._targetTempHigh = state.targetTempHigh;
this._targetTempLow = state.targetTempLow;
this._hvacMode = state.hvacMode;
this._hvacAction = state.hvacAction;
this._presetMode = state.presetMode;
this._fanMode = state.fanMode;
this._swingMode = state.swingMode;
this._humidity = state.humidity;
this._targetHumidity = state.targetHumidity;
this._auxHeat = state.auxHeat;
}
// ============================================================================
// Feature Info
// ============================================================================
public getFeatureInfo(): IClimateFeatureInfo {
return {
...this.getBaseFeatureInfo(),
type: 'climate',
protocol: this.protocol,
capabilities: this.capabilities,
currentState: this.getState(),
};
}
}

View File

@@ -0,0 +1,278 @@
/**
* Cover Feature
* Provides control for covers, blinds, garage doors, etc.
*/
import { Feature, type TDeviceReference } from './feature.abstract.js';
import type { IFeatureOptions } from '../interfaces/feature.interfaces.js';
import type {
TCoverProtocol,
TCoverDeviceClass,
TCoverState,
ICoverCapabilities,
ICoverStateInfo,
ICoverFeatureInfo,
ICoverProtocolClient,
} from '../interfaces/smarthome.interfaces.js';
/**
* Options for creating a CoverFeature
*/
export interface ICoverFeatureOptions extends IFeatureOptions {
/** Protocol type */
protocol: TCoverProtocol;
/** Entity ID (for Home Assistant) */
entityId: string;
/** Protocol client for controlling the cover */
protocolClient: ICoverProtocolClient;
/** Device class */
deviceClass?: TCoverDeviceClass;
/** Cover capabilities */
capabilities?: Partial<ICoverCapabilities>;
}
/**
* Cover Feature - control for blinds, garage doors, etc.
*
* Protocol-agnostic: works with Home Assistant, MQTT, Somfy, etc.
*
* @example
* ```typescript
* const cover = device.getFeature<CoverFeature>('cover');
* if (cover) {
* await cover.open();
* await cover.setPosition(50); // 50% open
* }
* ```
*/
export class CoverFeature extends Feature {
public readonly type = 'cover' as const;
public readonly protocol: TCoverProtocol;
/** Entity ID (e.g., "cover.garage_door") */
public readonly entityId: string;
/** Capabilities */
public readonly capabilities: ICoverCapabilities;
/** Current cover state (not connection state) */
protected _coverState: TCoverState = 'unknown';
protected _position?: number;
protected _tiltPosition?: number;
/** Protocol client for controlling the cover */
private protocolClient: ICoverProtocolClient;
constructor(
device: TDeviceReference,
port: number,
options: ICoverFeatureOptions
) {
super(device, port, options);
this.protocol = options.protocol;
this.entityId = options.entityId;
this.protocolClient = options.protocolClient;
this.capabilities = {
deviceClass: options.deviceClass,
supportsOpen: options.capabilities?.supportsOpen ?? true,
supportsClose: options.capabilities?.supportsClose ?? true,
supportsStop: options.capabilities?.supportsStop ?? true,
supportsPosition: options.capabilities?.supportsPosition ?? false,
supportsTilt: options.capabilities?.supportsTilt ?? false,
};
}
// ============================================================================
// Properties
// ============================================================================
/**
* Get current state (cached)
*/
public get coverState(): TCoverState {
return this._coverState;
}
/**
* Get current position 0-100 (cached)
* 0 = closed, 100 = fully open
*/
public get position(): number | undefined {
return this._position;
}
/**
* Get current tilt position 0-100 (cached)
*/
public get tiltPosition(): number | undefined {
return this._tiltPosition;
}
/**
* Check if cover is open
*/
public get isOpen(): boolean {
return this._coverState === 'open';
}
/**
* Check if cover is closed
*/
public get isClosed(): boolean {
return this._coverState === 'closed';
}
/**
* Check if cover is opening
*/
public get isOpening(): boolean {
return this._coverState === 'opening';
}
/**
* Check if cover is closing
*/
public get isClosing(): boolean {
return this._coverState === 'closing';
}
// ============================================================================
// 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
}
// ============================================================================
// Cover Control
// ============================================================================
/**
* Open the cover
*/
public async open(): Promise<void> {
if (!this.capabilities.supportsOpen) {
throw new Error('Cover does not support open');
}
await this.protocolClient.open(this.entityId);
this._coverState = 'opening';
this.emit('state:changed', this.getState());
}
/**
* Close the cover
*/
public async close(): Promise<void> {
if (!this.capabilities.supportsClose) {
throw new Error('Cover does not support close');
}
await this.protocolClient.close(this.entityId);
this._coverState = 'closing';
this.emit('state:changed', this.getState());
}
/**
* Stop the cover
*/
public async stop(): Promise<void> {
if (!this.capabilities.supportsStop) {
throw new Error('Cover does not support stop');
}
await this.protocolClient.stop(this.entityId);
this._coverState = 'stopped';
this.emit('state:changed', this.getState());
}
/**
* Set cover position
* @param position Position 0-100 (0 = closed, 100 = open)
*/
public async setPosition(position: number): Promise<void> {
if (!this.capabilities.supportsPosition) {
throw new Error('Cover does not support position control');
}
const clamped = Math.max(0, Math.min(100, Math.round(position)));
await this.protocolClient.setPosition(this.entityId, clamped);
this._position = clamped;
this._coverState = clamped === 0 ? 'closed' : clamped === 100 ? 'open' : 'stopped';
this.emit('state:changed', this.getState());
}
/**
* Set tilt position
* @param position Tilt position 0-100
*/
public async setTiltPosition(position: number): Promise<void> {
if (!this.capabilities.supportsTilt) {
throw new Error('Cover does not support tilt control');
}
const clamped = Math.max(0, Math.min(100, Math.round(position)));
await this.protocolClient.setTiltPosition(this.entityId, clamped);
this._tiltPosition = clamped;
this.emit('state:changed', this.getState());
}
/**
* Get current state as object
*/
public getState(): ICoverStateInfo {
return {
state: this._coverState,
position: this._position,
tiltPosition: this._tiltPosition,
};
}
/**
* Refresh state from the device
*/
public async refreshState(): Promise<ICoverStateInfo> {
const state = await this.protocolClient.getState(this.entityId);
this.updateStateInternal(state);
return state;
}
/**
* Update state from external source
*/
public updateState(state: ICoverStateInfo): void {
this.updateStateInternal(state);
this.emit('state:changed', state);
}
/**
* Internal state update
*/
private updateStateInternal(state: ICoverStateInfo): void {
this._coverState = state.state;
this._position = state.position;
this._tiltPosition = state.tiltPosition;
}
// ============================================================================
// Feature Info
// ============================================================================
public getFeatureInfo(): ICoverFeatureInfo {
return {
...this.getBaseFeatureInfo(),
type: 'cover',
protocol: this.protocol,
capabilities: this.capabilities,
currentState: this.getState(),
};
}
}

296
ts/features/feature.fan.ts Normal file
View File

@@ -0,0 +1,296 @@
/**
* Fan Feature
* Provides control for fans (speed, oscillation, direction)
*/
import { Feature, type TDeviceReference } from './feature.abstract.js';
import type { IFeatureOptions } from '../interfaces/feature.interfaces.js';
import type {
TFanProtocol,
TFanDirection,
IFanCapabilities,
IFanState,
IFanFeatureInfo,
IFanProtocolClient,
} from '../interfaces/smarthome.interfaces.js';
/**
* Options for creating a FanFeature
*/
export interface IFanFeatureOptions extends IFeatureOptions {
/** Protocol type */
protocol: TFanProtocol;
/** Entity ID (for Home Assistant) */
entityId: string;
/** Protocol client for controlling the fan */
protocolClient: IFanProtocolClient;
/** Fan capabilities */
capabilities?: Partial<IFanCapabilities>;
}
/**
* Fan Feature - speed, oscillation, and direction control
*
* Protocol-agnostic: works with Home Assistant, MQTT, Bond, etc.
*
* @example
* ```typescript
* const fan = device.getFeature<FanFeature>('fan');
* if (fan) {
* await fan.turnOn(75); // 75% speed
* await fan.setOscillating(true);
* }
* ```
*/
export class FanFeature extends Feature {
public readonly type = 'fan' as const;
public readonly protocol: TFanProtocol;
/** Entity ID (e.g., "fan.bedroom") */
public readonly entityId: string;
/** Capabilities */
public readonly capabilities: IFanCapabilities;
/** Current state */
protected _isOn: boolean = false;
protected _percentage?: number;
protected _presetMode?: string;
protected _oscillating?: boolean;
protected _direction?: TFanDirection;
/** Protocol client for controlling the fan */
private protocolClient: IFanProtocolClient;
constructor(
device: TDeviceReference,
port: number,
options: IFanFeatureOptions
) {
super(device, port, options);
this.protocol = options.protocol;
this.entityId = options.entityId;
this.protocolClient = options.protocolClient;
this.capabilities = {
supportsSpeed: options.capabilities?.supportsSpeed ?? true,
supportsOscillate: options.capabilities?.supportsOscillate ?? false,
supportsDirection: options.capabilities?.supportsDirection ?? false,
supportsPresetModes: options.capabilities?.supportsPresetModes ?? false,
presetModes: options.capabilities?.presetModes,
speedCount: options.capabilities?.speedCount,
};
}
// ============================================================================
// Properties
// ============================================================================
/**
* Get current on/off state (cached)
*/
public get isOn(): boolean {
return this._isOn;
}
/**
* Get current speed percentage 0-100 (cached)
*/
public get percentage(): number | undefined {
return this._percentage;
}
/**
* Get current preset mode (cached)
*/
public get presetMode(): string | undefined {
return this._presetMode;
}
/**
* Get oscillating state (cached)
*/
public get oscillating(): boolean | undefined {
return this._oscillating;
}
/**
* Get direction (cached)
*/
public get direction(): TFanDirection | undefined {
return this._direction;
}
/**
* Get available preset modes
*/
public get presetModes(): string[] | undefined {
return this.capabilities.presetModes;
}
// ============================================================================
// 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
}
// ============================================================================
// Fan Control
// ============================================================================
/**
* Turn on the fan
* @param percentage Optional speed percentage
*/
public async turnOn(percentage?: number): Promise<void> {
await this.protocolClient.turnOn(this.entityId, percentage);
this._isOn = true;
if (percentage !== undefined) {
this._percentage = percentage;
}
this.emit('state:changed', this.getState());
}
/**
* Turn off the fan
*/
public async turnOff(): Promise<void> {
await this.protocolClient.turnOff(this.entityId);
this._isOn = false;
this.emit('state:changed', this.getState());
}
/**
* Toggle the fan
*/
public async toggle(): Promise<void> {
await this.protocolClient.toggle(this.entityId);
this._isOn = !this._isOn;
this.emit('state:changed', this.getState());
}
/**
* Set speed percentage
* @param percentage Speed 0-100
*/
public async setPercentage(percentage: number): Promise<void> {
if (!this.capabilities.supportsSpeed) {
throw new Error('Fan does not support speed control');
}
const clamped = Math.max(0, Math.min(100, Math.round(percentage)));
await this.protocolClient.setPercentage(this.entityId, clamped);
this._percentage = clamped;
this._isOn = clamped > 0;
this.emit('state:changed', this.getState());
}
/**
* Set preset mode
* @param mode Preset mode name
*/
public async setPresetMode(mode: string): Promise<void> {
if (!this.capabilities.supportsPresetModes) {
throw new Error('Fan does not support preset modes');
}
await this.protocolClient.setPresetMode(this.entityId, mode);
this._presetMode = mode;
this._isOn = true;
this.emit('state:changed', this.getState());
}
/**
* Set oscillating state
* @param oscillating Whether to oscillate
*/
public async setOscillating(oscillating: boolean): Promise<void> {
if (!this.capabilities.supportsOscillate) {
throw new Error('Fan does not support oscillation');
}
await this.protocolClient.setOscillating(this.entityId, oscillating);
this._oscillating = oscillating;
this.emit('state:changed', this.getState());
}
/**
* Set direction
* @param direction forward or reverse
*/
public async setDirection(direction: TFanDirection): Promise<void> {
if (!this.capabilities.supportsDirection) {
throw new Error('Fan does not support direction control');
}
await this.protocolClient.setDirection(this.entityId, direction);
this._direction = direction;
this.emit('state:changed', this.getState());
}
/**
* Get current state as object
*/
public getState(): IFanState {
return {
isOn: this._isOn,
percentage: this._percentage,
presetMode: this._presetMode,
oscillating: this._oscillating,
direction: this._direction,
};
}
/**
* Refresh state from the device
*/
public async refreshState(): Promise<IFanState> {
const state = await this.protocolClient.getState(this.entityId);
this.updateStateInternal(state);
return state;
}
/**
* Update state from external source
*/
public updateState(state: IFanState): void {
this.updateStateInternal(state);
this.emit('state:changed', state);
}
/**
* Internal state update
*/
private updateStateInternal(state: IFanState): void {
this._isOn = state.isOn;
this._percentage = state.percentage;
this._presetMode = state.presetMode;
this._oscillating = state.oscillating;
this._direction = state.direction;
}
// ============================================================================
// Feature Info
// ============================================================================
public getFeatureInfo(): IFanFeatureInfo {
return {
...this.getBaseFeatureInfo(),
type: 'fan',
protocol: this.protocol,
capabilities: this.capabilities,
currentState: this.getState(),
};
}
}

View File

@@ -0,0 +1,369 @@
/**
* Light Feature
* Provides control for smart lights (brightness, color, effects)
*/
import { Feature, type TDeviceReference } from './feature.abstract.js';
import type { IFeatureOptions } from '../interfaces/feature.interfaces.js';
import type {
TLightProtocol,
ILightCapabilities,
ILightState,
ILightFeatureInfo,
ILightProtocolClient,
} from '../interfaces/smarthome.interfaces.js';
/**
* Options for creating a LightFeature
*/
export interface ILightFeatureOptions extends IFeatureOptions {
/** Protocol type */
protocol: TLightProtocol;
/** Entity ID (for Home Assistant) */
entityId: string;
/** Protocol client for controlling the light */
protocolClient: ILightProtocolClient;
/** Light capabilities */
capabilities?: Partial<ILightCapabilities>;
}
/**
* Light Feature - brightness, color, and effect control
*
* Protocol-agnostic: works with Home Assistant, Hue, MQTT, Zigbee, etc.
*
* @example
* ```typescript
* const light = device.getFeature<LightFeature>('light');
* if (light) {
* await light.turnOn({ brightness: 200 });
* await light.setColorTemp(4000); // 4000K warm white
* await light.setRgbColor(255, 100, 50);
* }
* ```
*/
export class LightFeature extends Feature {
public readonly type = 'light' as const;
public readonly protocol: TLightProtocol;
/** Entity ID (e.g., "light.living_room") */
public readonly entityId: string;
/** Capabilities */
public readonly capabilities: ILightCapabilities;
/** Current state */
protected _isOn: boolean = false;
protected _brightness?: number;
protected _colorTemp?: number;
protected _colorTempMireds?: number;
protected _rgbColor?: [number, number, number];
protected _hsColor?: [number, number];
protected _xyColor?: [number, number];
protected _effect?: string;
/** Protocol client for controlling the light */
private protocolClient: ILightProtocolClient;
constructor(
device: TDeviceReference,
port: number,
options: ILightFeatureOptions
) {
super(device, port, options);
this.protocol = options.protocol;
this.entityId = options.entityId;
this.protocolClient = options.protocolClient;
// Set capabilities with defaults
this.capabilities = {
supportsBrightness: options.capabilities?.supportsBrightness ?? false,
supportsColorTemp: options.capabilities?.supportsColorTemp ?? false,
supportsRgb: options.capabilities?.supportsRgb ?? false,
supportsHs: options.capabilities?.supportsHs ?? false,
supportsXy: options.capabilities?.supportsXy ?? false,
supportsEffects: options.capabilities?.supportsEffects ?? false,
supportsTransition: options.capabilities?.supportsTransition ?? true,
effects: options.capabilities?.effects,
minMireds: options.capabilities?.minMireds,
maxMireds: options.capabilities?.maxMireds,
minColorTempKelvin: options.capabilities?.minColorTempKelvin,
maxColorTempKelvin: options.capabilities?.maxColorTempKelvin,
};
}
// ============================================================================
// Properties
// ============================================================================
/**
* Get current on/off state (cached)
*/
public get isOn(): boolean {
return this._isOn;
}
/**
* Get current brightness 0-255 (cached)
*/
public get brightness(): number | undefined {
return this._brightness;
}
/**
* Get current color temperature in Kelvin (cached)
*/
public get colorTemp(): number | undefined {
return this._colorTemp;
}
/**
* Get current color temperature in Mireds (cached)
*/
public get colorTempMireds(): number | undefined {
return this._colorTempMireds;
}
/**
* Get current RGB color (cached)
*/
public get rgbColor(): [number, number, number] | undefined {
return this._rgbColor;
}
/**
* Get current HS color [hue 0-360, saturation 0-100] (cached)
*/
public get hsColor(): [number, number] | undefined {
return this._hsColor;
}
/**
* Get current XY color (cached)
*/
public get xyColor(): [number, number] | undefined {
return this._xyColor;
}
/**
* Get current effect (cached)
*/
public get effect(): string | undefined {
return this._effect;
}
/**
* Get available effects
*/
public get effects(): string[] | undefined {
return this.capabilities.effects;
}
// ============================================================================
// Connection
// ============================================================================
protected async doConnect(): Promise<void> {
// Fetch initial state
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
}
// ============================================================================
// Light Control
// ============================================================================
/**
* Turn on the light
* @param options Optional settings to apply when turning on
*/
public async turnOn(options?: {
brightness?: number;
colorTemp?: number;
rgb?: [number, number, number];
transition?: number;
}): Promise<void> {
await this.protocolClient.turnOn(this.entityId, options);
this._isOn = true;
if (options?.brightness !== undefined) {
this._brightness = options.brightness;
}
if (options?.colorTemp !== undefined) {
this._colorTemp = options.colorTemp;
}
if (options?.rgb !== undefined) {
this._rgbColor = options.rgb;
}
this.emit('state:changed', this.getState());
}
/**
* Turn off the light
* @param options Optional transition time
*/
public async turnOff(options?: { transition?: number }): Promise<void> {
await this.protocolClient.turnOff(this.entityId, options);
this._isOn = false;
this.emit('state:changed', this.getState());
}
/**
* Toggle the light
*/
public async toggle(): Promise<void> {
await this.protocolClient.toggle(this.entityId);
this._isOn = !this._isOn;
this.emit('state:changed', this.getState());
}
/**
* Set brightness level
* @param brightness Brightness 0-255
* @param transition Optional transition time in seconds
*/
public async setBrightness(brightness: number, transition?: number): Promise<void> {
if (!this.capabilities.supportsBrightness) {
throw new Error('Light does not support brightness control');
}
const clamped = Math.max(0, Math.min(255, Math.round(brightness)));
await this.protocolClient.setBrightness(this.entityId, clamped, transition);
this._brightness = clamped;
if (clamped > 0) {
this._isOn = true;
}
this.emit('state:changed', this.getState());
}
/**
* Set color temperature in Kelvin
* @param kelvin Color temperature in Kelvin (e.g., 2700 warm, 6500 cool)
* @param transition Optional transition time in seconds
*/
public async setColorTemp(kelvin: number, transition?: number): Promise<void> {
if (!this.capabilities.supportsColorTemp) {
throw new Error('Light does not support color temperature');
}
// Clamp to supported range if available
let clamped = kelvin;
if (this.capabilities.minColorTempKelvin && this.capabilities.maxColorTempKelvin) {
clamped = Math.max(
this.capabilities.minColorTempKelvin,
Math.min(this.capabilities.maxColorTempKelvin, kelvin)
);
}
await this.protocolClient.setColorTemp(this.entityId, clamped, transition);
this._colorTemp = clamped;
this._colorTempMireds = Math.round(1000000 / clamped);
this._isOn = true;
this.emit('state:changed', this.getState());
}
/**
* Set RGB color
* @param r Red 0-255
* @param g Green 0-255
* @param b Blue 0-255
* @param transition Optional transition time in seconds
*/
public async setRgbColor(r: number, g: number, b: number, transition?: number): Promise<void> {
if (!this.capabilities.supportsRgb) {
throw new Error('Light does not support RGB color');
}
const clampedR = Math.max(0, Math.min(255, Math.round(r)));
const clampedG = Math.max(0, Math.min(255, Math.round(g)));
const clampedB = Math.max(0, Math.min(255, Math.round(b)));
await this.protocolClient.setRgbColor(this.entityId, clampedR, clampedG, clampedB, transition);
this._rgbColor = [clampedR, clampedG, clampedB];
this._isOn = true;
this.emit('state:changed', this.getState());
}
/**
* Set light effect
* @param effect Effect name from available effects
*/
public async setEffect(effect: string): Promise<void> {
if (!this.capabilities.supportsEffects) {
throw new Error('Light does not support effects');
}
await this.protocolClient.setEffect(this.entityId, effect);
this._effect = effect;
this._isOn = true;
this.emit('state:changed', this.getState());
}
/**
* Get current state as object
*/
public getState(): ILightState {
return {
isOn: this._isOn,
brightness: this._brightness,
colorTemp: this._colorTemp,
colorTempMireds: this._colorTempMireds,
rgbColor: this._rgbColor,
hsColor: this._hsColor,
xyColor: this._xyColor,
effect: this._effect,
};
}
/**
* Refresh state from the device
*/
public async refreshState(): Promise<ILightState> {
const state = await this.protocolClient.getState(this.entityId);
this.updateStateInternal(state);
return state;
}
/**
* Update state from external source (e.g., state change event)
*/
public updateState(state: ILightState): void {
this.updateStateInternal(state);
this.emit('state:changed', state);
}
/**
* Internal state update
*/
private updateStateInternal(state: ILightState): void {
this._isOn = state.isOn;
this._brightness = state.brightness;
this._colorTemp = state.colorTemp;
this._colorTempMireds = state.colorTempMireds;
this._rgbColor = state.rgbColor;
this._hsColor = state.hsColor;
this._xyColor = state.xyColor;
this._effect = state.effect;
}
// ============================================================================
// Feature Info
// ============================================================================
public getFeatureInfo(): ILightFeatureInfo {
return {
...this.getBaseFeatureInfo(),
type: 'light',
protocol: this.protocol,
capabilities: this.capabilities,
currentState: this.getState(),
};
}
}

223
ts/features/feature.lock.ts Normal file
View File

@@ -0,0 +1,223 @@
/**
* Lock Feature
* Provides control for smart locks
*/
import { Feature, type TDeviceReference } from './feature.abstract.js';
import type { IFeatureOptions } from '../interfaces/feature.interfaces.js';
import type {
TLockProtocol,
TLockState,
ILockCapabilities,
ILockStateInfo,
ILockFeatureInfo,
ILockProtocolClient,
} from '../interfaces/smarthome.interfaces.js';
/**
* Options for creating a LockFeature
*/
export interface ILockFeatureOptions extends IFeatureOptions {
/** Protocol type */
protocol: TLockProtocol;
/** Entity ID (for Home Assistant) */
entityId: string;
/** Protocol client for controlling the lock */
protocolClient: ILockProtocolClient;
/** Whether the lock supports physical open */
supportsOpen?: boolean;
}
/**
* Lock Feature - control for smart locks
*
* Protocol-agnostic: works with Home Assistant, MQTT, August, Yale, etc.
*
* @example
* ```typescript
* const lock = device.getFeature<LockFeature>('lock');
* if (lock) {
* await lock.lock();
* console.log(`Lock is ${lock.isLocked ? 'locked' : 'unlocked'}`);
* }
* ```
*/
export class LockFeature extends Feature {
public readonly type = 'lock' as const;
public readonly protocol: TLockProtocol;
/** Entity ID (e.g., "lock.front_door") */
public readonly entityId: string;
/** Capabilities */
public readonly capabilities: ILockCapabilities;
/** Current state */
protected _lockState: TLockState = 'unknown';
protected _isLocked: boolean = false;
/** Protocol client for controlling the lock */
private protocolClient: ILockProtocolClient;
constructor(
device: TDeviceReference,
port: number,
options: ILockFeatureOptions
) {
super(device, port, options);
this.protocol = options.protocol;
this.entityId = options.entityId;
this.protocolClient = options.protocolClient;
this.capabilities = {
supportsOpen: options.supportsOpen ?? false,
};
}
// ============================================================================
// Properties
// ============================================================================
/**
* Get current lock state (cached)
*/
public get lockState(): TLockState {
return this._lockState;
}
/**
* Check if locked (cached)
*/
public get isLocked(): boolean {
return this._isLocked;
}
/**
* Check if unlocked
*/
public get isUnlocked(): boolean {
return this._lockState === 'unlocked';
}
/**
* Check if currently locking
*/
public get isLocking(): boolean {
return this._lockState === 'locking';
}
/**
* Check if currently unlocking
*/
public get isUnlocking(): boolean {
return this._lockState === 'unlocking';
}
/**
* Check if jammed
*/
public get isJammed(): boolean {
return this._lockState === 'jammed';
}
// ============================================================================
// 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
}
// ============================================================================
// Lock Control
// ============================================================================
/**
* Lock the lock
*/
public async lock(): Promise<void> {
await this.protocolClient.lock(this.entityId);
this._lockState = 'locking';
this.emit('state:changed', this.getState());
}
/**
* Unlock the lock
*/
public async unlock(): Promise<void> {
await this.protocolClient.unlock(this.entityId);
this._lockState = 'unlocking';
this.emit('state:changed', this.getState());
}
/**
* Open the lock (physically open the door if supported)
*/
public async open(): Promise<void> {
if (!this.capabilities.supportsOpen) {
throw new Error('Lock does not support physical open');
}
await this.protocolClient.open(this.entityId);
this._lockState = 'unlocked';
this._isLocked = false;
this.emit('state:changed', this.getState());
}
/**
* Get current state as object
*/
public getState(): ILockStateInfo {
return {
state: this._lockState,
isLocked: this._isLocked,
};
}
/**
* Refresh state from the device
*/
public async refreshState(): Promise<ILockStateInfo> {
const state = await this.protocolClient.getState(this.entityId);
this.updateStateInternal(state);
return state;
}
/**
* Update state from external source
*/
public updateState(state: ILockStateInfo): void {
this.updateStateInternal(state);
this.emit('state:changed', state);
}
/**
* Internal state update
*/
private updateStateInternal(state: ILockStateInfo): void {
this._lockState = state.state;
this._isLocked = state.isLocked;
}
// ============================================================================
// Feature Info
// ============================================================================
public getFeatureInfo(): ILockFeatureInfo {
return {
...this.getBaseFeatureInfo(),
type: 'lock',
protocol: this.protocol,
capabilities: this.capabilities,
currentState: this.getState(),
};
}
}

View File

@@ -0,0 +1,202 @@
/**
* Sensor Feature
* Provides read-only state for sensors (temperature, humidity, power, etc.)
*/
import { Feature, type TDeviceReference } from './feature.abstract.js';
import type { IFeatureOptions } from '../interfaces/feature.interfaces.js';
import type {
TSensorProtocol,
TSensorDeviceClass,
TSensorStateClass,
ISensorCapabilities,
ISensorState,
ISensorFeatureInfo,
ISensorProtocolClient,
} from '../interfaces/smarthome.interfaces.js';
/**
* Options for creating a SensorFeature
*/
export interface ISensorFeatureOptions extends IFeatureOptions {
/** Protocol type */
protocol: TSensorProtocol;
/** Entity ID (for Home Assistant) */
entityId: string;
/** Protocol client for reading sensor state */
protocolClient: ISensorProtocolClient;
/** Device class (temperature, humidity, etc.) */
deviceClass?: TSensorDeviceClass;
/** State class (measurement, total, etc.) */
stateClass?: TSensorStateClass;
/** Unit of measurement */
unit?: string;
/** Precision (decimal places) */
precision?: number;
}
/**
* Sensor Feature - read-only state values
*
* Protocol-agnostic: works with Home Assistant, MQTT, SNMP, etc.
*
* @example
* ```typescript
* const sensor = device.getFeature<SensorFeature>('sensor');
* if (sensor) {
* await sensor.refreshState();
* console.log(`Temperature: ${sensor.value} ${sensor.unit}`);
* }
* ```
*/
export class SensorFeature extends Feature {
public readonly type = 'sensor' as const;
public readonly protocol: TSensorProtocol;
/** Entity ID (e.g., "sensor.living_room_temperature") */
public readonly entityId: string;
/** Capabilities */
public readonly capabilities: ISensorCapabilities;
/** Current state */
protected _value: string | number | boolean = '';
protected _numericValue?: number;
protected _unit?: string;
protected _lastUpdated: Date = new Date();
/** Protocol client for reading sensor state */
private protocolClient: ISensorProtocolClient;
constructor(
device: TDeviceReference,
port: number,
options: ISensorFeatureOptions
) {
super(device, port, options);
this.protocol = options.protocol;
this.entityId = options.entityId;
this.protocolClient = options.protocolClient;
this.capabilities = {
deviceClass: options.deviceClass,
stateClass: options.stateClass,
unit: options.unit,
precision: options.precision,
};
this._unit = options.unit;
}
// ============================================================================
// Properties
// ============================================================================
/**
* Get current value (cached)
*/
public get value(): string | number | boolean {
return this._value;
}
/**
* Get numeric value if available (cached)
*/
public get numericValue(): number | undefined {
return this._numericValue;
}
/**
* Get unit of measurement
*/
public get unit(): string | undefined {
return this._unit;
}
/**
* Get device class
*/
public get deviceClass(): TSensorDeviceClass | undefined {
return this.capabilities.deviceClass;
}
/**
* Get state class
*/
public get stateClass(): TSensorStateClass | undefined {
return this.capabilities.stateClass;
}
/**
* Get last updated timestamp
*/
public get lastUpdated(): Date {
return this._lastUpdated;
}
// ============================================================================
// Connection
// ============================================================================
protected async doConnect(): Promise<void> {
// Fetch initial state
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
}
// ============================================================================
// Sensor Reading
// ============================================================================
/**
* Refresh state from the device
*/
public async refreshState(): Promise<ISensorState> {
const state = await this.protocolClient.getState(this.entityId);
this.updateStateInternal(state);
return state;
}
/**
* Update state from external source (e.g., state change event)
*/
public updateState(state: ISensorState): void {
this.updateStateInternal(state);
this.emit('state:changed', state);
}
/**
* Internal state update
*/
private updateStateInternal(state: ISensorState): void {
this._value = state.value;
this._numericValue = state.numericValue;
this._unit = state.unit || this._unit;
this._lastUpdated = state.lastUpdated;
}
// ============================================================================
// Feature Info
// ============================================================================
public getFeatureInfo(): ISensorFeatureInfo {
return {
...this.getBaseFeatureInfo(),
type: 'sensor',
protocol: this.protocol,
capabilities: this.capabilities,
currentState: {
value: this._value,
numericValue: this._numericValue,
unit: this._unit,
lastUpdated: this._lastUpdated,
},
};
}
}

View File

@@ -0,0 +1,170 @@
/**
* Switch Feature
* Provides binary on/off control for smart switches, outlets, etc.
*/
import { Feature, type TDeviceReference } from './feature.abstract.js';
import type { IFeatureOptions } from '../interfaces/feature.interfaces.js';
import type {
TSwitchProtocol,
ISwitchCapabilities,
ISwitchState,
ISwitchFeatureInfo,
ISwitchProtocolClient,
} from '../interfaces/smarthome.interfaces.js';
/**
* Options for creating a SwitchFeature
*/
export interface ISwitchFeatureOptions extends IFeatureOptions {
/** Protocol type */
protocol: TSwitchProtocol;
/** Entity ID (for Home Assistant) */
entityId: string;
/** Protocol client for controlling the switch */
protocolClient: ISwitchProtocolClient;
/** Device class */
deviceClass?: 'outlet' | 'switch';
}
/**
* Switch Feature - binary on/off control
*
* Protocol-agnostic: works with Home Assistant, MQTT, Tasmota, Tuya, etc.
*
* @example
* ```typescript
* const sw = device.getFeature<SwitchFeature>('switch');
* if (sw) {
* await sw.turnOn();
* await sw.toggle();
* console.log(`Switch is ${sw.isOn ? 'on' : 'off'}`);
* }
* ```
*/
export class SwitchFeature extends Feature {
public readonly type = 'switch' as const;
public readonly protocol: TSwitchProtocol;
/** Entity ID (e.g., "switch.living_room") */
public readonly entityId: string;
/** Capabilities */
public readonly capabilities: ISwitchCapabilities;
/** Current state */
protected _isOn: boolean = false;
/** Protocol client for controlling the switch */
private protocolClient: ISwitchProtocolClient;
constructor(
device: TDeviceReference,
port: number,
options: ISwitchFeatureOptions
) {
super(device, port, options);
this.protocol = options.protocol;
this.entityId = options.entityId;
this.protocolClient = options.protocolClient;
this.capabilities = {
deviceClass: options.deviceClass,
};
}
// ============================================================================
// Properties
// ============================================================================
/**
* Get current on/off state (cached)
*/
public get isOn(): boolean {
return this._isOn;
}
// ============================================================================
// Connection
// ============================================================================
protected async doConnect(): Promise<void> {
// Fetch initial state
try {
const state = await this.protocolClient.getState(this.entityId);
this._isOn = state.isOn;
} catch {
// Ignore errors fetching initial state
}
}
protected async doDisconnect(): Promise<void> {
// Nothing to disconnect
}
// ============================================================================
// Switch Control
// ============================================================================
/**
* Turn on the switch
*/
public async turnOn(): Promise<void> {
await this.protocolClient.turnOn(this.entityId);
this._isOn = true;
this.emit('state:changed', { isOn: true });
}
/**
* Turn off the switch
*/
public async turnOff(): Promise<void> {
await this.protocolClient.turnOff(this.entityId);
this._isOn = false;
this.emit('state:changed', { isOn: false });
}
/**
* Toggle the switch
*/
public async toggle(): Promise<void> {
await this.protocolClient.toggle(this.entityId);
this._isOn = !this._isOn;
this.emit('state:changed', { isOn: this._isOn });
}
/**
* Refresh state from the device
*/
public async refreshState(): Promise<ISwitchState> {
const state = await this.protocolClient.getState(this.entityId);
this._isOn = state.isOn;
return state;
}
/**
* Update state from external source (e.g., state change event)
*/
public updateState(state: ISwitchState): void {
const changed = this._isOn !== state.isOn;
this._isOn = state.isOn;
if (changed) {
this.emit('state:changed', state);
}
}
// ============================================================================
// Feature Info
// ============================================================================
public getFeatureInfo(): ISwitchFeatureInfo {
return {
...this.getBaseFeatureInfo(),
type: 'switch',
protocol: this.protocol,
capabilities: this.capabilities,
currentState: {
isOn: this._isOn,
},
};
}
}

View File

@@ -6,10 +6,20 @@
// Abstract base
export { Feature, type TDeviceReference } from './feature.abstract.js';
// Concrete features
// Concrete features - Document/Infrastructure
export { ScanFeature, type IScanFeatureOptions } from './feature.scan.js';
export { PrintFeature, type IPrintFeatureOptions } from './feature.print.js';
export { PlaybackFeature, type IPlaybackFeatureOptions } from './feature.playback.js';
export { VolumeFeature, type IVolumeFeatureOptions, type IVolumeController } from './feature.volume.js';
export { PowerFeature, type IPowerFeatureOptions } from './feature.power.js';
export { SnmpFeature, type ISnmpFeatureOptions } from './feature.snmp.js';
// Smart Home Features (protocol-agnostic: home-assistant, hue, mqtt, etc.)
export { SwitchFeature, type ISwitchFeatureOptions } from './feature.switch.js';
export { SensorFeature, type ISensorFeatureOptions } from './feature.sensor.js';
export { LightFeature, type ILightFeatureOptions } from './feature.light.js';
export { CoverFeature, type ICoverFeatureOptions } from './feature.cover.js';
export { LockFeature, type ILockFeatureOptions } from './feature.lock.js';
export { FanFeature, type IFanFeatureOptions } from './feature.fan.js';
export { ClimateFeature, type IClimateFeatureOptions } from './feature.climate.js';
export { CameraFeature, type ICameraFeatureOptions } from './feature.camera.js';