/* Copyright(C) 2022-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. * * thermostats.ts: homebridge-firstalert. */ import type { CharacteristicValue, PlatformAccessory, Service } from 'homebridge' // import { request } from 'undici'; import { interval, Subject } from 'rxjs' import { skipWhile } from 'rxjs/operators' import type { ResideoDevice } from '../api/resideoClient.js' import type { ResideoPlatform } from '../platform.js' import { ResideoModes } from '../utils.js' import { deviceBase } from './device.js' /** * Platform Accessory * An instance of this class is created for each accessory your platform registers * Each accessory may expose multiple services of different service types. */ export class Thermostats extends deviceBase { // Services private Thermostat: { Name: CharacteristicValue Service: Service TargetTemperature: CharacteristicValue CurrentTemperature: CharacteristicValue TemperatureDisplayUnits: CharacteristicValue TargetHeatingCoolingState: CharacteristicValue CurrentHeatingCoolingState: CharacteristicValue CoolingThresholdTemperature: CharacteristicValue HeatingThresholdTemperature: CharacteristicValue } // State private state: any = {} private updateInProgress = false private doUpdate = new Subject() constructor( readonly platform: ResideoPlatform, accessory: PlatformAccessory, device: ResideoDevice, ) { super(platform, accessory, device) // Initialize Thermostat Service accessory.context.Thermostat = accessory.context.Thermostat ?? {} this.Thermostat = { Name: accessory.context.Thermostat.Name ?? accessory.displayName, Service: accessory.getService(this.hap.Service.Thermostat) ?? this.accessory.addService(this.hap.Service.Thermostat) as Service, TargetTemperature: accessory.context.TargetTemperature ?? 20, CurrentTemperature: accessory.context.CurrentTemperature ?? 20, TemperatureDisplayUnits: accessory.context.TemperatureDisplayUnits ?? this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS, TargetHeatingCoolingState: accessory.context.TargetHeatingCoolingState ?? this.hap.Characteristic.TargetHeatingCoolingState.AUTO, CurrentHeatingCoolingState: accessory.context.CurrentHeatingCoolingState ?? this.hap.Characteristic.CurrentHeatingCoolingState.OFF, CoolingThresholdTemperature: accessory.context.CoolingThresholdTemperature ?? 20, HeatingThresholdTemperature: accessory.context.HeatingThresholdTemperature ?? 22, } accessory.context.Thermostat = this.Thermostat as object // Service Name this.Thermostat.Service .setCharacteristic(this.hap.Characteristic.Name, this.Thermostat.Name) .setCharacteristic(this.hap.Characteristic.CurrentHeatingCoolingState, this.Thermostat.CurrentHeatingCoolingState) .getCharacteristic(this.hap.Characteristic.TemperatureDisplayUnits) .onGet(() => this.Thermostat.TemperatureDisplayUnits) .onSet(this.setTemperatureDisplayUnits.bind(this)) // Set Min and Max (static for now, as ResideoDevice does not provide setpoints) this.Thermostat.Service .getCharacteristic(this.hap.Characteristic.TargetTemperature) .setProps({ minValue: 10, maxValue: 32, minStep: 0.5, }) .onGet(() => this.Thermostat.TargetTemperature) // Only allow AUTO, HEAT, COOL, OFF this.Thermostat.Service .getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState) .setProps({ validValues: [ this.hap.Characteristic.TargetHeatingCoolingState.OFF, this.hap.Characteristic.TargetHeatingCoolingState.HEAT, this.hap.Characteristic.TargetHeatingCoolingState.COOL, this.hap.Characteristic.TargetHeatingCoolingState.AUTO, ], }) .onGet(() => this.Thermostat.TargetHeatingCoolingState) .onSet(this.setTargetHeatingCoolingState.bind(this)) this.Thermostat.Service .getCharacteristic(this.hap.Characteristic.HeatingThresholdTemperature) .onGet(() => this.Thermostat.HeatingThresholdTemperature) .onSet(this.setHeatingThresholdTemperature.bind(this)) this.Thermostat.Service .getCharacteristic(this.hap.Characteristic.CoolingThresholdTemperature) .onGet(() => this.Thermostat.CoolingThresholdTemperature) .onSet(this.setCoolingThresholdTemperature.bind(this)) this.Thermostat.Service .getCharacteristic(this.hap.Characteristic.TargetTemperature) .onGet(() => this.Thermostat.TargetTemperature) .onSet(this.setTargetTemperature.bind(this)) // Fan and humidity services omitted for new ResideoDevice structure (add if API provides state) // Humidity service omitted for new ResideoDevice structure (add if API provides state) // StatefulProgrammableSwitch omitted for new ResideoDevice structure // Initial refresh this.refreshStatus() interval(120 * 1000) .pipe(skipWhile(() => this.updateInProgress)) .subscribe(async () => { await this.refreshStatus() }) } /** * Parse the device status from the FirstAlert api */ async parseStatus(): Promise { // Example: parse state from API response (simulate for now) // In a real implementation, fetch and parse state from the API // For now, just set some dummy values this.Thermostat.CurrentTemperature = 21 this.Thermostat.TargetTemperature = 21 this.Thermostat.TemperatureDisplayUnits = this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS this.Thermostat.TargetHeatingCoolingState = this.hap.Characteristic.TargetHeatingCoolingState.AUTO this.Thermostat.CurrentHeatingCoolingState = this.hap.Characteristic.CurrentHeatingCoolingState.OFF this.Thermostat.HeatingThresholdTemperature = 20 this.Thermostat.CoolingThresholdTemperature = 24 } /** * Asks the FirstAlert Home API for the latest device information */ async refreshStatus(): Promise { try { const deviceId = String(this.device.deviceId) const deviceState = await this.platform.client.getDeviceState(deviceId) this.state = deviceState.deviceState || {} await this.parseStatus() await this.updateHomeKitCharacteristics() } catch (e: any) { const action = 'refreshStatus' this.resideoAPIError(e, action) } } // ...existing code... /** * Pushes the requested changes to the FirstAlert API */ async pushChanges(): Promise { // Placeholder for sending state changes to the API this.debugLog('pushChanges called (not implemented)') } async ResideoMode() { switch (this.Thermostat.TargetHeatingCoolingState) { case this.hap.Characteristic.TargetHeatingCoolingState.HEAT: return ResideoModes.Heat case this.hap.Characteristic.TargetHeatingCoolingState.COOL: return ResideoModes.Cool case this.hap.Characteristic.TargetHeatingCoolingState.AUTO: return ResideoModes.Auto case this.hap.Characteristic.TargetHeatingCoolingState.OFF: return ResideoModes.Off default: return 'Unknown' } } /** * Updates the status for each of the HomeKit Characteristics */ async updateHomeKitCharacteristics(): Promise { this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.TemperatureDisplayUnits, this.Thermostat.TemperatureDisplayUnits) this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, this.Thermostat.CurrentTemperature) this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.TargetTemperature, this.Thermostat.TargetTemperature) this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.HeatingThresholdTemperature, this.Thermostat.HeatingThresholdTemperature) this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.CoolingThresholdTemperature, this.Thermostat.CoolingThresholdTemperature) this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState, this.Thermostat.TargetHeatingCoolingState) this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.CurrentHeatingCoolingState, this.Thermostat.CurrentHeatingCoolingState) } async apiError(e: any): Promise { this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.TemperatureDisplayUnits, e) this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, e) this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.TargetTemperature, e) this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.HeatingThresholdTemperature, e) this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.CoolingThresholdTemperature, e) this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState, e) this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.CurrentHeatingCoolingState, e) } // ...existing code... async setTargetHeatingCoolingState(value: CharacteristicValue): Promise { this.Thermostat.TargetHeatingCoolingState = value // Example: update TargetTemperature based on mode (simulate for now) this.Thermostat.TargetTemperature = 21 this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.TargetTemperature, this.Thermostat.TargetTemperature) // In a real implementation, trigger API update here } async setHeatingThresholdTemperature(value: CharacteristicValue): Promise { this.Thermostat.HeatingThresholdTemperature = value // In a real implementation, trigger API update here } async setCoolingThresholdTemperature(value: CharacteristicValue): Promise { this.Thermostat.CoolingThresholdTemperature = value // In a real implementation, trigger API update here } async setTargetTemperature(value: CharacteristicValue): Promise { this.Thermostat.TargetTemperature = value // In a real implementation, trigger API update here } async setTemperatureDisplayUnits(): Promise { // Changing hardware display units from HomeKit is not supported this.warnLog('Changing the Hardware Display Units from HomeKit is not supported.') // Always revert to Celsius for now this.Thermostat.TemperatureDisplayUnits = this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS setTimeout(() => { this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.TemperatureDisplayUnits, this.Thermostat.TemperatureDisplayUnits) }, 100) } // ...existing code... // ...existing code... // ...existing code... }