@homebridge-plugins/homebridge-govee 11.34.2-beta.1 → 11.34.2-beta.3

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
@@ -9,6 +9,14 @@ All notable changes to `@homebridge-plugins/homebridge-govee` will be documented
9
9
  - feat: control the H1250 zones with the device's own frame, as govee ignores its toggles (#1343) (@Principe1218)
10
10
  - fix: explain in the log when zone tiles are missing because aws credentials are not set
11
11
  - fix: restore debug logging when the plugin runs in a child bridge
12
+ - fix: turn adaptive lighting off when a colour is chosen outside HomeKit
13
+ - fix: send a colour change when only the saturation moves, on every night light
14
+ - fix: listen for saturation on the main light too, not only hue
15
+ fix: accept 20-byte H5106 status frames (#1345) (@danieloleary)
16
+ - fix: stop a lan scan that cannot reach the network from crashing the bridge
17
+ - fix: apply the openapi disable and refresh settings instead of asking for them to be removed
18
+ - fix: say once when a bluetooth reading arrives for an unknown device, not on every broadcast
19
+ - fix: reject a refresh time so large it would make the plugin poll every millisecond
12
20
 
13
21
  ## v11.34.1 (2026-08-06)
14
22
 
@@ -49,7 +49,8 @@
49
49
  "type": "integer",
50
50
  "description": "An interval (in seconds) in which your HTTP devices will refresh with the plugin. Must be 30 or more.",
51
51
  "placeholder": 30,
52
- "minimum": 30
52
+ "minimum": 30,
53
+ "maximum": 2073600
53
54
  },
54
55
  "awsDisable": {
55
56
  "title": "Disable AWS",
@@ -69,7 +70,8 @@
69
70
  "minimum": 60,
70
71
  "condition": {
71
72
  "functionBody": "return (!model.bleDisable);"
72
- }
73
+ },
74
+ "maximum": 2073600
73
75
  },
74
76
  "lanDisable": {
75
77
  "title": "Disable LAN",
@@ -84,7 +86,8 @@
84
86
  "minimum": 10,
85
87
  "condition": {
86
88
  "functionBody": "return (!model.lanDisable);"
87
- }
89
+ },
90
+ "maximum": 2073600
88
91
  },
89
92
  "lanScanInterval": {
90
93
  "title": "LAN Scan Interval",
@@ -94,7 +97,8 @@
94
97
  "minimum": 30,
95
98
  "condition": {
96
99
  "functionBody": "return (!model.lanDisable);"
97
- }
100
+ },
101
+ "maximum": 2073600
98
102
  },
99
103
  "lanScanSubnets": {
100
104
  "title": "LAN Scan Subnets",
@@ -118,7 +122,8 @@
118
122
  "minimum": 30,
119
123
  "condition": {
120
124
  "functionBody": "return (!model.openApiDisable);"
121
- }
125
+ },
126
+ "maximum": 2073600
122
127
  },
123
128
  "bleControlInterval": {
124
129
  "title": "BLE Control Interval",
@@ -181,6 +181,12 @@ export default class {
181
181
  reject(err)
182
182
  })
183
183
 
184
+ // Handle errors on the sending socket. Without a listener here, node throws
185
+ // the error object instead of emitting it, which takes the bridge down.
186
+ this.sender.on('error', (err) => {
187
+ this.log.warn('[LAN] client error: %s.', parseError(err))
188
+ })
189
+
184
190
  // Handle started listening for messages
