@homebridge-plugins/homebridge-govee 11.23.0 → 11.23.1-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  All notable changes to `@homebridge-plugins/homebridge-govee` will be documented in this file.
4
4
 
5
+ ## v11.24.0 (Pending Release)
6
+
7
+ ### Changes
8
+
9
+ - feat: add support for Lantern Floor Lamp H1630 (@alexjsp)
10
+ - feat: add support for various new models
11
+ - fix: ignore undefined battery values to stop characteristic warnings (#1266)
12
+ - fix: guard against NaN temperature values to stop characteristic warnings (#1291)
13
+ - chore: dependency updates
14
+
5
15
  ## v11.23.0 (2026-05-16)
6
16
 
7
17
  ### Changes
@@ -0,0 +1,272 @@
1
+ import {
2
+ base64ToHex,
3
+ getTwoItemPosition,
4
+ hexToTwoItems,
5
+ parseError,
6
+ } from '../utils/functions.js'
7
+ import platformLang from '../utils/lang-en.js'
8
+
9
+ export default class {
10
+ constructor(platform, accessory) {
11
+ // Set up variables from the platform
12
+ this.hapChar = platform.api.hap.Characteristic
13
+ this.hapErr = platform.api.hap.HapStatusError
14
+ this.hapServ = platform.api.hap.Service
15
+ this.platform = platform
16
+
17
+ // Set up variables from the accessory
18
+ this.accessory = accessory
19
+
20
+ // Codes etc
21
+ this.speedCodes = {
22
+ 1: 'MwUBAQAAAAAAAAAAAAAAAAAAADY=',
23
+ 2: 'MwUBAgAAAAAAAAAAAAAAAAAAADU=',
24
+ 3: 'MwUBAwAAAAAAAAAAAAAAAAAAADQ=',
25
+ 4: 'MwUBBAAAAAAAAAAAAAAAAAAAADM=',
26
+ 5: 'MwUBBQAAAAAAAAAAAAAAAAAAADI=',
27
+ 6: 'MwUBBgAAAAAAAAAAAAAAAAAAADE=',
28
+ 7: 'MwUBBwAAAAAAAAAAAAAAAAAAADA=',
29
+ 8: 'MwUBCAAAAAAAAAAAAAAAAAAAAD8=',
30
+ 9: 'MwUAAwAAAAAAAAAAAAAAAAAAADU=', // auto mode
31
+ }
32
+ this.autoSpeed = 9
33
+
34
+ // Remove any old original Fan services
35
+ if (this.accessory.getService(this.hapServ.Fan)) {
36
+ this.accessory.removeService(this.accessory.getService(this.hapServ.Fan))
37
+ }
38
+
39
+ // Migrate old %-rotation speed to unitless
40
+ const existingService = this.accessory.getService(this.hapServ.Fanv2)
41
+ if (existingService) {
42
+ if (existingService.getCharacteristic(this.hapChar.RotationSpeed).props.unit === 'percentage') {
43
+ this.accessory.removeService(existingService)
44
+ }
45
+ }
46
+
47
+ // Add the fan service for the fan if it doesn't already exist
48
+ this.service = this.accessory.getService(this.hapServ.Fanv2) || this.accessory.addService(this.hapServ.Fanv2)
49
+
50
+ // Add the set handler to the fan on/off characteristic
51
+ this.service
52
+ .getCharacteristic(this.hapChar.Active)
53
+ .onSet(async value => this.internalStateUpdate(value))
54
+ this.cacheState = this.service.getCharacteristic(this.hapChar.Active).value ? 'on' : 'off'
55
+
56
+ // Add the set handler to the fan rotation speed characteristic
57
+ this.service
58
+ .getCharacteristic(this.hapChar.RotationSpeed)
59
+ .setProps({
60
+ maxValue: this.autoSpeed,
61
+ minStep: 1,
62
+ minValue: 0,
63
+ unit: 'unitless',
64
+ })
65
+ .onSet(async value => this.internalSpeedUpdate(value))
66
+ this.cacheSpeed = this.service.getCharacteristic(this.hapChar.RotationSpeed).value
67
+
68
+ // Add the set handler to the fan swing mode
69
+ this.service
70
+ .getCharacteristic(this.hapChar.SwingMode)
71
+ .onSet(async value => this.internalSwingUpdate(value))
72
+ this.cacheSwing = this.service.getCharacteristic(this.hapChar.SwingMode).value === 1 ? 'on' : 'off'
73
+
74
+ // Output the customised options to the log
75
+ const opts = JSON.stringify({})
76
+ platform.log('[%s] %s %s.', accessory.displayName, platformLang.devInitOpts, opts)
77
+ }
78
+
79
+ async internalStateUpdate(value) {
80
+ try {
81
+ const newValue = value ? 'on' : 'off'
82
+
83
+ // Don't continue if the new value is the same as before
84
+ if (this.cacheState === newValue) {
85
+ return
86
+ }
87
+
88
+ // Send the request to the platform sender function
89
+ await this.platform.sendDeviceUpdate(this.accessory, {
90
+ cmd: 'ptReal',
91
+ value: value ? 'MwEBAAAAAAAAAAAAAAAAAAAAADM=' : 'MwEAAAAAAAAAAAAAAAAAAAAAADI=',
92
+ })
93
+
94
+ // Cache the new state and log if appropriate
95
+ if (this.cacheState !== newValue) {
96
+ this.cacheState = newValue
97
+ this.accessory.log(`${platformLang.curState} [${newValue}]`)
98
+ }
99
+ } catch (err) {
100
+ // Catch any errors during the process
101
+ this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
102
+
103
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
104
+ setTimeout(() => {
105
+ this.service.updateCharacteristic(this.hapChar.Active, this.cacheState === 'on' ? 1 : 0)
106
+ }, 2000)
107
+ throw new this.hapErr(-70402)
108
+ }
109
+ }
110
+
111
+ async internalSpeedUpdate(value) {
112
+ try {
113
+ // Don't continue if the value is 0
114
+ if (value === 0) {
115
+ return
116
+ }
117
+
118
+ // Don't continue if the new value is the same as before
119
+ if (this.cacheSpeed === value) {
120
+ return
121
+ }
122
+
123
+ const codeToSend = this.speedCodes[value]
124
+
125
+ const isAuto = value === this.autoSpeed
126
+ await this.platform.sendDeviceUpdate(this.accessory, {
127
+ cmd: 'ptReal',
128
+ value: codeToSend,
129
+ openApi: this.accessory.context.openApiCapabilities?.workMode
130
+ ? { instance: 'workMode', capabilityType: 'devices.capabilities.work_mode', value: isAuto ? { workMode: 3 } : { workMode: 1, modeValue: value } }
131
+ : undefined,
132
+ })
133
+
134
+ // Cache the new state and log if appropriate
135
+ if (this.cacheSpeed !== value) {
136
+ this.cacheSpeed = value
137
+ this.accessory.log(`${platformLang.curSpeed} [${value}]`)
138
+ }
139
+ } catch (err) {
140
+ // Catch any errors during the process
141
+ this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
142
+
143
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
144
+ setTimeout(() => {
145
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
146
+ }, 2000)
147
+ throw new this.hapErr(-70402)
148
+ }
149
+ }
150
+
151
+ async internalSwingUpdate(value) {
152
+ try {
153
+ const newValue = value ? 'on' : 'off'
154
+ // Don't continue if the new value is the same as before
155
+ if (this.cacheSwing === value) {
156
+ return
157
+ }
158
+
159
+ await this.platform.sendDeviceUpdate(this.accessory, {
160
+ cmd: 'ptReal',
161
+ value: value ? 'Mx8BAQAAAAAAAAAAAAAAAAAAACw=' : 'Mx8BAAAAAAAAAAAAAAAAAAAAAC0=',
162
+ openApi: this.accessory.context.openApiCapabilities?.oscillationToggle
163
+ ? { instance: 'oscillationToggle', capabilityType: 'devices.capabilities.toggle', value: value ? 1 : 0 }
164
+ : undefined,
165
+ })
166
+
167
+ // Cache the new state and log if appropriate
168
+ if (this.cacheSwing !== newValue) {
169
+ this.cacheSwing = newValue
170
+ this.accessory.log(`${platformLang.curSwing} [${newValue}]`)
171
+ }
172
+ } catch (err) {
173
+ // Catch any errors during the process
174
+ this.accessory.logWarn(`${platformLang.devNotUpdated} ${parseError(err)}`)
175
+
176
+ // Throw a 'no response' error and set a timeout to revert this after 2 seconds
177
+ setTimeout(() => {
178
+ this.service.updateCharacteristic(this.hapChar.SwingMode, this.cacheSwing === 'on' ? 1 : 0)
179
+ }, 2000)
180
+ throw new this.hapErr(-70402)
181
+ }
182
+ }
183
+
184
+ externalUpdate(params) {
185
+ // Update the active characteristic
186
+ if (params.state && params.state !== this.cacheState) {
187
+ this.cacheState = params.state
188
+ this.service.updateCharacteristic(this.hapChar.Active, this.cacheState === 'on' ? 1 : 0)
189
+ this.accessory.log(`${platformLang.curState} [${this.cacheState}]`)
190
+ }
191
+
192
+ // Handle OpenAPI workMode (speed or auto)
193
+ if (params.workMode) {
194
+ let newSpeed
195
+ if (params.workMode.workMode === 3) {
196
+ newSpeed = this.autoSpeed
197
+ } else {
198
+ newSpeed = params.workMode.modeValue
199
+ }
200
+ if (Number.isFinite(newSpeed) && newSpeed >= 1 && newSpeed <= this.autoSpeed && this.cacheSpeed !== newSpeed) {
201
+ this.cacheSpeed = newSpeed
202
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
203
+ this.accessory.log(`${platformLang.curSpeed} [${this.cacheSpeed}]`)
204
+ }
205
+ }
206
+
207
+ // Handle OpenAPI toggles
208
+ if (params.toggles?.oscillationToggle !== undefined) {
209
+ const newSwing = params.toggles.oscillationToggle ? 'on' : 'off'
210
+ if (this.cacheSwing !== newSwing) {
211
+ this.cacheSwing = newSwing
212
+ this.service.updateCharacteristic(this.hapChar.SwingMode, this.cacheSwing === 'on' ? 1 : 0)
213
+ this.accessory.log(`${platformLang.curSwing} [${this.cacheSwing}]`)
214
+ }
215
+ }
216
+
217
+ // Check for some other scene/mode change
218
+ (params.commands || []).forEach((command) => {
219
+ const hexString = base64ToHex(command)
220
+ const hexParts = hexToTwoItems(hexString)
221
+
222
+ // Return now if not a device query update code
223
+ if (getTwoItemPosition(hexParts, 1) !== 'aa') {
224
+ return
225
+ }
226
+
227
+ if (getTwoItemPosition(hexParts, 2) === '08') {
228
+ // Sensor Attached?
229
+ const dev = hexString.substring(4, hexString.length - 24)
230
+ this.accessory.context.sensorAttached = dev !== '000000000000'
231
+ return
232
+ }
233
+
234
+ const deviceFunction = `${getTwoItemPosition(hexParts, 2)}${getTwoItemPosition(hexParts, 3)}`
235
+
236
+ switch (deviceFunction) {
237
+ case '0501': {
238
+ // Fan speed
239
+ const newSpeed = Number.parseInt(getTwoItemPosition(hexParts, 4), 16)
240
+ if (newSpeed >= 1 && newSpeed <= 8 && this.cacheSpeed !== newSpeed) {
241
+ this.cacheSpeed = newSpeed
242
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
243
+ this.accessory.log(`${platformLang.curSpeed} [${this.cacheSpeed}]`)
244
+ }
245
+ break
246
+ }
247
+ case '0500': {
248
+ // Mode change (auto = 0x03)
249
+ if (getTwoItemPosition(hexParts, 4) === '03' && this.cacheSpeed !== this.autoSpeed) {
250
+ this.cacheSpeed = this.autoSpeed
251
+ this.service.updateCharacteristic(this.hapChar.RotationSpeed, this.cacheSpeed)
252
+ this.accessory.log(`${platformLang.curSpeed} [auto]`)
253
+ }
254
+ break
255
+ }
256
+ // case '1f01': {
257
+ // // Swing Mode
258
+ // const newSwing = getTwoItemPosition(hexParts, 4) === '01' ? 'on' : 'off'
259
+ // if (this.cacheSwing !== newSwing) {
260
+ // this.cacheSwing = newSwing
261
+ // this.service.updateCharacteristic(this.hapChar.SwingMode, this.cacheSwing === 'on' ? 1 : 0)
262
+ // this.accessory.log(`${platformLang.curSwing} [${this.cacheSwing}]`)
263
+ // }
264
+ // break
265
+ // }
266
+ default:
267
+ this.accessory.logDebugWarn(`${platformLang.newScene}: [${command}] [${hexString}]`)
268
+ break
269
+ }
270
+ })
271
+ }
272
+ }
@@ -4,6 +4,7 @@ import deviceDehumidifierH7151 from './dehumidifier-H7151.js'
4
4
  import deviceDehumidifierH7152 from './dehumidifier-H7152.js'
5
5
  import deviceDiffuserH7161 from './diffuser-H7161.js'
6
6
  import deviceDiffuserH7162 from './diffuser-H7162.js'
7
+ import deviceFanH1310 from './fan-H1310.js'
7
8
  import deviceFanH7100 from './fan-H7100.js'
8
9
  import deviceFanH7101 from './fan-H7101.js'
9
10
  import deviceFanH7102 from './fan-H7102.js'
@@ -69,6 +70,7 @@ export default {
69
70
  deviceDehumidifierH7152,
70
71
  deviceDiffuserH7161,
71
72
  deviceDiffuserH7162,
73
+ deviceFanH1310,
72
74
  deviceFanH7100,
73
75
  deviceFanH7101,
74
76
  deviceFanH7102,
@@ -53,7 +53,7 @@ export default class {
53
53
  }
54
54
 
55
55
  // Check to see if the provided battery is different from the cached state
56
- if (params.battery !== this.cacheBatt) {
56
+ if (Number.isFinite(params.battery) && params.battery !== this.cacheBatt) {
57
57
  // Battery is different so update Homebridge with new values
58
58
  this.cacheBatt = params.battery
59
59
  this.battService.updateCharacteristic(this.hapChar.BatteryLevel, this.cacheBatt)
@@ -110,7 +110,7 @@ export default class {
110
110
  }
111
111
 
112
112
  // Check to see if the provided battery is different from the cached state
113
- if (params.battery !== this.cacheBatt && this.battService) {
113
+ if (Number.isFinite(params.battery) && params.battery !== this.cacheBatt && this.battService) {
114
114
  // Battery is different so update Homebridge with new values
115
115
  this.cacheBatt = params.battery
116
116
  this.battService.updateCharacteristic(this.hapChar.BatteryLevel, this.cacheBatt)
@@ -127,7 +127,7 @@ export default class {
127
127
  if (hasProperty(params, 'temperature')) {
128
128
  let newTemp = Number.parseInt(params.temperature + this.accessory.context.offTemp, 10)
129
129
  newTemp /= 100
130
- if (newTemp !== this.cacheTemp) {
130
+ if (Number.isFinite(newTemp) && newTemp !== this.cacheTemp) {
131
131
  // Temperature is different so update Homebridge with new values
132
132
  this.cacheTemp = newTemp
133
133
  this.thermoService.updateCharacteristic(this.hapChar.CurrentTemperature, this.cacheTemp)
@@ -147,7 +147,7 @@ export default class {
147
147
  let newHumi = Number.parseInt(params.humidity + this.accessory.context.offHumi, 10)
148
148
  newHumi /= 100
149
149
  newHumi = Math.max(Math.min(newHumi, 100), 0)
150
- if (newHumi !== this.cacheHumi) {
150
+ if (Number.isFinite(newHumi) && newHumi !== this.cacheHumi) {
151
151
  // Humidity is different so update Homebridge with new values
152
152
  this.cacheHumi = newHumi
153
153
  this.humiService.updateCharacteristic(this.hapChar.CurrentRelativeHumidity, this.cacheHumi)
@@ -84,7 +84,7 @@ export default class {
84
84
  }
85
85
 
86
86
  // Check to see if the provided battery is different from the cached state
87
- if (params.battery !== this.cacheBatt && this.battService) {
87
+ if (Number.isFinite(params.battery) && params.battery !== this.cacheBatt && this.battService) {
88
88
  // Battery is different so update Homebridge with new values
89
89
  this.cacheBatt = params.battery
90
90
  this.battService.updateCharacteristic(this.hapChar.BatteryLevel, this.cacheBatt)
@@ -101,7 +101,7 @@ export default class {
101
101
  if (hasProperty(params, 'temperature')) {
102
102
  let newTemp = Number.parseInt(params.temperature + this.accessory.context.offTemp, 10)
103
103
  newTemp /= 100
104
- if (newTemp !== this.cacheTemp) {
104
+ if (Number.isFinite(newTemp) && newTemp !== this.cacheTemp) {
105
105
  // Temperature is different so update Homebridge with new values
106
106
  this.cacheTemp = newTemp
107
107
  this.tempService.updateCharacteristic(this.hapChar.CurrentTemperature, this.cacheTemp)
@@ -120,7 +120,7 @@ export default class {
120
120
  let newHumi = Number.parseInt(params.humidity + this.accessory.context.offHumi, 10)
121
121
  newHumi /= 100
122
122
  newHumi = Math.max(Math.min(newHumi, 100), 0)
123
- if (newHumi !== this.cacheHumi) {
123
+ if (Number.isFinite(newHumi) && newHumi !== this.cacheHumi) {
124
124
  // Humidity is different so update Homebridge with new values
125
125
  this.cacheHumi = newHumi
126
126
  this.humiService.updateCharacteristic(this.hapChar.CurrentRelativeHumidity, this.cacheHumi)
@@ -129,8 +129,11 @@ export default {
129
129
  models: {
130
130
  rgb: [
131
131
  'H1401',
132
+ 'H1630',
132
133
  'H16B0', // https://github.com/homebridge-plugins/homebridge-govee/issues/1278
134
+ 'H16C0', // https://github.com/homebridge-plugins/homebridge-govee/issues/1286
133
135
  'H1741', // https://github.com/homebridge-plugins/homebridge-govee/issues/1278
136
+ 'H3200', // https://github.com/homebridge-plugins/homebridge-govee/issues/1292
134
137
  'H6001',
135
138
  'H6002',
136
139
  'H6003',
@@ -515,11 +518,12 @@ export default {
515
518
  'H5179',
516
519
  'H5183',
517
520
  'H5190',
521
+ 'H5310', // https://github.com/homebridge-plugins/homebridge-govee/issues/1293
518
522
  ],
519
523
  sensorThermo4: ['H5198'],
520
524
  sensorMonitor: ['H5106'],
521
525
  sensorCO2: ['H5140'], // CO2 + temp + humidity monitor, AWS opcode 0x0A — closes #1179
522
- fan: ['H7100', 'H7101', 'H7102', 'H7105', 'H7106', 'H7107', 'H7111'],
526
+ fan: ['H1310', 'H7100', 'H7101', 'H7102', 'H7105', 'H7106', 'H7107', 'H7111'],
523
527
  heater1: ['H7130', 'H713A', 'H713B', 'H713C'],
524
528
  heater2: ['H7131', 'H7132', 'H7133', 'H7134', 'H7135'],
525
529
  dehumidifier: ['H7150', 'H7151', 'H7152'],
@@ -548,11 +552,13 @@ export default {
548
552
  'H5125', // https://github.com/homebridge-plugins/homebridge-govee/issues/981
549
553
  'H5185', // https://github.com/homebridge-plugins/homebridge-govee/issues/804
550
554
  'H5191', // https://github.com/homebridge-plugins/homebridge-govee/issues/1121
555
+ 'H7184', // https://github.com/homebridge-plugins/homebridge-govee/issues/1282
551
556
  ],
552
557
  },
553
558
 
554
559
  matterModels: [
555
560
  'H1401',
561
+ 'H3200',
556
562
  'H5085',
557
563
  'H600B',
558
564
  'H600D',
@@ -572,6 +578,7 @@ export default {
572
578
  'H6641',
573
579
  'H6811',
574
580
  'H6840',
581
+ 'H7025',
575
582
  'H7063',
576
583
  'H7067',
577
584
  'H7068',
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@homebridge-plugins/homebridge-govee",
3
3
  "alias": "Govee",
4
4
  "type": "module",
5
- "version": "11.23.0",
5
+ "version": "11.23.1-beta.0",
6
6
  "description": "Homebridge plugin to integrate Govee devices into HomeKit.",
7
7
  "author": {
8
8
  "name": "bwp91",
@@ -56,12 +56,12 @@
56
56
  "postinstall": "patch-package"
57
57
  },
58
58
  "dependencies": {
59
- "@homebridge/plugin-ui-utils": "^2.2.3",
59
+ "@homebridge/plugin-ui-utils": "^2.2.4",
60
60
  "aws-iot-device-sdk": "^2.2.16",
61
61
  "axios": "^1.16.1",
62
62
  "mqtt": "^5.12.1",
63
63
  "node-persist": "^4.0.4",
64
- "node-rsa": "^1.1.1",
64
+ "node-rsa": "^2.0.0",
65
65
  "p-queue": "^9.3.0",
66
66
  "patch-package": "^8.0.1",
67
67
  "pem": "^1.14.8"