/** * Lock Feature * Provides control for smart locks */ import { Feature, type TDeviceReference } from './feature.abstract.js'; import type { IFeatureOptions } from '../interfaces/feature.interfaces.js'; import type { TLockProtocol, TLockState, ILockCapabilities, ILockStateInfo, ILockFeatureInfo, ILockProtocolClient, } from '../interfaces/smarthome.interfaces.js'; /** * Options for creating a LockFeature */ export interface ILockFeatureOptions extends IFeatureOptions { /** Protocol type */ protocol: TLockProtocol; /** Entity ID (for Home Assistant) */ entityId: string; /** Protocol client for controlling the lock */ protocolClient: ILockProtocolClient; /** Whether the lock supports physical open */ supportsOpen?: boolean; } /** * Lock Feature - control for smart locks * * Protocol-agnostic: works with Home Assistant, MQTT, August, Yale, etc. * * @example * ```typescript * const lock = device.getFeature('lock'); * if (lock) { * await lock.lock(); * console.log(`Lock is ${lock.isLocked ? 'locked' : 'unlocked'}`); * } * ``` */ export class LockFeature extends Feature { public readonly type = 'lock' as const; public readonly protocol: TLockProtocol; /** Entity ID (e.g., "lock.front_door") */ public readonly entityId: string; /** Capabilities */ public readonly capabilities: ILockCapabilities; /** Current state */ protected _lockState: TLockState = 'unknown'; protected _isLocked: boolean = false; /** Protocol client for controlling the lock */ private protocolClient: ILockProtocolClient; constructor( device: TDeviceReference, port: number, options: ILockFeatureOptions ) { super(device, port, options); this.protocol = options.protocol; this.entityId = options.entityId; this.protocolClient = options.protocolClient; this.capabilities = { supportsOpen: options.supportsOpen ?? false, }; } // ============================================================================ // Properties // ============================================================================ /** * Get current lock state (cached) */ public get lockState(): TLockState { return this._lockState; } /** * Check if locked (cached) */ public get isLocked(): boolean { return this._isLocked; } /** * Check if unlocked */ public get isUnlocked(): boolean { return this._lockState === 'unlocked'; } /** * Check if currently locking */ public get isLocking(): boolean { return this._lockState === 'locking'; } /** * Check if currently unlocking */ public get isUnlocking(): boolean { return this._lockState === 'unlocking'; } /** * Check if jammed */ public get isJammed(): boolean { return this._lockState === 'jammed'; } // ============================================================================ // 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 } // ============================================================================ // Lock Control // ============================================================================ /** * Lock the lock */ public async lock(): Promise { await this.protocolClient.lock(this.entityId); this._lockState = 'locking'; this.emit('state:changed', this.getState()); } /** * Unlock the lock */ public async unlock(): Promise { await this.protocolClient.unlock(this.entityId); this._lockState = 'unlocking'; this.emit('state:changed', this.getState()); } /** * Open the lock (physically open the door if supported) */ public async open(): Promise { if (!this.capabilities.supportsOpen) { throw new Error('Lock does not support physical open'); } await this.protocolClient.open(this.entityId); this._lockState = 'unlocked'; this._isLocked = false; this.emit('state:changed', this.getState()); } /** * Get current state as object */ public getState(): ILockStateInfo { return { state: this._lockState, isLocked: this._isLocked, }; } /** * 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: ILockStateInfo): void { this.updateStateInternal(state); this.emit('state:changed', state); } /** * Internal state update */ private updateStateInternal(state: ILockStateInfo): void { this._lockState = state.state; this._isLocked = state.isLocked; } // ============================================================================ // Feature Info // ============================================================================ public getFeatureInfo(): ILockFeatureInfo { return { ...this.getBaseFeatureInfo(), type: 'lock', protocol: this.protocol, capabilities: this.capabilities, currentState: this.getState(), }; } }