UNPKG

@homebridge-plugins/homebridge-govee

Version:

Homebridge plugin to integrate Govee devices into HomeKit.

157 lines (133 loc) 6.39 kB
import { base64ToHex, cenToFar, getTwoItemPosition, hexToTwoItems, parseError, } from '../utils/functions.js' import platformLang from '../utils/lang-en.js' // HomeKit triggers "CO2 detected" abnormal flag at this threshold (ppm). // Govee app default warn level is 1000 ppm; override via deviceConf.co2AbnormalThreshold. const DEFAULT_CO2_ABNORMAL_PPM = 1000 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 // Set up variables from the accessory this.accessory = accessory // Set up custom variables for this device type const deviceConf = platform.deviceConf[accessory.context.gvDeviceId] || {} this.co2AbnormalThreshold = deviceConf.co2AbnormalThreshold || DEFAULT_CO2_ABNORMAL_PPM // Add the CO2 sensor service (with level + peak characteristics) if it doesn't already exist this.co2Service = this.accessory.getService(this.hapServ.CarbonDioxideSensor) || this.accessory.addService(this.hapServ.CarbonDioxideSensor) if (!this.co2Service.testCharacteristic(this.hapChar.CarbonDioxideLevel)) { this.co2Service.addCharacteristic(this.hapChar.CarbonDioxideLevel) } if (!this.co2Service.testCharacteristic(this.hapChar.CarbonDioxidePeakLevel)) { this.co2Service.addCharacteristic(this.hapChar.CarbonDioxidePeakLevel) } this.cacheCO2 = this.co2Service.getCharacteristic(this.hapChar.CarbonDioxideLevel).value || 0 this.cacheCO2Peak = this.co2Service.getCharacteristic(this.hapChar.CarbonDioxidePeakLevel).value || 0 this.cacheCO2Detected = this.co2Service.getCharacteristic(this.hapChar.CarbonDioxideDetected).value || 0 // Add the temperature service if it doesn't already exist this.tempService = this.accessory.getService(this.hapServ.TemperatureSensor) || this.accessory.addService(this.hapServ.TemperatureSensor) this.cacheTemp = this.tempService.getCharacteristic(this.hapChar.CurrentTemperature).value // Add the humidity service if it doesn't already exist this.humiService = this.accessory.getService(this.hapServ.HumiditySensor) || this.accessory.addService(this.hapServ.HumiditySensor) this.cacheHumi = this.humiService.getCharacteristic(this.hapChar.CurrentRelativeHumidity).value // No Battery service — H5140 is mains-powered via USB; the Govee cloud // stream doesn't carry a meaningful battery level. Remove any stale service // left over from earlier versions of this handler. const staleBattery = this.accessory.getService(this.hapServ.Battery) if (staleBattery) { this.accessory.removeService(staleBattery) } this.updateCache() // Pass the accessory to Fakegato to set up with Eve this.accessory.eveService = new platform.eveService('custom', this.accessory, { log: () => {}, }) // Output the customised options to the log const opts = JSON.stringify({ co2AbnormalThreshold: this.co2AbnormalThreshold, }) platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts) } async externalUpdate(params) { // Parse AWS reading packets — opcode 0x0A carries live CO2 / temp / humidity const commands = params.commands || [] commands.forEach((command) => { const hexString = base64ToHex(command) const hexParts = hexToTwoItems(hexString) if (!hexParts || hexParts.length < 20) { return } if (getTwoItemPosition(hexParts, 1) !== 'aa') { return } if (getTwoItemPosition(hexParts, 2) !== '0a') { return } // 1-indexed: position N -> byte (N-1). LE u16: low byte at lower position. const u16le = (lsbPos, msbPos) => Number.parseInt( `${getTwoItemPosition(hexParts, msbPos)}${getTwoItemPosition(hexParts, lsbPos)}`, 16, ) const offTemp = this.accessory.context.offTemp || 0 const offHumi = this.accessory.context.offHumi || 0 const tempRaw = u16le(3, 4) // bytes 2-3 of packet, °C × 100 const humiRaw = u16le(5, 6) // bytes 4-5, %RH × 100 const co2Raw = u16le(7, 8) // bytes 6-7, ppm const newTemp = Math.round(tempRaw + offTemp) / 100 const newHumi = Math.max(0, Math.min(100, Math.round((humiRaw + offHumi) / 100))) const newCO2 = co2Raw if (newTemp !== this.cacheTemp && newTemp > -40 && newTemp < 100) { this.cacheTemp = newTemp this.tempService.updateCharacteristic(this.hapChar.CurrentTemperature, this.cacheTemp) this.accessory.eveService.addEntry({ temp: this.cacheTemp }) this.accessory.log(`${platformLang.curTemp} [${this.cacheTemp}°C / ${cenToFar(this.cacheTemp)}°F]`) this.updateCache() } if (newHumi !== this.cacheHumi) { this.cacheHumi = newHumi this.humiService.updateCharacteristic(this.hapChar.CurrentRelativeHumidity, this.cacheHumi) this.accessory.eveService.addEntry({ humidity: this.cacheHumi }) this.accessory.log(`${platformLang.curHumi} [${this.cacheHumi}%]`) } if (newCO2 !== this.cacheCO2 && newCO2 >= 0 && newCO2 <= 40000) { this.cacheCO2 = newCO2 this.co2Service.updateCharacteristic(this.hapChar.CarbonDioxideLevel, this.cacheCO2) if (newCO2 > this.cacheCO2Peak) { this.cacheCO2Peak = newCO2 this.co2Service.updateCharacteristic(this.hapChar.CarbonDioxidePeakLevel, this.cacheCO2Peak) } const detected = newCO2 >= this.co2AbnormalThreshold ? 1 : 0 if (detected !== this.cacheCO2Detected) { this.cacheCO2Detected = detected this.co2Service.updateCharacteristic(this.hapChar.CarbonDioxideDetected, detected) } this.accessory.eveService.addEntry({ ppm: this.cacheCO2 }) this.accessory.log(`${platformLang.curCO2} [${this.cacheCO2} ppm]`) } }) } async updateCache() { if (!this.platform.storageClientData) { return } try { await this.platform.storageData.setItem( `${this.accessory.context.gvDeviceId}_temp`, this.cacheTemp, ) } catch (err) { this.accessory.logWarn(`${platformLang.storageWriteErr} ${parseError(err)}`) } } }