diff --git a/CHANGELOG.md b/CHANGELOG.md index 62c81c4c..890aae2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,16 @@ This project tries to adhere to [Semantic Versioning](http://semver.org/). In pr - `MINOR` version when a new device type is added, or when a new feature is added that is backwards-compatible - `PATCH` version when backwards-compatible bug fixes are implemented -## BETA +## v10.13.0 (2024-12-08) + +### Added + +- added models H8072 and H80C4 (#969) (@EricHigdon) +- added recent models + +### Changed + +- Bump `node` recommended versions to `v18.20.5` or `v20.18.1` or `v22.12.0` ### Fixed diff --git a/lib/device/humidifier-H7147.js b/lib/device/humidifier-H7147.js new file mode 100644 index 00000000..0a0df119 --- /dev/null +++ b/lib/device/humidifier-H7147.js @@ -0,0 +1,157 @@ +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(Number.parseInt(Math.round(speed / 10), 10), 1), 8) + + // Speed codes + this.value2Code = { + 1: 'MwUBAQAAAAAAAAAAAAAAAAAAADY=', + 2: 'MwUBAgAAAAAAAAAAAAAAAAAAADU=', + 3: 'MwUBAwAAAAAAAAAAAAAAAAAAADQ=', + 4: 'MwUBBAAAAAAAAAAAAAAAAAAAADM=', + 5: 'MwUBBQAAAAAAAAAAAAAAAAAAADI=', + 6: 'MwUBBgAAAAAAAAAAAAAAAAAAADE=', + 7: 'MwUBBwAAAAAAAAAAAAAAAAAAADA=', + 8: 'MwUBCAAAAAAAAAAAAAAAAAAAAD8=', + } + + // 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' + + // Add the set handler to the fan rotation speed characteristic + this.service + .getCharacteristic(this.hapChar.RotationSpeed) + .setProps({ + minStep: 10, + validValues: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100], + }) + .onSet(async value => this.internalSpeedUpdate(value)) + this.cacheSpeed = this.service.getCharacteristic(this.hapChar.RotationSpeed).value + + // 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 + 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 0 + if (value === 0) { + 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] + + // 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, 1)}${getTwoItemPosition(hexParts, 2)}` + + switch (deviceFunction) { + default: + this.accessory.logDebugWarn(`${platformLang.newScene}: [${command}] [${hexString}]`) + break + } + }) + } +} diff --git a/lib/device/ice-maker-H7162.js b/lib/device/ice-maker-H7172.js similarity index 100% rename from lib/device/ice-maker-H7162.js rename to lib/device/ice-maker-H7172.js diff --git a/lib/device/index.js b/lib/device/index.js index 8a5f39ef..f4d985f4 100644 --- a/lib/device/index.js +++ b/lib/device/index.js @@ -9,20 +9,21 @@ import deviceFanH7102 from './fan-H7102.js' import deviceFanH7105 from './fan-H7105.js' import deviceFanH7106 from './fan-H7106.js' import deviceFanH7111 from './fan-H7111.js' -import deviceHeaterSingle from './heater-single.js' import deviceHeater1A from './heater1a.js' import deviceHeater1B from './heater1b.js' import deviceHeater2 from './heater2.js' +import deviceHeaterSingle from './heater-single.js' import deviceHumidifierH7140 from './humidifier-H7140.js' import deviceHumidifierH7141 from './humidifier-H7141.js' import deviceHumidifierH7142 from './humidifier-H7142.js' import deviceHumidifierH7143 from './humidifier-H7143.js' +import deviceHumidifierH7147 from './humidifier-H7147.js' import deviceHumidifierH7148 from './humidifier-H7148.js' import deviceHumidifierH7160 from './humidifier-H7160.js' -import deviceIceMakerH7162 from './ice-maker-H7162.js' +import deviceIceMakerH7172 from './ice-maker-H7172.js' import deviceKettle from './kettle.js' -import deviceLight from './light.js' import deviceLightSwitch from './light-switch.js' +import deviceLight from './light.js' import deviceOutletDouble from './outlet-double.js' import deviceOutletSingle from './outlet-single.js' import deviceOutletTriple from './outlet-triple.js' @@ -34,14 +35,15 @@ import devicePurifierH7123 from './purifier-H7123.js' import devicePurifierH7124 from './purifier-H7124.js' import devicePurifierH7126 from './purifier-H7126.js' import devicePurifierH7127 from './purifier-H7127.js' +import devicePurifierH7129 from './purifier-H7129.js' import devicePurifierSingle from './purifier-single.js' import deviceSensorButton from './sensor-button.js' import deviceSensorContact from './sensor-contact.js' import deviceSensorLeak from './sensor-leak.js' import deviceSensorMonitor from './sensor-monitor.js' import deviceSensorPresence from './sensor-presence.js' -import deviceSensorThermo from './sensor-thermo.js' import deviceSensorThermo4 from './sensor-thermo4.js' +import deviceSensorThermo from './sensor-thermo.js' import deviceSwitchDouble from './switch-double.js' import deviceSwitchSingle from './switch-single.js' import deviceSwitchTriple from './switch-triple.js' @@ -70,9 +72,10 @@ export default { deviceHumidifierH7141, deviceHumidifierH7142, deviceHumidifierH7143, + deviceHumidifierH7147, deviceHumidifierH7148, deviceHumidifierH7160, - deviceIceMakerH7162, + deviceIceMakerH7172, deviceKettle, deviceLight, deviceLightSwitch, @@ -87,6 +90,7 @@ export default { devicePurifierH7124, devicePurifierH7126, devicePurifierH7127, + devicePurifierH7129, devicePurifierSingle, deviceSensorButton, deviceSensorContact, diff --git a/lib/device/purifier-H7129.js b/lib/device/purifier-H7129.js new file mode 100644 index 00000000..fab12151 --- /dev/null +++ b/lib/device/purifier-H7129.js @@ -0,0 +1,296 @@ +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.cusChar = platform.cusChar + 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, 3} + this.speed2Value = speed => Math.min(Math.max(Number.parseInt(speed / 33, 10), 1), 3) + + // Speed codes + this.value2Code = { + 1: 'MwUBAQAAAAAAAAAAAAAAAAAAADY=', // sleep + 2: 'MwUBAgAAAAAAAAAAAAAAAAAAADU=', // low + 3: 'MwUBAwAAAAAAAAAAAAAAAAAAADQ=', // high + } + + // Lock codes + this.lock2Code = { + on: 'MxABAAAAAAAAAAAAAAAAAAAAACI=', + off: 'MxAAAAAAAAAAAAAAAAAAAAAAACM=', + } + + // Display codes + this.display2Code = { + on: 'MxYBAAAAAAAAAAAAAAAAAAAAACQ=', + off: 'MxYAAAAAAAAAAAAAAAAAAAAAACU=', + } + + // Add the purifier service if it doesn't already exist + this.service = this.accessory.getService(this.hapServ.AirPurifier) + || this.accessory.addService(this.hapServ.AirPurifier) + + // Add the set handler to the switch on/off characteristic + this.service.getCharacteristic(this.hapChar.Active).onSet(async (value) => { + await this.internalStateUpdate(value) + }) + this.cacheState = this.service.getCharacteristic(this.hapChar.Active).value === 1 ? 'on' : 'off' + + // Add options to the purifier target state characteristic + this.service + .getCharacteristic(this.hapChar.TargetAirPurifierState) + .updateValue(1) + // .setProps({ + // minValue: 1, + // maxValue: 1, + // validValues: [1], + // }) + + // Add the set handler to the fan rotation speed characteristic + this.service + .getCharacteristic(this.hapChar.RotationSpeed) + .setProps({ + minStep: 25, + validValues: [0, 33, 66, 99], + }) + .onSet(async value => this.internalSpeedUpdate(value)) + this.cacheSpeed = this.service.getCharacteristic(this.hapChar.RotationSpeed).value + + // Add the set handler to the lock controls characteristic + this.service.getCharacteristic(this.hapChar.LockPhysicalControls).onSet(async (value) => { + await this.internalLockUpdate(value) + }) + this.cacheLock = this.service.getCharacteristic(this.hapChar.LockPhysicalControls).value === 1 ? 'on' : 'off' + + // Add display light Eve characteristic if it doesn't exist already + if (!this.service.testCharacteristic(this.cusChar.DisplayLight)) { + this.service.addCharacteristic(this.cusChar.DisplayLight) + } + + // Add the set handler to the custom display light characteristic + this.service.getCharacteristic(this.cusChar.DisplayLight).onSet(async (value) => { + await this.internalDisplayLightUpdate(value) + }) + this.cacheDisplay = this.service.getCharacteristic(this.cusChar.DisplayLight).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 === 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: 'statePuri', + value: value ? 1 : 0, + }) + + // Update the current state characteristic + this.service.updateCharacteristic(this.hapChar.CurrentAirPurifierState, value === 1 ? 2 : 0) + + // Cache the new state and log if appropriate + this.cacheState = newValue + this.accessory.log(`${platformLang.curState} [${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.Active, this.cacheState === 'on' ? 1 : 0) + }, 2000) + throw new this.hapErr(-70402) + } + } + + async internalSpeedUpdate(value) { + try { + // Don't continue if the speed is 0 + if (value === 0) { + 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 * 33 === this.cacheSpeed) { + return + } + + // Get the scene code for this value + const newCode = this.value2Code[newValue] + + // 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 * 33 + this.accessory.log(`${platformLang.curSpeed} [${this.cacheSpeed}%]`) + } 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) + } + } + + async internalLockUpdate(value) { + try { + 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: this.lock2Code[newValue], + }) + + // Cache the new state and log if appropriate + 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 internalDisplayLightUpdate(value) { + try { + const newValue = value ? 'on' : 'off' + + // Don't continue if the new value is the same as before + if (this.cacheDisplay === newValue) { + return + } + + // Send the request to the platform sender function + await this.platform.sendDeviceUpdate(this.accessory, { + cmd: 'ptReal', + value: this.display2Code[newValue], + }) + + // Cache the new state and log if appropriate + this.cacheDisplay = newValue + this.accessory.log(`${platformLang.curDisplay} [${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.cusChar.DisplayLight, this.cacheLight === 'on') + }, 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.Active, this.cacheState === 'on') + this.service.updateCharacteristic(this.hapChar.CurrentAirPurifierState, this.cacheState === 'on' ? 2 : 0) + + // 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 '0501': { + // Manual speed + const newSpeedRaw = getTwoItemPosition(hexParts, 4) + if (newSpeedRaw !== this.cacheSpeedRaw) { + this.cacheSpeedRaw = newSpeedRaw + this.cacheSpeed = Number.parseInt(newSpeedRaw, 10) * 10 + this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed) + this.accessory.log(`${platformLang.curSpeed} [${this.cacheSpeed}]`) + } + break + } + case 'aa10': { // lock + const newLock = getTwoItemPosition(hexParts, 3) === '01' ? 'on' : 'off' + if (newLock !== this.cacheLock) { + this.cacheLock = newLock + this.service.updateCharacteristic(this.hapChar.LockPhysicalControls, this.cacheLock === 'on' ? 1 : 0) + this.accessory.log(`${platformLang.curLock} [${this.cacheLock}]`) + } + break + } + case 'aa16': { // display light + const newDisplay = getTwoItemPosition(hexParts, 3) === '01' ? 'on' : 'off' + if (newDisplay === 'on') { + this.accessory.context.cacheDisplayCode = hexString + } + if (newDisplay !== this.cacheDisplay) { + this.cacheDisplay = newDisplay + this.service.updateCharacteristic(this.cusChar.DisplayLight, this.cacheDisplay === 'on') + + // Log the change + this.accessory.log(`${platformLang.curDisplay} [${this.cacheDisplay}]`) + } + break + } + default: + this.accessory.logDebugWarn(`${platformLang.newScene}: [${command}] [${hexString}]`) + break + } + }) + } +} diff --git a/lib/utils/constants.js b/lib/utils/constants.js index 7120fe49..d3e9eb9e 100644 --- a/lib/utils/constants.js +++ b/lib/utils/constants.js @@ -138,6 +138,8 @@ export default { 'H601B', 'H601C', 'H601D', + 'H6022', + 'H6039', 'H6042', 'H6043', 'H6046', @@ -203,6 +205,10 @@ export default { 'H611C', 'H611Z', 'H6121', + 'H612B', + 'H612C', + 'H612D', + 'H612F', 'H6135', 'H6137', 'H6141', @@ -227,6 +233,7 @@ export default { 'H6163', 'H6167', 'H6168', + 'H6169', 'H616C', 'H616D', 'H616E', @@ -272,13 +279,18 @@ export default { 'H61E0', 'H61E1', 'H61E5', + 'H61E6', 'H61F5', 'H6601', 'H6602', + 'H6604', 'H6609', 'H6640', 'H6641', + 'H6800', + 'H6810', 'H6811', + 'H6840', 'H7005', 'H7006', 'H7007', @@ -303,6 +315,7 @@ export default { 'H7052', 'H7053', 'H7055', + 'H7057', 'H705A', 'H705B', 'H705C', @@ -315,6 +328,8 @@ export default { 'H7063', 'H7065', 'H7066', + 'H7067', + 'H7069', 'H706A', 'H706B', 'H706C', @@ -328,7 +343,9 @@ export default { 'H70BC', 'H70C1', 'H70C2', + 'H70C4', 'H70C5', + 'H70C7', 'H70D1', 'H8072', 'H801B', @@ -421,8 +438,8 @@ export default { heater1: ['H7130', 'H713A', 'H713B', 'H713C'], heater2: ['H7131', 'H7132', 'H7133', 'H7134', 'H7135'], dehumidifier: ['H7150', 'H7151'], - humidifier: ['H7140', 'H7141', 'H7142', 'H7143', 'H7148', 'H7160'], - purifier: ['H7120', 'H7121', 'H7122', 'H7123', 'H7124', 'H7126', 'H7127', 'H712C'], + humidifier: ['H7140', 'H7141', 'H7142', 'H7143', 'H7147', 'H7148', 'H7160'], + purifier: ['H7120', 'H7121', 'H7122', 'H7123', 'H7124', 'H7126', 'H7127', 'H7129', 'H712C'], diffuser: ['H7161', 'H7162'], iceMaker: ['H7172'], sensorButton: ['H5122'], @@ -434,10 +451,12 @@ export default { 'H5024', // https://github.com/homebridge-plugins/homebridge-govee/issues/835 'H5042', // https://github.com/homebridge-plugins/homebridge-govee/issues/849 'H5043', // https://github.com/homebridge-plugins/homebridge-govee/issues/558 + 'H5085', // https://github.com/homebridge-plugins/homebridge-govee/issues/951 'H5121', // https://github.com/homebridge-plugins/homebridge-govee/issues/913 'H5126', // https://github.com/homebridge-plugins/homebridge-govee/issues/910 'H5107', // https://github.com/homebridge-plugins/homebridge-govee/issues/803 'H5109', // https://github.com/homebridge-plugins/homebridge-govee/issues/823 + 'H5125', // https://github.com/homebridge-plugins/homebridge-govee/issues/981 'H5185', // https://github.com/homebridge-plugins/homebridge-govee/issues/804 ], },