UNPKG

homebridge-econet-rheem

Version:

Homebridge plugin for control of Rheem and Ruud thermostats and water heaters

228 lines 12.4 kB
import { TemperatureControlAccessory, TemperatureDefaults } from './temperatureControl.js'; import { strings } from '../../i18n/i18n.js'; import { AccessoryType, HKCharacteristicKey, MQTTKey, MQTTKeys, ThermostatMode } from '../../model/enums.js'; import { HistoryType } from '../../model/history.js'; import { fromCelsius } from '../../tools/temperature.js'; const DEFAULT_SETPOINT = 20; const DEFAULT_LOWER_LIMIT = 10; const DEFAULT_UPPER_LIMIT = 30; const DEFAULT_HUMIDITY = 50; export class ThermostatAccessory extends TemperatureControlAccessory { getAccessoryType() { return AccessoryType.Thermostat; } currentStateMap; targetStateMap; deadband; modes = new Map(); mode; running; heatThresholds; coolThresholds; constructor(dependency, data) { super(dependency, data, TemperatureDefaults(DEFAULT_SETPOINT, DEFAULT_LOWER_LIMIT, DEFAULT_UPPER_LIMIT)); this.deadband = data['@DEADBAND']?.value ?? 0; const textModes = data['@MODE']?.constraints?.enumText ?? []; textModes.forEach((textMode, index) => { let mode; const cleanedString = textMode.trim().replace(' ', '_').toUpperCase(); switch (cleanedString) { case 'OFF': mode = ThermostatMode.OFF; break; case 'HEATING': mode = ThermostatMode.HEATING; break; case 'COOLING': mode = ThermostatMode.COOLING; break; case 'AUTO': mode = ThermostatMode.AUTO; break; case 'FAN_ONLY': mode = ThermostatMode.FAN_ONLY; break; case 'EMERGENCY_HEAT': mode = ThermostatMode.EMERGENCY_HEAT; break; default: this.log.warning(`${this.name} has unknown mode '${textMode}'`); return; } this.modes.set(index, mode); }); this.currentStateMap = new Map([ [ThermostatMode.OFF, dependency.Characteristic.CurrentHeatingCoolingState.OFF], [ThermostatMode.HEATING, dependency.Characteristic.CurrentHeatingCoolingState.HEAT], [ThermostatMode.EMERGENCY_HEAT, dependency.Characteristic.CurrentHeatingCoolingState.HEAT], [ThermostatMode.COOLING, dependency.Characteristic.CurrentHeatingCoolingState.COOL], [ThermostatMode.FAN_ONLY, dependency.Characteristic.CurrentHeatingCoolingState.COOL], ]); this.targetStateMap = new Map([ [ThermostatMode.OFF, dependency.Characteristic.TargetHeatingCoolingState.OFF], [ThermostatMode.HEATING, dependency.Characteristic.TargetHeatingCoolingState.HEAT], [ThermostatMode.EMERGENCY_HEAT, dependency.Characteristic.TargetHeatingCoolingState.HEAT], [ThermostatMode.COOLING, dependency.Characteristic.TargetHeatingCoolingState.COOL], [ThermostatMode.FAN_ONLY, dependency.Characteristic.TargetHeatingCoolingState.COOL], [ThermostatMode.AUTO, dependency.Characteristic.TargetHeatingCoolingState.AUTO], ]); this.heatThresholds = this.setupThreshold(HKCharacteristicKey.HeatingThresholdTemperature, data['@HEATSETPOINT'], MQTTKeys(MQTTKey.UNKNOWN, MQTTKey.HEAT_SETPOINT_U)); this.coolThresholds = this.setupThreshold(HKCharacteristicKey.CoolingThresholdTemperature, data['@COOLSETPOINT'], MQTTKeys(MQTTKey.UNKNOWN, MQTTKey.COOL_SETPOINT_U)); this.service.getCharacteristic(this.characteristicFromKey(HKCharacteristicKey.TargetTemperature)) .onGet(this.getTargetTemperature.bind(this)) .onSet(this.setTargetTemperature.bind(this)); this.mode = this.modes.get(data['@MODE']?.value ?? -1) ?? ThermostatMode.OFF; this.running = (this.getValue(data['@RUNNINGSTATUS'])?.replace(/\s/g, '') ?? '').length > 0; this.setup(HKCharacteristicKey.CurrentHeatingCoolingState, this.currentState, MQTTKeys(MQTTKey.UNKNOWN, MQTTKey.RUNNINGSTATUS_U), this.bindOnUpdateCurrentMode()); this.setup(HKCharacteristicKey.TargetHeatingCoolingState, this.targetState, MQTTKeys(MQTTKey.UNKNOWN, MQTTKey.MODE_U), this.bindOnUpdateTargetMode(), this.bindOnSetTargetMode()); const currentHumidity = data['@HUMIDITY']?.value ?? DEFAULT_HUMIDITY; this.setup(HKCharacteristicKey.CurrentRelativeHumidity, currentHumidity, MQTTKeys(MQTTKey.UNKNOWN, MQTTKey.HUMIDITY_U), this.bindOnUpdateNumeric(HKCharacteristicKey.CurrentRelativeHumidity, strings.thermostat.humidity, (value) => { this.recordHistory(HistoryType.WEATHER, { humidity: value }); })); this.recordHistory(HistoryType.WEATHER, { humidity: currentHumidity }); } get currentState() { if (!this.running) { return this.Characteristic.CurrentHeatingCoolingState.OFF; } const currentState = this.currentStateMap.get(this.mode); if (currentState !== undefined) { return currentState; } const currentTemp = this.getProperty(HKCharacteristicKey.CurrentTemperature); if (this.mode === ThermostatMode.AUTO && currentTemp !== undefined) { const targetTemp = this.getTargetTemperature(); return currentTemp < targetTemp ? this.Characteristic.CurrentHeatingCoolingState.HEAT : this.Characteristic.CurrentHeatingCoolingState.COOL; } this.log.warning(`${this.name} unabled to get current state for mode '${this.mode}'`); return this.Characteristic.CurrentHeatingCoolingState.OFF; } get targetState() { const targetState = this.targetStateMap.get(this.mode); if (targetState !== undefined) { return targetState; } this.log.warning(`${this.name} unabled to get target state for mode '${this.mode}'`); return this.Characteristic.TargetHeatingCoolingState.OFF; } bindOnUpdateCurrentMode() { return (async (value) => { if (typeof value !== 'number') { this.log.error(strings.accessory.badValue, this.name, 'number', HKCharacteristicKey.CurrentHeatingCoolingState, `${value.toString()}`); return; } const currentState = this.currentState; this.onUpdate(HKCharacteristicKey.CurrentHeatingCoolingState, currentState, this.logStringForState(currentState)); }).bind(this); } bindOnUpdateTargetMode() { return (async (value) => { if (typeof value !== 'number') { this.log.error(strings.accessory.badValue, this.name, 'number', HKCharacteristicKey.TargetHeatingCoolingState, `${value.toString()}`); return; } const mode = this.modes.get(value); if (mode === undefined) { this.log.warning(`${this.name} unabled to get target mode for value '${value}'`); return; } this.mode = mode; const targetState = this.targetState; this.onUpdate(HKCharacteristicKey.TargetHeatingCoolingState, targetState, this.logStringForState(targetState, true)); }).bind(this); } bindOnSetTargetMode() { return (async (value) => { for (const [mode, state] of this.targetStateMap.entries()) { if (state === value) { for (const [publish, compare] of this.modes.entries()) { if (compare === mode) { const logString = this.logStringForState(value); this.onSet(HKCharacteristicKey.TargetHeatingCoolingState, MQTTKeys(MQTTKey.UNKNOWN, MQTTKey.MODE_U), value, publish, logString); return; } } } } this.log.warning(strings.thermostat.targetSetFailed, this.name, `'${value}'`); }).bind(this); } logStringForState(state, future = false) { switch (state) { case this.Characteristic.CurrentHeatingCoolingState.OFF: case this.Characteristic.TargetHeatingCoolingState.OFF: return future ? strings.thermostat.stateOffFuture : strings.thermostat.stateOff; case this.Characteristic.CurrentHeatingCoolingState.HEAT: case this.Characteristic.TargetHeatingCoolingState.HEAT: return future ? strings.thermostat.stateHeatFuture : strings.thermostat.stateHeat; case this.Characteristic.CurrentHeatingCoolingState.COOL: case this.Characteristic.TargetHeatingCoolingState.COOL: return future ? strings.thermostat.stateCoolFuture : strings.thermostat.stateCool; case this.Characteristic.TargetHeatingCoolingState.AUTO: return strings.thermostat.stateAutoFuture; } } getTargetTemperature() { const heatingThreshold = this.getProperty(HKCharacteristicKey.HeatingThresholdTemperature); const coolingThreshold = this.getProperty(HKCharacteristicKey.CoolingThresholdTemperature); let targetTemp; const targetState = this.getProperty(HKCharacteristicKey.TargetHeatingCoolingState); switch (targetState) { case undefined: case this.Characteristic.TargetHeatingCoolingState.OFF: break; case this.Characteristic.TargetHeatingCoolingState.HEAT: targetTemp = heatingThreshold; break; case this.Characteristic.TargetHeatingCoolingState.COOL: targetTemp = coolingThreshold; break; case this.Characteristic.TargetHeatingCoolingState.AUTO: targetTemp = (heatingThreshold !== undefined && coolingThreshold !== undefined) ? (heatingThreshold + coolingThreshold) / 2 : undefined; break; } if (targetTemp === undefined) { this.log.warning(`${this.name} is unable to determine the target temperature`); } return targetTemp ?? DEFAULT_SETPOINT; } async setTargetTemperature(value) { const temperature = fromCelsius(value, this.units); const payload = {}; const targetState = this.getProperty(HKCharacteristicKey.TargetHeatingCoolingState); switch (targetState) { case undefined: case this.Characteristic.TargetHeatingCoolingState.OFF: break; case this.Characteristic.TargetHeatingCoolingState.HEAT: payload[this.isDeviceAuth ? MQTTKey.UNKNOWN : MQTTKey.HEAT_SETPOINT_U] = temperature; break; case this.Characteristic.TargetHeatingCoolingState.COOL: payload[this.isDeviceAuth ? MQTTKey.UNKNOWN : MQTTKey.COOL_SETPOINT_U] = temperature; break; case this.Characteristic.TargetHeatingCoolingState.AUTO: { let heatSetPoint = temperature - this.deadband / 2; let coolSetPoint = temperature + this.deadband / 2; heatSetPoint = Math.max(this.heatThresholds.minimum, Math.min(heatSetPoint, this.heatThresholds.maximum)); coolSetPoint = Math.max(this.coolThresholds.minimum, Math.min(coolSetPoint, this.coolThresholds.maximum)); if (coolSetPoint < heatSetPoint + this.deadband) { if (heatSetPoint + this.deadband <= this.coolThresholds.maximum) { coolSetPoint = heatSetPoint + this.deadband; } else { heatSetPoint = coolSetPoint - this.deadband; heatSetPoint = Math.max(this.heatThresholds.minimum, heatSetPoint); coolSetPoint = Math.max(this.coolThresholds.minimum, heatSetPoint + this.deadband); } } payload[this.isDeviceAuth ? MQTTKey.UNKNOWN : MQTTKey.HEAT_SETPOINT_U] = heatSetPoint; payload[this.isDeviceAuth ? MQTTKey.UNKNOWN : MQTTKey.COOL_SETPOINT_U] = coolSetPoint; break; } } if (Object.keys(payload).length > 0) { this.publishPayload(payload); } } } //# sourceMappingURL=thermostat.js.map