/** * 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; } /** * Light Feature - brightness, color, and effect control * * Protocol-agnostic: works with Home Assistant, Hue, MQTT, Zigbee, etc. * * @example * ```typescript * const light = device.getFeature('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 { // 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 { // 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 { 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 { await this.protocolClient.turnOff(this.entityId, options); this._isOn = false; this.emit('state:changed', this.getState()); } /** * Toggle the light */ public async toggle(): Promise { 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 { 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 { 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 { 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 { 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 { 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(), }; } }