224 lines
5.4 KiB
TypeScript
224 lines
5.4 KiB
TypeScript
|
|
/**
|
||
|
|
* 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<LockFeature>('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<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
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// Lock Control
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Lock the lock
|
||
|
|
*/
|
||
|
|
public async lock(): Promise<void> {
|
||
|
|
await this.protocolClient.lock(this.entityId);
|
||
|
|
this._lockState = 'locking';
|
||
|
|
this.emit('state:changed', this.getState());
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Unlock the lock
|
||
|
|
*/
|
||
|
|
public async unlock(): Promise<void> {
|
||
|
|
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<void> {
|
||
|
|
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<ILockStateInfo> {
|
||
|
|
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(),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|