/** * 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; } /** * Cover Feature - control for blinds, garage doors, etc. * * Protocol-agnostic: works with Home Assistant, MQTT, Somfy, etc. * * @example * ```typescript * const cover = device.getFeature('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 { 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 } // ============================================================================ // Cover Control // ============================================================================ /** * Open the cover */ public async open(): Promise { 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 { 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 { 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 { 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 { 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 { 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(), }; } }