/** * 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; } /** * Fan Feature - speed, oscillation, and direction control * * Protocol-agnostic: works with Home Assistant, MQTT, Bond, etc. * * @example * ```typescript * const fan = device.getFeature('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 { 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 } // ============================================================================ // Fan Control // ============================================================================ /** * Turn on the fan * @param percentage Optional speed percentage */ public async turnOn(percentage?: number): Promise { 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 { await this.protocolClient.turnOff(this.entityId); this._isOn = false; this.emit('state:changed', this.getState()); } /** * Toggle the fan */ public async toggle(): Promise { 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 { 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 { 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 { 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 { 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 { 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(), }; } }