@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 +3 -1
- package/README.md +1 -1
- package/lib/connection/ble.js +52 -6
- package/lib/platform.js +22 -10
- package/package.json +1 -1
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
|
|
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,
|
|
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
|
|
package/lib/connection/ble.js
CHANGED
|
@@ -147,11 +147,12 @@ export default class BLEConnection {
|
|
|
147
147
|
raw,
|
|
148
148
|
})}`
|
|
149
149
|
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
// the
|
|
153
|
-
//
|
|
154
|
-
|
|
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
|
-
|
|
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}
|
|
569
|
+
realtime.push(`AWS IoT MQTT (${awsCount} device(s))`)
|
|
570
570
|
}
|
|
571
571
|
if (this.openApiClient?.mqttConnected) {
|
|
572
|
-
realtime.push(`OpenAPI MQTT (${openApiCount}
|
|
572
|
+
realtime.push(`OpenAPI MQTT (${openApiCount} device(s))`)
|
|
573
573
|
}
|
|
574
574
|
if (this.lanClient && lanCount > 0) {
|
|
575
|
-
realtime.push(`LAN UDP (${lanCount}
|
|
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}
|
|
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
|
|
1064
|
+
* Whether a bluetooth address belongs to one of this account's sensors.
|
|
1065
1065
|
*
|
|
1066
|
-
*
|
|
1067
|
-
*
|
|
1068
|
-
*
|
|
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
|
-
|
|
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