Files
devicemanager/ts/features/feature.fan.ts

297 lines
7.8 KiB
TypeScript
Raw Permalink Normal View History

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