@homebridge-plugins/homebridge-govee 11.34.1-beta.0 → 11.34.2-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 +9 -1
- package/lib/device/light.js +84 -21
- package/lib/utils/command-builder.js +29 -1
- package/lib/utils/constants.js +14 -0
- package/lib/utils/lang-en.js +1 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,11 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@homebridge-plugins/homebridge-govee` will be documented in this file.
|
|
4
4
|
|
|
5
|
-
## v11.34.
|
|
5
|
+
## v11.34.2 (Pending Release)
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- feat: control the H1250 zones with the device's own frame, as govee ignores its toggles (#1343) (@Principe1218)
|
|
10
|
+
- fix: explain in the log when zone tiles are missing because aws credentials are not set
|
|
11
|
+
|
|
12
|
+
## v11.34.1 (2026-08-06)
|
|
6
13
|
|
|
7
14
|
### Changed
|
|
8
15
|
|
|
9
16
|
- fix: log unrecognised device status at debug, not as a warning per frame (#1341, #1342)
|
|
17
|
+
- chore(deps): dependency updates
|
|
10
18
|
|
|
11
19
|
## v11.34.0 (2026-08-05)
|
|
12
20
|
|
package/lib/device/light.js
CHANGED
|
@@ -6,14 +6,43 @@ import {
|
|
|
6
6
|
} from '../utils/colour.js'
|
|
7
7
|
import platformConsts from '../utils/constants.js'
|
|
8
8
|
import {
|
|
9
|
+
base64ToHex,
|
|
9
10
|
generateRandomString,
|
|
11
|
+
getTwoItemPosition,
|
|
10
12
|
hasProperty,
|
|
13
|
+
hexToTwoItems,
|
|
11
14
|
parseError,
|
|
12
15
|
sleep,
|
|
13
16
|
} from '../utils/functions.js'
|
|
14
17
|
import platformLang from '../utils/lang-en.js'
|
|
15
18
|
import GoveeDevice from './base.js'
|
|
16
19
|
|
|
20
|
+
/**
|
|
21
|
+
* The zone state of a light that reports both of its zones in one frame, as
|
|
22
|
+
* `aa 30 <main> <background>`.
|
|
23
|
+
*
|
|
24
|
+
* Reported under the same capability instances the OpenAPI uses, so zone state
|
|
25
|
+
* reads the same whichever connection carried it. This frame arrives unprompted
|
|
26
|
+
* whenever the zones change, including from the Govee app or the wall switch,
|
|
27
|
+
* which is how the tiles stay right rather than only echoing our own writes -
|
|
28
|
+
* all the toggles could ever manage on these models (#1333).
|
|
29
|
+
*
|
|
30
|
+
* @param {string[]} [commands] the base64 frames from the payload
|
|
31
|
+
* @returns {object} the zones found, keyed by capability instance
|
|
32
|
+
*/
|
|
33
|
+
function readZoneFrame(commands = []) {
|
|
34
|
+
const zones = {}
|
|
35
|
+
commands.forEach((command) => {
|
|
36
|
+
const hexParts = hexToTwoItems(base64ToHex(command))
|
|
37
|
+
if (`${getTwoItemPosition(hexParts, 1)}${getTwoItemPosition(hexParts, 2)}` !== 'aa30') {
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
zones.mainLightToggle = getTwoItemPosition(hexParts, 3) === '01'
|
|
41
|
+
zones.backgroundLightToggle = getTwoItemPosition(hexParts, 4) === '01'
|
|
42
|
+
})
|
|
43
|
+
return zones
|
|
44
|
+
}
|
|
45
|
+
|
|
17
46
|
export default class extends GoveeDevice {
|
|
18
47
|
constructor(platform, accessory) {
|
|
19
48
|
super(platform, accessory)
|
|
@@ -199,30 +228,51 @@ export default class extends GoveeDevice {
|
|
|
199
228
|
// Some lights (e.g. the H1270 Ceiling Light Ultra) have two independently
|
|
200
229
|
// controllable zones - a main/bottom panel and a background/top light. Govee
|
|
201
230
|
// exposes these as OpenAPI toggle capabilities, so add a switch for each zone
|
|
202
|
-
// the device reports
|
|
231
|
+
// the device reports, controlled over the OpenAPI connection.
|
|
232
|
+
//
|
|
233
|
+
// The models in `zoneLightModels` advertise those same capabilities but the
|
|
234
|
+
// cloud never delivers them, so they are driven by a raw frame over AWS
|
|
235
|
+
// instead. That is the only difference - the tiles, their state and the way
|
|
236
|
+
// a failure is undone are the same either way (#1333)
|
|
203
237
|
this.zoneServices = {}
|
|
204
238
|
this.zoneCache = {}
|
|
239
|
+
this.hasRawZones = platformConsts.zoneLightModels.includes(accessory.context.gvModel)
|
|
205
240
|
const zoneToggleDefs = [
|
|
206
|
-
{ instance: 'mainLightToggle', name: 'Main Light' },
|
|
207
|
-
{ instance: 'backgroundLightToggle', name: 'Background Light' },
|
|
241
|
+
{ instance: 'mainLightToggle', zone: 'main', name: 'Main Light' },
|
|
242
|
+
{ instance: 'backgroundLightToggle', zone: 'background', name: 'Background Light' },
|
|
208
243
|
]
|
|
209
244
|
const openApiCaps = accessory.context.openApiCapabilities || {}
|
|
210
|
-
zoneToggleDefs.forEach(({ instance, name }) => {
|
|
245
|
+
zoneToggleDefs.forEach(({ instance, zone, name }) => {
|
|
211
246
|
const existingService = this.accessory.getService(name)
|
|
212
|
-
|
|
247
|
+
|
|
248
|
+
// The raw frame goes over AWS, which needs Govee account credentials
|
|
249
|
+
// rather than just an API key
|
|
250
|
+
const isControllable = this.hasRawZones
|
|
251
|
+
? accessory.context.useAwsControl
|
|
252
|
+
: accessory.context.useOpenApiControl && openApiCaps[instance]
|
|
253
|
+
|
|
254
|
+
if (isControllable) {
|
|
213
255
|
const service = existingService
|
|
214
256
|
|| this.accessory.addService(this.hapServ.Switch, name, instance)
|
|
215
257
|
service
|
|
216
258
|
.getCharacteristic(this.hapChar.On)
|
|
217
|
-
.onSet(async value => this.internalZoneUpdate(instance, name, value))
|
|
259
|
+
.onSet(async value => this.internalZoneUpdate(instance, zone, name, value))
|
|
218
260
|
this.zoneServices[instance] = service
|
|
219
261
|
this.zoneCache[instance] = service.getCharacteristic(this.hapChar.On).value ? 1 : 0
|
|
220
262
|
} else if (existingService) {
|
|
221
|
-
// Capability no longer available (or
|
|
263
|
+
// Capability no longer available (or the connection it needs is off) so
|
|
264
|
+
// remove the switch
|
|
222
265
|
this.accessory.removeService(existingService)
|
|
223
266
|
}
|
|
224
267
|
})
|
|
225
268
|
|
|
269
|
+
// Withholding the tiles without AWS is deliberate - tiles that swallow taps
|
|
270
|
+
// are worse than none - but silence would leave the owner with no idea the
|
|
271
|
+
// zones exist or what is missing, so say so once at startup
|
|
272
|
+
if (this.hasRawZones && !accessory.context.useAwsControl) {
|
|
273
|
+
this.accessory.log(platformLang.zoneNeedsAws)
|
|
274
|
+
}
|
|
275
|
+
|
|
226
276
|
// Output the customised options to the log
|
|
227
277
|
const useAwsControl = accessory.context.useAwsControl ? 'enabled' : 'disabled'
|
|
228
278
|
const useBleControl = accessory.context.useBleControl ? 'enabled' : 'disabled'
|
|
@@ -274,7 +324,7 @@ export default class extends GoveeDevice {
|
|
|
274
324
|
}
|
|
275
325
|
}
|
|
276
326
|
|
|
277
|
-
async internalZoneUpdate(instance, name, value) {
|
|
327
|
+
async internalZoneUpdate(instance, zone, name, value) {
|
|
278
328
|
const service = this.zoneServices[instance]
|
|
279
329
|
try {
|
|
280
330
|
const newValue = value ? 1 : 0
|
|
@@ -284,15 +334,26 @@ export default class extends GoveeDevice {
|
|
|
284
334
|
return
|
|
285
335
|
}
|
|
286
336
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
337
|
+
if (this.hasRawZones) {
|
|
338
|
+
// This model's toggles are accepted and ignored, so send the frame the
|
|
339
|
+
// device itself uses. The command builder leaves lanParams off, so this
|
|
340
|
+
// one skips LAN - which the device ignores - and goes over AWS
|
|
341
|
+
await this.platform.sendDeviceUpdate(this.accessory, {
|
|
342
|
+
cmd: 'zoneState',
|
|
343
|
+
zone,
|
|
344
|
+
value: newValue === 1,
|
|
345
|
+
})
|
|
346
|
+
} else {
|
|
347
|
+
// These toggles are only controllable via the OpenAPI (cloud) connection
|
|
348
|
+
await this.platform.sendDeviceUpdate(this.accessory, {
|
|
349
|
+
cmd: 'openApi',
|
|
350
|
+
openApi: {
|
|
351
|
+
instance,
|
|
352
|
+
capabilityType: 'devices.capabilities.toggle',
|
|
353
|
+
value: newValue,
|
|
354
|
+
},
|
|
355
|
+
})
|
|
356
|
+
}
|
|
296
357
|
|
|
297
358
|
// Cache the new state and log
|
|
298
359
|
this.zoneCache[instance] = newValue
|
|
@@ -663,11 +724,13 @@ export default class extends GoveeDevice {
|
|
|
663
724
|
}
|
|
664
725
|
}
|
|
665
726
|
|
|
666
|
-
// Update any independently-controllable light zones (e.g. main/background)
|
|
667
|
-
|
|
727
|
+
// Update any independently-controllable light zones (e.g. main/background),
|
|
728
|
+
// read from the device's own frame where the toggles report nothing
|
|
729
|
+
const zoneToggles = this.hasRawZones ? readZoneFrame(params.commands) : params.toggles
|
|
730
|
+
if (zoneToggles) {
|
|
668
731
|
Object.entries(this.zoneServices).forEach(([instance, service]) => {
|
|
669
|
-
if (hasProperty(
|
|
670
|
-
const newValue =
|
|
732
|
+
if (hasProperty(zoneToggles, instance)) {
|
|
733
|
+
const newValue = zoneToggles[instance] ? 1 : 0
|
|
671
734
|
if (this.zoneCache[instance] !== newValue) {
|
|
672
735
|
this.zoneCache[instance] = newValue
|
|
673
736
|
service.updateCharacteristic(this.hapChar.On, newValue === 1)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CMD } from './ble-protocol.js'
|
|
2
2
|
import { k2rgb } from './colour.js'
|
|
3
3
|
import { getDeviceCapabilities } from './device-capabilities.js'
|
|
4
|
-
import { base64ToHex } from './functions.js'
|
|
4
|
+
import { base64ToHex, generateCodeFromHexValues } from './functions.js'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Builds connection-specific command params from a high-level device command.
|
|
@@ -26,6 +26,8 @@ export function buildCommand(params, context) {
|
|
|
26
26
|
return buildPtRealCommand(params)
|
|
27
27
|
case 'openApi':
|
|
28
28
|
return buildOpenApiCommand(params)
|
|
29
|
+
case 'zoneState':
|
|
30
|
+
return buildZoneStateCommand(params)
|
|
29
31
|
case 'brightness':
|
|
30
32
|
return buildBrightnessCommand(params, context)
|
|
31
33
|
case 'color':
|
|
@@ -117,6 +119,32 @@ function buildOpenApiCommand(params) {
|
|
|
117
119
|
}
|
|
118
120
|
}
|
|
119
121
|
|
|
122
|
+
// A two-zone light reports its zones as `aa 30 <main> <background>` and takes
|
|
123
|
+
// `33 30 <zone> <state>` to set one of them, where zone 00 is the main panel
|
|
124
|
+
// and 01 the background ring (#1333)
|
|
125
|
+
const ZONE_BYTE = { main: 0x00, background: 0x01 }
|
|
126
|
+
|
|
127
|
+
function buildZoneStateCommand(params) {
|
|
128
|
+
const zone = ZONE_BYTE[params.zone]
|
|
129
|
+
if (zone === undefined) {
|
|
130
|
+
// Falling back to the main panel would switch the wrong light, which is far
|
|
131
|
+
// harder to notice than an outright failure
|
|
132
|
+
throw new Error(`Unknown light zone [${params.zone}] for ${params.cmd}`)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Deliberately no lanParams. This model ignores `ptReal` over LAN, and LAN is
|
|
136
|
+
// fire-and-forget UDP, so sending it there would resolve and count as done -
|
|
137
|
+
// the command would never be retried over AWS. Leaving lanParams off lets the
|
|
138
|
+
// existing guard skip LAN for this one command, and every other command on
|
|
139
|
+
// the device carries on taking the faster local path.
|
|
140
|
+
return {
|
|
141
|
+
awsParams: {
|
|
142
|
+
cmd: 'ptReal',
|
|
143
|
+
data: { command: [generateCodeFromHexValues([0x33, 0x30, zone, params.value ? 0x01 : 0x00])] },
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
120
148
|
function buildBrightnessCommand(params, context) {
|
|
121
149
|
const caps = getDeviceCapabilities(context.gvModel, context.firmware)
|
|
122
150
|
return {
|
package/lib/utils/constants.js
CHANGED
|
@@ -714,6 +714,20 @@ export default {
|
|
|
714
714
|
],
|
|
715
715
|
},
|
|
716
716
|
|
|
717
|
+
// Two-zone lights whose zones Govee lists but never routes. Not a device type
|
|
718
|
+
// of its own - these stay in `models.rgb` like any other light, and only the
|
|
719
|
+
// way their zones are driven differs.
|
|
720
|
+
//
|
|
721
|
+
// Govee advertises `mainLightToggle` and `backgroundLightToggle` for these,
|
|
722
|
+
// and its cloud answers `{"status":"success"}` to both, but the command never
|
|
723
|
+
// reaches the device. Nothing reports a failure, so the plugin cannot tell -
|
|
724
|
+
// the tiles simply do nothing. The models here take the raw frame the device
|
|
725
|
+
// itself reports its zones with instead (#1333).
|
|
726
|
+
//
|
|
727
|
+
// The H1270 is deliberately absent. It advertises the same capabilities, but
|
|
728
|
+
// nobody has confirmed it shares the fault, so it stays on the toggle path.
|
|
729
|
+
zoneLightModels: ['H1250'],
|
|
730
|
+
|
|
717
731
|
matterModels: [
|
|
718
732
|
'H1401',
|
|
719
733
|
'H14C0',
|
package/lib/utils/lang-en.js
CHANGED
|
@@ -113,6 +113,7 @@ export default {
|
|
|
113
113
|
noExtFunc: 'no externalUpdate function to process incoming update',
|
|
114
114
|
notAWSConn: 'not connected to AWS',
|
|
115
115
|
notAWSSent: 'not using AWS connection as',
|
|
116
|
+
zoneNeedsAws: 'this light has separately switchable zones, but the zone tiles need your Govee account username and password in the plugin settings - an API key alone is not enough for them',
|
|
116
117
|
notBLESent: 'not using BLE connection as',
|
|
117
118
|
notLANSent: 'not using LAN connection as',
|
|
118
119
|
noOpenssl: 'See https://github.com/homebridge-plugins/homebridge-govee/wiki/OpenSSL for more info about installing OpenSSL',
|
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.
|
|
5
|
+
"version": "11.34.2-beta.0",
|
|
6
6
|
"description": "Homebridge plugin to integrate Govee devices into HomeKit.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "bwp91",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"@stoprocent/noble": "^2.7.1"
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
|
-
"@antfu/eslint-config": "^9.
|
|
79
|
+
"@antfu/eslint-config": "^9.3.0",
|
|
80
80
|
"@vitest/coverage-v8": "^4.1.10",
|
|
81
81
|
"vitest": "^4.1.10"
|
|
82
82
|
},
|