185
191
  this.receiver.on('listening', () => {
186
192
  const { address, port } = this.receiver.address()
@@ -233,16 +239,25 @@ export default class {
233
239
  sendScanCommand() {
234
240
  const scanCommand = JSON.stringify({ msg: { cmd: commands.scan, data: { account_topic: 'reserve' } } })
235
241
  this.log.debug('[LAN] scanning for devices over LAN...')
236
- this.sender.send(scanCommand, scanCommandPort, multicastIp)
242
+ this.sender.send(scanCommand, scanCommandPort, multicastIp, err => this.logScanSendError(multicastIp, err))
237
243
 
238
244
  // Also scan any configured subnets directly. Multicast doesn't cross
239
245
  // between subnets, so devices on a different subnet than Homebridge are
240
246
  // reached by sending the scan straight to each host (they reply on 4002).
241
247
  this.scanHosts.forEach((host) => {
242
- this.sender.send(scanCommand, scanCommandPort, host)
248
+ this.sender.send(scanCommand, scanCommandPort, host, err => this.logScanSendError(host, err))
243
249
  })
244
250
  }
245
251
 
252
+ // A scan send can fail on a host with no route to the target, for example when
253
+ // multicast is unavailable. Report it and carry on rather than letting dgram
254
+ // emit an unhandled 'error' event, which would crash the bridge every scan.
255
+ logScanSendError(target, err) {
256
+ if (err) {
257
+ this.log.debug('[LAN] scan request to %s failed [%s].', target, parseError(err))
258
+ }
259
+ }
260
+
246
261
  // Get all available LAN devices
247
262
  async getDevices() {
248
263
  return new Promise((resolve) => {
@@ -111,6 +111,14 @@ export default class extends GoveeDevice {
111
111
  this.lightService.getCharacteristic(this.hapChar.Hue).onSet(async (value) => {
112
112
  await this.internalColourUpdate(value)
113
113
  })
114
+ // Saturation needs a handler of its own. HomeKit writes hue and saturation
115
+ // separately, so with only hue listened for, dragging saturation alone -
116
+ // the same colour made paler or richer - reached nothing and the light
117
+ // never changed. Both handlers share one update, and the debounce inside
118
+ // it collapses a hue-and-saturation pair into a single command.
119
+ this.lightService.getCharacteristic(this.hapChar.Saturation).onSet(async () => {
120
+ await this.internalColourUpdate(this.lightService.getCharacteristic(this.hapChar.Hue).value)
121
+ })
114
122
  this.cacheHue = this.lightService.getCharacteristic(this.hapChar.Hue).value
115
123
  this.cacheSat = this.lightService.getCharacteristic(this.hapChar.Saturation).value
116
124
  }
@@ -319,13 +327,16 @@ export default class extends GoveeDevice {
319
327
  return
320
328
  }
321
329
 
322
- // Don't continue if the new value is the same as before
323
- if (value === this.cacheHue) {
330
+ // Don't continue if the new colour is the same as before. Saturation is
331
+ // part of that question: comparing hue alone meant a change of saturation
332
+ // at the same hue looked like no change at all, and nothing was sent.
333
+ const currentSat = this.lightService.getCharacteristic(this.hapChar.Saturation).value
334
+ if (value === this.cacheHue && currentSat === this.cacheSat) {
324
335
  return
325
336
  }
326
337
 
327
338
  // Calculate RGB values
328
- const newRGB = hs2rgb(value, this.lightService.getCharacteristic(this.hapChar.Saturation).value)
339
+ const newRGB = hs2rgb(value, currentSat)
329
340
 
330
341
  // Generate the hex values for the code
331
342
  const hexValues = [0x3A, 0x1B, 0x05, 0x0D, ...newRGB]
@@ -336,7 +347,10 @@ export default class extends GoveeDevice {
336
347
  value: generateCodeFromHexValues(hexValues),
337
348
  })
338
349
 
339
- // Cache the new state and log if appropriate
350
+ // Cache the new state and log if appropriate. Saturation is cached on
351
+ // every send, not only when the hue moved, or a change of saturation at
352
+ // the same hue would be sent once and then look unchanged ever after.
353
+ this.cacheSat = currentSat
340
354
  if (this.cacheHue !== value) {
341
355
  this.cacheHue = value
342
356
  this.accessory.log(`${platformLang.curColour} [rgb ${newRGB.join(' ')}]`)
@@ -450,12 +464,14 @@ export default class extends GoveeDevice {
450
464
 
451
465
  const hs = rgb2hs(newR, newG, newB)
452
466
 
453
- // Check for a colour change
454
- if (hs[0] !== this.cacheHue) {
467
+ // Check for a colour change. Saturation counts as much as hue, or a
468
+ // change of saturation made in the Govee app is reported back and
469
+ // ignored, leaving the tile showing the old colour.
470
+ if (hs[0] !== this.cacheHue || hs[1] !== this.cacheSat) {
455
471
  // Colour is different so update Homebridge with new values
456
472
  this.lightService.updateCharacteristic(this.hapChar.Hue, hs[0])
457
473
  this.lightService.updateCharacteristic(this.hapChar.Saturation, hs[1]);
458
- [this.cacheHue] = hs
474
+ [this.cacheHue, this.cacheSat] = hs
459
475
 
460
476
  // Log the change
461
477
  this.accessory.log(`${platformLang.curColour} [rgb ${newR} ${newG} ${newB}]`)
@@ -111,6 +111,14 @@ export default class extends GoveeDevice {
111
111
  this.lightService.getCharacteristic(this.hapChar.Hue).onSet(async (value) => {
112
112
  await this.internalColourUpdate(value)
113
113
  })
114
+ // Saturation needs a handler of its own. HomeKit writes hue and saturation
115
+ // separately, so with only hue listened for, dragging saturation alone -
116
+ // the same colour made paler or richer - reached nothing and the light
117
+ // never changed. Both handlers share one update, and the debounce inside
118
+ // it collapses a hue-and-saturation pair into a single command.
119
+ this.lightService.getCharacteristic(this.hapChar.Saturation).onSet(async () => {
120
+ await this.internalColourUpdate(this.lightService.getCharacteristic(this.hapChar.Hue).value)
121
+ })
114
122
  this.cacheHue = this.lightService.getCharacteristic(this.hapChar.Hue).value
115
123
  this.cacheSat = this.lightService.getCharacteristic(this.hapChar.Saturation).value
116
124
  }
@@ -323,13 +331,16 @@ export default class extends GoveeDevice {
323
331
  return
324
332
  }
325
333
 
326
- // Don't continue if the new value is the same as before
327
- if (value === this.cacheHue) {
334
+ // Don't continue if the new colour is the same as before. Saturation is
335
+ // part of that question: comparing hue alone meant a change of saturation
336
+ // at the same hue looked like no change at all, and nothing was sent.
337
+ const currentSat = this.lightService.getCharacteristic(this.hapChar.Saturation).value
338
+ if (value === this.cacheHue && currentSat === this.cacheSat) {
328
339
  return
329
340
  }
330
341
 
331
342
  // Calculate RGB values
332
- const newRGB = hs2rgb(value, this.lightService.getCharacteristic(this.hapChar.Saturation).value)
343
+ const newRGB = hs2rgb(value, currentSat)
333
344
 
334
345
  // Generate the hex values for the code
335
346
  const hexValues = [0x3A, 0x1B, 0x05, 0x0D, ...newRGB]
@@ -340,7 +351,10 @@ export default class extends GoveeDevice {
340
351
  value: generateCodeFromHexValues(hexValues),
341
352
  })
342
353
 
343
- // Cache the new state and log if appropriate
354
+ // Cache the new state and log if appropriate. Saturation is cached on
355
+ // every send, not only when the hue moved, or a change of saturation at
356
+ // the same hue would be sent once and then look unchanged ever after.
357
+ this.cacheSat = currentSat
344
358
  if (this.cacheHue !== value) {
345
359
  this.cacheHue = value
346
360
  this.accessory.log(`${platformLang.curColour} [rgb ${newRGB.join(' ')}]`)
@@ -454,12 +468,14 @@ export default class extends GoveeDevice {
454
468
 
455
469
  const hs = rgb2hs(newR, newG, newB)
456
470
 
457
- // Check for a colour change
458
- if (hs[0] !== this.cacheHue) {
471
+ // Check for a colour change. Saturation counts as much as hue, or a
472
+ // change of saturation made in the Govee app is reported back and
473
+ // ignored, leaving the tile showing the old colour.
474
+ if (hs[0] !== this.cacheHue || hs[1] !== this.cacheSat) {
459
475
  // Colour is different so update Homebridge with new values
460
476
  this.lightService.updateCharacteristic(this.hapChar.Hue, hs[0])
461
477
  this.lightService.updateCharacteristic(this.hapChar.Saturation, hs[1]);
462
- [this.cacheHue] = hs
478
+ [this.cacheHue, this.cacheSat] = hs
463
479
 
464
480
  // Log the change
465
481
  this.accessory.log(`${platformLang.curColour} [rgb ${newR} ${newG} ${newB}]`)
@@ -221,6 +221,14 @@ export default class extends GoveeDevice {
221
221
  this.lightService.getCharacteristic(this.hapChar.Hue).onSet(async (value) => {
222
222
  await this.internalColourUpdate(value)
223
223
  })
224
+ // Saturation needs a handler of its own. HomeKit writes hue and saturation
225
+ // separately, so with only hue listened for, dragging saturation alone -
226
+ // the same colour made paler or richer - reached nothing and the light
227
+ // never changed. Both handlers share one update, and the debounce inside
228
+ // it collapses a hue-and-saturation pair into a single command.
229
+ this.lightService.getCharacteristic(this.hapChar.Saturation).onSet(async () => {
230
+ await this.internalColourUpdate(this.lightService.getCharacteristic(this.hapChar.Hue).value)
231
+ })
224
232
  this.cacheHue = this.lightService.getCharacteristic(this.hapChar.Hue).value
225
233
  this.cacheSat = this.lightService.getCharacteristic(this.hapChar.Saturation).value
226
234
 
@@ -574,13 +582,16 @@ export default class extends GoveeDevice {
574
582
  return
575
583
  }
576
584
 
577
- // Don't continue if the new value is the same as before
578
- if (value === this.cacheHue) {
585
+ // Don't continue if the new colour is the same as before. Saturation is
586
+ // part of that question: comparing hue alone meant a change of saturation
587
+ // at the same hue looked like no change at all, and nothing was sent.
588
+ const currentSat = this.lightService.getCharacteristic(this.hapChar.Saturation).value
589
+ if (value === this.cacheHue && currentSat === this.cacheSat) {
579
590
  return
580
591
  }
581
592
 
582
593
  // Calculate RGB values
583
- const newRGB = hs2rgb(value, this.lightService.getCharacteristic(this.hapChar.Saturation).value)
594
+ const newRGB = hs2rgb(value, currentSat)
584
595
 
585
596
  // Generate the hex values for the code
586
597
  const hexValues = [0x3A, 0x1B, 0x05, 0x0D, ...newRGB]
@@ -591,7 +602,10 @@ export default class extends GoveeDevice {
591
602
  value: generateCodeFromHexValues(hexValues),
592
603
  })
593
604
 
594
- // Cache the new state and log if appropriate
605
+ // Cache the new state and log if appropriate. Saturation is cached on
606
+ // every send, not only when the hue moved, or a change of saturation at
607
+ // the same hue would be sent once and then look unchanged ever after.
608
+ this.cacheSat = currentSat
595
609
  if (this.cacheHue !== value) {
596
610
  this.cacheHue = value
597
611
  this.accessory.log(`${platformLang.curColour} [rgb ${newRGB.join(' ')}]`)
@@ -731,12 +745,14 @@ export default class extends GoveeDevice {
731
745
 
732
746
  const hs = rgb2hs(newR, newG, newB)
733
747
 
734
- // Check for a colour change
735
- if (hs[0] !== this.cacheHue) {
748
+ // Check for a colour change. Saturation counts as much as hue, or a
749
+ // change of saturation made in the Govee app is reported back and
750
+ // ignored, leaving the tile showing the old colour.
751
+ if (hs[0] !== this.cacheHue || hs[1] !== this.cacheSat) {
736
752
  // Colour is different so update Homebridge with new values
737
753
  this.lightService.updateCharacteristic(this.hapChar.Hue, hs[0])
738
754
  this.lightService.updateCharacteristic(this.hapChar.Saturation, hs[1]);
739
- [this.cacheHue] = hs
755
+ [this.cacheHue, this.cacheSat] = hs
740
756
 
741
757
  // Log the change
742
758
  this.accessory.log(`${platformLang.curColour} [rgb ${newR} ${newG} ${newB}]`)
@@ -122,6 +122,14 @@ export default class extends GoveeDevice {
122
122
  this.lightService.getCharacteristic(this.hapChar.Hue).onSet(async (value) => {
123
123
  await this.internalColourUpdate(value)
124
124
  })
125
+ // Saturation needs a handler of its own. HomeKit writes hue and saturation
126
+ // separately, so with only hue listened for, dragging saturation alone -
127
+ // the same colour made paler or richer - reached nothing and the light
128
+ // never changed. Both handlers share one update, and the debounce inside
129
+ // it collapses a hue-and-saturation pair into a single command.
130
+ this.lightService.getCharacteristic(this.hapChar.Saturation).onSet(async () => {
131
+ await this.internalColourUpdate(this.lightService.getCharacteristic(this.hapChar.Hue).value)
132
+ })
125
133
  this.cacheHue = this.lightService.getCharacteristic(this.hapChar.Hue).value
126
134
  this.cacheSat = this.lightService.getCharacteristic(this.hapChar.Saturation).value
127
135
 
@@ -318,15 +326,18 @@ export default class extends GoveeDevice {
318
326
  return
319
327
  }
320
328
 
321
- // Don't continue if the new value is the same as before
322
- if (value === this.cacheHue) {
329
+ // Don't continue if the new colour is the same as before. Saturation is
330
+ // part of that question: comparing hue alone meant a change of saturation
331
+ // at the same hue looked like no change at all, and nothing was sent.
332
+ const currentSat = this.lightService.getCharacteristic(this.hapChar.Saturation).value
333
+ if (value === this.cacheHue && currentSat === this.cacheSat) {
323
334
  return
324
335
  }
325
336
 
326
337
  // Generate the hex values for the code
327
338
  const newRGB = hs2rgb(
328
339
  value,
329
- this.lightService.getCharacteristic(this.hapChar.Saturation).value,
340
+ currentSat,
330
341
  )
331
342
  const hexValues = [0x33, 0x1B, 0x01, this.cacheBright, ...newRGB]
332
343
 
@@ -336,7 +347,10 @@ export default class extends GoveeDevice {
336
347
  value: generateCodeFromHexValues(hexValues),
337
348
  })
338
349
 
339
- // Cache the new state and log if appropriate
350
+ // Cache the new state and log if appropriate. Saturation is cached on
351
+ // every send, not only when the hue moved, or a change of saturation at
352
+ // the same hue would be sent once and then look unchanged ever after.
353
+ this.cacheSat = currentSat
340
354
  if (this.cacheHue !== value) {
341
355
  this.cacheHue = value
342
356
  this.accessory.log(`${platformLang.curColour} [rgb ${newRGB.join(' ')}]`)
@@ -487,12 +501,14 @@ export default class extends GoveeDevice {
487
501
  const newB = hexToDecimal(getTwoItemPosition(hexParts, 7))
488
502
  const hs = rgb2hs(newR, newG, newB)
489
503
 
490
- // Check for a colour change
491
- if (hs[0] !== this.cacheHue) {
504
+ // Check for a colour change. Saturation counts as much as hue, or a
505
+ // change of saturation made in the Govee app is reported back and
506
+ // ignored, leaving the tile showing the old colour.
507
+ if (hs[0] !== this.cacheHue || hs[1] !== this.cacheSat) {
492
508
  // Colour is different so update Homebridge with new values
493
509
  this.lightService.updateCharacteristic(this.hapChar.Hue, hs[0])
494
510
  this.lightService.updateCharacteristic(this.hapChar.Saturation, hs[1]);
495
- [this.cacheHue] = hs
511
+ [this.cacheHue, this.cacheSat] = hs
496
512
 
497
513
  // Log the change
498
514
  this.accessory.log(`${platformLang.curColour} [rgb ${newR} ${newG} ${newB}]`)
@@ -85,6 +85,14 @@ export default class extends GoveeDevice {
85
85
  this.lightService.getCharacteristic(this.hapChar.Hue).onSet(async (value) => {
86
86
  await this.internalColourUpdate(value)
87
87
  })
88
+ // Saturation needs a handler of its own. HomeKit writes hue and saturation
89
+ // separately, so with only hue listened for, dragging saturation alone -
90
+ // the same colour made paler or richer - reached nothing and the light
91
+ // never changed. Both handlers share one update, and the debounce inside
92
+ // it collapses a hue-and-saturation pair into a single command.
93
+ this.lightService.getCharacteristic(this.hapChar.Saturation).onSet(async () => {
94
+ await this.internalColourUpdate(this.lightService.getCharacteristic(this.hapChar.Hue).value)
95
+ })
88
96
  this.cacheHue = this.lightService.getCharacteristic(this.hapChar.Hue).value
89
97
  this.cacheSat = this.lightService.getCharacteristic(this.hapChar.Saturation).value
90
98
 
@@ -266,15 +274,18 @@ export default class extends GoveeDevice {
266
274
  return
267
275
  }
268
276
 
269
- // Don't continue if the new value is the same as before
270
- if (value === this.cacheHue) {
277
+ // Don't continue if the new colour is the same as before. Saturation is
278
+ // part of that question: comparing hue alone meant a change of saturation
279
+ // at the same hue looked like no change at all, and nothing was sent.
280
+ const currentSat = this.lightService.getCharacteristic(this.hapChar.Saturation).value
281
+ if (value === this.cacheHue && currentSat === this.cacheSat) {
271
282
  return
272
283
  }
273
284
 
274
285
  // Generate the hex values for the code
275
286
  const newRGB = hs2rgb(
276
287
  value,
277
- this.lightService.getCharacteristic(this.hapChar.Saturation).value,
288
+ currentSat,
278
289
  )
279
290
  const hexValues = [0x33, 0x1B, 0x01, this.cacheBright, ...newRGB]
280
291
 
@@ -284,7 +295,10 @@ export default class extends GoveeDevice {
284
295
  value: generateCodeFromHexValues(hexValues),
285
296
  })
286
297
 
287
- // Cache the new state and log if appropriate
298
+ // Cache the new state and log if appropriate. Saturation is cached on
299
+ // every send, not only when the hue moved, or a change of saturation at
300
+ // the same hue would be sent once and then look unchanged ever after.
301
+ this.cacheSat = currentSat
288
302
  if (this.cacheHue !== value) {
289
303
  this.cacheHue = value
290
304
  this.accessory.log(`${platformLang.curColour} [rgb ${newRGB.join(' ')}]`)
@@ -411,12 +425,14 @@ export default class extends GoveeDevice {
411
425
  const newB = hexToDecimal(getTwoItemPosition(hexParts, 7))
412
426
  const hs = rgb2hs(newR, newG, newB)
413
427
 
414
- // Check for a colour change
415
- if (hs[0] !== this.cacheHue) {
428
+ // Check for a colour change. Saturation counts as much as hue, or a
429
+ // change of saturation made in the Govee app is reported back and
430
+ // ignored, leaving the tile showing the old colour.
431
+ if (hs[0] !== this.cacheHue || hs[1] !== this.cacheSat) {
416
432
  // Colour is different so update Homebridge with new values
417
433
  this.lightService.updateCharacteristic(this.hapChar.Hue, hs[0])
418
434
  this.lightService.updateCharacteristic(this.hapChar.Saturation, hs[1]);
419
- [this.cacheHue] = hs
435
+ [this.cacheHue, this.cacheSat] = hs
420
436
 
421
437
  // Log the change
422
438
  this.accessory.log(`${platformLang.curColour} [rgb ${newR} ${newG} ${newB}]`)
@@ -72,10 +72,22 @@ export default class extends GoveeDevice {
72
72
  this.service = this.accessory.getService(this.hapServ.Lightbulb)
73
73
  || this.accessory.addService(this.hapServ.Lightbulb)
74
74
 
75
- // If adaptive lighting has just been disabled then remove and re-add service to hide AL icon
75
+ // Adaptive lighting has just been switched off, so take away the three
76
+ // characteristics that make Home offer it. Only these need to go: the whole
77
+ // Lightbulb service used to be thrown away and rebuilt here, which does hide
78
+ // the icon but presents Home with what looks like a different light, and an
79
+ // owner can lose that light's room and its automations as a result.
76
80
  if ((this.colourSafeMode || this.alShift === -1) && this.accessory.context.adaptiveLighting) {
77
- this.accessory.removeService(this.service)
78
- this.service = this.accessory.addService(this.hapServ.Lightbulb)
81
+ [
82
+ 'SupportedCharacteristicValueTransitionConfiguration',
83
+ 'CharacteristicValueTransitionControl',
84
+ 'CharacteristicValueActiveTransitionCount',
85
+ ].forEach((charName) => {
86
+ const char = this.hapChar[charName]
87
+ if (char && this.service.testCharacteristic(char)) {
88
+ this.service.removeCharacteristic(this.service.getCharacteristic(char))
89
+ }
90
+ })
79
91
  this.accessory.context.adaptiveLighting = false
80
92
  }
81
93
 
@@ -200,6 +212,14 @@ export default class extends GoveeDevice {
200
212
  this.service.getCharacteristic(this.hapChar.Hue).onSet(async (value) => {
201
213
  await this.internalColourUpdate(value)
202
214
  })
215
+ // Saturation needs a handler of its own. HomeKit writes hue and saturation
216
+ // separately, so with only hue listened for, dragging saturation alone -
217
+ // the same colour made paler or richer - reached nothing at all. Both
218
+ // handlers share one update, and the debounce inside it collapses a
219
+ // hue-and-saturation pair into a single command.
220
+ this.service.getCharacteristic(this.hapChar.Saturation).onSet(async () => {
221
+ await this.internalColourUpdate(this.service.getCharacteristic(this.hapChar.Hue).value)
222
+ })
203
223
  this.cacheHue = this.service.getCharacteristic(this.hapChar.Hue).value
204
224
  this.cacheSat = this.service.getCharacteristic(this.hapChar.Saturation).value
205
225
 
@@ -663,7 +683,7 @@ export default class extends GoveeDevice {
663
683
  let rgb
664
684
  let mired
665
685
  let colourChange = false
666
- let sigColourChange = false
686
+ let changedElsewhere = false
667
687
  if (params.kelvin) {
668
688
  mired = Math.round(1000000 / params.kelvin)
669
689
  hs = m2hs(mired)
@@ -672,28 +692,28 @@ export default class extends GoveeDevice {
672
692
  // Check for a colour change
673
693
  if (params.kelvin !== this.cacheKelvin) {
674
694
  colourChange = true
675
-
676
- // Check for a significant colour change
677
- const kelvinDiff = Math.abs(params.kelvin - this.cacheKelvin)
678
- if (kelvinDiff > 100) {
679
- sigColourChange = true
680
- }
695
+ // Commands go out rounded to the nearest 100K, so a report that lands
696
+ // within that is this light repeating back what it was just told.
697
+ // Anything further apart came from somewhere else - see below
698
+ changedElsewhere = Math.abs(params.kelvin - this.cacheKelvin) > 100
681
699
  }
682
700
  } else {
683
701
  rgb = [params.rgb.r, params.rgb.g, params.rgb.b]
684
702
  hs = rgb2hs(rgb[0], rgb[1], rgb[2])
685
703
 
686
- // Check for a colour change
687
- if (hs[0] !== this.cacheHue) {
704
+ // Check for a colour change. Saturation matters as much as hue: going
705
+ // from a pale colour to a vivid one at the same hue is a real change,
706
+ // and comparing hue alone missed it entirely - so the light was left
707
+ // following adaptive lighting after its owner had chosen a colour, and
708
+ // wandered off that colour a few minutes later (#1333)
709
+ if (hs[0] !== this.cacheHue || hs[1] !== this.cacheSat) {
688
710
  colourChange = true
689
-
690
- // Check for a significant colour change
691
- const rgbDiff = Math.abs(rgb[0] - this.cacheR)
692
- + Math.abs(rgb[1] - this.cacheG)
693
- + Math.abs(rgb[2] - this.cacheB)
694
- if (rgbDiff > 50) {
695
- sigColourChange = true
696
- }
711
+ // A light repeating our own command back can drift by a unit or two
712
+ // through the colour conversions on the way out and back. Beyond that
713
+ // drift, somebody else set this colour
714
+ changedElsewhere = Math.abs(rgb[0] - this.cacheR) > 5
715
+ || Math.abs(rgb[1] - this.cacheG) > 5
716
+ || Math.abs(rgb[2] - this.cacheB) > 5
697
717
  }
698
718
  }
699
719
 
@@ -703,7 +723,7 @@ export default class extends GoveeDevice {
703
723
  this.service.updateCharacteristic(this.hapChar.Hue, hs[0])
704
724
  this.service.updateCharacteristic(this.hapChar.Saturation, hs[1]);
705
725
  [this.cacheR, this.cacheG, this.cacheB] = rgb;
706
- [this.cacheHue] = hs
726
+ [this.cacheHue, this.cacheSat] = hs
707
727
 
708
728
  if (mired) {
709
729
  if (!this.colourSafeMode) {
@@ -716,8 +736,11 @@ export default class extends GoveeDevice {
716
736
  this.accessory.log(`${platformLang.curColour} [rgb ${this.cacheR} ${this.cacheG} ${this.cacheB}]`)
717
737
  }
718
738
 
719
- // If the difference is significant then disable adaptive lighting
720
- if (!this.colourSafeMode && this.alController?.isAdaptiveLightingActive?.() && sigColourChange) {
739
+ // Somebody chose this colour outside HomeKit - in the Govee app, on a
740
+ // remote, or at the wall. HomeKit's own colour changes are caught by
741
+ // Homebridge itself, but nothing else sees these, so adaptive lighting
742
+ // would carry on and move the light off that colour within minutes.
743
+ if (!this.colourSafeMode && this.alController?.isAdaptiveLightingActive?.() && changedElsewhere) {
721
744
  this.alController.disableAdaptiveLighting()
722
745
  this.accessory.log(platformLang.alDisabled)
723
746
  }
@@ -108,7 +108,7 @@ export default class extends GoveeDevice {
108
108
  case 'aa0e':
109
109
  break
110
110
  default: {
111
- if (hexParts.length < 21) {
111
+ if (hexParts.length < 20) {
112
112
  break
113
113
  }
114
114
  const tempInCen = Math.round((hexToDecimal(`0x${deviceFunction}`) + (this.accessory.context.offTemp / 100)) / 10) / 10
package/lib/platform.js CHANGED
@@ -29,7 +29,7 @@ import {
29
29
  updateFailureError,
30
30
  } from './utils/functions.js'
31
31
  import platformLang from './utils/lang-en.js'
32
- import { logUnknownData } from './utils/report-unknown.js'
32
+ import { isFirstReport, logUnknownData } from './utils/report-unknown.js'
33
33
  import { parseDeviceUpdate } from './utils/response-parser.js'
34
34
 
35
35
  const require = createRequire(import.meta.url)
@@ -825,8 +825,21 @@ export default class {
825
825
  battery: goveeReading.battery,
826
826
  source: 'BLE',
827
827
  })
828
- } else {
829
- this.log.warn('[BLE] %s [%s].', platformLang.bleScanUnknown, goveeReading.address)
828
+ } else if (accessory) {
829
+ // A monitor's readings arrive over its own connection, so there is
830
+ // nothing to do with this one and nothing for the owner to act on
831
+ accessory.logDebug(`[BLE] ${platformLang.bleScanUnknown}`)
832
+ } else if (isFirstReport(`ble-reading|${goveeReading.address}`)) {
833
+ // A sensor broadcasts every few seconds, so say this once per session
834
+ // rather than on every advertisement. Only an address that belongs to
835
+ // this account is worth a warning - anything else may be a neighbour's
836
+ // device, which the owner cannot do anything about.
837
+ const line = `[BLE] ${platformLang.bleScanUnknown} [${goveeReading.address}].`
838
+ if (this.hasBleSensor(goveeReading.address)) {
839
+ this.log.warn(line)
840
+ } else {
841
+ this.log.debug(line)
842
+ }
830
843
  }
831
844
  } catch (err) {
832
845
  this.log.warn('[BLE] error processing reading: %s', parseError(err))
@@ -33,6 +33,9 @@ export function applyUserConfig(platform, config) {
33
33
  const logIncrease = (k, min) => {
34
34
  platform.log.warn('%s [%s] %s %s.', platformLang.cfgItem, k, platformLang.cfgLow, min)
35
35
  }
36
+ const logDecrease = (k, max) => {
37
+ platform.log.warn('%s [%s] %s %s.', platformLang.cfgItem, k, platformLang.cfgHigh, max)
38
+ }
36
39
  const logQuotes = (k) => {
37
40
  platform.log.warn('%s [%s] %s.', platformLang.cfgItem, k, platformLang.cfgQts)
38
41
  }
@@ -77,6 +80,7 @@ export function applyUserConfig(platform, config) {
77
80
  case 'disableDeviceLogging':
78
81
  case 'ignoreMatter':
79
82
  case 'lanDisable':
83
+ case 'openApiDisable':
80
84
  if (typeof val === 'string') {
81
85
  logQuotes(key)
82
86
  }
@@ -86,7 +90,8 @@ export function applyUserConfig(platform, config) {
86
90
  case 'bleRefreshTime':
87
91
  case 'httpRefreshTime':
88
92
  case 'lanRefreshTime':
89
- case 'lanScanInterval': {
93
+ case 'lanScanInterval':
94
+ case 'openApiRefreshTime': {
90
95
  if (typeof val === 'string') {
91
96
  logQuotes(key)
92
97
  }
@@ -97,6 +102,9 @@ export function applyUserConfig(platform, config) {
97
102
  } else if (intVal < platformConsts.minValues[key]) {
98
103
  logIncrease(key, platformConsts.minValues[key])
99
104
  platform.config[key] = platformConsts.minValues[key]
105
+ } else if (platformConsts.maxValues[key] !== undefined && intVal > platformConsts.maxValues[key]) {
106
+ logDecrease(key, platformConsts.maxValues[key])
107
+ platform.config[key] = platformConsts.maxValues[key]
100
108
  } else {
101
109
  platform.config[key] = intVal
102
110
  }
@@ -15,6 +15,8 @@ export default {
15
15
  lanDisable: false,
16
16
  lanRefreshTime: 30,
17
17
  lanScanInterval: 60,
18
+ openApiDisable: false,
19
+ openApiRefreshTime: 30,
18
20
  bleControlInterval: 5,
19
21
  colourSafeMode: false,
20
22
  lightDevices: [],
@@ -42,6 +44,7 @@ export default {
42
44
  lanRefreshTime: 30,
43
45
  lanScanInterval: 60,
44
46
  lowBattThreshold: 20,
47
+ openApiRefreshTime: 30,
45
48
  showAs: 'default',
46
49
  },
47
50
 
@@ -54,6 +57,19 @@ export default {
54
57
  lanRefreshTime: 10,
55
58
  lanScanInterval: 30,
56
59
  lowBattThreshold: 1,
60
+ openApiRefreshTime: 30,
61
+ },
62
+
63
+ // A Node timer holds its delay in a signed 32-bit integer. Over 2147483647 ms
64
+ // it does not throw - it quietly becomes 1 ms, so any of these set past that
65
+ // point would poll a thousand times a second rather than slowly. 2073600
66
+ // seconds is 24 days, comfortably inside the limit once multiplied up.
67
+ maxValues: {
68
+ bleRefreshTime: 2073600,
69
+ httpRefreshTime: 2073600,
70
+ lanRefreshTime: 2073600,
71
+ lanScanInterval: 2073600,
72
+ openApiRefreshTime: 2073600,
57
73
  },
58
74
 
59
75
  allowed: {
@@ -6,8 +6,8 @@ export default {
6
6
  accTokenNoExist: 'does not exist, this is normally not an issue',
7
7
  accTokenStoreErr: 'could not store access token as',
8
8
  accTokenUserChange: 'username has changed',
9
- alDisabled: 'adaptive lighting disabled due to significant colour change',
10
- alDisabledScene: 'adaptive lighting disabled due to scene change',
9
+ alDisabled: 'adaptive lighting turned off, as the colour was changed outside HomeKit',
10
+ alDisabledScene: 'adaptive lighting turned off, as a scene was selected',
11
11
  awsEventClose: 'close event',
12
12
  awsEventConnect: 'connect event',
13
13
  awsEventError: 'error event',
@@ -34,6 +34,7 @@ export default {
34
34
  cfgIgn: 'is not configured correctly so ignoring',
35
35
  cfgIgnItem: 'has an invalid entry which will be ignored',
36
36
  cfgItem: 'Config entry',
37
+ cfgHigh: 'is set too high so reducing to',
37
38
  cfgLow: 'is set too low so increasing to',
38
39
  cfgRmv: 'is unused and can be removed',
39
40
  cfgQts: 'should not have quotes around its entry',
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.34.2-beta.1",
5
+ "version": "11.34.2-beta.3",
6
6
  "description": "Homebridge plugin to integrate Govee devices into HomeKit.",
7
7
  "author": {
8
8
  "name": "bwp91",