homebridge-resideo
Version:
The Resideo plugin allows you to access your Resideo device(s) from HomeKit.
811 lines (810 loc) • 56.3 kB
JavaScript
// import { request } from 'undici';
import { interval, Subject } from 'rxjs';
import { debounceTime, skipWhile, take, tap } from 'rxjs/operators';
import { DeviceURL } from '../settings.js';
import { HomeKitModes, ResideoModes, toCelsius, toFahrenheit } 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 {
platform;
// Services
Thermostat;
Fan;
HumiditySensor;
StatefulProgrammableSwitch;
// Config
thermostatSetpointStatus;
// Thermostat Updates
thermostatUpdateInProgress;
doThermostatUpdate;
// Fan Updates
fanStatus;
fanUpdateInProgress;
doFanUpdate;
// Room Updates - T9 Only
roomPriorityStatus;
roomUpdateInProgress;
doRoomUpdate;
constructor(platform, accessory, location, device) {
super(platform, accessory, location, device);
this.platform = platform;
this.getThermostatConfigSettings(accessory, device);
// this is subject we use to track when we need to POST Room Priority changes to the Resideo API for Room Changes - T9 Only
this.doRoomUpdate = new Subject();
this.roomUpdateInProgress = false;
// this is subject we use to track when we need to POST Thermostat changes to the Resideo API
this.doThermostatUpdate = new Subject();
this.thermostatUpdateInProgress = false;
// this is subject we use to track when we need to POST Fan changes to the Resideo API
this.doFanUpdate = new Subject();
this.fanUpdateInProgress = false;
// 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),
TargetTemperature: accessory.context.TargetTemperature ?? 20,
CurrentTemperature: accessory.context.CurrentTemperature ?? 20,
TemperatureDisplayUnits: accessory.context.TemperatureDisplayUnits,
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;
// 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 = device.units === 'Celsius'
? this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS
: this.hap.Characteristic.TemperatureDisplayUnits.FAHRENHEIT;
accessory.context.TemperatureDisplayUnits = this.Thermostat.TemperatureDisplayUnits;
this.debugLog(`${this.device.deviceClass} ${accessory.displayName} TemperatureDisplayUnits: ${this.Thermostat.TemperatureDisplayUnits}`);
return this.Thermostat.TemperatureDisplayUnits;
})
.onSet(this.setTemperatureDisplayUnits.bind(this));
// Set Min and Max
if (device.minHeatSetpoint && device.maxHeatSetpoint) {
this.debugLog(`${this.device.deviceClass} ${accessory.displayName} minHeatSetpoint: ${device.minHeatSetpoint}, maxHeatSetpoint: ${device.maxHeatSetpoint}, TemperatureDisplayUnits: ${this.Thermostat.TemperatureDisplayUnits}`);
const minValue = toCelsius(device.minHeatSetpoint, Number(this.Thermostat.TemperatureDisplayUnits));
const maxValue = toCelsius(device.maxHeatSetpoint, Number(this.Thermostat.TemperatureDisplayUnits));
this.debugLog(`${this.device.deviceClass} ${accessory.displayName} minValue: ${minValue}, maxValue: ${maxValue}`);
if (device.changeableValues.heatCoolMode === 'Heat') {
this.debugLog(`${device.deviceClass} ${accessory.displayName} is in "${device.changeableValues?.heatCoolMode}" mode`);
this.Thermostat.Service
.getCharacteristic(this.hap.Characteristic.TargetTemperature)
.setProps({
minValue,
maxValue,
minStep: 0.1,
})
.onGet(() => {
return this.Thermostat.TargetTemperature;
});
this.debugWarnLog(`${device.deviceClass} ${accessory.displayName} (HEAT) Min Heat Setpoint: ${device.minHeatSetpoint}. Max Heat Setpoint: ${device.maxHeatSetpoint} TemperatureDisplayUnits: ${this.Thermostat.TemperatureDisplayUnits}`);
}
else {
this.debugLog(`${device.deviceClass} ${accessory.displayName} is in "${device.changeableValues?.heatCoolMode}" mode`);
this.Thermostat.Service
.getCharacteristic(this.hap.Characteristic.TargetTemperature)
.setProps({
minValue,
maxValue,
minStep: 0.1,
})
.onGet(() => {
return this.Thermostat.TargetTemperature;
});
this.debugWarnLog(`${device.deviceClass} ${accessory.displayName} (NOT HEAT) Min Heat Setpoint: ${device.minHeatSetpoint}. Max Heat Setpoint: ${device.maxHeatSetpoint} TemperatureDisplayUnits: ${this.Thermostat.TemperatureDisplayUnits}`);
}
}
// The value property of TargetHeaterCoolerState must be one of the following:
// AUTO = 3; HEAT = 1; COOL = 2; OFF = 0;
// Set control bindings
const TargetState = [4];
TargetState.pop();
if (this.device.allowedModes?.includes('Cool')) {
TargetState.push(this.hap.Characteristic.TargetHeatingCoolingState.COOL);
}
if (this.device.allowedModes?.includes('Heat')) {
TargetState.push(this.hap.Characteristic.TargetHeatingCoolingState.HEAT);
}
if (this.device.allowedModes?.includes('Off')) {
TargetState.push(this.hap.Characteristic.TargetHeatingCoolingState.OFF);
}
if (this.device.allowedModes?.includes('Auto') || this.device.thermostat?.show_auto) {
TargetState.push(this.hap.Characteristic.TargetHeatingCoolingState.AUTO);
}
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} allowedModes: ${this.device.allowedModes}`);
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} Only Show These Modes: ${JSON.stringify(TargetState)}`);
this.Thermostat.Service
.getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState)
.setProps({
validValues: TargetState,
})
.onGet(() => {
return this.Thermostat.TargetHeatingCoolingState;
})
.onSet(this.setTargetHeatingCoolingState.bind(this));
this.Thermostat.Service
.getCharacteristic(this.hap.Characteristic.HeatingThresholdTemperature)
.onGet(() => {
return this.Thermostat.HeatingThresholdTemperature;
})
.onSet(this.setHeatingThresholdTemperature.bind(this));
this.Thermostat.Service
.getCharacteristic(this.hap.Characteristic.CoolingThresholdTemperature)
.onGet(() => {
return this.Thermostat.CoolingThresholdTemperature;
})
.onSet(this.setCoolingThresholdTemperature.bind(this));
this.Thermostat.Service
.getCharacteristic(this.hap.Characteristic.TargetTemperature)
.onGet(() => {
return this.Thermostat.TargetTemperature;
})
.onSet(this.setTargetTemperature.bind(this));
// Initialize Fan Service
if (device.thermostat?.hide_fan) {
if (this.Fan) {
this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Removing Fan Service`);
this.Fan.Service = accessory.getService(this.hap.Service.Fanv2);
accessory.removeService(this.Fan.Service);
}
else {
this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Fan Service Not Found`);
}
}
else if (device.settings?.fan) {
this.debugLog(`${device.deviceClass} ${accessory.displayName} Available Fan Settings ${JSON.stringify(device.settings.fan)}`);
accessory.context.Fan = accessory.context.Fan ?? {};
this.Fan = {
Name: accessory.context.Fan.Name ?? `${accessory.displayName} Fan`,
Service: accessory.getService(this.hap.Service.Fanv2) ?? accessory.addService(this.hap.Service.Fanv2),
Active: accessory.context.Active ?? this.hap.Characteristic.Active.ACTIVE,
TargetFanState: accessory.context.TargetFanState ?? this.hap.Characteristic.TargetFanState.MANUAL,
};
accessory.context.Fan = this.Fan;
// Initialize Fan Characteristic
this.Fan.Service
.setCharacteristic(this.hap.Characteristic.Name, this.Fan.Name)
.getCharacteristic(this.hap.Characteristic.Active)
.onGet(() => {
return this.Fan.Active;
})
.onSet(this.setActive.bind(this));
this.Fan.Service
.getCharacteristic(this.hap.Characteristic.TargetFanState)
.onGet(() => {
return this.Fan.TargetFanState;
})
.onSet(this.setTargetFanState.bind(this));
}
else {
this.debugLog(`${device.deviceClass} ${accessory.displayName} Fanv2 Service Not Added`);
}
// Initialize Humidity Sensor Service
if (device.thermostat?.hide_humidity) {
if (this.HumiditySensor) {
this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Removing Humidity Sensor Service`);
this.HumiditySensor.Service = this.accessory.getService(this.hap.Service.HumiditySensor);
accessory.removeService(this.HumiditySensor.Service);
}
else {
this.debugLog(`${this.device.deviceType}: ${accessory.displayName} Humidity Sensor Service Not Found`);
}
}
else if (device.indoorHumidity) {
this.debugLog(`${device.deviceClass} ${accessory.displayName} Add Humidity Sensor Service`);
accessory.context.HumiditySensor = accessory.context.HumiditySensor ?? {};
this.HumiditySensor = {
Name: accessory.context.HumiditySensor.Name ?? `${accessory.displayName} Humidity Sensor`,
Service: accessory.getService(this.hap.Service.HumiditySensor) ?? accessory.addService(this.hap.Service.HumiditySensor),
CurrentRelativeHumidity: accessory.context.CurrentRelativeHumidity ?? 50,
};
accessory.context.HumiditySensor = this.HumiditySensor;
// Initialize Humidity Sensor Characteristic
this.HumiditySensor.Service
.setCharacteristic(this.hap.Characteristic.Name, this.HumiditySensor.Name);
this.HumiditySensor.Service
.getCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity)
.setProps({
minStep: 0.1,
})
.onGet(() => {
return this.HumiditySensor.CurrentRelativeHumidity;
});
}
else {
this.debugLog(`${device.deviceClass} ${accessory.displayName} Humidity Sensor Service Not Added`);
}
// Initialize StatefulProgrammableSwitch property
accessory.context.StatefulProgrammableSwitch = accessory.context.StatefulProgrammableSwitch ?? {};
this.StatefulProgrammableSwitch = {
Name: accessory.context.StatefulProgrammableSwitch.Name ?? accessory.displayName,
Service: accessory.getService(this.hap.Service.StatefulProgrammableSwitch) ?? accessory.addService(this.hap.Service.StatefulProgrammableSwitch),
ProgrammableSwitchEvent: accessory.context.ProgrammableSwitchEvent ?? this.hap.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS,
ProgrammableSwitchOutputState: accessory.context.ProgrammableSwitchOutputState ?? 0,
};
accessory.context.StatefulProgrammableSwitch = this.StatefulProgrammableSwitch;
this.StatefulProgrammableSwitch.Service
.setCharacteristic(this.hap.Characteristic.Name, this.StatefulProgrammableSwitch.Name)
.getCharacteristic(this.hap.Characteristic.ProgrammableSwitchEvent)
.setProps({
validValues: [this.hap.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS],
})
.onGet(() => {
return this.StatefulProgrammableSwitch.ProgrammableSwitchEvent;
});
this.StatefulProgrammableSwitch.Service
.getCharacteristic(this.hap.Characteristic.ProgrammableSwitchOutputState)
.onGet(() => {
return this.StatefulProgrammableSwitch.ProgrammableSwitchOutputState;
})
.onSet(this.handleProgrammableSwitchOutputStateSet.bind(this));
// Intial Refresh
this.refreshStatus();
// Start an update interval
interval(this.deviceRefreshRate * 1000)
.pipe(skipWhile(() => this.thermostatUpdateInProgress))
.subscribe(async () => {
await this.refreshStatus();
});
// Watch for thermostat change events
// We put in a debounce of 100ms so we don't make duplicate calls
if (device.thermostat?.roompriority?.deviceType === 'Thermostat' && device.deviceModel === 'T9-T10') {
this.doRoomUpdate
.pipe(tap(() => {
this.roomUpdateInProgress = true;
}), debounceTime(this.devicePushRate * 500))
.subscribe(async () => {
try {
await this.pushRoomChanges();
}
catch (e) {
const action = 'pushRoomChanges';
if (this.device.retry) {
// Refresh the status from the API
interval(5000)
.pipe(skipWhile(() => this.thermostatUpdateInProgress))
.pipe(take(1))
.subscribe(async () => {
await this.pushRoomChanges();
});
}
this.resideoAPIError(e, action);
this.platform.refreshAccessToken();
this.apiError(e);
}
this.roomUpdateInProgress = false;
// Refresh the status from the API
interval(5000)
.pipe(skipWhile(() => this.thermostatUpdateInProgress))
.pipe(take(1))
.subscribe(async () => {
await this.refreshStatus();
});
});
}
this.doThermostatUpdate
.pipe(tap(() => {
this.thermostatUpdateInProgress = true;
}), debounceTime(this.devicePushRate * 1000))
.subscribe(async () => {
try {
await this.pushChanges();
}
catch (e) {
const action = 'pushChanges';
if (this.device.retry) {
// Refresh the status from the API
interval(5000)
.pipe(skipWhile(() => this.thermostatUpdateInProgress))
.pipe(take(1))
.subscribe(async () => {
await this.pushChanges();
});
}
this.resideoAPIError(e, action);
this.platform.refreshAccessToken();
this.apiError(e);
}
this.thermostatUpdateInProgress = false;
// Refresh the status from the API
interval(15000)
.pipe(skipWhile(() => this.thermostatUpdateInProgress))
.pipe(take(1))
.subscribe(async () => {
await this.refreshStatus();
});
});
if (device.settings?.fan && !device.thermostat?.hide_fan) {
this.doFanUpdate
.pipe(tap(() => {
this.fanUpdateInProgress = true;
}), debounceTime(this.devicePushRate * 1000))
.subscribe(async () => {
try {
await this.pushFanChanges();
}
catch (e) {
const action = 'pushFanChanges';
if (this.device.retry) {
// Refresh the status from the API
interval(5000)
.pipe(skipWhile(() => this.thermostatUpdateInProgress))
.pipe(take(1))
.subscribe(async () => {
await this.pushFanChanges();
});
}
this.resideoAPIError(e, action);
this.platform.refreshAccessToken();
this.apiError(e);
}
this.fanUpdateInProgress = false;
// Refresh the status from the API
interval(5000)
.pipe(skipWhile(() => this.thermostatUpdateInProgress))
.pipe(take(1))
.subscribe(async () => {
await this.refreshStatus();
});
});
}
}
/**
* Parse the device status from the Resideo api
*/
async parseStatus() {
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} parseStatus`);
if (this.device.units === 'Fahrenheit') {
this.Thermostat.TemperatureDisplayUnits = this.hap.Characteristic.TemperatureDisplayUnits.FAHRENHEIT;
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} parseStatus TemperatureDisplayUnits: ${this.hap.Characteristic.TemperatureDisplayUnits.FAHRENHEIT}`);
}
if (this.device.units === 'Celsius') {
this.Thermostat.TemperatureDisplayUnits = this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS;
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} parseStatus TemperatureDisplayUnits: ${this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS}`);
}
this.Thermostat.CurrentTemperature = toCelsius(this.device.indoorTemperature, Number(this.Thermostat.TemperatureDisplayUnits));
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} parseStatus CurrentTemperature: ${toCelsius(this.device.indoorTemperature, Number(this.Thermostat.TemperatureDisplayUnits))}`);
if (this.device.indoorHumidity) {
if (this.HumiditySensor) {
this.HumiditySensor.CurrentRelativeHumidity = this.device.indoorHumidity;
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} parseStatus CurrentRelativeHumidity: ${this.HumiditySensor.CurrentRelativeHumidity}`);
}
}
if (this.device.changeableValues.heatSetpoint > 0) {
this.Thermostat.HeatingThresholdTemperature = toCelsius(this.device.changeableValues.heatSetpoint, Number(this.Thermostat.TemperatureDisplayUnits));
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} parseStatus HeatingThresholdTemperature: ${toCelsius(this.device.changeableValues.heatSetpoint, Number(this.Thermostat.TemperatureDisplayUnits))}`);
}
if (this.device.changeableValues.coolSetpoint > 0) {
this.Thermostat.CoolingThresholdTemperature = toCelsius(this.device.changeableValues.coolSetpoint, Number(this.Thermostat.TemperatureDisplayUnits));
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} parseStatus CoolingThresholdTemperature: ${toCelsius(this.device.changeableValues.coolSetpoint, Number(this.Thermostat.TemperatureDisplayUnits))}`);
}
this.Thermostat.TargetHeatingCoolingState = HomeKitModes[this.device.changeableValues.mode];
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} parseStatus TargetHeatingCoolingState: ${HomeKitModes[this.device.changeableValues.mode]}`);
/**
* The CurrentHeatingCoolingState is either 'Heat', 'Cool', or 'Off'
* CurrentHeatingCoolingState = OFF = 0, HEAT = 1, COOL = 2
*/
switch (this.device.operationStatus.mode) {
case 'Heat':
this.Thermostat.CurrentHeatingCoolingState = this.hap.Characteristic.CurrentHeatingCoolingState.HEAT;
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} parseStatus Currently Mode (HEAT): ${this.device.operationStatus.mode}(${this.Thermostat.CurrentHeatingCoolingState})`);
break;
case 'Cool':
this.Thermostat.CurrentHeatingCoolingState = this.hap.Characteristic.CurrentHeatingCoolingState.COOL;
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} parseStatus Currently Mode (COOL): ${this.device.operationStatus.mode}(${this.Thermostat.CurrentHeatingCoolingState})`);
break;
default:
this.Thermostat.CurrentHeatingCoolingState = this.hap.Characteristic.CurrentHeatingCoolingState.OFF;
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} parseStatus Currently Mode (OFF): ${this.device.operationStatus.mode}(${this.Thermostat.CurrentHeatingCoolingState})`);
}
// Set the TargetTemperature value based on the current mode
if (this.Thermostat.TargetHeatingCoolingState === this.hap.Characteristic.TargetHeatingCoolingState.HEAT) {
if (this.device.changeableValues.heatSetpoint > 0) {
this.Thermostat.TargetTemperature = toCelsius(this.device.changeableValues.heatSetpoint, Number(this.Thermostat.TemperatureDisplayUnits));
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} parseStatus TargetTemperature (HEAT): ${toCelsius(this.device.changeableValues.heatSetpoint, Number(this.Thermostat.TemperatureDisplayUnits))})`);
}
}
else {
if (this.device.changeableValues) {
if (this.device.changeableValues.coolSetpoint > 0) {
this.Thermostat.TargetTemperature = toCelsius(this.device.changeableValues.coolSetpoint, Number(this.Thermostat.TemperatureDisplayUnits));
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} parseStatus TargetTemperature (OFF/COOL): ${toCelsius(this.device.changeableValues.coolSetpoint, Number(this.Thermostat.TemperatureDisplayUnits))})`);
}
}
}
// Set the Target Fan State
if (this.device.settings?.fan && !this.device.thermostat?.hide_fan) {
if (this.Fan) {
if (this.fanStatus) {
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} fanStatus: ${JSON.stringify(this.fanStatus)}`);
if (this.fanStatus.changeableValues.mode === 'Auto') {
this.Fan.TargetFanState = this.hap.Characteristic.TargetFanState.AUTO;
this.Fan.Active = this.hap.Characteristic.Active.INACTIVE;
}
else if (this.fanStatus.changeableValues.mode === 'On') {
this.Fan.TargetFanState = this.hap.Characteristic.TargetFanState.MANUAL;
this.Fan.Active = this.hap.Characteristic.Active.ACTIVE;
}
else if (this.fanStatus.changeableValues.mode === 'Circulate') {
this.Fan.TargetFanState = this.hap.Characteristic.TargetFanState.MANUAL;
this.Fan.Active = this.hap.Characteristic.Active.INACTIVE;
}
}
}
}
// Set the Room Priority Status - T9 Only
if (this.device.thermostat?.roompriority?.deviceType === 'Thermostat' && this.device.deviceModel === 'T9-T10') {
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} roomPriorityStatus: ${JSON.stringify(this.roomPriorityStatus)}`);
}
}
/**
* Asks the Resideo Home API for the latest device information
*/
async refreshStatus() {
try {
const device = await this.getDeviceStatus();
const fanStatus = await this.getFanStatus();
const roomPriorityStatus = await this.getRoomPriorityStatus();
this.device = device;
this.fanStatus = fanStatus;
this.roomPriorityStatus = roomPriorityStatus;
this.parseStatus();
this.updateHomeKitCharacteristics();
}
catch (e) {
const action = 'refreshStatus';
if (this.device.retry) {
// Refresh the status from the API
interval(5000)
.pipe(skipWhile(() => this.thermostatUpdateInProgress))
.pipe(take(1))
.subscribe(async () => {
await this.refreshStatus();
});
}
this.resideoAPIError(e, action);
this.apiError(e);
}
}
async getDeviceStatus() {
const device = (await this.platform.axios.get(`${DeviceURL}/thermostats/${this.device.deviceID}`, {
params: {
locationId: this.location.locationID,
},
})).data;
this.debugLog(`(refreshStatus) ${device.deviceClass} device: ${JSON.stringify(device)}`);
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} refreshStatus for ${this.device.name} from Resideo API: ${JSON.stringify(this.device.changeableValues)}`);
return device;
}
async getRoomPriorityStatus() {
if (this.device.thermostat?.roompriority?.deviceType === 'Thermostat' && this.device.deviceModel === 'T9-T10') {
const roompriority = (await this.platform.axios.get(`${DeviceURL}/thermostats/${this.device.deviceID}/priority`, {
params: {
locationId: this.location.locationID,
},
})).data;
this.debugLog(`Thermostat: ${this.accessory.displayName} Priority: ${JSON.stringify(roompriority.data)}`);
const action = 'refreshRoomPriority';
await this.statusCode(200, action);
return roompriority;
}
}
async getFanStatus() {
if (this.device.settings?.fan && !this.device.thermostat?.hide_fan) {
const fan = (await this.platform.axios.get(`${DeviceURL}/thermostats/${this.device.deviceID}/fan`, {
params: {
locationId: this.location.locationID,
},
})).data;
const action = 'refreshFanStatus';
await this.statusCode(200, action);
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} (refreshFanStatus) fanMode: ${JSON.stringify(fan)}`);
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} fanMode: ${JSON.stringify(fan)}`);
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} refreshStatus for ${this.device.name} Fan from Resideo Fan API: ${JSON.stringify(fan)}`);
return fan;
}
return null;
}
/**
* Pushes the requested changes to the Resideo API
*/
async pushChanges() {
try {
const payload = {};
// Only include mode on certain models
switch (this.device.deviceModel) {
case 'Unknown':
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} didn't send TargetHeatingCoolingState, Model: ${this.device.deviceModel}`);
break;
default:
payload.mode = await this.ResideoMode();
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} send TargetHeatingCoolingState: ${payload.mode}`);
}
// Only include thermostatSetpointStatus on certain models
switch (this.device.deviceModel) {
case 'Round':
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} didn't send thermostatSetpointStatus, Model: ${this.device.deviceModel}`);
break;
default:
this.pushChangesthermostatSetpointStatus();
payload.thermostatSetpointStatus = this.thermostatSetpointStatus;
if (this.thermostatSetpointStatus === 'TemporaryHold') {
this.warnLog(`${this.device.deviceClass} ${this.accessory.displayName} send thermostatSetpointStatus: `
+ `${payload.thermostatSetpointStatus}, Model: ${this.device.deviceModel}`);
}
else {
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} send thermostatSetpointStatus: `
+ `${payload.thermostatSetpointStatus}, Model: ${this.device.deviceModel}`);
}
}
switch (this.device.deviceModel) {
case 'Round':
case 'D6':
if (this.deviceLogging.includes('debug')) {
this.warnLog(`${this.device.deviceClass} ${this.accessory.displayName} set autoChangeoverActive, Model: ${this.device.deviceModel}`);
}
// for Round the 'Auto' feature is enabled via the special mode so only flip this bit when
// the heating/cooling state is set to `Auto
if (this.Thermostat.TargetHeatingCoolingState === this.hap.Characteristic.TargetHeatingCoolingState.AUTO) {
payload.autoChangeoverActive = true;
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} Heating/Cooling state set to Auto for`
+ ` Model: ${this.device.deviceModel}, Force autoChangeoverActive: ${payload.autoChangeoverActive}`);
}
else {
payload.autoChangeoverActive = this.device.changeableValues?.autoChangeoverActive;
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} Heating/cooling state not set to Auto for`
+ ` Model: ${this.device.deviceModel}, Using device setting`
+ ` autoChangeoverActive: ${this.device.changeableValues.autoChangeoverActive}`);
}
break;
case 'Unknown':
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} do not send autoChangeoverActive, Model: ${this.device.deviceModel}`);
break;
default:
payload.autoChangeoverActive = this.device.changeableValues.autoChangeoverActive;
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} set autoChangeoverActive to ${this.device.changeableValues.autoChangeoverActive} for Model: ${this.device.deviceModel}`);
}
switch (this.device.deviceModel) {
case 'Unknown':
this.errorLog(JSON.stringify(this.device));
payload.thermostatSetpoint = toFahrenheit(Number(this.Thermostat.TargetTemperature), Number(this.Thermostat.TemperatureDisplayUnits));
switch (this.device.units) {
case 'Fahrenheit':
payload.unit = 'Fahrenheit';
break;
case 'Celsius':
payload.unit = 'Celsius';
break;
}
this.successLog(`${this.device.deviceClass} ${this.accessory.displayName} sent request to Resideo API thermostatSetpoint: ${payload.thermostatSetpoint}, unit: ${payload.unit}`);
break;
default:
// Set the heat and cool set point value based on the selected mode
switch (this.Thermostat.TargetHeatingCoolingState) {
case this.hap.Characteristic.TargetHeatingCoolingState.HEAT:
payload.heatSetpoint = toFahrenheit(Number(this.Thermostat.TargetTemperature), Number(this.Thermostat.TemperatureDisplayUnits));
payload.coolSetpoint = toFahrenheit(Number(this.Thermostat.CoolingThresholdTemperature), Number(this.Thermostat.TemperatureDisplayUnits));
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} TargetHeatingCoolingState (HEAT): ${this.Thermostat.TargetHeatingCoolingState}, TargetTemperature: ${toFahrenheit(Number(this.Thermostat.TargetTemperature), Number(this.Thermostat.TemperatureDisplayUnits))} heatSetpoint, CoolingThresholdTemperature: ${toFahrenheit(Number(this.Thermostat.CoolingThresholdTemperature), Number(this.Thermostat.TemperatureDisplayUnits))} coolSetpoint`);
break;
case this.hap.Characteristic.TargetHeatingCoolingState.COOL:
payload.coolSetpoint = toFahrenheit(Number(this.Thermostat.TargetTemperature), Number(this.Thermostat.TemperatureDisplayUnits));
payload.heatSetpoint = toFahrenheit(Number(this.Thermostat.HeatingThresholdTemperature), Number(this.Thermostat.TemperatureDisplayUnits));
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} TargetHeatingCoolingState (COOL): ${this.Thermostat.TargetHeatingCoolingState}, TargetTemperature: ${toFahrenheit(Number(this.Thermostat.TargetTemperature), Number(this.Thermostat.TemperatureDisplayUnits))} coolSetpoint, CoolingThresholdTemperature: ${toFahrenheit(Number(this.Thermostat.HeatingThresholdTemperature), Number(this.Thermostat.TemperatureDisplayUnits))} heatSetpoint`);
break;
case this.hap.Characteristic.TargetHeatingCoolingState.AUTO:
payload.coolSetpoint = toFahrenheit(Number(this.Thermostat.CoolingThresholdTemperature), Number(this.Thermostat.TemperatureDisplayUnits));
payload.heatSetpoint = toFahrenheit(Number(this.Thermostat.HeatingThresholdTemperature), Number(this.Thermostat.TemperatureDisplayUnits));
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} TargetHeatingCoolingState (AUTO): ${this.Thermostat.TargetHeatingCoolingState}, CoolingThresholdTemperature: ${toFahrenheit(Number(this.Thermostat.CoolingThresholdTemperature), Number(this.Thermostat.TemperatureDisplayUnits))} coolSetpoint, HeatingThresholdTemperature: ${toFahrenheit(Number(this.Thermostat.HeatingThresholdTemperature), Number(this.Thermostat.TemperatureDisplayUnits))} heatSetpoint`);
break;
default:
payload.coolSetpoint = toFahrenheit(Number(this.Thermostat.CoolingThresholdTemperature), Number(this.Thermostat.TemperatureDisplayUnits));
payload.heatSetpoint = toFahrenheit(Number(this.Thermostat.HeatingThresholdTemperature), Number(this.Thermostat.TemperatureDisplayUnits));
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} TargetHeatingCoolingState (OFF): ${this.Thermostat.TargetHeatingCoolingState}, CoolingThresholdTemperature: ${toFahrenheit(Number(this.Thermostat.CoolingThresholdTemperature), Number(this.Thermostat.TemperatureDisplayUnits))} coolSetpoint, HeatingThresholdTemperature: ${toFahrenheit(Number(this.Thermostat.HeatingThresholdTemperature), Number(this.Thermostat.TemperatureDisplayUnits))} heatSetpoint`);
}
this.successLog(`${this.device.deviceClass} ${this.accessory.displayName} set request (${JSON.stringify(payload)}) to Resideo API.`);
}
// Attempt to make the API request
await this.platform.axios.post(`${DeviceURL}/thermostats/${this.device.deviceID}`, payload, {
params: {
locationId: this.location.locationID,
},
});
const action = 'pushChanges';
await this.statusCode(200, action);
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} pushChanges: ${JSON.stringify(payload)}`);
await this.updateHomeKitCharacteristics();
}
catch (e) {
const action = 'pushChanges';
if (this.device.retry) {
// Refresh the status from the API
interval(5000)
.pipe(skipWhile(() => this.thermostatUpdateInProgress))
.pipe(take(1))
.subscribe(async () => {
await this.pushChanges();
});
}
this.resideoAPIError(e, action);
this.apiError(e);
}
}
async ResideoMode() {
let resideoMode;
switch (this.Thermostat.TargetHeatingCoolingState) {
case this.hap.Characteristic.TargetHeatingCoolingState.HEAT:
resideoMode = ResideoModes.Heat;
break;
case this.hap.Characteristic.TargetHeatingCoolingState.COOL:
resideoMode = ResideoModes.Cool;
break;
case this.hap.Characteristic.TargetHeatingCoolingState.AUTO:
resideoMode = ResideoModes.Auto;
break;
case this.hap.Characteristic.TargetHeatingCoolingState.OFF:
resideoMode = ResideoModes.Off;
break;
default:
resideoMode = 'Unknown';
this.debugErrorLog(`${this.device.deviceClass} ${this.accessory.displayName} Unknown TargetHeatingCoolingState: ${this.Thermostat.TargetHeatingCoolingState}`);
}
return resideoMode;
}
async pushChangesthermostatSetpointStatus() {
if (this.thermostatSetpointStatus) {
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} thermostatSetpointStatus config set to ${this.thermostatSetpointStatus}`);
}
else {
this.thermostatSetpointStatus = 'PermanentHold';
this.accessory.context.thermostatSetpointStatus = this.thermostatSetpointStatus;
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} thermostatSetpointStatus config not set`);
}
}
/**
* Pushes the requested changes for Room Priority to the Resideo API
*/
async pushRoomChanges() {
this.debugLog(`Thermostat Room Priority for ${this.accessory.displayName}
Current Room: ${JSON.stringify(this.roomPriorityStatus.currentPriority.selectedRooms)},
Changing Room: [${this.device.inBuiltSensorState.roomId}]`);
if (`[${this.device.inBuiltSensorState.roomId}]` !== `[${this.roomPriorityStatus.currentPriority.selectedRooms}]`) {
const payload = {
currentPriority: {
priorityType: this.device.thermostat?.roompriority?.priorityType,
},
};
if (this.device.thermostat?.roompriority?.priorityType === 'PickARoom') {
payload.currentPriority.selectedRooms = [this.device.inBuiltSensorState.roomId];
}
/**
* For "LCC-" devices only.
* "NoHold" will return to schedule.
* "TemporaryHold" will hold the set temperature until next schedule.
* "PermanentHold" will hold the setpoint until user requests another change.
*/
if (this.device.thermostat?.roompriority?.deviceType === 'Thermostat') {
if (this.device.priorityType === 'FollowMe') {
this.successLog(`Sending request for ${this.accessory.displayName} to Resideo API Priority Type: ${this.device.priorityType}, Built-in Occupancy Sensor(s) Will be used to set Priority Automatically`);
}
else if (this.device.priorityType === 'WholeHouse') {
this.successLog(`Sending request for ${this.accessory.displayName} to Resideo API Priority Type:` + ` ${this.device.priorityType}`);
}
else if (this.device.priorityType === 'PickARoom') {
this.successLog(`Sending request for ${this.accessory.displayName} to Resideo API Room Priority: ${this.device.inBuiltSensorState.roomName}, Priority Type: ${this.device.thermostat?.roompriority.priorityType}`);
}
// Make the API request
await this.platform.axios.put(`${DeviceURL}/thermostats/${this.device.deviceID}/priority`, payload, {
params: {
locationId: this.location.locationID,
},
});
const action = 'pushRoomChanges';
await this.statusCode(200, action);
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} pushRoomChanges: ${JSON.stringify(payload)}`);
}
}
}
/**
* Updates the status for each of the HomeKit Characteristics
*/
async updateHomeKitCharacteristics() {
if (this.Thermostat.TemperatureDisplayUnits === undefined) {
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} TemperatureDisplayUnits: ${this.Thermostat.TemperatureDisplayUnits}`);
}
else {
this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.TemperatureDisplayUnits, Number(this.Thermostat.TemperatureDisplayUnits));
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} updateCharacteristic TemperatureDisplayUnits: ${this.Thermostat.TemperatureDisplayUnits}`);
}
if (this.Thermostat.CurrentTemperature === undefined) {
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} CurrentTemperature: ${this.Thermostat.CurrentTemperature}`);
}
else {
this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, this.Thermostat.CurrentTemperature);
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} updateCharacteristic CurrentTemperature: ${this.Thermostat.CurrentTemperature}`);
}
if (this.Thermostat.TargetTemperature === undefined) {
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} TargetTemperature: ${this.Thermostat.TargetTemperature}`);
}
else {
this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.TargetTemperature, this.Thermostat.TargetTemperature);
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} updateCharacteristic TargetTemperature: ${this.Thermostat.TargetTemperature}`);
}
if (this.Thermostat.HeatingThresholdTemperature === undefined) {
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} HeatingThresholdTemperature: ${this.Thermostat.HeatingThresholdTemperature}`);
}
else {
this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.HeatingThresholdTemperature, this.Thermostat.HeatingThresholdTemperature);
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} updateCharacteristic HeatingThresholdTemperature: ${this.Thermostat.HeatingThresholdTemperature}`);
}
if (this.Thermostat.CoolingThresholdTemperature === undefined) {
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} CoolingThresholdTemperature: ${this.Thermostat.CoolingThresholdTemperature}`);
}
else {
this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.CoolingThresholdTemperature, this.Thermostat.CoolingThresholdTemperature);
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} updateCharacteristic CoolingThresholdTemperature: ${this.Thermostat.CoolingThresholdTemperature}`);
}
if (this.Thermostat.TargetHeatingCoolingState === undefined) {
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} TargetHeatingCoolingState: ${this.Thermostat.TargetHeatingCoolingState}`);
}
else {
this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState, this.Thermostat.TargetHeatingCoolingState);
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} updateCharacteristic TargetHeatingCoolingState: ${this.Thermostat.TargetHeatingCoolingState}`);
}
if (this.Thermostat.CurrentHeatingCoolingState === undefined) {
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} CurrentHeatingCoolingState: ${this.Thermostat.CurrentHeatingCoolingState}`);
}
else {
this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.CurrentHeatingCoolingState, this.Thermostat.CurrentHeatingCoolingState);
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} updateCharacteristic CurrentHeatingCoolingState: ${this.Thermostat.TargetHeatingCoolingState}`);
}
if (!this.device.thermostat?.hide_humidity) {
if (this.device.indoorHumidity) {
if (this.HumiditySensor?.CurrentRelativeHumidity === undefined) {
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} CurrentRelativeHumidity: ${this.HumiditySensor?.CurrentRelativeHumidity}`);
}
else {
this.HumiditySensor.Service.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, this.HumiditySensor.CurrentRelativeHumidity);
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} updateCharacteristic CurrentRelativeHumidity: ${this.HumiditySensor.CurrentRelativeHumidity}`);
}
}
}
if (!this.device.thermostat?.hide_fan) {
if (this.device.settings?.fan) {
if (this.Fan?.TargetFanState === undefined) {
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} Fan TargetFanState: ${this.Fan?.TargetFanState}`);
}
else {
this.Fan.Service.updateCharacteristic(this.hap.Characteristic.TargetFanState, this.Fan.TargetFanState);
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} Fan updateCharacteristic TargetFanState: ${this.Fan.TargetFanState}`);
}
if (this.Fan?.Active === undefined) {
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} Fan Active: ${this.Fan?.Active}`);
}
else {
this.Fan.Service.updateCharacteristic(this.hap.Characteristic.Active, this.Fan.Active);
this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} Fan updateCharacteristic Active: ${this.Fan.Active}`);
}
}
}
}
async apiError(e) {
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);
if (!this.device.thermostat?.hide_humidity) {
if (this.device.indoorHumidity) {