From 5b45ede6ef8e5bc8e74ca7a117cd9ed476e43c5b Mon Sep 17 00:00:00 2001 From: Ben <43026681+bwp91@users.noreply.github.com> Date: Thu, 28 Dec 2023 18:49:30 +0000 Subject: [PATCH] add diffusers --- CHANGELOG.md | 3 +- config.schema.json | 48 +++++++++++ lib/device/diffuser.js | 181 +++++++++++++++++++++++++++++++++++++++++ lib/device/index.js | 2 + lib/platform.js | 6 ++ lib/utils/constants.js | 4 + 6 files changed, 243 insertions(+), 1 deletion(-) create mode 100644 lib/device/diffuser.js diff --git a/CHANGELOG.md b/CHANGELOG.md index ce1d5fff..9c478c45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,8 @@ This project tries to adhere to [Semantic Versioning](http://semver.org/). In pr ### Added - Support for `H6004`, `H601D`, `H70A1`, `H706A` lights -- Support for `H7173`, `H7175` kettle +- Support for `H7173`, `H7175` kettles +- Support for `H7161`, `H7162` diffusers ### Changed diff --git a/config.schema.json b/config.schema.json index 32d77143..a0b68791 100644 --- a/config.schema.json +++ b/config.schema.json @@ -1268,6 +1268,36 @@ } } }, + "diffuserDevices": { + "title": "Diffuser Devices", + "description": "Optional settings for Govee Diffuser devices.", + "type": "array", + "items": { + "type": "object", + "properties": { + "label": { + "title": "Label", + "type": "string", + "description": "This setting is only used for config identification." + }, + "deviceId": { + "title": "Device ID", + "type": "string", + "description": "Enter the 23 digit Govee Device ID to begin (e.g. 12:AB:A1:C5:A8:99:D2:17).", + "minLength": 23, + "maxLength": 23 + }, + "ignoreDevice": { + "type": "boolean", + "title": "Hide From HomeKit", + "description": "If true, this accessory will be removed and ignored from HomeKit.", + "condition": { + "functionBody": "return (model.diffuserDevices && model.diffuserDevices[arrayIndices] && model.diffuserDevices[arrayIndices].deviceId && model.diffuserDevices[arrayIndices].deviceId.length === 23);" + } + } + } + } + }, "kettleDevices": { "title": "Kettle Devices", "description": "Optional settings for Govee Kettle devices.", @@ -1633,6 +1663,24 @@ } ] }, + { + "key": "diffuserDevices", + "expandable": true, + "title": "Diffuser Devices", + "description": "Optional settings for Govee Diffuser devices.", + "add": "Add Another Device", + "type": "array", + "items": [ + { + "type": "fieldset", + "items": [ + "diffuserDevices[].label", + "diffuserDevices[].deviceId", + "diffuserDevices[].ignoreDevice" + ] + } + ] + }, { "key": "kettleDevices", "expandable": true, diff --git a/lib/device/diffuser.js b/lib/device/diffuser.js new file mode 100644 index 00000000..da2419df --- /dev/null +++ b/lib/device/diffuser.js @@ -0,0 +1,181 @@ +import { + base64ToHex, + getTwoItemPosition, + hexToTwoItems, + parseError, +} from '../utils/functions.js'; +import platformLang from '../utils/lang-en.js'; + +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; + + // Rotation speed to value in {1, 2, ..., 8} + this.speed2Value = (speed) => Math.min(Math.max(parseInt(Math.round(speed / 10), 10), 1), 9); + + // Speed codes + this.value2Code = { + 1: 'MwUBAQAAAAAAAAAAAAAAAAAAADY=', + 2: 'MwUBAgAAAAAAAAAAAAAAAAAAADU=', + 3: 'MwUBAwAAAAAAAAAAAAAAAAAAADQ=', + 4: 'MwUBBAAAAAAAAAAAAAAAAAAAADM=', + 5: 'MwUBBQAAAAAAAAAAAAAAAAAAADI=', + 6: 'MwUBBgAAAAAAAAAAAAAAAAAAADE=', + 7: 'MwUBBwAAAAAAAAAAAAAAAAAAADA=', + 8: 'MwUBCAAAAAAAAAAAAAAAAAAAAD8=', + 9: 'MwUBCQAAAAAAAAAAAAAAAAAAAD4=', + }; + + // Add the fan service if it doesn't already exist + this.service = this.accessory.getService(this.hapServ.Fan) || this.accessory.addService(this.hapServ.Fan); + + // Add the set handler to the fan on/off characteristic + this.service + .getCharacteristic(this.hapChar.On) + .onSet(async (value) => this.internalStateUpdate(value)); + this.cacheState = this.service.getCharacteristic(this.hapChar.On).value ? 'on' : 'off'; + + // Output the customised options to the log + const opts = JSON.stringify({}); + platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts); + } + + async internalStateUpdate(value) { + try { + const newValue = value ? '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: 'stateHumi', + value: value ? 1 : 0, + }); + + // Cache the new state and log if appropriate + if (this.cacheState !== newValue) { + this.cacheState = newValue; + this.accessory.log(`${platformLang.curState} [${this.cacheState}]`); + } + } 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.On, this.cacheState === 'on'); + }, 2000); + throw new this.hapErr(-70402); + } + } + + async internalSpeedUpdate(value) { + try { + // Don't continue if the speed is <=10 + if (value === 0 || value === 10) { + return; + } + + // Get the single Govee value {1, 2, ..., 8} + const newValue = this.speed2Value(value); + + // Don't continue if the speed value won't have effect + if (newValue * 10 === this.cacheSpeed) { + return; + } + + // Get the scene code for this value + const newCode = this.value2Code[newValue]; + + this.accessory.log(newCode); + + // Send the request to the platform sender function + await this.platform.sendDeviceUpdate(this.accessory, { + cmd: 'ptReal', + value: newCode, + }); + + // Cache the new state and log if appropriate + this.cacheSpeed = newValue * 10; + this.accessory.log(`${platformLang.curSpeed} [${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.RotationSpeed, this.cacheSpeed); + }, 2000); + throw new this.hapErr(-70402); + } + } + + externalUpdate(params) { + // Check for an ON/OFF change + if (params.state && params.state !== this.cacheState) { + this.cacheState = params.state; + this.service.updateCharacteristic(this.hapChar.On, this.cacheState === 'on'); + + // Log the change + this.accessory.log(`${platformLang.curState} [${this.cacheState}]`); + } + + // 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 '0500': { // mode + // Mode + const newModeRaw = getTwoItemPosition(hexParts, 4); + let newMode; + switch (newModeRaw) { + case '01': { + // Manual + newMode = 'manual'; + break; + } + case '02': { + // Custom + newMode = 'custom'; + break; + } + case '03': { + // Auto + newMode = 'auto'; + break; + } + default: + return; + } + if (this.cacheMode !== newMode) { + this.cacheMode = newMode; + this.accessory.log(`${platformLang.curMode} [${this.cacheMode}]`); + } + break; + } + default: + this.accessory.logDebugWarn(`${platformLang.newScene}: [${command}] [${hexString}]`); + break; + } + }); + } +} diff --git a/lib/device/index.js b/lib/device/index.js index ea740988..14ebb4c3 100644 --- a/lib/device/index.js +++ b/lib/device/index.js @@ -1,5 +1,6 @@ import deviceCoolerSingle from './cooler-single.js'; import deviceDehumidifier from './dehumidifier.js'; +import deviceDiffuser from './diffuser.js'; import deviceFan from './fan.js'; import deviceHeaterSingle from './heater-single.js'; import deviceHeater1A from './heater1a.js'; @@ -36,6 +37,7 @@ import deviceValveSingle from './valve-single.js'; export default { deviceCoolerSingle, deviceDehumidifier, + deviceDiffuser, deviceFan, deviceHeaterSingle, deviceHeater1A, diff --git a/lib/platform.js b/lib/platform.js index fc148b62..45339602 100644 --- a/lib/platform.js +++ b/lib/platform.js @@ -153,6 +153,7 @@ export default class { case 'leakDevices': case 'lightDevices': case 'purifierDevices': + case 'diffuserDevices': case 'switchDevices': case 'thermoDevices': if (Array.isArray(val) && val.length > 0) { @@ -1012,6 +1013,11 @@ export default class { } doAWSPolling = true; accessory = devicesInHB.get(uuid) || this.addAccessory(device); + } else if (platformConsts.models.diffuser.includes(device.model)) { + // Device is a diffuser + devInstance = deviceTypes.deviceDiffuser; + doAWSPolling = true; + accessory = devicesInHB.get(uuid) || this.addAccessory(device); } else if (platformConsts.models.sensorButton.includes(device.model)) { // Device is a button devInstance = deviceTypes.deviceSensorButton; diff --git a/lib/utils/constants.js b/lib/utils/constants.js index d31629f1..be2b1b63 100644 --- a/lib/utils/constants.js +++ b/lib/utils/constants.js @@ -21,6 +21,8 @@ export default { heaterDevices: [], dehumidifierDevices: [], humidifierDevices: [], + purifierDevices: [], + diffuserDevices: [], kettleDevices: [], iceMakerDevices: [], platform: 'Govee', @@ -93,6 +95,7 @@ export default { humidifierDevices: ['label', 'deviceId', 'ignoreDevice'], dehumidifierDevices: ['label', 'deviceId', 'ignoreDevice'], purifierDevices: ['label', 'deviceId', 'ignoreDevice'], + diffuserDevices: ['label', 'deviceId', 'ignoreDevice'], kettleDevices: [ 'label', 'deviceId', @@ -360,6 +363,7 @@ export default { dehumidifier: ['H7150', 'H7151'], humidifier: ['H7140', 'H7141', 'H7142', 'H7143', 'H7160'], purifier: ['H7120', 'H7121', 'H7122', 'H7123', 'H7126'], + diffuser: ['H7161', 'H7162'], iceMaker: ['H7172'], sensorButton: ['H5122'], sensorContact: ['H5123'],