370 lines
10 KiB
TypeScript
370 lines
10 KiB
TypeScript
|
|
/**
|
||
|
|
* 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(),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|