@homebridge-plugins/homebridge-govee 11.25.0 → 11.26.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,23 @@
2
2
 
3
3
  All notable changes to `@homebridge-plugins/homebridge-govee` will be documented in this file.
4
4
 
5
+ ## v11.26.1 (Pending Release)
6
+
7
+ ### Changes
8
+
9
+ - fix: report leak events relayed by a wifi gateway over AWS (#1276)
10
+
11
+ ## v11.26.0 (2026-07-12)
12
+
13
+ ### Changes
14
+
15
+ - feat: add support for the H609D Galaxy Light Projector (#1298)
16
+ - fix: never remove accessories based on a failed device fetch (#1264)
17
+ - fix: skip Govee app device groups from the OpenAPI device list (#1309)
18
+ - feat: add under-construction support for the H1370 fan (#1307)
19
+ - fix: use the segment-based BLE colour command for the H617E (#1290)
20
+ - fix: send the on command first so scenes from off apply their colour (#1277)
21
+
5
22
  ## v11.25.0 (2026-07-12)
6
23
 
7
24
  ### Changes
@@ -250,8 +250,13 @@ export default class {
250
250
  return
251
251
  }
252
252
 
253
- // Await slightly longer than brightness and colour so on/off is sent last
254
- await sleep(400)
253
+ // Send off after colour/brightness (a colour command can implicitly power a
254
+ // bulb back on), but send on straight away so the bulb is awake in time to
255
+ // receive the colour and brightness a HomeKit scene sends alongside it, as
256
+ // some models ignore these commands while switched off (#1277)
257
+ if (newValue === 'off') {
258
+ await sleep(400)
259
+ }
255
260
 
256
261
  // Send the request to the platform sender function
257
262
  await this.platform.sendDeviceUpdate(this.accessory, {
@@ -316,9 +321,11 @@ export default class {
316
321
  async internalBrightnessUpdate(value) {
317
322
  try {
318
323
  // This acts like a debounce function when endlessly sliding the brightness scale
324
+ // If the light is off (e.g. a HomeKit scene turning it on), wait longer so the
325
+ // on command sent by internalStateUpdate has time to wake the bulb first (#1277)
319
326
  const updateKeyBright = generateRandomString(5)
320
327
  this.updateKeyBright = updateKeyBright
321
- await sleep(350)
328
+ await sleep(this.cacheState !== 'on' ? 1050 : 350)
322
329
  if (updateKeyBright !== this.updateKeyBright) {
323
330
  return
324
331
  }
@@ -367,9 +374,11 @@ export default class {
367
374
  async internalColourUpdate(value, force = false) {
368
375
  try {
369
376
  // This acts like a debounce function when endlessly sliding the colour wheel
377
+ // If the light is off (e.g. a HomeKit scene turning it on), wait longer so the
378
+ // on command sent by internalStateUpdate has time to wake the bulb first (#1277)
370
379
  const updateKeyColour = generateRandomString(5)
371
380
  this.updateKeyColour = updateKeyColour
372
- await sleep(300)
381
+ await sleep(this.cacheState !== 'on' ? 1000 : 300)
373
382
  if (updateKeyColour !== this.updateKeyColour) {
374
383
  return
375
384
  }
@@ -440,9 +449,12 @@ export default class {
440
449
  async internalCTUpdate(value) {
441
450
  try {
442
451
  // This acts like a debounce function when endlessly sliding the colour wheel
452
+ // If the light is off (e.g. a HomeKit scene turning it on), wait longer so the
453
+ // on command sent by internalStateUpdate has time to wake the bulb first, and
454
+ // so the 'skip when off' check below sees the updated state (#1277)
443
455
  const updateKeyCT = generateRandomString(5)
444
456
  this.updateKeyCT = updateKeyCT
445
- await sleep(300)
457
+ await sleep(this.cacheState !== 'on' ? 1000 : 300)
446
458
  if (updateKeyCT !== this.updateKeyCT) {
447
459
  return
448
460
  }
@@ -67,7 +67,7 @@ export default class {
67
67
  }
68
68
 
69
69
  // Check to see if the provided leak status is different from the cached state
70
- if (params.leakDetected !== this.cacheLeak) {
70
+ if (hasProperty(params, 'leakDetected') && params.leakDetected !== this.cacheLeak) {
71
71
  // Leak status is different so update Homebridge with new values
72
72
  this.cacheLeak = params.leakDetected
73
73
  this.service.updateCharacteristic(this.hapChar.LeakDetected, this.cacheLeak ? 1 : 0)
package/lib/platform.js CHANGED
@@ -19,6 +19,7 @@ import platformChars from './utils/custom-chars.js'
19
19
  import { getDeviceCapabilities } from './utils/device-capabilities.js'
20
20
  import eveChars from './utils/eve-chars.js'
21
21
  import {
22
+ base64ToHex,
22
23
  hasProperty,
23
24
  parseDeviceId,
24
25
  parseError,
@@ -654,6 +655,12 @@ export default class {
654
655
 
655
656
  if (openApiDevices && openApiDevices.length > 0) {
656
657
  openApiDevices.forEach((openApiDevice) => {
658
+ // Skip Govee app device groups as these are not real devices (#1309)
659
+ if (['BaseGroup', 'SameModeGroup'].includes(openApiDevice.model)) {
660
+ this.log.debug('[%s] %s.', openApiDevice.deviceName, platformLang.devIsGroup)
661
+ return
662
+ }
663
+
657
664
  if (this.ignoredDevices.includes(openApiDevice.device)) {
658
665
  return
659
666
  }
@@ -717,18 +724,28 @@ export default class {
717
724
  }
718
725
 
719
726
  // Check for redundant Homebridge accessories
727
+ // Only treat 'not found in Govee' as a reason to remove an accessory when the
728
+ // configured cloud clients actually returned their device lists this session,
729
+ // otherwise a failed fetch would remove the accessory and destroy any HomeKit
730
+ // scenes and automations it is part of (#1264)
731
+ const cloudListsFetched = (!this.config.username || !!this.httpClient)
732
+ && (!!this.config.openApiDisable || !this.config.apiKey || !!this.openApiClient)
720
733
  devicesInHB.forEach((accessory) => {
721
- // If the accessory doesn't exist in Govee then remove it. Also remove it
722
- // if it is now being ignored, or if it is a matter model and the user has
723
- // enabled ignoreMatter (otherwise the cached accessory lingers even though
724
- // the device is skipped during setup) (#1287).
725
- if (
726
- (!httpDevices.some(el => el.device === accessory.context.gvDeviceId)
727
- && !lanDevices.some(el => el.device === accessory.context.gvDeviceId)
728
- && !openApiDevices.some(el => el.device === accessory.context.gvDeviceId))
729
- || this.ignoredDevices.includes(accessory.context.gvDeviceId)
734
+ // Remove the accessory if it is now being ignored, or if it is a matter model
735
+ // and the user has enabled ignoreMatter (otherwise the cached accessory lingers
736
+ // even though the device is skipped during setup) (#1287)
737
+ const isIgnored = this.ignoredDevices.includes(accessory.context.gvDeviceId)
730
738
  || (this.config.ignoreMatter && platformConsts.matterModels.includes(accessory.context.gvModel))
731
- ) {
739
+
740
+ // Also remove it if the device no longer exists in Govee, but never based on
741
+ // an incomplete cloud list, and never for LAN-only devices which may simply
742
+ // have missed a scan window (#1264)
743
+ const isMissing = !httpDevices.some(el => el.device === accessory.context.gvDeviceId)
744
+ && !lanDevices.some(el => el.device === accessory.context.gvDeviceId)
745
+ && !openApiDevices.some(el => el.device === accessory.context.gvDeviceId)
746
+ const isLanOnly = accessory.context.hasLanControl && !accessory.context.firmware
747
+
748
+ if (isIgnored || (isMissing && cloudListsFetched && !isLanOnly)) {
732
749
  this.removeAccessory(accessory)
733
750
  }
734
751
  })
@@ -1356,6 +1373,14 @@ export default class {
1356
1373
  }
1357
1374
  }
1358
1375
 
1376
+ // Gateway subdevices (e.g. leak sensors paired to an H5044) include
1377
+ // their gateway id and slot number, which we need to match real-time
1378
+ // leak events relayed by the gateway over AWS (#1276)
1379
+ if (hasProperty(parsed, 'sno') && parsed.gatewayInfo?.device) {
1380
+ accessory.context.gatewaySno = parsed.sno
1381
+ accessory.context.gatewayDevice = parsed.gatewayInfo.device
1382
+ }
1383
+
1359
1384
  // Get a min and max temperature/humidity range to show in the homebridge-ui
1360
1385
  if (hasProperty(parsed, 'temCali')) {
1361
1386
  accessory.context.minTemp = parsed.temMin / 100
@@ -1506,6 +1531,13 @@ export default class {
1506
1531
  if (platformConsts.models.sensorLeak.includes(device1.sku)) {
1507
1532
  accessory.logDebug(`raw data: ${JSON.stringify({ ...parsedData, ...parsedSettings })}`)
1508
1533
 
1534
+ // Keep the gateway details up to date for matching real-time leak
1535
+ // events relayed by the gateway over AWS (#1276)
1536
+ if (hasProperty(parsedSettings, 'sno') && parsedSettings.gatewayInfo?.device) {
1537
+ accessory.context.gatewaySno = parsedSettings.sno
1538
+ accessory.context.gatewayDevice = parsedSettings.gatewayInfo.device
1539
+ }
1540
+
1509
1541
  // Leak Sensors - check to see of any warnings if the lastTime is above 0
1510
1542
  let hasUnreadLeak = false
1511
1543
  if (parsedData.lastTime > 0) {
@@ -1789,9 +1821,46 @@ export default class {
1789
1821
  receiveUpdateAWS(payload) {
1790
1822
  const accessoryUUID = this.api.hap.uuid.generate(payload.device)
1791
1823
  const accessory = devicesInHB.get(accessoryUUID)
1792
- this.receiveDeviceUpdate(accessory, {
1793
- source: 'AWS',
1794
- ...payload,
1824
+ if (accessory) {
1825
+ this.receiveDeviceUpdate(accessory, {
1826
+ source: 'AWS',
1827
+ ...payload,
1828
+ })
1829
+ return
1830
+ }
1831
+
1832
+ // No matching accessory - the message may be from a wifi gateway (e.g. H5044)
1833
+ // relaying an update for one of its subdevices, such as a leak sensor (#1276)
1834
+ this.receiveUpdateAWSGateway(payload)
1835
+ }
1836
+
1837
+ receiveUpdateAWSGateway(payload) {
1838
+ (payload?.op?.command || []).forEach((command) => {
1839
+ let hexString
1840
+ try {
1841
+ hexString = base64ToHex(command)
1842
+ } catch {
1843
+ return
1844
+ }
1845
+
1846
+ // 0xEE34 is a gateway subdevice status, laid out as:
1847
+ // ee 34 [subdevice number] 02 00 64 1e 14 [seq] [unix time x4] [leak flags x4] 80 00 [checksum]
1848
+ if (!hexString.startsWith('ee34') || hexString.length < 36) {
1849
+ return
1850
+ }
1851
+ const subdeviceNo = Number.parseInt(hexString.substring(4, 6), 16)
1852
+ const leakDetected = hexString.substring(26, 34) !== '00000000'
1853
+
1854
+ // Match the update to the leak sensor with this gateway and slot number
1855
+ devicesInHB.forEach((accessory) => {
1856
+ if (
1857
+ accessory.context.gatewayDevice === payload.device
1858
+ && accessory.context.gatewaySno === subdeviceNo
1859
+ && platformConsts.models.sensorLeak.includes(accessory.context.gvModel)
1860
+ ) {
1861
+ this.receiveDeviceUpdate(accessory, { source: 'AWS', leakDetected })
1862
+ }
1863
+ })
1795
1864
  })
1796
1865
  }
1797
1866
 
@@ -219,6 +219,7 @@ export default {
219
219
  'H6097',
220
220
  'H6098',
221
221
  'H6099',
222
+ 'H609D',
222
223
  'H60A0',
223
224
  'H60A1',
224
225
  'H60A4',
@@ -542,6 +543,7 @@ export default {
542
543
  kettle: ['H7170', 'H7171', 'H7173', 'H7175', 'H717A'],
543
544
  template: [
544
545
  'H1162', // https://github.com/homebridge-plugins/homebridge-govee/issues/422
546
+ 'H1370', // https://github.com/homebridge-plugins/homebridge-govee/issues/1307
545
547
  'H1167', // https://github.com/homebridge-plugins/homebridge-govee/issues/1142
546
548
  'H5010', // https://github.com/homebridge-plugins/homebridge-govee/issues/1235
547
549
  'H5024', // https://github.com/homebridge-plugins/homebridge-govee/issues/835
@@ -14,12 +14,14 @@ const modelOverrides = {
14
14
  H6102: { bleColorCmd: [COLOR_SUB.RGB_ALT], bleBrightnessScale: 0x64 },
15
15
  H613B: { bleColorCmd: [COLOR_SUB.RGB_ALT], bleBrightnessScale: 0x64 },
16
16
  H613D: { bleColorCmd: [COLOR_SUB.RGB_ALT], bleBrightnessScale: 0x64 },
17
- H617E: { bleColorCmd: [COLOR_SUB.RGB_ALT], bleBrightnessScale: 0x64 },
18
17
 
19
18
  // BLE color sub-command: 0x15 0x01 (extended) with trailing bytes
20
19
  H6053: { bleColorCmd: COLOR_SUB.RGB_EXTENDED, bleColorCmdSuffix: [0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x7F] },
21
20
  H6072: { bleColorCmd: COLOR_SUB.RGB_EXTENDED, bleColorCmdSuffix: [0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x7F] },
22
21
  H6199: { bleColorCmd: COLOR_SUB.RGB_EXTENDED, bleColorCmdSuffix: [0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x7F] },
22
+ // H617E is an RGBIC strip which ignores the 0x0D command - it needs the
23
+ // segment-based extended format, as seen in its own TTR rule data (#1290)
24
+ H617E: { bleColorCmd: COLOR_SUB.RGB_EXTENDED, bleColorCmdSuffix: [0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x7F], bleBrightnessScale: 0x64 },
23
25
 
24
26
  // AWS outlet uses 17/16 for on/off instead of 1/0
25
27
  H5080: { awsPowerOn: 17, awsPowerOff: 16 },
@@ -68,6 +68,7 @@ export default {
68
68
  curWatt: 'current wattage',
69
69
  devAdd: 'has been added to Homebridge',
70
70
  devInit: 'initialised with id',
71
+ devIsGroup: 'is a Govee app device group so will not be added to Homebridge',
71
72
  devInitOpts: 'initialising with options',
72
73
  devMaySupp: 'is not currently supported, do create a Github issue with the following info',
73
74
  devNoBlePackage: 'will not be added as BLE is disabled in the config',
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.25.0",
5
+ "version": "11.26.1-beta.0",
6
6
  "description": "Homebridge plugin to integrate Govee devices into HomeKit.",
7
7
  "author": {
8
8
  "name": "bwp91",