@homebridge-plugins/homebridge-govee 11.32.0-beta.26 → 11.32.0-beta.27

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
@@ -30,6 +30,7 @@ All notable changes to `@homebridge-plugins/homebridge-govee` will be documented
30
30
  - fix(fans): rebuild the fan service once when it was cached under the old speed scale, so the top of the slider stops setting the slowest speed (#1326) (@cwhett)
31
31
  - fix: show the H5089 night light tile again, but refuse control commands, so it reports the real state instead of quietly ignoring taps (#1323)
32
32
  - fix(ice-makers): stop the home app showing an ice size of 0 when one has never been set (#1329) (@cwhett)
33
+ - fix(ble): size the update queue timeout from the connection stages, so a slow device still has time to send its command (#1328)
33
34
 
34
35
  ## v11.31.0 (2026-07-24)
35
36
 
@@ -3,7 +3,7 @@ import process from 'node:process'
3
3
 
4
4
  import btClient from '@stoprocent/noble'
5
5
 
6
- import { UUID } from '../utils/ble-protocol.js'
6
+ import { BLE_STAGE_TIMEOUTS, UUID } from '../utils/ble-protocol.js'
7
7
  import { decodeAny } from '../utils/decode.js'
8
8
  import { base64ToHex, generateCodeFromHexValues, hexToTwoItems, sleep } from '../utils/functions.js'
9
9
  import platformLang from '../utils/lang-en.js'
@@ -15,8 +15,11 @@ process.env.NOBLE_REPORT_ALL_HCI_EVENTS = '1' // needed on Linux including Raspb
15
15
 
16
16
  const H5075_UUID = 'ec88'
17
17
  const H5101_UUID = '0001'
18
- const CONNECTION_TIMEOUT = 10000 // 10 seconds
19
- const WRITE_TIMEOUT = 5000 // 5 seconds
18
+
19
+ const POWER_ON_TIMEOUT = BLE_STAGE_TIMEOUTS.powerOn
20
+ const CONNECTION_TIMEOUT = BLE_STAGE_TIMEOUTS.connect
21
+ const WRITE_TIMEOUT = BLE_STAGE_TIMEOUTS.write
22
+ const SETTLE_DELAY = BLE_STAGE_TIMEOUTS.settle
20
23
 
21
24
  /*
22
25
  The necessary commands to send and functions are taken from and credit to:
@@ -169,7 +172,7 @@ export default class BLEConnection {
169
172
  }
170
173
  }
171
174
 
172
- async waitForPowerOn(timeout = 5000) {
175
+ async waitForPowerOn(timeout = POWER_ON_TIMEOUT) {
173
176
  if (this.btState === 'poweredOn') {
174
177
  return true
175
178
  }
@@ -240,7 +243,7 @@ export default class BLEConnection {
240
243
  // Write without response then allow time for the BLE controller to transmit before disconnecting
241
244
  await this.writeWithTimeout(characteristic, finalBuffer, WRITE_TIMEOUT)
242
245
  accessory.logDebug('command sent successfully')
243
- await sleep(100)
246
+ await sleep(SETTLE_DELAY)
244
247
  } catch (err) {
245
248
  accessory.logWarn(`BLE update failed: ${err.message}`)
246
249
  throw err
package/lib/platform.js CHANGED
@@ -13,6 +13,7 @@ import lanClient from './connection/lan.js'
13
13
  import openApiClient from './connection/openapi.js'
14
14
  import deviceTypes from './device/index.js'
15
15
  import eveService from './fakegato/fakegato-history.js'
16
+ import { BLE_UPDATE_TIMEOUT } from './utils/ble-protocol.js'
16
17
  import { buildCommand } from './utils/command-builder.js'
17
18
  import platformConsts from './utils/constants.js'
18
19
  import platformChars from './utils/custom-chars.js'
@@ -595,7 +596,10 @@ export default class {
595
596
  concurrency: 1,
596
597
  interval: this.config.bleControlInterval * 1000,
597
598
  intervalCap: 1,
598
- timeout: 10000,
599
+ // Sized from the BLE stage timeouts so the two cannot drift apart. This
600
+ // is a backstop for a hung task, and must stay longer than the stages
601
+ // it wraps - see BLE_UPDATE_TIMEOUT.
602
+ timeout: BLE_UPDATE_TIMEOUT,
599
603
  throwOnTimeout: true,
600
604
  })
601
605
 
@@ -61,3 +61,35 @@ export const UUID = {
61
61
 
62
62
  // Total BLE packet size
63
63
  export const PACKET_SIZE = 20
64
+
65
+ // Timeouts for the individual stages of a single BLE updateDevice() call.
66
+ // They live here, rather than beside the code that uses them, so the platform
67
+ // can size its queue from them without importing the BLE client — that client
68
+ // pulls in a native module which is not present on every install.
69
+ export const BLE_STAGE_TIMEOUTS = {
70
+ powerOn: 5000,
71
+ connect: 10000,
72
+ write: 5000,
73
+ settle: 100, // let the controller transmit before disconnecting
74
+
75
+ // Discovering services, and the disconnect on the way out, have no timeout of
76
+ // their own, so leave them room in the total.
77
+ untimedStages: 3000,
78
+
79
+ // Headroom on top, so that a stage which times out reports its own error
80
+ // instead of being cut short by the queue's much vaguer one.
81
+ queueHeadroom: 2000,
82
+ }
83
+
84
+ /**
85
+ * The longest a single BLE updateDevice() call can legitimately take.
86
+ *
87
+ * The platform queue sizes its own timeout from this. It is meant as a backstop
88
+ * for a task that has hung, not a second deadline racing the stage timeouts.
89
+ * Both used to be a flat 10 seconds, which meant the queue killed a connection
90
+ * attempt at the exact moment that attempt gave up — and a light that connected
91
+ * slowly never got as far as sending its command at all (#1328).
92
+ */
93
+ export const BLE_UPDATE_TIMEOUT = Object
94
+ .values(BLE_STAGE_TIMEOUTS)
95
+ .reduce((total, stage) => total + stage, 0)
@@ -0,0 +1,44 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { BLE_STAGE_TIMEOUTS, BLE_UPDATE_TIMEOUT } from './ble-protocol.js'
4
+
5
+ /**
6
+ * The bug these guard against (#1328): the BLE connect timeout and the platform
7
+ * queue timeout were both a flat 10000ms. The queue therefore killed a task at
8
+ * the exact moment the connect gave up, and any light that connected slowly had
9
+ * nothing left of the budget for discovering services and writing its command.
10
+ */
11
+ describe('bLE timeout budget', () => {
12
+ it('leaves room for every stage of an update', () => {
13
+ const stages = BLE_STAGE_TIMEOUTS.powerOn
14
+ + BLE_STAGE_TIMEOUTS.connect
15
+ + BLE_STAGE_TIMEOUTS.untimedStages
16
+ + BLE_STAGE_TIMEOUTS.write
17
+ + BLE_STAGE_TIMEOUTS.settle
18
+
19
+ expect(BLE_UPDATE_TIMEOUT).toBeGreaterThan(stages)
20
+ })
21
+
22
+ it('is not simply the connect timeout, which is what caused #1328', () => {
23
+ expect(BLE_UPDATE_TIMEOUT).not.toBe(BLE_STAGE_TIMEOUTS.connect)
24
+ })
25
+
26
+ it('still has time left after the slowest possible connect', () => {
27
+ // A connect that only just succeeds must leave enough behind to discover
28
+ // services and send the command - otherwise a slow device is
29
+ // indistinguishable from an unreachable one.
30
+ const afterSlowestConnect = BLE_UPDATE_TIMEOUT
31
+ - BLE_STAGE_TIMEOUTS.powerOn
32
+ - BLE_STAGE_TIMEOUTS.connect
33
+
34
+ expect(afterSlowestConnect).toBeGreaterThanOrEqual(
35
+ BLE_STAGE_TIMEOUTS.untimedStages + BLE_STAGE_TIMEOUTS.write + BLE_STAGE_TIMEOUTS.settle,
36
+ )
37
+ })
38
+
39
+ it('keeps every stage timeout positive', () => {
40
+ Object.entries(BLE_STAGE_TIMEOUTS).forEach(([stage, ms]) => {
41
+ expect(ms, stage).toBeGreaterThan(0)
42
+ })
43
+ })
44
+ })
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.0-beta.26",
5
+ "version": "11.32.0-beta.27",
6
6
  "description": "Homebridge plugin to integrate Govee devices into HomeKit.",
7
7
  "author": {
8
8
  "name": "bwp91",