UNPKG

@maxsaber/homebridge-govee

Version:

Homebridge plugin to integrate Govee devices into HomeKit.

617 lines (549 loc) 22.5 kB
import { base64ToHex, farToCen, getTwoItemPosition, hasProperty, hexToTwoItems, nearestHalf, parseError, } from '../utils/functions.js' import platformLang from '../utils/lang-en.js' /* H7130 (with temperature reporting) { "mode": { "options": [ { "name": "Low", "value": "1" }, { "name": "Medium", "value": "2" }, { "name": "High", "value": "3" } ] } } */ export default class { constructor(platform, accessory) { // Set up variables from the platform this.hapChar = platform.api.hap.Characteristic this.hapErr = platform.api.hap.HapStatusError this.hapServ = platform.api.hap.Service this.platform = platform this.log = platform.log // Set up variables from the accessory this.accessory = accessory // Set up objects this.speedCode = { 33: 'MwUBAAAAAAAAAAAAAAAAAAAAADc=', 66: 'MwUCAAAAAAAAAAAAAAAAAAAAADQ=', 99: 'MwUDAAAAAAAAAAAAAAAAAAAAADU=', } this.speedCodeLabel = { 33: 'low', 66: 'medium', 99: 'high', } this.tempCodeAuto = { 5: 'MxoBAJAEAAAAAAAAAAAAAAAAALw=', 6: 'MxoBAJBoAAAAAAAAAAAAAAAAANA=', 7: 'MxoBAJEwAAAAAAAAAAAAAAAAAIk=', 8: 'MxoBAJH4AAAAAAAAAAAAAAAAAEE=', 9: 'MxoBAJLAAAAAAAAAAAAAAAAAAHo=', 10: 'MxoBAJOIAAAAAAAAAAAAAAAAADM=', 11: 'MxoBAJPsAAAAAAAAAAAAAAAAAFc=', 12: 'MxoBAJS0AAAAAAAAAAAAAAAAAAg=', 13: 'MxoBAJV8AAAAAAAAAAAAAAAAAME=', 14: 'MxoBAJZEAAAAAAAAAAAAAAAAAPo=', 15: 'MxoBAJcMAAAAAAAAAAAAAAAAALM=', 16: 'MxoBAJdwAAAAAAAAAAAAAAAAAM8=', 17: 'MxoBAJg4AAAAAAAAAAAAAAAAAIg=', 18: 'MxoBAJkAAAAAAAAAAAAAAAAAALE=', 19: 'MxoBAJnIAAAAAAAAAAAAAAAAAHk=', 20: 'MxoBAJqQAAAAAAAAAAAAAAAAACI=', 21: 'MxoBAJr0AAAAAAAAAAAAAAAAAEY=', 22: 'MxoBAJu8AAAAAAAAAAAAAAAAAA8=', 23: 'MxoBAJyEAAAAAAAAAAAAAAAAADA=', 24: 'MxoBAJ1MAAAAAAAAAAAAAAAAAPk=', 25: 'MxoBAJ4UAAAAAAAAAAAAAAAAAKI=', 26: 'MxoBAJ54AAAAAAAAAAAAAAAAAM4=', 27: 'MxoBAJ9AAAAAAAAAAAAAAAAAAPc=', 28: 'MxoBAKAIAAAAAAAAAAAAAAAAAIA=', 29: 'MxoBAKDQAAAAAAAAAAAAAAAAAFg=', 30: 'MxoBAKGYAAAAAAAAAAAAAAAAABE=', } this.tempCodeHeat = { 5: 'MxoAAJAEAAAAAAAAAAAAAAAAAL0=', 6: 'MxoAAJBoAAAAAAAAAAAAAAAAANE=', 7: 'MxoAAJEwAAAAAAAAAAAAAAAAAIg=', 8: 'MxoAAJH4AAAAAAAAAAAAAAAAAEA=', 9: 'MxoAAJLAAAAAAAAAAAAAAAAAAHs=', 10: 'MxoAAJOIAAAAAAAAAAAAAAAAADI=', 11: 'MxoAAJPsAAAAAAAAAAAAAAAAAFY=', 12: 'MxoAAJS0AAAAAAAAAAAAAAAAAAk=', 13: 'MxoAAJV8AAAAAAAAAAAAAAAAAMA=', 14: 'MxoAAJZEAAAAAAAAAAAAAAAAAPs=', 15: 'MxoAAJcMAAAAAAAAAAAAAAAAALI=', 16: 'MxoAAJdwAAAAAAAAAAAAAAAAAM4=', 17: 'MxoAAJg4AAAAAAAAAAAAAAAAAIk=', 18: 'MxoAAJkAAAAAAAAAAAAAAAAAALA=', 19: 'MxoAAJnIAAAAAAAAAAAAAAAAAHg=', 20: 'MxoAAJqQAAAAAAAAAAAAAAAAACM=', 21: 'MxoAAJr0AAAAAAAAAAAAAAAAAEc=', 22: 'MxoAAJu8AAAAAAAAAAAAAAAAAA4=', 23: 'MxoAAJyEAAAAAAAAAAAAAAAAADE=', 24: 'MxoAAJ1MAAAAAAAAAAAAAAAAAPg=', 25: 'MxoAAJ4UAAAAAAAAAAAAAAAAAKM=', 26: 'MxoAAJ54AAAAAAAAAAAAAAAAAM8=', 27: 'MxoAAJ9AAAAAAAAAAAAAAAAAAPY=', 28: 'MxoAAKAIAAAAAAAAAAAAAAAAAIE=', 29: 'MxoAAKDQAAAAAAAAAAAAAAAAAFk=', 30: 'MxoAAKGYAAAAAAAAAAAAAAAAABA=', } // Remove any old light service if (this.accessory.getService(this.hapServ.Lightbulb)) { this.accessory.removeService(this.accessory.getService(this.hapServ.Lightbulb)) } // Remove any old fanv2 service if (this.accessory.getService(this.hapServ.Fanv2)) { this.accessory.removeService(this.accessory.getService(this.hapServ.Fanv2)) } // Add the heater service if it doesn't already exist this.service = this.accessory.getService(this.hapServ.HeaterCooler) if (!this.service) { this.service = this.accessory.addService(this.hapServ.HeaterCooler) this.service.updateCharacteristic(this.hapChar.CurrentTemperature, 20) this.service.updateCharacteristic(this.hapChar.HeatingThresholdTemperature, 20) } // Add the fan service if it doesn't already exist this.fanService = this.accessory.getService(this.hapServ.Fan) || this.accessory.addService(this.hapServ.Fan) // Add the set handler to the heater active characteristic this.service .getCharacteristic(this.hapChar.Active) .onSet(async value => this.internalStateUpdate(value)) this.cacheState = this.service.getCharacteristic(this.hapChar.Active).value === 1 ? 'on' : 'off' // Add options to the heater target state characteristic this.service .getCharacteristic(this.hapChar.TargetHeaterCoolerState) .setProps({ minValue: 0, maxValue: 1, validValues: [0, 1], }) .onSet(async value => this.internalModeUpdate(value)) this.cacheMode = this.service.getCharacteristic(this.hapChar.TargetHeaterCoolerState).value === 0 ? 'auto' : 'heat' this.cacheTemp = this.service.getCharacteristic(this.hapChar.CurrentTemperature).value // Add the set handler and a range to the heater target temperature characteristic this.service .getCharacteristic(this.hapChar.HeatingThresholdTemperature) .setProps({ minValue: 5, maxValue: 30, minStep: 1, }) .onSet(async value => this.internalTempUpdate(value)) this.cacheTarg = this.service.getCharacteristic(this.hapChar.HeatingThresholdTemperature).value // Add the set handler to the heater swing mode characteristic (for oscillation) this.service .getCharacteristic(this.hapChar.SwingMode) .onSet(async value => this.internalSwingUpdate(value)) this.cacheSwing = this.service.getCharacteristic(this.hapChar.SwingMode).value === 1 ? 'on' : 'off' // Add the set handler to the heater lock characteristic this.service .getCharacteristic(this.hapChar.LockPhysicalControls) .onSet(async value => this.internalLockUpdate(value)) this.cacheLock = this.service.getCharacteristic(this.hapChar.LockPhysicalControls).value === 1 ? 'on' : 'off' // Add the set handler to the fan on/off characteristic this.fanService .getCharacteristic(this.hapChar.On) .onSet(async value => this.internalFanStateUpdate(value)) this.cacheFanState = this.fanService.getCharacteristic(this.hapChar.On).value ? 'on' : 'off' // Add the set handler to the fan rotation speed characteristic this.fanService .getCharacteristic(this.hapChar.RotationSpeed) .setProps({ minStep: 33, validValues: [0, 33, 66, 99], }) .onSet(async value => this.internalSpeedUpdate(value)) this.cacheSpeed = this.fanService.getCharacteristic(this.hapChar.RotationSpeed).value // Output the customised options to the log const opts = JSON.stringify({ tempReporting: true, }) platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts) } async internalStateUpdate(value) { try { const newValue = value === 1 ? 'on' : 'off' // Don't continue if the new value is the same as before if (this.cacheState === newValue) { return } // Send the request to the platform sender function await this.platform.sendDeviceUpdate(this.accessory, { cmd: 'ptReal', value: value ? 'MwEBAAAAAAAAAAAAAAAAAAAAADM=' : 'MwEAAAAAAAAAAAAAAAAAAAAAADI=', }) // Cache the new state and log if appropriate if (this.cacheState !== newValue) { this.cacheState = newValue this.accessory.log(`${platformLang.curState} [${newValue}]`) } // Fan state should also match the new state if (this.cacheFanState !== newValue) { this.cacheFanState = newValue this.fanService.updateCharacteristic(this.hapChar.On, newValue === 'on') } } catch (err) { // Catch any errors during the process this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`) // Throw a 'no response' error and set a timeout to revert this after 2 seconds setTimeout(() => { this.service.updateCharacteristic(this.hapChar.Active, this.cacheState === 'on' ? 1 : 0) }, 2000) throw new this.hapErr(-70402) } } async internalModeUpdate(value) { try { const newMode = value === 0 ? 'auto' : 'heat' // Don't continue if the new value is the same as before if (this.cacheMode === newMode) { return } // Get the current state of the heater const objectToChoose = newMode === 'auto' ? this.tempCodeAuto : this.tempCodeHeat // Send the request to the platform sender function await this.platform.sendDeviceUpdate(this.accessory, { cmd: 'ptReal', value: objectToChoose[this.cacheTemp], }) // Cache the new state and log if appropriate if (this.cacheMode !== newMode) { this.cacheMode = newMode this.accessory.log(`${platformLang.curMode} [${newMode}]`) } } catch (err) { // Catch any errors during the process this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`) // Throw a 'no response' error and set a timeout to revert this after 2 seconds setTimeout(() => { this.service.updateCharacteristic( this.hapChar.TargetHeaterCoolerState, this.cacheMode === 'auto' ? 0 : 1, ) }, 2000) throw new this.hapErr(-70402) } } async internalTempUpdate(value) { try { // Don't continue if the new value is the same as before if (this.cacheTarg === value) { return } // Get the current state of the heater const objectToChoose = this.cacheMode === 'auto' ? this.tempCodeAuto : this.tempCodeHeat // Send the request to the platform sender function await this.platform.sendDeviceUpdate(this.accessory, { cmd: 'ptReal', value: objectToChoose[value], }) // Cache the new state and log if appropriate if (this.cacheTarg !== value) { this.cacheTarg = value this.accessory.log(`${platformLang.curTarg} [${this.cacheTarg}°C]`) } } catch (err) { // Catch any errors during the process this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`) // Throw a 'no response' error and set a timeout to revert this after 2 seconds setTimeout(() => { this.service.updateCharacteristic(this.hapChar.HeatingThresholdTemperature, this.cacheTarg) }, 2000) throw new this.hapErr(-70402) } } async internalSwingUpdate(value) { try { // value === 0 -> swing mode OFF // value === 1 -> swing mode ON const newValue = value === 1 ? 'on' : 'off' // Don't continue if the new value is the same as before if (this.cacheSwing === newValue) { return } // Send the request to the platform sender function await this.platform.sendDeviceUpdate(this.accessory, { cmd: 'ptReal', value: value ? 'MxgBAAAAAAAAAAAAAAAAAAAAACo=' : 'MxgAAAAAAAAAAAAAAAAAAAAAACs=', }) // Cache the new state and log if appropriate if (this.cacheSwing !== newValue) { this.cacheSwing = newValue this.accessory.log(`${platformLang.curSwing} [${newValue}]`) } } catch (err) { // Catch any errors during the process this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`) // Throw a 'no response' error and set a timeout to revert this after 2 seconds setTimeout(() => { this.service.updateCharacteristic( this.hapChar.SwingMode, this.cacheSwing === 'on' ? 1 : 0, ) }, 2000) throw new this.hapErr(-70402) } } async internalLockUpdate(value) { try { // value === 0 -> child lock OFF // value === 1 -> child lock ON const newValue = value === 1 ? 'on' : 'off' // Don't continue if the new value is the same as before if (this.cacheLock === newValue) { return } // Send the request to the platform sender function await this.platform.sendDeviceUpdate(this.accessory, { cmd: 'ptReal', value: value ? 'MxABAAAAAAAAAAAAAAAAAAAAACI=' : 'MxAAAAAAAAAAAAAAAAAAAAAAACM=', }) // Cache the new state and log if appropriate if (this.cacheLock !== newValue) { this.cacheLock = newValue this.accessory.log(`${platformLang.curLock} [${newValue}]`) } } catch (err) { // Catch any errors during the process this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`) // Throw a 'no response' error and set a timeout to revert this after 2 seconds setTimeout(() => { this.service.updateCharacteristic( this.hapChar.LockPhysicalControls, this.cacheLock === 'on' ? 1 : 0, ) }, 2000) throw new this.hapErr(-70402) } } async internalFanStateUpdate(value) { try { const newValue = value ? 'on' : 'off' // The fan is used for the following modes (basically all except Auto): // - 0%_: No effect - revert to previous speed // - 33%: Low Mode // - 66%: Medium Mode // - 99%: High Mode // If the main heater is turned off then this fan should be turned off too // If the main heater is turned on then this fan speed should revert to the current mode // Don't continue if the new value is the same as before if (this.cacheFanState === newValue) { return } // Turning the fan on should only be possible if the main heater is off, and this should not do anything if (newValue === 'on') { // Wait a few seconds then turn back off setTimeout(() => { this.fanService.updateCharacteristic(this.hapChar.On, false) }, 3000) return } // Turning fan off: // We should wait a few seconds and then just revert to the previous fan speed setTimeout(() => { this.fanService.updateCharacteristic(this.hapChar.On, true) this.fanService.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed) }, 2000) } catch (err) { // Catch any errors during the process this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`) // Throw a 'no response' error and set a timeout to revert this after 2 seconds setTimeout(() => { this.fanService.updateCharacteristic(this.hapChar.On, this.cacheFanState === 'on') }, 2000) throw new this.hapErr(-70402) } } async internalSpeedUpdate(value) { try { // The fan is used for the following modes (basically all except Auto): // - 0%_: Not sure what to do with this yet // - 33%: Low Mode // - 66%: Medium Mode // - 99%: High Mode // If the main heater is turned off then this fan should be turned off too // If the main heater is turned on then this fan speed should revert to the current mode // Don't continue if the new value is the same as before // If the new speed is 0, the on/off handler should take care of resetting to the speed before (home app only) if (this.cacheSpeed === value || value === 0) { return } // Send the request to the platform sender function await this.platform.sendDeviceUpdate(this.accessory, { cmd: 'ptReal', value: this.speedCode[value], }) // Cache the new state and log if appropriate if (this.cacheSpeed !== value) { this.cacheSpeed = value this.accessory.log(`${platformLang.curSpeed} [${this.speedCodeLabel[value]}]`) } } catch (err) { // Catch any errors during the process this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`) // Throw a 'no response' error and set a timeout to revert this after 2 seconds setTimeout(() => { this.fanService.updateCharacteristic(this.hapChar.On, this.cacheState === 'on') }, 2000) throw new this.hapErr(-70402) } } externalUpdate(params) { // Update the active characteristic if (params.state && params.state !== this.cacheState) { this.cacheState = params.state this.service.updateCharacteristic(this.hapChar.Active, this.cacheState === 'on') this.accessory.log(`${platformLang.curState} [${this.cacheState}]`) // Fan state should also match the main heater state if (this.cacheFanState !== this.cacheState) { this.cacheFanState = this.cacheState this.fanService.updateCharacteristic(this.hapChar.On, this.cacheState === 'on') } } // Update the current temperature characteristic if (hasProperty(params, 'temperature')) { const newTemp = nearestHalf(farToCen(params.temperature / 100)) if (newTemp !== this.cacheTemp) { if (newTemp > 100) { // Device must be one that does not support ambient temperature this.accessory.logWarn('you should disable `tempReporting` in the config for this device') } else { this.cacheTemp = newTemp this.service.updateCharacteristic(this.hapChar.CurrentTemperature, this.cacheTemp) this.accessory.log(`${platformLang.curTemp} [${this.cacheTemp}°C]`) } } } // Update the target temperature characteristic if (hasProperty(params, 'setTemperature')) { const newTemp = Math.round(farToCen(params.setTemperature / 100)) if (newTemp !== this.cacheTarg) { this.cacheTarg = newTemp this.service.updateCharacteristic(this.hapChar.HeatingThresholdTemperature, this.cacheTarg) this.accessory.log(`${platformLang.curTarg} [${this.cacheTarg}°C]`) } } // Check for some other scene/mode change (params.commands || []).forEach((command) => { const hexString = base64ToHex(command) const hexParts = hexToTwoItems(hexString) // Return now if not a device query update code if (getTwoItemPosition(hexParts, 1) !== 'aa') { return } const deviceFunction = `${getTwoItemPosition(hexParts, 2)}${getTwoItemPosition(hexParts, 3)}` switch (deviceFunction) { case '1800': case '1801': { // Swing Mode const newSwing = getTwoItemPosition(hexParts, 3) === '01' ? 'on' : 'off' if (this.cacheSwing !== newSwing) { this.cacheSwing = newSwing this.service.updateCharacteristic(this.hapChar.SwingMode, this.cacheSwing === 'on' ? 1 : 0) this.accessory.log(`${platformLang.curSwing} [${this.cacheSwing}]`) } break } case '1000': case '1001': { // Child Lock const newLock = getTwoItemPosition(hexParts, 3) === '01' ? 'on' : 'off' if (this.cacheLock !== newLock) { this.cacheLock = newLock this.service.updateCharacteristic(this.hapChar.LockPhysicalControls, this.cacheLock === 'on' ? 1 : 0) this.accessory.log(`${platformLang.curLock} [${this.cacheLock}]`) } break } case '0501': // fan speed low case '0502': // fan speed medium case '0503': { // fan speed high switch (getTwoItemPosition(hexParts, 3)) { case '01': { // Fan is low if (this.cacheState === 'on' && this.cacheFanState !== 'on') { this.cacheFanState = 'on' this.fanService.updateCharacteristic(this.hapChar.On, true) this.accessory.log(`${platformLang.curMode} [${this.cacheFanState}]`) } if (this.cacheSpeed !== 33) { this.cacheSpeed = 33 this.fanService.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed) this.accessory.log(`${platformLang.curSpeed} [${this.speedCodeLabel[this.cacheSpeed]}]`) } break } case '02': { // Fan is medium if (this.cacheState === 'on' && this.cacheFanState !== 'on') { this.cacheFanState = 'on' this.fanService.updateCharacteristic(this.hapChar.On, true) this.accessory.log(`${platformLang.curMode} [${this.cacheFanState}]`) } if (this.cacheSpeed !== 66) { this.cacheSpeed = 66 this.fanService.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed) this.accessory.log(`${platformLang.curSpeed} [${this.speedCodeLabel[this.cacheSpeed]}]`) } break } case '03': { // Fan is high if (this.cacheState === 'on' && this.cacheFanState !== 'on') { this.cacheFanState = 'on' this.fanService.updateCharacteristic(this.hapChar.On, true) this.accessory.log(`${platformLang.curMode} [${this.cacheFanState}]`) } if (this.cacheSpeed !== 99) { this.cacheSpeed = 99 this.fanService.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed) this.accessory.log(`${platformLang.curSpeed} [${this.speedCodeLabel[this.cacheSpeed]}]`) } break } } break } case '1a00': // Target temperature (thermostat mode off) case '1a01': { // Target temperature (thermostat mode on) const newMode = getTwoItemPosition(hexParts, 3) === '01' ? 'auto' : 'heat' if (this.cacheMode !== newMode) { this.cacheMode = newMode this.service.updateCharacteristic(this.hapChar.TargetHeaterCoolerState, this.cacheMode === 'auto' ? 0 : 1) this.accessory.log(`${platformLang.curMode} [${this.cacheMode}]`) } break } case '1100': // timer off? case '1101':// timer on case '1300': // scheduling case '1600': // DND off? case '1601': // DND on? break default: this.accessory.logDebugWarn(`${platformLang.newScene}: [${command}] [${hexString}]`) break } }) } }