@homebridge-plugins/homebridge-govee 11.34.2-beta.9 → 11.35.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,7 +2,13 @@
2
2
 
3
3
  All notable changes to `@homebridge-plugins/homebridge-govee` will be documented in this file.
4
4
 
5
- ## v11.34.2 (Pending Release)
5
+ ## v11.35.1 (Pending Release)
6
+
7
+ ### Changed
8
+
9
+ - fix: stop reading an h7173 startup frame as a kettle preset temperature (#1351)
10
+
11
+ ## v11.35.0 (2026-08-09)
6
12
 
7
13
  ### Changed
8
14
 
@@ -23,6 +29,9 @@ All notable changes to `@homebridge-plugins/homebridge-govee` will be documented
23
29
  - fix: mark sensor batteries low at the configured threshold (#1349) (@danieloleary)
24
30
  - feat: show whether the kettle is heating, keeping warm or idle (#1337)
25
31
  - fix: give the H5106 a battery service, so its low battery setting does something
32
+ - fix: stop a light's broadcast being mistaken for an h5101 sensor reading
33
+ - fix: follow a device renamed in the govee app, instead of keeping its first name forever (#1344)
34
+ - fix: say why the bluetooth adapter would not start, rather than only that it timed out (#1344)
26
35
 
27
36
  ## v11.34.1 (2026-08-06)
28
37
 
@@ -4,7 +4,7 @@ import process from 'node:process'
4
4
  import btClient from '@stoprocent/noble'
5
5
 
6
6
  import { buildHandshake, isHandshakeFrame, parseHandshakeResponse, V2Session } from '../utils/ble-crypto.js'
7
- import { BLE_STAGE_TIMEOUTS, UUID } from '../utils/ble-protocol.js'
7
+ import { BLE_STAGE_TIMEOUTS, describeAdapterState, UUID } from '../utils/ble-protocol.js'
8
8
  import { decodeAny } from '../utils/decode.js'
9
9
  import { getDeviceCapabilities } from '../utils/device-capabilities.js'
10
10
  import { base64ToHex, generateCodeFromHexValues, hexToTwoItems, sleep } from '../utils/functions.js'
@@ -42,6 +42,9 @@ export default class BLEConnection {
42
42
  this.scanTimeoutId = null
43
43
  this.isShuttingDown = false
44
44
 
45
+ // The power-on failure is said once, not every few minutes - see waitForPowerOn
46
+ this.powerOnReported = false
47
+
45
48
  // Store event handler references for cleanup
46
49
  this.eventHandlers = {
47
50
  stateChange: null,
@@ -64,6 +67,11 @@ export default class BLEConnection {
64
67
  this.btState = state
65
68
  this.log.debug('[BLE] adapter state changed to: %s.', state)
66
69
 
70
+ // Someone has acted on it, so a later failure is worth saying again
71
+ if (state === 'poweredOn') {
72
+ this.powerOnReported = false
73
+ }
74
+
67
75
  // If adapter loses power while operations are in progress, clean up
68
76
  if (state !== 'poweredOn') {
69
77
  this.handleAdapterPowerLoss()
@@ -224,7 +232,21 @@ export default class BLEConnection {
224
232
  await btClient.waitForPoweredOnAsync(timeout)
225
233
  return true
226
234
  } catch (err) {
227
- this.log.warn('[BLE] failed to power on adapter: %s.', err.message)
235
+ // Say what to do about it, from the state noble reported, rather than
236
+ // repeating its "timed out" message for every possible cause (#1344).
237
+ const advice = describeAdapterState(this.btState, platformLang)
238
+ const message = advice
239
+ ? `${advice} [${this.btState}]`
240
+ : err.message
241
+
242
+ // The retry runs every few minutes, so say it once and then keep quiet -
243
+ // nothing changes until someone acts on it
244
+ if (this.powerOnReported) {
245
+ this.log.debug('[BLE] failed to power on adapter: %s.', message)
246
+ } else {
247
+ this.powerOnReported = true
248
+ this.log.warn('[BLE] failed to power on adapter: %s.', message)
249
+ }
228
250
  return false
229
251
  }
230
252
  }
@@ -75,6 +75,13 @@ function readingToCelsius(hexParts) {
75
75
  * Only these four have been seen, all with keep warm switched on. Anything else
76
76
  * falls through to the unknown-data report, which is how these were found.
77
77
  */
78
+ // The range a kettle preset can plausibly sit in. The lowest Govee kettles go
79
+ // is around 40°C for delicate teas, and water stops being water at 100°C - so
80
+ // 90°F to 212°F covers every real preset with room to spare, while excluding
81
+ // the 0 and 1 the H7173 puts in that byte (#1351).
82
+ const KETTLE_PRESET_MIN_F = 90
83
+ const KETTLE_PRESET_MAX_F = 212
84
+
78
85
  const HEATING_STATES = {
79
86
  '00': { label: 'idle', heating: false },
80
87
  '01': { label: 'heating', heating: true },
@@ -391,6 +398,27 @@ export default class extends GoveeDevice {
391
398
  if (getTwoItemPosition(hexParts, 2) === '05' && getTwoItemPosition(hexParts, 3) !== '00') {
392
399
  const slot = Number.parseInt(getTwoItemPosition(hexParts, 3), 16)
393
400
  const tempInF = Number.parseInt(getTwoItemPosition(hexParts, 4), 16)
401
+
402
+ // ⚠️ The `aa 05 <slot> <degrees F>` reading came from ONE kettle, the
403
+ // H7171 in #1337. The H7173 sends the same two leading bytes with a
404
+ // fourth byte of 0 or 1, which is plainly not a temperature - and the
405
+ // plugin cheerfully announced "preset 1 is set to 0°F [-18°C]" (#1351).
406
+ //
407
+ // So the shape alone is not enough to claim a meaning. A preset a
408
+ // kettle could actually hold is the test; anything else is a frame this
409
+ // plugin cannot read yet, and reporting it as such is what gets it
410
+ // decoded rather than guessed at.
411
+ if (tempInF < KETTLE_PRESET_MIN_F || tempInF > KETTLE_PRESET_MAX_F) {
412
+ logUnknownData(this.accessory, {
413
+ kind: 'status',
414
+ source: 'AWS',
415
+ raw: hexString,
416
+ hex: hexParts.join(''),
417
+ extra: { note: 'aa 05 frame whose fourth byte is not a preset temperature' },
418
+ })
419
+ return
420
+ }
421
+
394
422
  const line = `preset ${slot} is set to ${tempInF}°F [${Math.round(((tempInF - 32) * 5) / 9)}°C]`
395
423
  // Worth seeing in a normal log, but only once each. The kettle sends
396
424
  // these when it connects, and if a firmware ever repeats them on a
@@ -101,3 +101,31 @@ export const BLE_STAGE_TIMEOUTS = {
101
101
  export const BLE_UPDATE_TIMEOUT = Object
102
102
  .values(BLE_STAGE_TIMEOUTS)
103
103
  .reduce((total, stage) => total + stage, 0)
104
+
105
+ /**
106
+ * What a noble adapter state means for the person reading the log.
107
+ *
108
+ * ⚠️ The plugin used to report only noble's own message — "Timeout waiting for
109
+ * Noble to be powered on" — for every one of these. That reads as a stuck
110
+ * adapter no matter which it is, and the three real causes need completely
111
+ * different fixes: a blocked radio, a missing capability, or no adapter at all.
112
+ * Working out which cost a reporter four days of back-and-forth (#1344), and it
113
+ * was knowable from the state noble had already reported.
114
+ *
115
+ * Returns undefined when the state does not point at a specific cause, so the
116
+ * caller can fall back to noble's own wording.
117
+ */
118
+ export function describeAdapterState(state, lang) {
119
+ switch (state) {
120
+ case 'poweredOff':
121
+ return lang.blePowerBlocked
122
+ case 'unauthorized':
123
+ return lang.blePowerUnauthorized
124
+ case 'unsupported':
125
+ return lang.blePowerNoAdapter
126
+ case 'unknown':
127
+ return lang.blePowerUnknown
128
+ default:
129
+ return undefined
130
+ }
131
+ }
@@ -1,6 +1,7 @@
1
1
  import platformConsts from './constants.js'
2
2
  import { getDeviceCapabilities } from './device-capabilities.js'
3
3
  import { hasProperty } from './functions.js'
4
+ import platformLang from './lang-en.js'
4
5
 
5
6
  /**
6
7
  * Fills in everything a handler needs to know about a device before it is
@@ -69,9 +70,38 @@ export function applyDeviceContext(platform, accessory, device, deviceConf, join
69
70
  accessory.context.useLanControl = true
70
71
  }
71
72
 
72
- // If the device is LAN-only, then sync the display name with the label in the configuration
73
- if (device.isLanOnly) {
74
- accessory.displayName = device.deviceName
73
+ // Keep the name in step with Govee - or, for a LAN-only device, with the
74
+ // label in the configuration.
75
+ //
76
+ // ⚠️ This used to run for LAN-only devices only, so every other device kept
77
+ // whatever name it was discovered under, forever. A name HAP complains about
78
+ // (brackets, say) could then not be cleared without deleting the cached
79
+ // accessory and losing its room and automations (#1344).
80
+ //
81
+ // ⚠️ This does NOT rename anything in the Home app. HomeKit fixes the name a
82
+ // user sees when the accessory is first added, and a rename made there never
83
+ // comes back to us either. What this fixes is the name Homebridge itself
84
+ // shows and, through the Name characteristic, the HAP warning.
85
+ //
86
+ // `updateDisplayName` is the supported way to do it - assigning to
87
+ // `displayName` sets only Homebridge's copy and leaves the HAP accessory
88
+ // behind. It arrived in Homebridge v1.10.0, and this plugin still supports
89
+ // ^1.6.0, hence the check.
90
+ if (device.deviceName && accessory.displayName !== device.deviceName) {
91
+ platform.log('[%s] %s [%s].', accessory.displayName, platformLang.devRenamed, device.deviceName)
92
+
93
+ if (typeof accessory.updateDisplayName === 'function') {
94
+ accessory.updateDisplayName(device.deviceName)
95
+ } else {
96
+ accessory.displayName = device.deviceName
97
+ }
98
+
99
+ const hapChar = platform.api?.hap?.Characteristic
100
+ const infoService = accessory.getService?.(platform.api?.hap?.Service?.AccessoryInformation)
101
+ if (hapChar && infoService) {
102
+ infoService.updateCharacteristic(hapChar.Name, device.deviceName)
103
+ infoService.updateCharacteristic(hapChar.ConfiguredName, device.deviceName)
104
+ }
75
105
  }
76
106
 
77
107
  // See if we have extra HTTP client info for this device
@@ -25,6 +25,10 @@ export default {
25
25
  bleScanNoStart: 'could not start sync as',
26
26
  bleScanNoStop: 'could not stop sync as',
27
27
  bleScanUnknown: 'reading received for unknown accessory',
28
+ blePowerBlocked: 'the bluetooth adapter is switched off or blocked - check `rfkill list`, and `sudo rfkill unblock bluetooth` if it is blocked',
29
+ blePowerNoAdapter: 'no bluetooth adapter was found - check the adapter exists and the bluetooth service is running',
30
+ blePowerUnauthorized: 'not allowed to use the bluetooth adapter - node needs raw network access, which a node upgrade wipes: `sudo setcap cap_net_raw+eip $(readlink -f $(which node))`, then fully restart homebridge',
31
+ blePowerUnknown: 'the bluetooth adapter never reported its state - check the bluetooth service is running',
28
32
  bleStart: 'scanning started',
29
33
  bleStop: 'scanning stopped',
30
34
  bleTimeout: 'BLE took too long',
@@ -82,6 +86,7 @@ export default {
82
86
  devNotRet: 'skipping sync as device status not retrievable',
83
87
  devNotUpdated: 'could not be updated as',
84
88
  devRemove: 'has been removed from Homebridge',
89
+ devRenamed: 'has been renamed to',
85
90
  disableClient: 'disabling client as',
86
91
  disabledInConfig: 'disabled in config',
87
92
  disabling: 'Disabling plugin',
@@ -11,7 +11,23 @@ export function isHt5075(hex) { // Govee H5072/H5075
11
11
  return hex.includes(h5075_uuid_rev) && hex.length === 16
12
12
  }
13
13
 
14
- export const isHt5101 = hex => hex.includes(h5101_uuid_rev) // Govee H5100/H5101/H5102
14
+ // Govee H5100/H5101/H5102
15
+ //
16
+ // ⚠️ The length check matters as much as the marker. `0100` is only four hex
17
+ // digits, and plenty of Govee broadcasts happen to contain it - a light sending
18
+ // `4388ec00020100` did, which claimed the broadcast for the H5101 decoder, and
19
+ // that decoder reads sixteen characters and threw on fourteen (#1350). The
20
+ // throw surfaced as `error processing discovered peripheral` on every broadcast
21
+ // from a light that was working perfectly well.
22
+ //
23
+ // This is `>=` rather than the `===` its siblings use, deliberately: the
24
+ // decoder only ever reads the first sixteen characters, so anything at least
25
+ // that long decodes correctly today. Pinning an exact length would reject a
26
+ // longer payload from an H5111 or H5220 - both documented as sharing this
27
+ // format - and there is no sample of either to check that against.
28
+ export function isHt5101(hex) {
29
+ return hex.includes(h5101_uuid_rev) && hex.length >= 16
30
+ }
15
31
 
16
32
  export function isHt5179(hex) { // Govee H5179
17
33
  return hex.includes(h5179_uuid_rev) && hex.length === 22
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.9",
5
+ "version": "11.35.1-beta.0",
6
6
  "description": "Homebridge plugin to integrate Govee devices into HomeKit.",
7
7
  "author": {
8
8
  "name": "bwp91",