UNPKG

hap-homematic

Version:

provides a homekit bridge to the ccu

515 lines (460 loc) 21.2 kB
/* * File: HomeMaticThermostatAccessory.js * Project: hap-homematic * File Created: Wednesday, 25th March 2020 10:01:00 am * Author: Thomas Kluge (th.kluge@me.com) * ----- * The MIT License (MIT) * * Copyright (c) Thomas Kluge <th.kluge@me.com> (https://github.com/thkl) * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * ========================================================================== */ const path = require('path') const HomeMaticAccessory = require(path.join(__dirname, 'HomeMaticAccessory.js')) class HomeMaticThermostatAccessory extends HomeMaticAccessory { async publishServices(Service, Characteristic) { let self = this this.minSetTemp = 4.5 this.maxSetTemp = 30.5 this.offTemp = 4.5 this.currentTemperature = -255 this.currentHumidity = -255 this.targetTemperature = -255 this.lastTemperature = -255 let settings = this.getDeviceSettings() this.addBoostMode = settings.addBootMode || false this.showSepHumidityTile = settings.showSepHumidityTile || false // only fetch min max if we know where to get this if (this.getDataPointNameFromSettings('minTemp', null)) { this.getMinMaxTemp() } this.service = this.addService(new Service.Thermostat(this._name)) if (this.getDataPointNameFromSettings('ControlMode', null)) { this.debugLog('Registering Controle Mode Characteristic') this.curHeatingState = this.service.getCharacteristic(Characteristic.CurrentHeatingCoolingState) .on('get', async (callback) => { let modes = await self.calculateHeatingCoolingState(Characteristic) self.lastMode = modes.currentMode self.debugLog('getCurrentHeatingCoolingState %s', modes.currentMode) callback(null, modes.currentMode) }) .setProps({ format: Characteristic.Formats.UINT8, perms: [Characteristic.Perms.READ, Characteristic.Perms.NOTIFY], validValues: [0, 1, 3] }) this.tarHeatingState = this.service.getCharacteristic(Characteristic.TargetHeatingCoolingState) .setProps({ format: Characteristic.Formats.UINT8, perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY], validValues: [0, 1, 3] }) .on('get', async (callback) => { let modes = await self.calculateHeatingCoolingState(Characteristic) self.debugLog('getTargetHeatingCoolingState %s', modes.targetMode) callback(null, modes.targetMode) }) .on('set', async (newValue, callback) => { self.debugLog('setTargetHeatingCoolingState (ControlMode) %s', newValue) self.setControlMode(newValue) let modes = await self.calculateHeatingCoolingState(Characteristic) self.updateCharacteristic(self.curHeatingState, modes.currentMode) callback() }) this.registerAddressWithSettingsKeyForEventProcessingAtAccessory('ControlMode', null, (newValue) => { self.processSetControlEvent(Characteristic, newValue) }) } this.curTemperatureChar = this.service.getCharacteristic(Characteristic.CurrentTemperature) .on('get', async (callback) => { let value = await self.getValueForDataPointNameWithSettingsKey('Temperature', null, false) let fValue = parseFloat(value) self.debugLog('Current Temperature is %s', fValue) self.currentTemperature = fValue if (callback) callback(null, fValue) }) this.tarTemperatureChar = this.service.getCharacteristic(Characteristic.TargetTemperature) .on('get', async (callback) => { if (self.targetTemperature === -255) { let fValue = await self.getValueForDataPointNameWithSettingsKey('SetTemperature', null, false) self.targetTemperature = fValue } self.debugLog('Target Temperature is %s', self.targetTemperature) if (callback) callback(null, self.targetTemperature) }) .on('set', async (value, callback) => { self.debugLog('set TargetTemperature Event from HomeKit with vaue %s °C', value) self.targetTemperature = value if (self.targetTemperature !== self.offTemp) { self.debugLog('its != offTemp so save in lastTemperature') self.lastTemperature = self.targetTemperature } self.debugLog('(set TargetTemperature event from HomeKit) Will set Target Temp in CCU to %s', this.lastTemperature) await self.setValueForDataPointNameWithSettingsKey('SetTemperature', null, value) callback() }) .setProps({ format: Characteristic.Formats.FLOAT, unit: Characteristic.Units.CELSIUS, maxValue: self.maxSetTemp, minValue: self.minSetTemp, minStep: 0.1, perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY] }) this.service.getCharacteristic(Characteristic.TemperatureDisplayUnits) .on('get', (callback) => { if (callback) callback(null, Characteristic.TemperatureDisplayUnits.CELSIUS) }) this.enableLoggingService('weather') this.registerAddressWithSettingsKeyForEventProcessingAtAccessory('SetTemperature', null, (newValue) => { self.debugLog('SetTemp Event %s', newValue) if (!self.lockModes) { self.processSetTempEvent(Characteristic, newValue) } }) let hmDp = this.getDataPointNameFromSettings('Humidity', null) self.debugLog('check if datapoint for Humidity exists : %s', hmDp) if ((hmDp) && (await this._ccu.hazDatapoint(this.buildAddress(hmDp)))) { if (this.showSepHumidityTile) { self.debugLog('add humidity as sep tile') this.humidityService = this.addService(new Service.HumiditySensor(this._name)) this.curHumidity = this.humidityService.getCharacteristic(Characteristic.CurrentRelativeHumidity) } else { self.debugLog('add humidity as service') this.curHumidity = this.service.getCharacteristic(Characteristic.CurrentRelativeHumidity) } this.curHumidity .on('get', async (callback) => { let value = await self.getValueForDataPointNameWithSettingsKey('Humidity', null, false) let fValue = parseFloat(value) self.currentHumidity = fValue if (callback) callback(null, fValue) }) this.registerAddressWithSettingsKeyForEventProcessingAtAccessory('Humidity', null, (newValue) => { let pValue = parseFloat(newValue) self.currentHumidity = pValue self.curHumidity.updateValue(pValue, null) self.updateHistory() }) } else { self.currentHumidity = 0 } this.registerAddressWithSettingsKeyForEventProcessingAtAccessory('Temperature', null, (newValue) => { self.currentTemperature = parseFloat(newValue) self.debugLog('curTemperature Event is %s °C', self.currentTemperature) self.curTemperatureChar.updateValue(self.currentTemperature, null) self.updateHistory() }) this.boostState = 0 if (this.addBoostMode) { // add the Boost Mode Switch let boostService = this.addService(new Service.Switch(this._name + ' Boost Mode', 'Boost Mode')) this.boostMode = boostService.getCharacteristic(Characteristic.On) .on('get', (callback) => { if (callback) callback(null, (self.boostState > 0)) }) .on('set', (value, callback) => { self.debugLog('hk boost command %s', value) if (value === true) { self.setValue('BOOST_MODE', 1, () => { }) } else { if (self.controlMode === 0) { self.debugLog('boost is off restoring controlmode auto') self.setValue('AUTO_MODE', 1, () => { }) } else { self.debugLog('boost is off restoring controlmode manu') self.setValue('MANU_MODE', 1, () => { }) } } callback() }) // } if (this.boostMode) { this.registerAddressForEventProcessingAtAccessory(this.buildAddress('BOOST_STATE'), (newValue) => { self.boostState = parseInt(newValue) self.debugLog('BOOST STATE is %s (%s)', self.boostState, (self.boostState > 0)) self.boostMode.updateValue((self.boostState > 0)) }) } let strValveState = this.getDataPointNameFromSettings('ValveState', null) if (strValveState) { // check if there is a VALVE_STATE datapoint and add the valve if (await this._ccu.hazDatapoint(this.buildAddress(strValveState))) { const EveHomeKitValveTypes = require(path.join(__dirname, 'EveValve.js')) let eveValve = new EveHomeKitValveTypes(this.gatoHomeBridge.hap) this.service.addOptionalCharacteristic(eveValve.Characteristic.CurrentValveState) let chValve = this.service.getCharacteristic(eveValve.Characteristic.CurrentValveState) chValve.on('get', async (callback) => { let value = await self.getValueForDataPointNameWithSettingsKey('ValveState', null, false) let fValue = parseFloat(value) callback(null, (fValue * 100)) }) this.registerAddressWithSettingsKeyForEventProcessingAtAccessory('ValveState', null, (newValue) => { let fValue = parseFloat(newValue) self.updateCharacteristic(chValve, (fValue * 100)) }) } } this.addLowBatCharacteristic() } async getMinMaxTemp() { this.debugLog('Fetching setTemp boundaries from device') let deviceMasterData = await this._ccu.sendInterfaceCommand(this._interf, 'getParamset', [this._serial, 'MASTER']) if (deviceMasterData) { let minKey = this.getDataPointNameFromSettings('minTemp', null) let maxKey = this.getDataPointNameFromSettings('maxTemp', null) if ((minKey) && (maxKey)) { this.minSetTemp = deviceMasterData[minKey] || 4.5 this.maxSetTemp = deviceMasterData[maxKey] || 30.5 } } this.debugLog('min %s | max %s will be used in homekit for this device', this.minSetTemp, this.maxSetTemp) } async processSetTempEvent(Characteristic, newValue) { this.targetTemperature = parseFloat(newValue) this.debugLog('setTemperature Event is %s °C', newValue) if (!this.lockModes) { if (this.tarHeatingState) { let modes = await this.calculateHeatingCoolingState(Characteristic) this.debugLog('setModes is %s', modes) if (modes.currentMode === undefined) { modes.currentMode = Characteristic.CurrentHeatingCoolingState.HEAT } if (modes.targetMode === undefined) { modes.targetMode = Characteristic.TargetHeatingCoolingState.AUTO } this.debugLog('corr setModes is %s', modes) this.lastMode = modes.currentMode this.updateCharacteristic(this.curHeatingState, modes.currentMode) this.updateCharacteristic(this.tarHeatingState, modes.targetMode) } this.debugLog('targetTemperature is %s', this.targetTemperature) this.updateCharacteristic(this.tarTemperatureChar, this.targetTemperature) } } async processSetControlEvent(Characteristic, newValue) { if (this.lockModes) { this.debugLog('event ControlMode ModeChange is locked due to manual set') return } this.debugLog('event ControlMode is %s', newValue) this.controlMode = parseInt(newValue) let modes = await this.calculateHeatingCoolingState(Characteristic) this.debugLog('processSetControlEvent set current Mode %s set targetMode %s', modes.currentMode, modes.targetMode) this.lastMode = modes.currentMode this.updateCharacteristic(this.curHeatingState, modes.currentMode) this.updateCharacteristic(this.tarHeatingState, modes.targetMode) } async calculateHeatingCoolingState(Characteristic) { // We have to calculate the state depending on the controlmode let result = {} // if not set update the values - further updates will come thru events if (this.targetTemperature === -255) { this.debugLog('unknown target temp request it...') let value = await this.getValueForDataPointNameWithSettingsKey('SetTemperature', null, false) if (value) { this.debugLog('result for target temperature is %s', value) this.targetTemperature = parseFloat(value) } } if (this.currentTemperature === -255) { this.debugLog('unknown current temp request it...') let value = await this.getValueForDataPointNameWithSettingsKey('Temperature', null, false) if (value) { this.debugLog('result for current temperature is %s', value) this.currentTemperature = parseFloat(value) } } if ((this.controlMode === undefined) && (this.tarHeatingState)) { this.debugLog('setTemp != offTemp getControlMode') await this.getControlMode() } this.debugLog('getting Mode from these Values : CT: %s TT: %s CM: %s', this.currentTemperature, this.targetTemperature, this.controlMode) switch (this.controlMode) { case 0: this.debugLog('targetMode = AUTO') this.debugLog('currentMode = AUTO') result.targetMode = Characteristic.TargetHeatingCoolingState.AUTO result.currentMode = Characteristic.CurrentHeatingCoolingState.HEAT break case 1: case 2: this.debugLog('targetMode = HEAT (Manu fromCCU)') result.targetMode = Characteristic.TargetHeatingCoolingState.HEAT this.debugLog('currentMode = HEAT (Manu fromCCU)') result.currentMode = Characteristic.CurrentHeatingCoolingState.HEAT break case 3: this.debugLog('targetMode = HEAT (Boost fromCCU)') result.targetMode = Characteristic.TargetHeatingCoolingState.HEAT // when Boost the mode is heating this.debugLog('currentMode = HEAT (Boost fromCCU)') result.currentMode = Characteristic.CurrentHeatingCoolingState.HEAT } if (this.targetTemperature === this.offTemp) { this.debugLog('targetMode = OFF (tagetTemp == offTemp)') result.currentMode = Characteristic.CurrentHeatingCoolingState.OFF result.targetMode = Characteristic.TargetHeatingCoolingState.OFF } return result } getControlMode() { let self = this return new Promise((resolve, reject) => { if (self.controlMode === undefined) { self.getValueForDataPointNameWithSettingsKey('ControlMode', null, true).then((newValue) => { self.controlMode = parseInt(newValue) resolve(self.controlMode) }) } else { resolve(self.controlMode) } }) } async setControlMode(newMode) { if (this.controlMode === newMode) { // do nothing when we allready in this mode return } if (this.tarHeatingState !== undefined) { // only set control modes if we have a target Heating state which means we know how to set the mode let self = this this.debugLog('setControlMode %s', newMode) switch (newMode) { case 0: // OFF // set ControlMode to manu this.lockModes = true // check if we have a last temp if ((this.lastTemperature === -255) || (this.lastTemperature === this.offTemp)) { this.debugLog('first Run unknown lastTemp so fetch the current Temp from CCU') let fValue = await this.getValueForDataPointNameWithSettingsKey('SetTemperature', null, false) this.lastTemperature = fValue // First Start get the target Temp from the thermostat this.debugLog('CCU returns %s °C for lastTemo. Save it and return', this.lastTemperature) } this.targetTemperature = this.offTemp this.debugLog('setControlMode OFF - Manu Mode targetTemp is %s', this.targetTemperature) this.debugLog('setManu Mode in CCU and OffTemp %s', this.targetTemperature) this.setValueForDataPointNameWithSettingsKey('SetManuMode', null, this.getDataPointValueFromSettings('SetManuMode')) this.controlMode = 0 this.debugLog('Will set Target Temp in CCU to %s', this.targetTemperature) this.setValueForDataPointNameWithSettingsKey('SetTemperature', null, this.targetTemperature).then(() => { setTimeout(() => { self.lockModes = false self.debugLog('setControlMode OFF unlock events') }, 2000) }) break case 1: // HEAT this.controlMode = 1 this.targetTemperature = this.lastTemperature this.lockModes = true this.debugLog('Will set Target Temp in CCU to %s', this.lastTemperature) this.debugLog('setControlMode HEAT - Manu Mode lastTemperature is %s', this.lastTemperature) let newMode = this.getDataPointValueFromSettings('SetManuMode') this.debugLog('setControlMode HEAT - set Manu Mode in thermostat %s', newMode) await this.setValueForDataPointNameWithSettingsKey('SetManuMode', null, newMode) this.setValueForDataPointNameWithSettingsKey('SetTemperature', null, this.lastTemperature).then(() => { setTimeout(() => { self.lockModes = false self.debugLog('setControlMode HEAT unlock events') }, 2000) }) break case 3: // AUTO this.lockModes = true this.debugLog('setControlMode AUTO - Auto Mode') this.setValueForDataPointNameWithSettingsKey('SetAutoMode', null, this.getDataPointValueFromSettings('SetAutoMode')).then(() => { self.controlMode = 3 setTimeout(() => { self.lockModes = false self.debugLog('setControlMode AUTO unlock events') }, 2000) }) break default: break } } } updateHistory() { // do not add the first 0 after reboot if ((this.currentTemperature !== -255) && (this.currentHumidity !== -255)) { this.addLogEntry({ temp: this.currentTemperature, pressure: 0, humidity: this.currentHumidity }) } } static channelTypes() { return ['CLIMATECONTROL_RT_TRANSCEIVER', 'THERMALCONTROL_TRANSMIT', 'CLIMATECONTROL_REGULATOR'] } static serviceDescription() { return 'This service provides a thermostat for HomeKit' } initServiceSettings() { return { '*': { Temperature: { name: 'ACTUAL_TEMPERATURE' }, SetTemperature: { name: 'SET_TEMPERATURE' }, ControlMode: { name: 'CONTROL_MODE' }, SetManuMode: { name: 'MANU_MODE', value: 1 }, SetAutoMode: { name: 'AUTO_MODE', value: 1 }, Humidity: { name: 'ACTUAL_HUMIDITY' }, ValveState: { name: 'VALVE_STATE' }, minTemp: { name: 'TEMPERATURE_MINIMUM' }, maxTemp: { name: 'TEMPERATURE_MAXIMUM' } }, 'THERMALCONTROL_TRANSMIT': { Temperature: { name: 'ACTUAL_TEMPERATURE' }, SetTemperature: { name: 'SET_TEMPERATURE' }, Humidity: { name: 'ACTUAL_HUMIDITY' }, ControlMode: { name: 'CONTROL_MODE' }, SetManuMode: { name: 'MANU_MODE', value: 1 }, SetAutoMode: { name: 'AUTO_MODE', value: 1 }, minTemp: { name: 'TEMPERATURE_MINIMUM' }, maxTemp: { name: 'TEMPERATURE_MAXIMUM' } }, 'CLIMATECONTROL_REGULATOR': { Temperature: { name: '1.TEMPERATURE' }, SetTemperature: { name: '2.SETPOINT' }, Humidity: { name: '1.HUMIDITY' } } } } static configurationItems() { return { 'addBootMode': { type: 'checkbox', default: false, label: 'Add a boost mode switch', hint: 'adds a switch to turn the boost mode on' }, 'showSepHumidityTile': { type: 'checkbox', default: false, label: 'Show Humidity as separate tile', hint: 'this will remove the humidity sensor from the thermostat and will show the humidity as a separate tile' } } } static validate(configurationItem) { return false } } module.exports = HomeMaticThermostatAccessory