Files
devicemanager/ts/features/feature.cover.ts

279 lines
7.4 KiB
TypeScript

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