UNPKG

homebridge-nest-accfactory

Version:

Homebridge support for Nest/Google devices including HomeKit Secure Video (HKSV) support for doorbells and cameras

1,030 lines (941 loc) 50.9 kB
// Nest Thermostat // Part of homebridge-nest-accfactory // // Code version 15/10/2024 // Mark Hulskamp 'use strict'; // Define our modules import HomeKitDevice from './HomeKitDevice.js'; // Define nodejs module requirements import path from 'node:path'; const LOWBATTERYLEVEL = 10; // Low battery level percentage const MIN_TEMPERATURE = 9; // Minimum temperature for Nest Thermostat const MAX_TEMPERATURE = 32; // Maximum temperature for Nest Thermostat export default class NestThermostat extends HomeKitDevice { batteryService = undefined; occupancyService = undefined; humidityService = undefined; fanService = undefined; dehumidifierService = undefined; externalCool = undefined; // External module function externalHeat = undefined; // External module function externalFan = undefined; // External module function externalDehumidifier = undefined; // External module function constructor(accessory, api, log, eventEmitter, deviceData) { super(accessory, api, log, eventEmitter, deviceData); } // Class functions async addServices() { // Setup the thermostat service if not already present on the accessory this.thermostatService = this.accessory.getService(this.hap.Service.Thermostat); if (this.thermostatService === undefined) { this.thermostatService = this.accessory.addService(this.hap.Service.Thermostat, '', 1); } this.thermostatService.setPrimaryService(); if (this.thermostatService.testCharacteristic(this.hap.Characteristic.StatusActive) === false) { // Used to indicate active temperature if the thermostat is using its temperature sensor data // or an external temperature sensor ie: Nest Temperature Sensor this.thermostatService.addCharacteristic(this.hap.Characteristic.StatusActive); } if (this.thermostatService.testCharacteristic(this.hap.Characteristic.StatusFault) === false) { this.thermostatService.addCharacteristic(this.hap.Characteristic.StatusFault); } if (this.thermostatService.testCharacteristic(this.hap.Characteristic.LockPhysicalControls) === false) { // Setting can only be accessed via Eve App (or other 3rd party). this.thermostatService.addCharacteristic(this.hap.Characteristic.LockPhysicalControls); } if ( this.deviceData?.has_air_filter === true && this.thermostatService.testCharacteristic(this.hap.Characteristic.FilterChangeIndication) === false ) { // Setup air filter change characteristic this.thermostatService.addCharacteristic(this.hap.Characteristic.FilterChangeIndication); } if ( this.deviceData?.has_air_filter === false && this.thermostatService.testCharacteristic(this.hap.Characteristic.FilterChangeIndication) === true ) { // No longer configured to have an air filter, so remove characteristic from the accessory this.thermostatService.removeCharacteristic(this.hap.Characteristic.FilterChangeIndication); } if ( this.deviceData?.has_humidifier === true && this.thermostatService.testCharacteristic(this.hap.Characteristic.TargetRelativeHumidity) === false ) { // We have the capability for a humidifier, so setup target humidity characterisitc this.thermostatService.addCharacteristic(this.hap.Characteristic.TargetRelativeHumidity); } if ( this.deviceData?.has_humidifier === false && this.thermostatService.testCharacteristic(this.hap.Characteristic.TargetRelativeHumidity) === true ) { // No longer configured to use a humdifier, so remove characteristic from the accessory this.thermostatService.removeCharacteristic(this.hap.Characteristic.TargetRelativeHumidity); } if (this.thermostatService.testCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity) === false) { this.thermostatService.addCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity); } // Limit prop ranges if (this.deviceData?.can_cool === false && this.deviceData?.can_heat === true) { // Can heat only, so set values allowed for mode off/heat this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState).setProps({ validValues: [this.hap.Characteristic.TargetHeatingCoolingState.OFF, this.hap.Characteristic.TargetHeatingCoolingState.HEAT], }); } if (this.deviceData?.can_cool === true && this.deviceData?.can_heat === false) { // Can cool only this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState).setProps({ validValues: [this.hap.Characteristic.TargetHeatingCoolingState.OFF, this.hap.Characteristic.TargetHeatingCoolingState.COOL], }); } if (this.deviceData?.can_cool === true && this.deviceData?.can_heat === true) { // heat and cool this.thermostatService.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, ], }); } if (this.deviceData?.can_cool === false && this.deviceData?.can_heat === false) { // only off mode this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState).setProps({ validValues: [this.hap.Characteristic.TargetHeatingCoolingState.OFF], }); } // Set default ranges - based on celsuis ranges to which the Nest Thermostat operates this.thermostatService.getCharacteristic(this.hap.Characteristic.CurrentTemperature).setProps({ minStep: 0.5, }); this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetTemperature).setProps({ minStep: 0.5, minValue: MIN_TEMPERATURE, maxValue: MAX_TEMPERATURE, }); this.thermostatService.getCharacteristic(this.hap.Characteristic.CoolingThresholdTemperature).setProps({ minStep: 0.5, minValue: MIN_TEMPERATURE, maxValue: MAX_TEMPERATURE, }); this.thermostatService.getCharacteristic(this.hap.Characteristic.HeatingThresholdTemperature).setProps({ minStep: 0.5, minValue: MIN_TEMPERATURE, maxValue: MAX_TEMPERATURE, }); // Setup callbacks for characteristics this.thermostatService.getCharacteristic(this.hap.Characteristic.TemperatureDisplayUnits).onSet((value) => { this.setDisplayUnit(value); }); this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState).onSet((value) => { this.setMode(value); }); this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetTemperature).onSet((value) => { this.setTemperature(this.hap.Characteristic.TargetTemperature, value); }); this.thermostatService.getCharacteristic(this.hap.Characteristic.CoolingThresholdTemperature).onSet((value) => { this.setTemperature(this.hap.Characteristic.CoolingThresholdTemperature, value); }); this.thermostatService.getCharacteristic(this.hap.Characteristic.HeatingThresholdTemperature).onSet((value) => { this.setTemperature(this.hap.Characteristic.HeatingThresholdTemperature, value); }); this.thermostatService.getCharacteristic(this.hap.Characteristic.LockPhysicalControls).onSet((value) => { this.setChildlock('', value); }); this.thermostatService.getCharacteristic(this.hap.Characteristic.TemperatureDisplayUnits).onGet(() => { return this.deviceData.temperature_scale === 'C' ? this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS : this.hap.Characteristic.TemperatureDisplayUnits.FAHRENHEIT; }); this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetTemperature).onGet(() => { return this.getTemperature(this.hap.Characteristic.TargetTemperature); }); this.thermostatService.getCharacteristic(this.hap.Characteristic.CoolingThresholdTemperature).onGet(() => { return this.getTemperature(this.hap.Characteristic.CoolingThresholdTemperature); }); this.thermostatService.getCharacteristic(this.hap.Characteristic.HeatingThresholdTemperature).onGet(() => { return this.getTemperature(this.hap.Characteristic.HeatingThresholdTemperature); }); this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState).onGet(() => { return this.getMode(); }); this.thermostatService.getCharacteristic(this.hap.Characteristic.LockPhysicalControls).onGet(() => { return this.deviceData.temperature_lock === true ? this.hap.Characteristic.LockPhysicalControls.CONTROL_LOCK_ENABLED : this.hap.Characteristic.LockPhysicalControls.CONTROL_LOCK_DISABLED; }); // Setup occupancy service if not already present on the accessory this.occupancyService = this.accessory.getService(this.hap.Service.OccupancySensor); if (this.occupancyService === undefined) { this.occupancyService = this.accessory.addService(this.hap.Service.OccupancySensor, '', 1); } this.thermostatService.addLinkedService(this.occupancyService); // Setup battery service if not already present on the accessory this.batteryService = this.accessory.getService(this.hap.Service.Battery); if (this.batteryService === undefined) { this.batteryService = this.accessory.addService(this.hap.Service.Battery, '', 1); } this.batteryService.setHiddenService(true); this.thermostatService.addLinkedService(this.batteryService); // Setup fan service if supported by the thermostat and not already present on the accessory this.fanService = this.accessory.getService(this.hap.Service.Fanv2); if (this.deviceData?.has_fan === true) { if (this.fanService === undefined) { this.fanService = this.accessory.addService(this.hap.Service.Fanv2, '', 1); } if (this.fanService.testCharacteristic(this.hap.Characteristic.RotationSpeed) === false) { this.fanService.addCharacteristic(this.hap.Characteristic.RotationSpeed); } this.fanService.getCharacteristic(this.hap.Characteristic.RotationSpeed).setProps({ minStep: 100 / this.deviceData.fan_max_speed, }); this.thermostatService.addLinkedService(this.fanService); this.fanService.getCharacteristic(this.hap.Characteristic.Active).onSet((value) => { this.setFan( value, value === this.hap.Characteristic.Active.ACTIVE ? (this.deviceData.fan_timer_speed / this.deviceData.fan_max_speed) * 100 : 0, ); }); this.fanService.getCharacteristic(this.hap.Characteristic.RotationSpeed).onSet((value) => { this.setFan(value !== 0 ? this.hap.Characteristic.Active.ACTIVE : this.hap.Characteristic.Active.INACTIVE, value); }); this.fanService.getCharacteristic(this.hap.Characteristic.Active).onGet(() => { return this.deviceData.fan_state === true ? this.hap.Characteristic.Active.ACTIVE : this.hap.Characteristic.Active.INACTIVE; }); this.fanService.getCharacteristic(this.hap.Characteristic.RotationSpeed).onGet(() => { return (this.deviceData.fan_timer_speed / this.deviceData.fan_max_speed) * 100; }); } if (this.deviceData?.has_fan === false && this.fanService !== undefined) { // No longer have a Fan configured and service present, so removed it this.accessory.removeService(this.fanService); this.fanService = undefined; } // Setup dehumifider service if supported by the thermostat and not already present on the accessory this.dehumidifierService = this.accessory.getService(this.hap.Service.HumidifierDehumidifier); if (this.deviceData?.has_dehumidifier === true) { if (this.dehumidifierService === undefined) { this.dehumidifierService = this.accessory.addService(this.hap.Service.HumidifierDehumidifier, '', 1); } this.thermostatService.addLinkedService(this.dehumidifierService); this.dehumidifierService.getCharacteristic(this.hap.Characteristic.TargetHumidifierDehumidifierState).setProps({ validValues: [this.hap.Characteristic.TargetHumidifierDehumidifierState.DEHUMIDIFIER], }); this.dehumidifierService.getCharacteristic(this.hap.Characteristic.Active).onSet((value) => { this.setDehumidifier(value); }); this.dehumidifierService.getCharacteristic(this.hap.Characteristic.Active).onGet(() => { return this.deviceData.dehumidifier_state === true ? this.hap.Characteristic.Active.ACTIVE : this.hap.Characteristic.Active.INACTIVE; }); } if (this.deviceData?.has_dehumidifier === false && this.dehumidifierService !== undefined) { // No longer have a dehumidifier configured and service present, so removed it this.accessory.removeService(this.dehumidifierService); this.dehumidifierService = undefined; } // Setup humdity service if configured to be seperate and not already present on the accessory this.humidityService = this.accessory.getService(this.hap.Service.HumiditySensor); if (this.deviceData?.humiditySensor === true) { if (this.humidityService === undefined) { this.humidityService = this.accessory.addService(this.hap.Service.HumiditySensor, '', 1); } this.thermostatService.addLinkedService(this.humidityService); } if (this.deviceData?.humiditySensor === false && this.humidityService !== undefined) { // No longer have a seperate humidity sensor configure and service present, so removed it this.accessory.removeService(this.humidityService); this.humidityService = undefined; } // Setup linkage to EveHome app if configured todo so if ( this.deviceData?.eveHistory === true && this.thermostatService !== undefined && typeof this.historyService?.linkToEveHome === 'function' ) { this.historyService.linkToEveHome(this.thermostatService, { description: this.deviceData.description, getcommand: this.#EveHomeGetcommand.bind(this), setcommand: this.#EveHomeSetcommand.bind(this), }); } // Attempt to load any external modules for this thermostat // We support external cool/heat/fan/dehumidifier module functions // This is all undocumented on how to use, as its for my specific use case :-) const loadExternalModule = async (module) => { if (typeof module !== 'string' || module === '') { return; } let loadedModule = undefined; try { let values = module.match(/('.*?'|[^' ]+)(?=\s* |\s*$)/g); let script = path.resolve(values[0]); // external library name let options = values.slice(1); // options to be passed into the external library let externalModule = await import(script); if (typeof externalModule?.default === 'function') { loadedModule = externalModule.default(this.log, options); } // eslint-disable-next-line no-unused-vars } catch (error) { this?.log?.warn && this.log.warn('Failed to load specified external module for thermostat "%s"', module); } return loadedModule; }; this.externalCool = await loadExternalModule(this.deviceData?.externalCool); this.externalHeat = await loadExternalModule(this.deviceData?.externalHeat); this.externalFan = await loadExternalModule(this.deviceData?.externalFan); this.externalDehumidifier = await loadExternalModule(this.deviceData?.externalDehumidifier); // Create extra details for output let postSetupDetails = []; this.humidityService !== undefined && postSetupDetails.push('Seperate humidity sensor'); this.externalCool !== undefined && postSetupDetails.push('Using external cooling module'); this.externalHeat !== undefined && postSetupDetails.push('Using external heating module'); this.externalFan !== undefined && postSetupDetails.push('Using external fan module'); this.externalDehumidifier !== undefined && postSetupDetails.push('Using external dehumidification module'); return postSetupDetails; } setFan(fanState, speed) { if ( fanState !== this.fanService.getCharacteristic(this.hap.Characteristic.Active).value || speed !== this.fanService.getCharacteristic(this.hap.Characteristic.RotationSpeed).value ) { this.set({ uuid: this.deviceData.nest_google_uuid, fan_state: fanState === this.hap.Characteristic.Active.ACTIVE ? true : false, fan_timer_speed: Math.round((speed / 100) * this.deviceData.fan_max_speed), }); this.fanService.updateCharacteristic(this.hap.Characteristic.Active, fanState); this.fanService.updateCharacteristic(this.hap.Characteristic.RotationSpeed, speed); this?.log?.info && this.log.info( 'Set fan on thermostat "%s" to "%s"', this.deviceData.description, fanState === this.hap.Characteristic.Active.ACTIVE ? 'On with fan speed of ' + speed + '%' : 'Off', ); } } setDehumidifier(dehumidiferState) { this.set({ uuid: this.deviceData.nest_google_uuid, dehumidifer_State: dehumidiferState === this.hap.Characteristic.Active.ACTIVE ? true : false, }); this.dehumidifierService.updateCharacteristic(this.hap.Characteristic.Active, dehumidiferState); this?.log?.info && this.log.info( 'Set dehumidifer on thermostat "%s" to "%s"', this.deviceData.description, dehumidiferState === this.hap.Characteristic.Active.ACTIVE ? 'On with target humidity level of ' + this.deviceData.target_humidity + '%' : 'Off', ); } setDisplayUnit(temperatureUnit) { this.set({ uuid: this.deviceData.nest_google_uuid, temperature_scale: temperatureUnit === this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS ? 'C' : 'F', }); this.thermostatService.updateCharacteristic(this.hap.Characteristic.TemperatureDisplayUnits, temperatureUnit); this?.log?.info && this.log.info( 'Set temperature units on thermostat "%s" to "%s"', this.deviceData.description, temperatureUnit === this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS ? '°C' : '°F', ); } setMode(thermostatMode) { if (thermostatMode !== this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState).value) { // Work out based on the HomeKit requested mode, what can the thermostat really switch too // We may over-ride the requested HomeKit mode if (thermostatMode === this.hap.Characteristic.TargetHeatingCoolingState.HEAT && this.deviceData.can_heat === false) { thermostatMode = this.hap.Characteristic.TargetHeatingCoolingState.OFF; } if (thermostatMode === this.hap.Characteristic.TargetHeatingCoolingState.COOL && this.deviceData.can_cool === false) { thermostatMode = this.hap.Characteristic.TargetHeatingCoolingState.OFF; } if (thermostatMode === this.hap.Characteristic.TargetHeatingCoolingState.AUTO) { // Workaround for 'Hey Siri, turn on my thermostat' // Appears to automatically request mode as 'auto', but we need to see what Nest device supports if (this.deviceData.can_cool === true && this.deviceData.can_heat === false) { thermostatMode = this.hap.Characteristic.TargetHeatingCoolingState.COOL; } if (this.deviceData.can_cool === false && this.deviceData.can_heat === true) { thermostatMode = this.hap.Characteristic.TargetHeatingCoolingState.HEAT; } if (this.deviceData.can_cool === false && this.deviceData.can_heat === false) { thermostatMode = this.hap.Characteristic.TargetHeatingCoolingState.OFF; } } let mode = ''; if (thermostatMode === this.hap.Characteristic.TargetHeatingCoolingState.OFF) { this.thermostatService.updateCharacteristic(this.hap.Characteristic.TargetTemperature, this.deviceData.target_temperature); this.thermostatService.updateCharacteristic( this.hap.Characteristic.TargetHeatingCoolingState, this.hap.Characteristic.TargetHeatingCoolingState.OFF, ); mode = 'off'; } if (thermostatMode === this.hap.Characteristic.TargetHeatingCoolingState.COOL) { this.thermostatService.updateCharacteristic(this.hap.Characteristic.TargetTemperature, this.deviceData.target_temperature_high); this.thermostatService.updateCharacteristic( this.hap.Characteristic.TargetHeatingCoolingState, this.hap.Characteristic.TargetHeatingCoolingState.COOL, ); mode = 'cool'; } if (thermostatMode === this.hap.Characteristic.TargetHeatingCoolingState.HEAT) { this.thermostatService.updateCharacteristic(this.hap.Characteristic.TargetTemperature, this.deviceData.target_temperature_low); this.thermostatService.updateCharacteristic( this.hap.Characteristic.TargetHeatingCoolingState, this.hap.Characteristic.TargetHeatingCoolingState.HEAT, ); mode = 'heat'; } if (thermostatMode === this.hap.Characteristic.TargetHeatingCoolingState.AUTO) { this.thermostatService.updateCharacteristic( this.hap.Characteristic.TargetTemperature, (this.deviceData.target_temperature_low + this.deviceData.target_temperature_high) * 0.5, ); this.thermostatService.updateCharacteristic( this.hap.Characteristic.TargetHeatingCoolingState, this.hap.Characteristic.TargetHeatingCoolingState.AUTO, ); mode = 'range'; } this.set({ uuid: this.deviceData.nest_google_uuid, hvac_mode: mode }); this?.log?.info && this.log.info('Set mode on "%s" to "%s"', this.deviceData.description, mode); } } getMode() { let currentMode = null; if (this.deviceData.hvac_mode.toUpperCase() === 'HEAT' || this.deviceData.hvac_mode.toUpperCase() === 'ECOHEAT') { // heating mode, either eco or normal; currentMode = this.hap.Characteristic.TargetHeatingCoolingState.HEAT; } if (this.deviceData.hvac_mode.toUpperCase() === 'COOL' || this.deviceData.hvac_mode.toUpperCase() === 'ECOCOOL') { // cooling mode, either eco or normal currentMode = this.hap.Characteristic.TargetHeatingCoolingState.COOL; } if (this.deviceData.hvac_mode.toUpperCase() === 'RANGE' || this.deviceData.hvac_mode.toUpperCase() === 'ECORANGE') { // range mode, either eco or normal currentMode = this.hap.Characteristic.TargetHeatingCoolingState.AUTO; } if (this.deviceData.hvac_mode.toUpperCase() === 'OFF' || (this.deviceData.can_cool === false && this.deviceData.can_heat === false)) { // off mode or no heating or cooling capability currentMode = this.hap.Characteristic.TargetHeatingCoolingState.OFF; } return currentMode; } setTemperature(characteristic, temperature) { if (typeof characteristic === 'function' && typeof characteristic?.UUID === 'string') { if ( characteristic.UUID === this.hap.Characteristic.TargetTemperature.UUID && this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState).value !== this.hap.Characteristic.TargetHeatingCoolingState.AUTO ) { this.set({ uuid: this.deviceData.nest_google_uuid, target_temperature: temperature }); this?.log?.info && this.log.info( 'Set %s%s temperature on "%s" to "%s °C"', this.deviceData.hvac_mode.toUpperCase().includes('ECO') ? 'eco mode ' : '', this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState).value === this.hap.Characteristic.TargetHeatingCoolingState.HEAT ? 'heating' : 'cooling', this.deviceData.description, temperature, ); } if ( characteristic.UUID === this.hap.Characteristic.HeatingThresholdTemperature.UUID && this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState).value === this.hap.Characteristic.TargetHeatingCoolingState.AUTO ) { this.set({ uuid: this.deviceData.nest_google_uuid, target_temperature_low: temperature }); this?.log?.info && this.log.info( 'Set %sheating temperature on "%s" to "%s °C"', this.deviceData.hvac_mode.toUpperCase().includes('ECO') ? 'eco mode ' : '', this.deviceData.description, temperature, ); } if ( characteristic.UUID === this.hap.Characteristic.CoolingThresholdTemperature.UUID && this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState).value === this.hap.Characteristic.TargetHeatingCoolingState.AUTO ) { this.set({ uuid: this.deviceData.nest_google_uuid, target_temperature_high: temperature }); this?.log?.info && this.log.info( 'Set %scooling temperature on "%s" to "%s °C"', this.deviceData.hvac_mode.toUpperCase().includes('ECO') ? 'eco mode ' : '', this.deviceData.description, temperature, ); } this.thermostatService.updateCharacteristic(characteristic, temperature); // Update HomeKit with value } } getTemperature(characteristic) { let currentTemperature = null; if (typeof characteristic === 'function' && typeof characteristic?.UUID === 'string') { if (characteristic.UUID === this.hap.Characteristic.TargetTemperature.UUID) { currentTemperature = this.deviceData.target_temperature; } if (characteristic.UUID === this.hap.Characteristic.HeatingThresholdTemperature.UUID) { currentTemperature = this.deviceData.target_temperature_low; } if (characteristic.UUID === this.hap.Characteristic.CoolingThresholdTemperature.UUID) { currentTemperature = this.deviceData.target_temperature_high; } if (currentTemperature < MIN_TEMPERATURE) { currentTemperature = MIN_TEMPERATURE; } if (currentTemperature > MAX_TEMPERATURE) { currentTemperature = MAX_TEMPERATURE; } } return currentTemperature; } setChildlock(pin, value) { // TODO - pincode setting when turning on. // On REST API, writes to device.xxxxxxxx.temperature_lock_pin_hash. How is the hash calculated??? // Do we set temperature range limits when child lock on?? this.thermostatService.updateCharacteristic(this.hap.Characteristic.LockPhysicalControls, value); // Update HomeKit with value if (value === this.hap.Characteristic.LockPhysicalControls.CONTROL_LOCK_ENABLED) { // Set pin hash???? } if (value === this.hap.Characteristic.LockPhysicalControls.CONTROL_LOCK_DISABLED) { // Clear pin hash???? } this.set({ uuid: this.deviceData.nest_google_uuid, temperature_lock: value === this.hap.Characteristic.LockPhysicalControls.CONTROL_LOCK_ENABLED ? true : false, }); this?.log?.info && this.log.info( 'Setting Childlock on "%s" to "%s"', this.deviceData.description, value === this.hap.Characteristic.LockPhysicalControls.CONTROL_LOCK_ENABLED ? 'Enabled' : 'Disabled', ); } updateServices(deviceData) { if ( typeof deviceData !== 'object' || this.thermostatService === undefined || this.batteryService === undefined || this.occupancyService === undefined ) { return; } let historyEntry = {}; this.thermostatService.updateCharacteristic( this.hap.Characteristic.TemperatureDisplayUnits, deviceData.temperature_scale.toUpperCase() === 'C' ? this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS : this.hap.Characteristic.TemperatureDisplayUnits.FAHRENHEIT, ); this.thermostatService.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, deviceData.current_temperature); // If thermostat isn't online or removed from base, report in HomeKit this.thermostatService.updateCharacteristic( this.hap.Characteristic.StatusFault, deviceData.online === true && deviceData.removed_from_base === false ? this.hap.Characteristic.StatusFault.NO_FAULT : this.hap.Characteristic.StatusFault.GENERAL_FAULT, ); this.thermostatService.updateCharacteristic( this.hap.Characteristic.LockPhysicalControls, deviceData.temperature_lock === true ? this.hap.Characteristic.LockPhysicalControls.CONTROL_LOCK_ENABLED : this.hap.Characteristic.LockPhysicalControls.CONTROL_LOCK_DISABLED, ); // Update air filter sttaus if has been added if (this.thermostatService.testCharacteristic(this.hap.Characteristic.FilterChangeIndication) === true) { this.thermostatService.updateCharacteristic( this.hap.Characteristic.FilterChangeIndication, deviceData.has_air_filter && deviceData.filter_replacement_needed === true ? this.hap.Characteristic.FilterChangeIndication.CHANGE_FILTER : this.hap.Characteristic.FilterChangeIndication.FILTER_OK, ); } // Using a temperature sensor as active temperature? // Probably not the best way for HomeKit, but works ;-) // Maybe a custom characteristic would be better? this.thermostatService.updateCharacteristic(this.hap.Characteristic.StatusActive, deviceData.active_rcs_sensor === ''); // Update battery status this.batteryService.updateCharacteristic(this.hap.Characteristic.BatteryLevel, deviceData.battery_level); this.batteryService.updateCharacteristic( this.hap.Characteristic.StatusLowBattery, deviceData.battery_level > LOWBATTERYLEVEL ? this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL : this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW, ); this.batteryService.updateCharacteristic( this.hap.Characteristic.ChargingState, (deviceData.battery_level > this.deviceData.battery_level && this.deviceData.battery_level !== 0 ? true : false) ? this.hap.Characteristic.ChargingState.CHARGING : this.hap.Characteristic.ChargingState.NOT_CHARGING, ); // Update for away/home status. Away = no occupancy detected, Home = Occupancy Detected this.occupancyService.updateCharacteristic( this.hap.Characteristic.OccupancyDetected, deviceData.occupancy === true ? this.hap.Characteristic.OccupancyDetected.OCCUPANCY_DETECTED : this.hap.Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED, ); // Update seperate humidity sensor if configured todo so if (this.humidityService !== undefined) { this.humidityService.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, deviceData.current_humidity); } // Update humity on thermostat this.thermostatService.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, deviceData.current_humidity); // Check for fan setup change on thermostat if (deviceData.has_fan !== this.deviceData.has_fan) { if (deviceData.has_fan === true && this.deviceData.has_fan === false && this.fanService === undefined) { // Fan has been added this.fanService = this.accessory.addService(this.hap.Service.Fanv2, '', 1); if (this.fanService.testCharacteristic(this.hap.Characteristic.RotationSpeed) === false) { this.fanService.addCharacteristic(this.hap.Characteristic.RotationSpeed); } this.fanService.getCharacteristic(this.hap.Characteristic.RotationSpeed).setProps({ minStep: 100 / this.deviceData.fan_max_speed, }); this.thermostatService.addLinkedService(this.fanService); this.fanService.getCharacteristic(this.hap.Characteristic.Active).onSet((value) => { this.setFan(value); }); this.fanService.getCharacteristic(this.hap.Characteristic.Active).onGet(() => { return this.deviceData.fan_state === true ? this.hap.Characteristic.Active.ACTIVE : this.hap.Characteristic.Active.INACTIVE; }); } if (deviceData.has_fan === false && this.deviceData.has_fan === true && this.fanService !== undefined) { // Fan has been removed this.accessory.removeService(this.fanService); this.fanService = undefined; } this?.log?.info && this.log.info( 'Fan setup on thermostat "%s" has changed. Fan was', deviceData.description, this.fanService === undefined ? 'removed' : 'added', ); } // Check for dehumidifer setup change on thermostat if (deviceData.has_dehumidifier !== this.deviceData.has_dehumidifier) { if (deviceData.has_dehumidifier === true && this.deviceData.has_dehumidifier === false && this.dehumidifierService === undefined) { // Dehumidifier has been added this.dehumidifierService = this.accessory.addService(this.hap.Service.HumidifierDehumidifier, '', 1); this.thermostatService.addLinkedService(this.dehumidifierService); this.dehumidifierService.getCharacteristic(this.hap.Characteristic.TargetHumidifierDehumidifierState).setProps({ validValues: [this.hap.Characteristic.TargetHumidifierDehumidifierState.DEHUMIDIFIER], }); this.dehumidifierService.getCharacteristic(this.hap.Characteristic.Active).onSet((value) => { this.setDehumidifier(value); }); this.dehumidifierService.getCharacteristic(this.hap.Characteristic.Active).onGet(() => { return this.deviceData.dehumidifier_state === true ? this.hap.Characteristic.Active.ACTIVE : this.hap.Characteristic.Active.INACTIVE; }); } if (deviceData.has_dehumidifier === false && this.deviceData.has_dehumidifier === true && this.dehumidifierService !== undefined) { // Dehumidifer has been removed this.accessory.removeService(this.dehumidifierService); this.dehumidifierService = undefined; } this?.log?.info && this.log.info( 'Dehumidifier setup on thermostat "%s" has changed. Dehumidifier was', deviceData.description, this.dehumidifierService === undefined ? 'removed' : 'added', ); } if (deviceData.can_cool !== this.deviceData.can_cool || deviceData.can_heat !== this.deviceData.can_heat) { // Heating and/cooling setup has changed on thermostat // Limit prop ranges if (deviceData.can_cool === false && deviceData.can_heat === true) { // Can heat only, so set values allowed for mode off/heat this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState).setProps({ validValues: [this.hap.Characteristic.TargetHeatingCoolingState.OFF, this.hap.Characteristic.TargetHeatingCoolingState.HEAT], }); } if (deviceData.can_cool === true && deviceData.can_heat === false) { // Can cool only this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState).setProps({ validValues: [this.hap.Characteristic.TargetHeatingCoolingState.OFF, this.hap.Characteristic.TargetHeatingCoolingState.COOL], }); } if (deviceData.can_cool === true && deviceData.can_heat === true) { // heat and cool this.thermostatService.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, ], }); } if (deviceData.can_cool === false && deviceData.can_heat === false) { // only off mode this.thermostatService.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState).setProps({ validValues: [this.hap.Characteristic.TargetHeatingCoolingState.OFF], }); } this?.log?.info && this.log.info('Heating/cooling setup on thermostat on "%s" has changed', deviceData.description); } // Update current mode temperatures if ( deviceData.can_heat === true && (deviceData.hvac_mode.toUpperCase() === 'HEAT' || deviceData.hvac_mode.toUpperCase() === 'ECOHEAT') ) { // heating mode, either eco or normal this.thermostatService.updateCharacteristic(this.hap.Characteristic.HeatingThresholdTemperature, deviceData.target_temperature_low); this.thermostatService.updateCharacteristic(this.hap.Characteristic.TargetTemperature, deviceData.target_temperature_low); this.thermostatService.updateCharacteristic( this.hap.Characteristic.TargetHeatingCoolingState, this.hap.Characteristic.TargetHeatingCoolingState.HEAT, ); historyEntry.target = { low: 0, high: deviceData.target_temperature_low }; // single target temperature for heating limit } if ( deviceData.can_cool === true && (deviceData.hvac_mode.toUpperCase() === 'COOL' || deviceData.hvac_mode.toUpperCase() === 'ECOCOOL') ) { // cooling mode, either eco or normal this.thermostatService.updateCharacteristic(this.hap.Characteristic.CoolingThresholdTemperature, deviceData.target_temperature_high); this.thermostatService.updateCharacteristic(this.hap.Characteristic.TargetTemperature, deviceData.target_temperature_high); this.thermostatService.updateCharacteristic( this.hap.Characteristic.TargetHeatingCoolingState, this.hap.Characteristic.TargetHeatingCoolingState.COOL, ); historyEntry.target = { low: deviceData.target_temperature_high, high: 0 }; // single target temperature for cooling limit } if ( deviceData.can_cool === true && deviceData.can_heat === true && (deviceData.hvac_mode.toUpperCase() === 'RANGE' || deviceData.hvac_mode.toUpperCase() === 'ECORANGE') ) { // range mode, either eco or normal this.thermostatService.updateCharacteristic(this.hap.Characteristic.HeatingThresholdTemperature, deviceData.target_temperature_low); this.thermostatService.updateCharacteristic(this.hap.Characteristic.CoolingThresholdTemperature, deviceData.target_temperature_high); this.thermostatService.updateCharacteristic(this.hap.Characteristic.TargetTemperature, deviceData.target_temperature); this.thermostatService.updateCharacteristic( this.hap.Characteristic.TargetHeatingCoolingState, this.hap.Characteristic.TargetHeatingCoolingState.AUTO, ); historyEntry.target = { low: deviceData.target_temperature_low, high: deviceData.target_temperature_high }; } if (deviceData.can_cool === false && deviceData.can_heat === false && deviceData.hvac_mode.toUpperCase() === 'OFF') { // off mode. this.thermostatService.updateCharacteristic(this.hap.Characteristic.TargetTemperature, deviceData.target_temperature); this.thermostatService.updateCharacteristic( this.hap.Characteristic.TargetHeatingCoolingState, this.hap.Characteristic.TargetHeatingCoolingState.OFF, ); historyEntry.target = { low: 0, high: 0 }; // thermostat off, so no target temperatures } // Update current state if (deviceData.hvac_state.toUpperCase() === 'HEATING') { if (this.deviceData.hvac_state.toUpperCase() === 'COOLING' && this.externalCool !== undefined) { // Switched to heating mode and external cooling external code was being used, so stop cooling via cooling external code if (typeof this.externalCool.off === 'function') { this.externalCool.off(); } } if ( (this.deviceData.hvac_state.toUpperCase() !== 'HEATING' || deviceData.target_temperature_low !== this.deviceData.target_temperature_low) && this.externalHeat !== undefined ) { // Switched to heating mode and external heating external code is being used // Start heating via heating external code OR adjust heating target temperature due to change if (typeof this.externalHeat.heat === 'function') { this.externalHeat.heat(deviceData.deviceData.target_temperature_low); } } this.thermostatService.updateCharacteristic( this.hap.Characteristic.CurrentHeatingCoolingState, this.hap.Characteristic.CurrentHeatingCoolingState.HEAT, ); historyEntry.status = 2; // heating } if (deviceData.hvac_state.toUpperCase() === 'COOLING') { if (this.deviceData.hvac_state.toUpperCase() === 'HEATING' && this.externalHeat !== undefined) { // Switched to cooling mode and external heating external code was being used, so stop heating via heating external code if (typeof this.externalHeat.off === 'function') { this.externalHeat.off(); } } if ( (this.deviceData.hvac_state.toUpperCase() !== 'COOLING' || deviceData.target_temperature_high !== this.deviceData.target_temperature_high) && this.externalCool !== undefined ) { // Switched to cooling mode and external cooling external code is being used // Start cooling via cooling external code OR adjust cooling target temperature due to change if (typeof this.externalCool.cool === 'function') { this.externalCool.cool(deviceData.target_temperature_high); } } this.thermostatService.updateCharacteristic( this.hap.Characteristic.CurrentHeatingCoolingState, this.hap.Characteristic.CurrentHeatingCoolingState.COOL, ); historyEntry.status = 3; // cooling } if (deviceData.hvac_state.toUpperCase() === 'OFF') { if (this.deviceData.hvac_state.toUpperCase() === 'COOLING' && this.externalCool !== undefined) { // Switched to off mode and external cooling external code was being used, so stop cooling via cooling external code if (typeof this.externalCool.off === 'function') { this.externalCool.off(); } } if (this.deviceData.hvac_state.toUpperCase() === 'HEATING' && this.externalHeat !== undefined) { // Switched to off mode and external heating external code was being used, so stop heating via heating external code if (typeof this.externalHeat.heat === 'function') { this.externalHeat.off(); } } this.thermostatService.updateCharacteristic( this.hap.Characteristic.CurrentHeatingCoolingState, this.hap.Characteristic.CurrentHeatingCoolingState.OFF, ); historyEntry.status = 0; // off } if (this.fanService !== undefined) { if (this.deviceData.fan_state === false && deviceData.fan_state === true && this.externalFan !== undefined) { // Fan mode was switched on and external fan external code is being used, so start fan via fan external code if (typeof this.externalFan.fan === 'function') { this.externalFan.fan(0); // Fan speed will be auto } } if (this.deviceData.fan_state === true && deviceData.fan_state === false && this.externalFan !== undefined) { // Fan mode was switched off and external fan external code was being used, so stop fan via fan external code if (typeof this.externalFan.off === 'function') { this.externalFan.off(); } } this.fanService.updateCharacteristic( this.hap.Characteristic.RotationSpeed, deviceData.fan_state === true ? (deviceData.fan_timer_speed / deviceData.fan_max_speed) * 100 : 0, ); this.fanService.updateCharacteristic( this.hap.Characteristic.Active, deviceData.fan_state === true ? this.hap.Characteristic.Active.ACTIVE : this.hap.Characteristic.Active.INACTIVE, ); // fan status on or off historyEntry.status = 1; // fan } if (this.dehumidifierService !== undefined) { if ( this.deviceData.dehumidifier_state === false && deviceData.dehumidifier_state === true && this.externalDehumidifier !== undefined ) { // Dehumidifier mode was switched on and external dehumidifier external code is being used // Start dehumidifier via dehumidifier external code if (typeof this.externalDehumidifier.dehumififier === 'function') { this.externalDehumidifier.dehumififier(0); } } if ( this.deviceData.dehumidifier_state === true && deviceData.dehumidifier_state === false && this.externalDehumidifier !== undefined ) { // Dehumidifier mode was switched off and external dehumidifier external code was being used // Stop dehumidifier via dehumidifier external code if (typeof this.externalDehumidifier.off === 'function') { this.externalDehumidifier.off(); } } this.dehumidifierService.updateCharacteristic( this.hap.Characteristic.Active, deviceData.dehumidifier_state === true ? this.hap.Characteristic.Active.ACTIVE : this.hap.Characteristic.Active.INACTIVE, ); // dehumidifier status on or off historyEntry.status = 4; // dehumidifier } // Log thermostat metrics to history only if changed to previous recording if (this.thermostatService !== undefined && typeof this.historyService?.addHistory === 'function') { let tempEntry = this.historyService.lastHistory(this.thermostatService); if ( tempEntry === undefined || (typeof tempEntry === 'object' && tempEntry.status !== historyEntry.status) || tempEntry.temperature !== deviceData.current_temperature || JSON.stringify(tempEntry.target) !== JSON.stringify(historyEntry.target) || tempEntry.humidity !== deviceData.current_humidity ) { this.historyService.addHistory(this.thermostatService, { time: Math.floor(Date.now() / 1000), status: historyEntry.status, temperature: deviceData.current_temperature, target: historyEntry.target, humidity: deviceData.current_humidity, }); } } // Notify Eve App of device status changes if linked if ( this.deviceData.eveHistory === true && this.thermostatService !== undefined && typeof this.historyService?.updateEveHome === 'function' ) { // Update our internal data with properties Eve will need to process this.deviceData.online = deviceData.online; this.deviceData.removed_from_base = deviceData.removed_from_base; this.deviceData.vacation_mode = deviceData.vacation_mode; this.deviceData.hvac_mode = deviceData.hvac_mode; this.deviceData.schedules = deviceData.schedules; this.deviceData.schedule_mode = deviceData.schedule_mode; this.historyService.updateEveHome(this.thermostatService, this.#EveHomeGetcommand.bind(this)); } } #EveHomeGetcommand(EveHomeGetData) { // Pass back extra data for Eve Thermo onGet() to process command // Data will already be an object, our only job is to add/modify it if (typeof EveHomeGetData === 'object') { EveHomeGetData.enableschedule = this.deviceData.schedule_mode === 'heat'; // Schedules on/off EveHomeGetData.attached = this.deviceData.online === true && this.deviceData.removed_from_base === false; EveHomeGetData.vacation = this.deviceData.vacation_mode === true; // Vaction mode on/off EveHomeGetData.vacationtemp = this.deviceData.vacation_mode === true ? EveHomeGetData.vacationtemp : null; EveHomeGetData.programs = []; // No programs yet, we'll process this below if (this.deviceData.schedule_mode.toUpperCase() === 'HEAT' || this.deviceData.schedule_mode.toUpperCase() === 'RANGE') { const DAYSOFWEEK = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']; Object.entries(this.deviceData.schedules).forEach(([day, schedules]) => { let tempSchedule = []; let tempTemperatures = []; Object.values(schedules) .reverse() .forEach((schedule) => { if (schedule.entry_type === 'setpoint' && (schedule.type === 'HEAT' || schedule.type === 'RANGE')) { tempSchedule.push({ start: schedule.time, duration: 0, temperature: typeof schedule['temp-min'] === 'number' ? schedule['temp-min'] : schedule.temp, }); tempTemperatures.push(typeof schedule['temp-min'] === 'number' ? schedule['temp-min'] : schedule.temp); } }); // Sort the schedule array by start time tempSchedule = tempSchedule.sort((a, b) => { if (a.start < b.start) { return -1; } }); let ecoTemp = tempTemperatures.length === 0 ? 0 : Math.min(...tempTemperatures); let comfortTemp = tempTemperatures.length === 0 ? 0 : Math.max(...tempTemperatures); let program = {}; program.id = parseInt(day) + 1; program.days = DAYSOFWEEK[day]; program.schedule = []; let lastTime = 86400; // seconds in a day Object.values(tempSchedule) .reverse() .forEach((schedule) => { if (schedule.temperature === comfortTemp) { // We only want to add the schedule time if its using the 'max' temperature program.schedule.push({ start: schedule.start, duration: lastTime - sched