@homebridge-plugins/homebridge-govee 11.36.1-beta.13 → 11.36.1-beta.15
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 +4 -0
- package/lib/device/fan-H1310.js +68 -7
- package/lib/device/fan-H1370.js +43 -10
- package/lib/platform.js +6 -1
- package/lib/utils/lang-en.js +1 -0
- package/lib/utils/response-parser.js +18 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -22,6 +22,10 @@ All notable changes to `@homebridge-plugins/homebridge-govee` will be documented
|
|
|
22
22
|
- fix: stop calling a ceiling fan's speed report unrecognised while the fan is off (#1352)
|
|
23
23
|
- fix: read a ceiling fan's airflow direction from what the fan reports, so the tile follows the govee app (#1352)
|
|
24
24
|
- fix: show each of a ceiling fan's two lights as on or off from what the fan reports (#1352)
|
|
25
|
+
- fix: lead a ceiling fan's tile with the fan rather than one of its lights (#1352)
|
|
26
|
+
- fix: read a ceiling fan's light from what it says about its lights, not from whether the fan has power (#1352)
|
|
27
|
+
- fix: stop calling a ceiling fan's light status unrecognised on a handler that reads it (#1352)
|
|
28
|
+
- fix: stop reporting govee's command receipts as messages the plugin did not understand (#1352)
|
|
25
29
|
|
|
26
30
|
## v11.36.0 (2026-08-11)
|
|
27
31
|
|
package/lib/device/fan-H1310.js
CHANGED
|
@@ -30,9 +30,29 @@ const LIGHTS = [
|
|
|
30
30
|
/** The status frame carrying that mask: `aa 42 <mask>`. */
|
|
31
31
|
const LIGHT_MASK_CODE = '42'
|
|
32
32
|
|
|
33
|
+
/** Set in that mask whenever either light is lit, whichever one it is. */
|
|
34
|
+
const ANY_LIGHT_BIT = 0x80
|
|
35
|
+
|
|
33
36
|
/** The api switch that runs the fan backwards. */
|
|
34
37
|
const REVERSE_AIRFLOW = 'reverseAirflowToggle'
|
|
35
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Reads the `aa 42 <mask>` light mask out of a status, if it is in there.
|
|
41
|
+
*
|
|
42
|
+
* @param {string[]} commands base64 status codes from the fan
|
|
43
|
+
* @returns {number|undefined} the mask, or undefined if the fan did not send one
|
|
44
|
+
*/
|
|
45
|
+
function lightMaskFrom(commands) {
|
|
46
|
+
let mask
|
|
47
|
+
;(commands || []).forEach((command) => {
|
|
48
|
+
const hexParts = hexToTwoItems(base64ToHex(command))
|
|
49
|
+
if (getTwoItemPosition(hexParts, 1) === 'aa' && getTwoItemPosition(hexParts, 2) === LIGHT_MASK_CODE) {
|
|
50
|
+
mask = Number.parseInt(getTwoItemPosition(hexParts, 3), 16)
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
return mask
|
|
54
|
+
}
|
|
55
|
+
|
|
36
56
|
export default class GoveeFanH1310 extends deviceFanH1370 {
|
|
37
57
|
constructor(platform, accessory) {
|
|
38
58
|
super(platform, accessory)
|
|
@@ -41,6 +61,10 @@ export default class GoveeFanH1310 extends deviceFanH1370 {
|
|
|
41
61
|
// ceiling-fan range. (speedSteps now comes from device-capabilities or the
|
|
42
62
|
// device's OpenAPI fanSpeedMode; do not overwrite the superclass value here.)
|
|
43
63
|
|
|
64
|
+
// This handler reads the light mask itself, so the parent should stop
|
|
65
|
+
// announcing it as a status nobody understood
|
|
66
|
+
this.handledStatusCodes.add(LIGHT_MASK_CODE)
|
|
67
|
+
|
|
44
68
|
if (this.service.testCharacteristic(this.hapChar.SwingMode)) {
|
|
45
69
|
this.service.removeCharacteristic(this.service.getCharacteristic(this.hapChar.SwingMode))
|
|
46
70
|
}
|
|
@@ -127,16 +151,32 @@ export default class GoveeFanH1310 extends deviceFanH1370 {
|
|
|
127
151
|
}
|
|
128
152
|
}
|
|
129
153
|
|
|
154
|
+
/**
|
|
155
|
+
* The device-wide on/off field is the whole unit having power here, not the
|
|
156
|
+
* light: a fan reporting `onOff` 1 has been seen with `aa 36 00` (fan off)
|
|
157
|
+
* and `aa 42 00` (both lights off) in the same status. Letting it speak for
|
|
158
|
+
* the light lit the light tile every time the fan was switched on (#1352).
|
|
159
|
+
*
|
|
160
|
+
* The `aa 42` mask is what this fan actually says about its lights, so read
|
|
161
|
+
* the single tile off the bit that is set whenever either one is lit.
|
|
162
|
+
*
|
|
163
|
+
* @param {object} params a parsed device update
|
|
164
|
+
* @returns {'on'|'off'|undefined} the light's state, or undefined if unsaid
|
|
165
|
+
*/
|
|
166
|
+
readLightState(params) {
|
|
167
|
+
const mask = lightMaskFrom(params.commands)
|
|
168
|
+
if (mask === undefined) {
|
|
169
|
+
return undefined
|
|
170
|
+
}
|
|
171
|
+
return (mask & ANY_LIGHT_BIT) === ANY_LIGHT_BIT ? 'on' : 'off'
|
|
172
|
+
}
|
|
173
|
+
|
|
130
174
|
externalUpdate(params) {
|
|
131
175
|
super.externalUpdate(params)
|
|
132
176
|
|
|
133
177
|
// Each light's own on/off, read off the same mask
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (getTwoItemPosition(hexParts, 1) !== 'aa' || getTwoItemPosition(hexParts, 2) !== LIGHT_MASK_CODE) {
|
|
137
|
-
return
|
|
138
|
-
}
|
|
139
|
-
const mask = Number.parseInt(getTwoItemPosition(hexParts, 3), 16)
|
|
178
|
+
const mask = lightMaskFrom(params.commands)
|
|
179
|
+
if (mask !== undefined) {
|
|
140
180
|
LIGHTS.forEach(({ name, bit }) => {
|
|
141
181
|
const service = this.lightServices?.[name]
|
|
142
182
|
if (!service) {
|
|
@@ -150,7 +190,28 @@ export default class GoveeFanH1310 extends deviceFanH1370 {
|
|
|
150
190
|
this.accessory.log(`${name} ${newValue}`)
|
|
151
191
|
}
|
|
152
192
|
})
|
|
153
|
-
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// The fan has one brightness and one colour shared between both lights, so
|
|
196
|
+
// a reading of either belongs on both tiles. The parent puts these on its
|
|
197
|
+
// single light, which the named tiles replaced
|
|
198
|
+
if (this.lightServices) {
|
|
199
|
+
LIGHTS.forEach(({ name }) => {
|
|
200
|
+
const service = this.lightServices[name]
|
|
201
|
+
if (params.brightness && this[`${name}:brightness`] !== params.brightness) {
|
|
202
|
+
this[`${name}:brightness`] = params.brightness
|
|
203
|
+
service.updateCharacteristic(this.hapChar.Brightness, params.brightness)
|
|
204
|
+
}
|
|
205
|
+
if (params.kelvin) {
|
|
206
|
+
const k = Math.min(Math.max(params.kelvin, 2700), 6500)
|
|
207
|
+
const mired = Math.min(Math.max(Math.round(1000000 / k), 154), 370)
|
|
208
|
+
if (this[`${name}:ct`] !== mired) {
|
|
209
|
+
this[`${name}:ct`] = mired
|
|
210
|
+
service.updateCharacteristic(this.hapChar.ColorTemperature, mired)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
})
|
|
214
|
+
}
|
|
154
215
|
|
|
155
216
|
// Airflow direction is a separate capability from the lights, so it gets
|
|
156
217
|
// its own guard rather than sharing theirs
|
package/lib/device/fan-H1370.js
CHANGED
|
@@ -17,7 +17,7 @@ import GoveeDevice from './base.js'
|
|
|
17
17
|
/*
|
|
18
18
|
Codes below were decoded from a real device's AWS status reports in
|
|
19
19
|
https://github.com/homebridge-plugins/homebridge-govee/issues/1307
|
|
20
|
-
Status aa 31 [
|
|
20
|
+
Status aa 31 [?] [speed] [direction] = fan speed and airflow direction
|
|
21
21
|
Status aa 36 01 01 / aa 36 00 00 = fan power on/off
|
|
22
22
|
The light reports through the standard state/brightness/kelvin fields
|
|
23
23
|
(colour temperature only, 2700-6500K)
|
|
@@ -42,9 +42,20 @@ export default class GoveeFanH1370 extends GoveeDevice {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
// Status codes a subclass reads for itself. Without this the parent still
|
|
46
|
+
// reaches its default branch and announces a code that was in fact handled,
|
|
47
|
+
// which is the same unrecognised-status noise this model was reported for
|
|
48
|
+
this.handledStatusCodes = new Set()
|
|
49
|
+
|
|
45
50
|
// Add the fan service if it doesn't already exist
|
|
46
51
|
this.service = this.accessory.getService(this.hapServ.Fanv2) || this.accessory.addService(this.hapServ.Fanv2)
|
|
47
52
|
|
|
53
|
+
// This accessory is a fan that happens to have lights on it, so the fan is
|
|
54
|
+
// what its tile should lead with. Without saying so the Home app picks for
|
|
55
|
+
// itself and lands on a light, leaving an owner tapping through to reach
|
|
56
|
+
// the fan they came for (#1352)
|
|
57
|
+
this.service.setPrimaryService(true)
|
|
58
|
+
|
|
48
59
|
// Add the set handler to the fan on/off characteristic
|
|
49
60
|
this.service
|
|
50
61
|
.getCharacteristic(this.hapChar.Active)
|
|
@@ -281,11 +292,29 @@ export default class GoveeFanH1370 extends GoveeDevice {
|
|
|
281
292
|
}
|
|
282
293
|
}
|
|
283
294
|
|
|
295
|
+
/**
|
|
296
|
+
* Whether the light is lit, as this model reports it.
|
|
297
|
+
*
|
|
298
|
+
* On the H1370 the device-wide on/off field is the only word there is on the
|
|
299
|
+
* light, so it is taken at face value. A model that says so separately should
|
|
300
|
+
* override this rather than let that field speak for the light - see the
|
|
301
|
+
* H1310, where it stays on while both lights are off.
|
|
302
|
+
*
|
|
303
|
+
* @param {object} params a parsed device update
|
|
304
|
+
* @returns {'on'|'off'|undefined} the light's state, or undefined if unsaid
|
|
305
|
+
*/
|
|
306
|
+
readLightState(params) {
|
|
307
|
+
return params.state
|
|
308
|
+
}
|
|
309
|
+
|
|
284
310
|
externalUpdate(params) {
|
|
285
|
-
// The standard state/brightness/kelvin fields report the light
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
311
|
+
// The standard state/brightness/kelvin fields report the light. A subclass
|
|
312
|
+
// that replaces the single light with named ones leaves lightService unset,
|
|
313
|
+
// and updating it would throw on every status the fan sends
|
|
314
|
+
if (!this.hideLight && this.lightService) {
|
|
315
|
+
const lightState = this.readLightState(params)
|
|
316
|
+
if (lightState && lightState !== this.cacheLightState) {
|
|
317
|
+
this.cacheLightState = lightState
|
|
289
318
|
this.lightService.updateCharacteristic(this.hapChar.On, this.cacheLightState === 'on')
|
|
290
319
|
this.accessory.log(`${platformLang.curLight} [${this.cacheLightState}]`)
|
|
291
320
|
}
|
|
@@ -319,11 +348,12 @@ export default class GoveeFanH1370 extends GoveeDevice {
|
|
|
319
348
|
const deviceFunction = `${getTwoItemPosition(hexParts, 2)}${getTwoItemPosition(hexParts, 3)}`
|
|
320
349
|
|
|
321
350
|
switch (deviceFunction) {
|
|
322
|
-
// aa 31 <
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
// the noise that sent this model's owner
|
|
351
|
+
// aa 31 <?> <speed> <direction>. The third byte is not part of the
|
|
352
|
+
// code: an H1310 has been seen reporting both 00 and 01 for it with the
|
|
353
|
+
// fan running either way, so what it means is still unknown - but
|
|
354
|
+
// matching only 3101 announced every other status as unrecognised,
|
|
355
|
+
// which is the noise that sent this model's owner here (#1352).
|
|
356
|
+
// `aa 36` is the reliable word on whether the fan is running.
|
|
327
357
|
case '3100':
|
|
328
358
|
case '3101': {
|
|
329
359
|
// Fan speed
|
|
@@ -355,6 +385,9 @@ export default class GoveeFanH1370 extends GoveeDevice {
|
|
|
355
385
|
break
|
|
356
386
|
}
|
|
357
387
|
default:
|
|
388
|
+
if (this.handledStatusCodes.has(getTwoItemPosition(hexParts, 2))) {
|
|
389
|
+
break
|
|
390
|
+
}
|
|
358
391
|
logUnknownData(this.accessory, {
|
|
359
392
|
kind: 'status',
|
|
360
393
|
source: params.source,
|
package/lib/platform.js
CHANGED
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
} from './utils/functions.js'
|
|
31
31
|
import platformLang from './utils/lang-en.js'
|
|
32
32
|
import { isFirstReport, logUnknownData } from './utils/report-unknown.js'
|
|
33
|
-
import { parseDeviceUpdate } from './utils/response-parser.js'
|
|
33
|
+
import { isCommandAcknowledgement, parseDeviceUpdate } from './utils/response-parser.js'
|
|
34
34
|
|
|
35
35
|
const require = createRequire(import.meta.url)
|
|
36
36
|
const plugin = require('../package.json')
|
|
@@ -1422,6 +1422,11 @@ export default class {
|
|
|
1422
1422
|
} catch (err) {
|
|
1423
1423
|
this.log.warn('[%s] %s %s.', accessory.displayName, platformLang.devNotUpdated, parseError(err))
|
|
1424
1424
|
}
|
|
1425
|
+
} else if (isCommandAcknowledgement(params)) {
|
|
1426
|
+
// Govee answers a command with a receipt carrying nothing but whether it
|
|
1427
|
+
// worked. There is no status in it to miss, so reporting it as a payload
|
|
1428
|
+
// nobody understood buries the ones that are worth reading (#1352)
|
|
1429
|
+
accessory.logDebug(`[${params.source}] ${platformLang.cmdAcknowledged} [${params.cmd}]`)
|
|
1425
1430
|
} else {
|
|
1426
1431
|
// Nothing in the payload was understood. This is the most useful thing a
|
|
1427
1432
|
// user can send in when asking for a device or a function to be added, so
|
package/lib/utils/lang-en.js
CHANGED
|
@@ -47,6 +47,24 @@ function resolveOpenApiTempUnit(value, context) {
|
|
|
47
47
|
return null
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Whether a payload is only govee's receipt for a command we sent.
|
|
52
|
+
*
|
|
53
|
+
* These carry a command name and a `result` and nothing else - no status at
|
|
54
|
+
* all - so there is nothing in them to have failed to understand. Anything
|
|
55
|
+
* that names even one other field is a real payload and is not this.
|
|
56
|
+
*
|
|
57
|
+
* @param {object} params - Raw payload with source, cmd, state, etc.
|
|
58
|
+
* @returns {boolean} true if the payload is a bare acknowledgement
|
|
59
|
+
*/
|
|
60
|
+
export function isCommandAcknowledgement(params) {
|
|
61
|
+
if (!params?.cmd || !params.state || typeof params.state !== 'object') {
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
const keys = Object.keys(params.state)
|
|
65
|
+
return keys.length > 0 && keys.every(key => key === 'result')
|
|
66
|
+
}
|
|
67
|
+
|
|
50
68
|
/**
|
|
51
69
|
* Parses a raw device update payload into a standardised data object.
|
|
52
70
|
* Handles different payload structures from AWS, LAN, OpenAPI, and legacy devices.
|
package/package.json
CHANGED