@homebridge-plugins/homebridge-govee 11.37.1 → 11.38.0-beta.1
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 +8 -0
- package/lib/device/fan-H1370.js +19 -10
- package/lib/device/{fan-H1310.js → fan-ceiling.js} +88 -7
- package/lib/device/index.js +7 -5
- package/lib/device/kettle.js +41 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@homebridge-plugins/homebridge-govee` will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## v11.38.0 (Pending Release)
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- feat: show which slot the kettle target temperature comes from, settled by two owner runs
|
|
10
|
+
- fix(h1370): send fan power to the fan instead of the lights, and give it the two light tiles
|
|
11
|
+
- feat(h1370): add the oscillation control, read from the fan's own speed frame
|
|
12
|
+
|
|
5
13
|
## v11.37.1 (2026-08-21)
|
|
6
14
|
|
|
7
15
|
### Changed
|
package/lib/device/fan-H1370.js
CHANGED
|
@@ -15,15 +15,22 @@ import { logUnknownData } from '../utils/report-unknown.js'
|
|
|
15
15
|
import GoveeDevice from './base.js'
|
|
16
16
|
|
|
17
17
|
/*
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
18
|
+
⚠️ This is the BASE for the ceiling fans, not a handler any model is mapped
|
|
19
|
+
to. Every one of them - H1310, R1310 and H1370 - runs `fan-ceiling.js`, the
|
|
20
|
+
subclass, which reads the two lights and the fan correctly.
|
|
21
|
+
|
|
22
|
+
What is left here is the single-light fallback that subclass drops back to
|
|
23
|
+
when a device cannot reach its per-light api switches. Read it as "the least
|
|
24
|
+
this hardware can do", not as a description of the hardware.
|
|
25
|
+
|
|
26
|
+
⚠️ The comment that used to sit here said `aa 36 01 01` was fan power on the
|
|
27
|
+
H1370. It is not: it is the two lights, on every model. Labelled captures on
|
|
28
|
+
#1358 settled it - switching the fan on left `aa 36` at `00 00` and moved
|
|
29
|
+
`aa 31` byte three instead. `internalStateUpdate` below still sends the light
|
|
30
|
+
frame, which is exactly why no model is mapped straight to this class.
|
|
31
|
+
|
|
32
|
+
Speeds differ - six on the H1310, twelve on the H1370 - so the scale comes
|
|
33
|
+
from the capabilities table (#1352)
|
|
27
34
|
*/
|
|
28
35
|
export default class GoveeFanH1370 extends GoveeDevice {
|
|
29
36
|
constructor(platform, accessory) {
|
|
@@ -142,7 +149,9 @@ export default class GoveeFanH1370 extends GoveeDevice {
|
|
|
142
149
|
return
|
|
143
150
|
}
|
|
144
151
|
|
|
145
|
-
//
|
|
152
|
+
// ⚠️ `aa 36` is the two LIGHTS, not the fan (#1358). Kept only because a
|
|
153
|
+
// device without its api switches has nothing else to send; the subclass
|
|
154
|
+
// every model uses overrides this with the fan's own toggle
|
|
146
155
|
const hexValues = value ? [0x33, 0x36, 0x01, 0x01] : [0x33, 0x36, 0x00, 0x00]
|
|
147
156
|
|
|
148
157
|
// Send the request to the platform sender function
|
|
@@ -8,14 +8,23 @@ import {
|
|
|
8
8
|
sleep,
|
|
9
9
|
} from '../utils/functions.js'
|
|
10
10
|
import platformLang from '../utils/lang-en.js'
|
|
11
|
-
import
|
|
11
|
+
import deviceFanCeilingBase from './fan-H1370.js'
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* The two lights
|
|
15
|
-
*
|
|
14
|
+
* The ceiling fans with two lights: the H1310, its newer R1310 naming, and the
|
|
15
|
+
* H1370.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
* `
|
|
17
|
+
* ⚠️ The H1370 was on the base handler until captures posted on #1358 settled
|
|
18
|
+
* that it speaks exactly the same language as the H1310 - `aa 36` is the two
|
|
19
|
+
* lights and `aa 31` byte three is the fan, on both. The base handler had it
|
|
20
|
+
* the other way round, so switching the fan on in HomeKit sent `33 36 01 01`
|
|
21
|
+
* and turned both lights on without ever reaching the fan.
|
|
22
|
+
*
|
|
23
|
+
* The H1370 additionally oscillates, which the H1310 does not.
|
|
24
|
+
*
|
|
25
|
+
* The two lights, the api switch each one answers to, and the bit each one
|
|
26
|
+
* sets in the `aa 42` status. Those switches are the only way to reach one
|
|
27
|
+
* light without the other - `powerSwitch` is the whole unit, fan included.
|
|
19
28
|
*/
|
|
20
29
|
/**
|
|
21
30
|
* ⚠️ The fan has ONE brightness and ONE colour, shared by both lights - the
|
|
@@ -67,6 +76,9 @@ const FAN_CODE = '31'
|
|
|
67
76
|
const FAN_TOGGLE = 'fanToggle'
|
|
68
77
|
const REVERSE_AIRFLOW = 'reverseAirflowToggle'
|
|
69
78
|
|
|
79
|
+
/** Oscillation, which the H1370 has and the H1310 does not. */
|
|
80
|
+
const OSCILLATE = 'fanOscillateToggle'
|
|
81
|
+
|
|
70
82
|
/**
|
|
71
83
|
* Reads which lights are lit out of a status, from whichever of the two frames
|
|
72
84
|
* the fan sent, normalised to the `aa 42` mask.
|
|
@@ -111,7 +123,7 @@ function fanIsRunningFrom(commands) {
|
|
|
111
123
|
return running
|
|
112
124
|
}
|
|
113
125
|
|
|
114
|
-
export default class
|
|
126
|
+
export default class GoveeFanCeiling extends deviceFanCeilingBase {
|
|
115
127
|
constructor(platform, accessory) {
|
|
116
128
|
super(platform, accessory)
|
|
117
129
|
|
|
@@ -131,7 +143,19 @@ export default class GoveeFanH1310 extends deviceFanH1370 {
|
|
|
131
143
|
this.canSendFanToggle = !!accessory.context.openApiCapabilities?.[FAN_TOGGLE]
|
|
132
144
|
&& !!accessory.context.useOpenApiControl
|
|
133
145
|
|
|
134
|
-
|
|
146
|
+
// Oscillation. The H1310 cannot oscillate and the H1370 can, so this is
|
|
147
|
+
// gated on the capability rather than on the model - and anything left
|
|
148
|
+
// over on a fan that turns out not to have it is removed, which is what
|
|
149
|
+
// this handler already had to do for the tower fan's control (#1352)
|
|
150
|
+
this.canOscillate = !!accessory.context.openApiCapabilities?.[OSCILLATE]
|
|
151
|
+
&& !!accessory.context.useOpenApiControl
|
|
152
|
+
|
|
153
|
+
if (this.canOscillate) {
|
|
154
|
+
this.service
|
|
155
|
+
.getCharacteristic(this.hapChar.SwingMode)
|
|
156
|
+
.onSet(async value => this.internalSwingUpdate(value))
|
|
157
|
+
this.cacheSwing = this.service.getCharacteristic(this.hapChar.SwingMode).value
|
|
158
|
+
} else if (this.service.testCharacteristic(this.hapChar.SwingMode)) {
|
|
135
159
|
this.service.removeCharacteristic(this.service.getCharacteristic(this.hapChar.SwingMode))
|
|
136
160
|
}
|
|
137
161
|
|
|
@@ -263,6 +287,39 @@ export default class GoveeFanH1310 extends deviceFanH1370 {
|
|
|
263
287
|
}
|
|
264
288
|
}
|
|
265
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Oscillation on and off.
|
|
292
|
+
*
|
|
293
|
+
* The fan reports it in the eighth byte of its speed frame, but there is no
|
|
294
|
+
* frame known to SEND it, so this is api-only - which is why the control is
|
|
295
|
+
* gated on the capability being reachable rather than merely present.
|
|
296
|
+
*
|
|
297
|
+
* @param {number} value 1 swinging, 0 still
|
|
298
|
+
*/
|
|
299
|
+
async internalSwingUpdate(value) {
|
|
300
|
+
try {
|
|
301
|
+
if (this.cacheSwing === value) {
|
|
302
|
+
return
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
await this.platform.sendDeviceUpdate(this.accessory, {
|
|
306
|
+
cmd: 'openApi',
|
|
307
|
+
openApi: {
|
|
308
|
+
instance: OSCILLATE,
|
|
309
|
+
capabilityType: 'devices.capabilities.toggle',
|
|
310
|
+
value: value === 1 ? 1 : 0,
|
|
311
|
+
},
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
this.cacheSwing = value
|
|
315
|
+
this.accessory.log(`current oscillation [${value === 1 ? 'on' : 'off'}]`)
|
|
316
|
+
} catch (err) {
|
|
317
|
+
this.failUpdate(err, () => {
|
|
318
|
+
this.service.updateCharacteristic(this.hapChar.SwingMode, this.cacheSwing)
|
|
319
|
+
})
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
266
323
|
/**
|
|
267
324
|
* The device-wide on/off field is the whole unit having power here, not the
|
|
268
325
|
* light: a fan reporting `onOff` 1 has been seen with `aa 36 00` (fan off)
|
|
@@ -342,6 +399,30 @@ export default class GoveeFanH1310 extends deviceFanH1370 {
|
|
|
342
399
|
})
|
|
343
400
|
}
|
|
344
401
|
|
|
402
|
+
// Oscillation, read off the eighth byte of the same speed frame. Proven
|
|
403
|
+
// against ekalp's labelled captures on #1358, where switching it off, on
|
|
404
|
+
// and off again was the only thing that byte did - `aa 31 01 04 00 00 00
|
|
405
|
+
// 01` swinging and `... 00` still, with every other byte unchanged
|
|
406
|
+
if (this.canOscillate) {
|
|
407
|
+
let swinging
|
|
408
|
+
;(params.commands || []).forEach((command) => {
|
|
409
|
+
const hexParts = hexToTwoItems(base64ToHex(command))
|
|
410
|
+
if (getTwoItemPosition(hexParts, 1) === 'aa' && getTwoItemPosition(hexParts, 2) === FAN_CODE) {
|
|
411
|
+
swinging = getTwoItemPosition(hexParts, 8) === '01' ? 1 : 0
|
|
412
|
+
}
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
if (swinging === undefined && params.toggles?.[OSCILLATE] !== undefined) {
|
|
416
|
+
swinging = params.toggles[OSCILLATE] ? 1 : 0
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (swinging !== undefined && this.cacheSwing !== swinging) {
|
|
420
|
+
this.cacheSwing = swinging
|
|
421
|
+
this.service.updateCharacteristic(this.hapChar.SwingMode, swinging)
|
|
422
|
+
this.accessory.log(`current oscillation [${swinging === 1 ? 'on' : 'off'}]`)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
345
426
|
// Airflow direction is a separate capability from the lights, so it gets
|
|
346
427
|
// its own guard rather than sharing theirs
|
|
347
428
|
if (!this.canReverseAirflow) {
|
package/lib/device/index.js
CHANGED
|
@@ -10,8 +10,7 @@ import {
|
|
|
10
10
|
import deviceCoolerSingle from './cooler-single.js'
|
|
11
11
|
import deviceDehumidifierH7150 from './dehumidifier-H7150.js'
|
|
12
12
|
import deviceDiffuserH7161 from './diffuser-H7161.js'
|
|
13
|
-
import
|
|
14
|
-
import deviceFanH1370 from './fan-H1370.js'
|
|
13
|
+
import deviceFanCeiling from './fan-ceiling.js'
|
|
15
14
|
import deviceFanH7100 from './fan-H7100.js'
|
|
16
15
|
import deviceFanH7105 from './fan-H7105.js'
|
|
17
16
|
import deviceFanH7107 from './fan-H7107.js'
|
|
@@ -61,9 +60,12 @@ export default {
|
|
|
61
60
|
deviceDehumidifierBasic,
|
|
62
61
|
deviceDiffuserBasic,
|
|
63
62
|
deviceFanBasic,
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
// The three ceiling fans all speak the same language - two lights on `aa 36`
|
|
64
|
+
// and the fan on `aa 31` - settled by the captures on #1358. The H1370 was
|
|
65
|
+
// on the base handler, which had those the other way round
|
|
66
|
+
deviceFanH1310: deviceFanCeiling,
|
|
67
|
+
deviceFanH1370: deviceFanCeiling,
|
|
68
|
+
deviceFanR1310: deviceFanCeiling, // R1310 is the newer naming of the H1310
|
|
67
69
|
deviceFanH7100,
|
|
68
70
|
deviceFanH7102: deviceFanH7100, // identical to the H7100
|
|
69
71
|
deviceFanH7106: deviceFanH7100, // identical to the H7100
|
package/lib/device/kettle.js
CHANGED
|
@@ -503,14 +503,53 @@ export default class extends GoveeDevice {
|
|
|
503
503
|
}
|
|
504
504
|
|
|
505
505
|
switch (deviceFunction) {
|
|
506
|
-
case '0500': { //
|
|
506
|
+
case '0500': { // which slot the target temperature comes from
|
|
507
|
+
// Settled by two owner runs in #1337, both with the kettle at 212°F
|
|
508
|
+
// so that only the control used differed:
|
|
509
|
+
//
|
|
510
|
+
// aa 05 00 02 after tapping the first memory preset in the app
|
|
511
|
+
// aa 05 00 01 after setting 212°F by hand with the slider
|
|
512
|
+
//
|
|
513
|
+
// So the byte points at the temperature slot the target comes from:
|
|
514
|
+
// 01 is the custom target slot, 02-05 are the memory presets - the
|
|
515
|
+
// same numbering as the aa 05 <slot> frames above. And it tracks
|
|
516
|
+
// which CONTROL was used, not the number: 212°F set by slider still
|
|
517
|
+
// reads slot 1, even though the first preset holds exactly 212°F.
|
|
518
|
+
//
|
|
519
|
+
// Two honest gaps. Slots 03-05 are inferred from the pattern - only
|
|
520
|
+
// 01 and 02 have been seen. And the switch-command examples below,
|
|
521
|
+
// which predate those runs, number the drink modes differently
|
|
522
|
+
// (green tea 02, oolong 03...); both cannot be right for this
|
|
523
|
+
// kettle, so they are kept only until a mode-switch run settles it:
|
|
524
|
+
//
|
|
507
525
|
// switch to green tea_: aa050002000000000000000000000000000000ad
|
|
508
526
|
// switch to oolong tea: aa050003000000000000000000000000000000ac
|
|
509
527
|
// switch to coffee____: aa050004000000000000000000000000000000ab
|
|
510
528
|
// switch to black tea_: aa050005000000000000000000000000000000aa
|
|
511
529
|
// switch to preset1___: aa050001010000000000000000000000000000af
|
|
512
530
|
// switch to preset2___: aa050001020000000000000000000000000000ac
|
|
513
|
-
|
|
531
|
+
const slot = Number.parseInt(getTwoItemPosition(hexParts, 4), 16)
|
|
532
|
+
if (slot < 1 || slot > 5) {
|
|
533
|
+
logUnknownData(this.accessory, {
|
|
534
|
+
kind: 'status',
|
|
535
|
+
source: 'AWS',
|
|
536
|
+
raw: hexString,
|
|
537
|
+
hex: hexParts.join(''),
|
|
538
|
+
extra: { note: 'aa 05 00 frame pointing at a slot the kettle does not have' },
|
|
539
|
+
})
|
|
540
|
+
break
|
|
541
|
+
}
|
|
542
|
+
const line = slot === 1
|
|
543
|
+
? 'target temperature set from [custom temperature]'
|
|
544
|
+
: `target temperature set from [memory preset ${slot - 1}]`
|
|
545
|
+
// The kettle repeats this in every startup burst - a change is worth
|
|
546
|
+
// a normal log line, a repeat is not
|
|
547
|
+
if (this.cacheTargetSlot === slot) {
|
|
548
|
+
this.accessory.logDebug(line)
|
|
549
|
+
} else {
|
|
550
|
+
this.cacheTargetSlot = slot
|
|
551
|
+
this.accessory.log(line)
|
|
552
|
+
}
|
|
514
553
|
break
|
|
515
554
|
}
|
|
516
555
|
case '1001': { // current temperature of the water
|
package/package.json
CHANGED