@homebridge-plugins/homebridge-govee 11.32.2-beta.30 → 11.32.2-beta.32

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
@@ -7,6 +7,7 @@ All notable changes to `@homebridge-plugins/homebridge-govee` will be documented
7
7
  ### Changed
8
8
 
9
9
  - fix: stop shipping the test files to npm, since they are only needed in the repository
10
+ - fix: BLE device control on macOS by resolving peripherals via advertised name (#1336) (@Perafan18)
10
11
  - fix: implement the H7107's oscillation toggle, which threw a not-implemented error when tapped (#1334)
11
12
  - fix: also send the H7107's oscillation command as the multiSync frame the fan itself uses, since the first shape only turned it off sometimes (#1334)
12
13
  - fix: send the H7107's oscillation-off command without the swing-range bytes, matching the zero-tailed frame the fan echoes back (#1334)
@@ -45,7 +46,8 @@ All notable changes to `@homebridge-plugins/homebridge-govee` will be documented
45
46
  - feat: add the H5161 power strip and give the H7184 kitchen composter an on and off switch
46
47
  - feat: recognise three more sensors, which report what they send, and say plainly in the log why a sensor has no tile yet
47
48
  - feat: add ble encryption for the H3001 solar string lights, which silently ignore unencrypted frames (#1328) (@dominick-han)
48
- - fix: mention an unrecognised bluetooth broadcast once, and only when the device is on your own account, since a scan also hears the neighbours
49
+ - fix: mention an unrecognised bluetooth broadcast once, and only for one of your own sensors, since a scan also hears your lights and the neighbours
50
+ - fix: say "device(s)" in the connection summary, since the count is often one
49
51
 
50
52
  ## v11.32.1 (2026-08-02)
51
53
 
package/README.md CHANGED
@@ -29,7 +29,7 @@ Homebridge plugin to integrate Govee devices into HomeKit
29
29
  - To use this plugin, you will need to already have:
30
30
  - [Node](https://nodejs.org): latest version of `v22`, `v24` or `v26` - any other major version is not supported.
31
31
  - [Homebridge](https://homebridge.io): `v1.6` or above - refer to link for more information and installation instructions.
32
- - For bluetooth connectivity, it may be necessary to install extra packages on your system, see [Bluetooth Control](https://github.com/homebridge-plugins/homebridge-govee/wiki/Bluetooth-Control). Bluetooth works best when using a Raspberry Pi, not been tested on Windows, and Mac devices are unsupported.
32
+ - For bluetooth connectivity, it may be necessary to install extra packages on your system, see [Bluetooth Control](https://github.com/homebridge-plugins/homebridge-govee/wiki/Bluetooth-Control). Bluetooth works best when using a Raspberry Pi, and has not been tested on Windows. On macOS, the plugin resolves BLE devices via their advertised name (macOS does not expose bluetooth MAC addresses), and the `node` binary running Homebridge needs Bluetooth permission in System Settings → Privacy & Security → Bluetooth.
33
33
 
34
34
  ### Setup
35
35
 
@@ -147,11 +147,12 @@ export default class BLEConnection {
147
147
  raw,
148
148
  })}`
149
149
 
150
- // Bluetooth is a broadcast medium, so this scan hears every Govee
151
- // device in range - including a neighbour's, which is not something
152
- // the owner can do anything about. Only a device on this account is
153
- // worth putting in front of them.
154
- if (this.platform.hasBleDevice(address)) {
150
+ // Only one of this account's own sensors is worth putting in front
151
+ // of someone: that is the case where the plugin should be reading
152
+ // the broadcast and cannot. A light of theirs broadcasts just as
153
+ // often but is driven over aws/lan/ble, and an address that is not
154
+ // on the account at all may belong to a neighbour.
155
+ if (this.platform.hasBleSensor(address)) {
155
156
  this.log.warn(line)
156
157
  } else {
157
158
  this.log.debug(line)
@@ -389,7 +390,9 @@ export default class BLEConnection {
389
390
  let timeoutId
390
391
  try {
391
392
  return await Promise.race([
392
- btClient.connectAsync(address),
393
+ process.platform === 'darwin'
394
+ ? this.connectByNameDarwin(address, timeout)
395
+ : btClient.connectAsync(address),
393
396
  new Promise((_, reject) => {
394
397
  timeoutId = setTimeout(() => reject(new Error('Connection timeout')), timeout)
395
398
  }),
@@ -399,6 +402,49 @@ export default class BLEConnection {
399
402
  }
400
403
  }
401
404
 
405
+ // macOS: CoreBluetooth never exposes MAC addresses, so connect-by-address can
406
+ // never resolve. Govee devices advertise a local name whose suffix is the last
407
+ // two bytes of their MAC (e.g. Govee_H617A_325A for ...:32:5A), so scan and
408
+ // match on that instead, then connect to the discovered peripheral directly.
409
+ async connectByNameDarwin(address, timeout) {
410
+ const suffix = address.replace(/:/g, '').slice(-4).toLowerCase()
411
+ this.darwinPeripheralCache = this.darwinPeripheralCache ?? new Map()
412
+ const cached = this.darwinPeripheralCache.get(suffix)
413
+ if (cached) {
414
+ try {
415
+ await cached.connectAsync()
416
+ return cached
417
+ } catch (err) {
418
+ this.darwinPeripheralCache.delete(suffix)
419
+ this.log.debug('[BLE] cached peripheral for %s failed (%s), rescanning.', suffix, err.message)
420
+ }
421
+ }
422
+ const peripheral = await new Promise((resolve, reject) => {
423
+ let done = false
424
+ function onDiscover(p) {
425
+ const name = (p.advertisement?.localName ?? '').toLowerCase()
426
+ if (name.endsWith(suffix) && (name.startsWith('govee') || name.startsWith('ihoment'))) {
427
+ finish(resolve, p)
428
+ }
429
+ }
430
+ function finish(fn, value) {
431
+ if (done) {
432
+ return
433
+ }
434
+ done = true
435
+ btClient.removeListener('discover', onDiscover)
436
+ btClient.stopScanningAsync().catch(() => {})
437
+ fn(value)
438
+ }
439
+ btClient.on('discover', onDiscover)
440
+ btClient.startScanningAsync([], true).catch(err => finish(reject, err))
441
+ setTimeout(() => finish(reject, new Error(`no peripheral advertising name suffix ${suffix} found in scan`)), Math.max(timeout - 1000, 4000))
442
+ })
443
+ this.darwinPeripheralCache.set(suffix, peripheral)
444
+ await peripheral.connectAsync()
445
+ return peripheral
446
+ }
447
+
402
448
  async writeWithTimeout(characteristic, buffer, timeout) {
403
449
  let timeoutId
404
450
  try {
package/lib/platform.js CHANGED
@@ -566,18 +566,18 @@ export default class {
566
566
 
567
567
  // Real-time channels
568
568
  if (this.awsClient) {
569
- realtime.push(`AWS IoT MQTT (${awsCount} devices)`)
569
+ realtime.push(`AWS IoT MQTT (${awsCount} device(s))`)
570
570
  }
571
571
  if (this.openApiClient?.mqttConnected) {
572
- realtime.push(`OpenAPI MQTT (${openApiCount} devices)`)
572
+ realtime.push(`OpenAPI MQTT (${openApiCount} device(s))`)
573
573
  }
574
574
  if (this.lanClient && lanCount > 0) {
575
- realtime.push(`LAN UDP (${lanCount} devices)`)
575
+ realtime.push(`LAN UDP (${lanCount} device(s))`)
576
576
  }
577
577
 
578
578
  // Polling intervals
579
579
  if (this.refreshAWSInterval) {
580
- polling.push(`AWS every 60s (${awsCount} devices)`)
580
+ polling.push(`AWS every 60s (${awsCount} device(s))`)
581
581
  }
582
582
  if (this.refreshHTTPInterval) {
583
583
  polling.push(`HTTP every ${this.config.httpRefreshTime}s (sensors)`)
@@ -1061,19 +1061,31 @@ export default class {
1061
1061
  }
1062
1062
 
1063
1063
  /**
1064
- * Whether a bluetooth address belongs to one of this account's devices.
1064
+ * Whether a bluetooth address belongs to one of this account's sensors.
1065
1065
  *
1066
- * Bluetooth is a broadcast medium: a scan hears every Govee device in range,
1067
- * including a neighbour's. Without this the plugin cannot tell the two apart,
1068
- * and asking someone to report a device that is not theirs is noise.
1066
+ * An undecodable broadcast is only worth putting in front of someone when it
1067
+ * came from a sensor of theirs, because a sensor is the one kind of device
1068
+ * whose readings arrive this way and so the one kind the plugin is failing.
1069
+ * The two cases this deliberately leaves quiet:
1070
+ *
1071
+ * - a light or appliance of theirs, which also broadcasts constantly but is
1072
+ * controlled over aws/lan/ble and has nothing to gain from being decoded
1073
+ * - an address that is not on this account at all, which may be a neighbour's
1074
+ * device and is not something the owner can act on
1069
1075
  */
1070
- hasBleDevice(address) {
1076
+ hasBleSensor(address) {
1071
1077
  if (!address) {
1072
1078
  return false
1073
1079
  }
1074
1080
  const wanted = String(address).toLowerCase()
1081
+ // Any list named sensor* counts, so a new sensor type is covered on the day
1082
+ // it is added rather than needing this list updated too
1083
+ const sensorModels = Object.keys(platformConsts.models)
1084
+ .filter(key => key.startsWith('sensor'))
1085
+ .flatMap(key => platformConsts.models[key])
1075
1086
  return [...devicesInHB.values()].some(
1076
- accessory => String(accessory.context.bleAddress || '').toLowerCase() === wanted,
1087
+ accessory => String(accessory.context.bleAddress || '').toLowerCase() === wanted
1088
+ && sensorModels.includes(accessory.context.gvModel),
1077
1089
  )
1078
1090
  }
1079
1091
 
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.32.2-beta.30",
5
+ "version": "11.32.2-beta.32",
6
6
  "description": "Homebridge plugin to integrate Govee devices into HomeKit.",
7
7
  "author": {
8
8
  "name": "bwp91",