Files
devicemanager/ts/features/feature.climate.ts

408 lines
11 KiB
TypeScript
Raw Permalink Normal View History

/**
* Climate Feature
* Provides control for thermostats and HVAC systems
*/
import { Feature, type TDeviceReference } from './feature.abstract.js';
import type { IFeatureOptions } from '../interfaces/feature.interfaces.js';
import type {
TClimateProtocol,
THvacMode,
THvacAction,
IClimateCapabilities,
IClimateState,
IClimateFeatureInfo,
IClimateProtocolClient,
} from '../interfaces/smarthome.interfaces.js';
/**
* Options for creating a ClimateFeature
*/
export interface IClimateFeatureOptions extends IFeatureOptions {
/** Protocol type */
protocol: TClimateProtocol;
/** Entity ID (for Home Assistant) */
entityId: string;
/** Protocol client for controlling the climate device */
protocolClient: IClimateProtocolClient;
/** Climate capabilities */
capabilities?: Partial<IClimateCapabilities>;
}
/**
* Climate Feature - thermostat and HVAC control
*
* Protocol-agnostic: works with Home Assistant, Nest, Ecobee, MQTT, etc.
*
* @example
* ```typescript
* const climate = device.getFeature<ClimateFeature>('climate');
* if (climate) {
* await climate.setHvacMode('heat');
* await climate.setTargetTemp(21);
* console.log(`Current: ${climate.currentTemp}°C, Target: ${climate.targetTemp}°C`);
* }
* ```
*/
export class ClimateFeature extends Feature {
public readonly type = 'climate' as const;
public readonly protocol: TClimateProtocol;
/** Entity ID (e.g., "climate.living_room") */
public readonly entityId: string;
/** Capabilities */
public readonly capabilities: IClimateCapabilities;
/** Current state */
protected _currentTemp?: number;
protected _targetTemp?: number;
protected _targetTempHigh?: number;
protected _targetTempLow?: number;
protected _hvacMode: THvacMode = 'off';
protected _hvacAction?: THvacAction;
protected _presetMode?: string;
protected _fanMode?: string;
protected _swingMode?: string;
protected _humidity?: number;
protected _targetHumidity?: number;
protected _auxHeat?: boolean;
/** Protocol client for controlling the climate device */
private protocolClient: IClimateProtocolClient;
constructor(
device: TDeviceReference,
port: number,
options: IClimateFeatureOptions
) {
super(device, port, options);
this.protocol = options.protocol;
this.entityId = options.entityId;
this.protocolClient = options.protocolClient;
this.capabilities = {
hvacModes: options.capabilities?.hvacModes ?? ['off', 'heat', 'cool', 'auto'],
presetModes: options.capabilities?.presetModes,
fanModes: options.capabilities?.fanModes,
swingModes: options.capabilities?.swingModes,
supportsTargetTemp: options.capabilities?.supportsTargetTemp ?? true,
supportsTargetTempRange: options.capabilities?.supportsTargetTempRange ?? false,
supportsHumidity: options.capabilities?.supportsHumidity ?? false,
supportsAuxHeat: options.capabilities?.supportsAuxHeat ?? false,
minTemp: options.capabilities?.minTemp ?? 7,
maxTemp: options.capabilities?.maxTemp ?? 35,
tempStep: options.capabilities?.tempStep ?? 0.5,
minHumidity: options.capabilities?.minHumidity,
maxHumidity: options.capabilities?.maxHumidity,
};
}
// ============================================================================
// Properties
// ============================================================================
/**
* Get current temperature (cached)
*/
public get currentTemp(): number | undefined {
return this._currentTemp;
}
/**
* Get target temperature (cached)
*/
public get targetTemp(): number | undefined {
return this._targetTemp;
}
/**
* Get target temperature high (for heat_cool mode, cached)
*/
public get targetTempHigh(): number | undefined {
return this._targetTempHigh;
}
/**
* Get target temperature low (for heat_cool mode, cached)
*/
public get targetTempLow(): number | undefined {
return this._targetTempLow;
}
/**
* Get current HVAC mode (cached)
*/
public get hvacMode(): THvacMode {
return this._hvacMode;
}
/**
* Get current HVAC action (cached)
*/
public get hvacAction(): THvacAction | undefined {
return this._hvacAction;
}
/**
* Get current preset mode (cached)
*/
public get presetMode(): string | undefined {
return this._presetMode;
}
/**
* Get current fan mode (cached)
*/
public get fanMode(): string | undefined {
return this._fanMode;
}
/**
* Get current swing mode (cached)
*/
public get swingMode(): string | undefined {
return this._swingMode;
}
/**
* Get current humidity (cached)
*/
public get humidity(): number | undefined {
return this._humidity;
}
/**
* Get target humidity (cached)
*/
public get targetHumidity(): number | undefined {
return this._targetHumidity;
}
/**
* Get aux heat state (cached)
*/
public get auxHeat(): boolean | undefined {
return this._auxHeat;
}
/**
* Get available HVAC modes
*/
public get hvacModes(): THvacMode[] {
return this.capabilities.hvacModes;
}
/**
* Get available preset modes
*/
public get presetModes(): string[] | undefined {
return this.capabilities.presetModes;
}
/**
* Get available fan modes
*/
public get fanModes(): string[] | undefined {
return this.capabilities.fanModes;
}
// ============================================================================
// 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
}
// ============================================================================
// Climate Control
// ============================================================================
/**
* Set HVAC mode
* @param mode HVAC mode (off, heat, cool, etc.)
*/
public async setHvacMode(mode: THvacMode): Promise<void> {
if (!this.capabilities.hvacModes.includes(mode)) {
throw new Error(`HVAC mode ${mode} not supported`);
}
await this.protocolClient.setHvacMode(this.entityId, mode);
this._hvacMode = mode;
this.emit('state:changed', this.getState());
}
/**
* Set target temperature
* @param temp Target temperature
*/
public async setTargetTemp(temp: number): Promise<void> {
if (!this.capabilities.supportsTargetTemp) {
throw new Error('Climate device does not support target temperature');
}
const clamped = Math.max(
this.capabilities.minTemp,
Math.min(this.capabilities.maxTemp, temp)
);
await this.protocolClient.setTargetTemp(this.entityId, clamped);
this._targetTemp = clamped;
this.emit('state:changed', this.getState());
}
/**
* Set target temperature range (for heat_cool mode)
* @param low Low temperature
* @param high High temperature
*/
public async setTargetTempRange(low: number, high: number): Promise<void> {
if (!this.capabilities.supportsTargetTempRange) {
throw new Error('Climate device does not support temperature range');
}
const clampedLow = Math.max(this.capabilities.minTemp, Math.min(this.capabilities.maxTemp, low));
const clampedHigh = Math.max(this.capabilities.minTemp, Math.min(this.capabilities.maxTemp, high));
await this.protocolClient.setTargetTempRange(this.entityId, clampedLow, clampedHigh);
this._targetTempLow = clampedLow;
this._targetTempHigh = clampedHigh;
this.emit('state:changed', this.getState());
}
/**
* Set preset mode
* @param preset Preset mode name
*/
public async setPresetMode(preset: string): Promise<void> {
if (!this.capabilities.presetModes?.includes(preset)) {
throw new Error(`Preset mode ${preset} not supported`);
}
await this.protocolClient.setPresetMode(this.entityId, preset);
this._presetMode = preset;
this.emit('state:changed', this.getState());
}
/**
* Set fan mode
* @param mode Fan mode name
*/
public async setFanMode(mode: string): Promise<void> {
if (!this.capabilities.fanModes?.includes(mode)) {
throw new Error(`Fan mode ${mode} not supported`);
}
await this.protocolClient.setFanMode(this.entityId, mode);
this._fanMode = mode;
this.emit('state:changed', this.getState());
}
/**
* Set swing mode
* @param mode Swing mode name
*/
public async setSwingMode(mode: string): Promise<void> {
if (!this.capabilities.swingModes?.includes(mode)) {
throw new Error(`Swing mode ${mode} not supported`);
}
await this.protocolClient.setSwingMode(this.entityId, mode);
this._swingMode = mode;
this.emit('state:changed', this.getState());
}
/**
* Set aux heat
* @param enabled Whether aux heat is enabled
*/
public async setAuxHeat(enabled: boolean): Promise<void> {
if (!this.capabilities.supportsAuxHeat) {
throw new Error('Climate device does not support aux heat');
}
await this.protocolClient.setAuxHeat(this.entityId, enabled);
this._auxHeat = enabled;
this.emit('state:changed', this.getState());
}
/**
* Get current state as object
*/
public getState(): IClimateState {
return {
currentTemp: this._currentTemp,
targetTemp: this._targetTemp,
targetTempHigh: this._targetTempHigh,
targetTempLow: this._targetTempLow,
hvacMode: this._hvacMode,
hvacAction: this._hvacAction,
presetMode: this._presetMode,
fanMode: this._fanMode,
swingMode: this._swingMode,
humidity: this._humidity,
targetHumidity: this._targetHumidity,
auxHeat: this._auxHeat,
};
}
/**
* Refresh state from the device
*/
public async refreshState(): Promise<IClimateState> {
const state = await this.protocolClient.getState(this.entityId);
this.updateStateInternal(state);
return state;
}
/**
* Update state from external source
*/
public updateState(state: IClimateState): void {
this.updateStateInternal(state);
this.emit('state:changed', state);
}
/**
* Internal state update
*/
private updateStateInternal(state: IClimateState): void {
this._currentTemp = state.currentTemp;
this._targetTemp = state.targetTemp;
this._targetTempHigh = state.targetTempHigh;
this._targetTempLow = state.targetTempLow;
this._hvacMode = state.hvacMode;
this._hvacAction = state.hvacAction;
this._presetMode = state.presetMode;
this._fanMode = state.fanMode;
this._swingMode = state.swingMode;
this._humidity = state.humidity;
this._targetHumidity = state.targetHumidity;
this._auxHeat = state.auxHeat;
}
// ============================================================================
// Feature Info
// ============================================================================
public getFeatureInfo(): IClimateFeatureInfo {
return {
...this.getBaseFeatureInfo(),
type: 'climate',
protocol: this.protocol,
capabilities: this.capabilities,
currentState: this.getState(),
};
}
}