@homebridge-plugins/homebridge-govee 11.39.2-beta.8 → 11.40.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 +29 -2
- package/lib/connection/ble.js +60 -1
- package/lib/connection/http.js +8 -50
- package/lib/connection/openapi.js +23 -2
- package/lib/device/fan-H7107.js +54 -0
- package/lib/device/fan-ceiling.js +16 -10
- package/lib/device/humidifier-H7142.js +24 -14
- package/lib/device/light.js +201 -17
- package/lib/device/scene-tv.js +6 -5
- package/lib/device/valve-single.js +29 -19
- package/lib/platform.js +20 -45
- package/lib/utils/ble-protocol.js +7 -2
- package/lib/utils/command-builder.js +32 -8
- package/lib/utils/constants.js +30 -1
- package/lib/utils/device-context.js +1 -0
- package/lib/utils/dispatch.js +10 -2
- package/lib/utils/lang-en.js +2 -1
- package/lib/utils/response-parser.js +92 -0
- package/package.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@homebridge-plugins/homebridge-govee` will be documented in this file.
|
|
4
4
|
|
|
5
|
-
## v11.
|
|
5
|
+
## v11.40.0 (2026-11-14)
|
|
6
6
|
|
|
7
7
|
### Changed
|
|
8
8
|
|
|
@@ -20,7 +20,6 @@ All notable changes to `@homebridge-plugins/homebridge-govee` will be documented
|
|
|
20
20
|
- fix(leak): stop the h5059 and h5830 showing an old leak, the app reads their probes (#1381)
|
|
21
21
|
- fix(openapi): clear a leak when the sensor reports it is dry (#1381)
|
|
22
22
|
- fix(aws): start aws without waiting for the openapi live connection, it can retry for ever (#1383)
|
|
23
|
-
- chore(deps): dependency updates
|
|
24
23
|
- fix(lights): keep adaptive lighting on after a restart when it ran while the light was off
|
|
25
24
|
- fix(http): log in even when the tap-to-run server is down
|
|
26
25
|
- fix(aws): give each start its own aws connection id, so two instances on one account both work
|
|
@@ -29,6 +28,34 @@ All notable changes to `@homebridge-plugins/homebridge-govee` will be documented
|
|
|
29
28
|
- feat(purifiers): show the filter life left, when govee's api reports it
|
|
30
29
|
- fix(lights): keep the h6022 and h60a1 to the colour temperatures they can show
|
|
31
30
|
- feat(humidifiers): show when the water tank has run dry, when govee's api reports it (#1294)
|
|
31
|
+
- fix(h1310): stop the fan light throwing on every switch when there is no api key (#1384)
|
|
32
|
+
- fix(h713b): put the heater back on the handler its controls worked on (#1386)
|
|
33
|
+
- fix(aws): apply a light's aws brightness scale to 254 setting, which was ignored
|
|
34
|
+
- fix(ble): on a mac, point a bluetooth permission error at the privacy setting, not setcap
|
|
35
|
+
- fix(sockets): show a socket as a heater again, it failed with "not a constructor" (#1388)
|
|
36
|
+
- fix(valves): turn a valve off after its duration when it was on before a restart (#1387)
|
|
37
|
+
- fix(ble): connect to bluetooth-only lights on linux (@ldtnz)
|
|
38
|
+
- fix(http): fetch tap-to-runs with the account token (@StefanRiese)
|
|
39
|
+
- fix(ttr): keep the account topic out of the tap-to-run debug line (@StefanRiese)
|
|
40
|
+
- fix(ttr): read scene codes from govee's preset tap-to-runs (@StefanRiese)
|
|
41
|
+
- fix: read the h7142's humidity, which showed 0% (#1294)
|
|
42
|
+
- fix(openapi): keep a toggle's state when govee reports it as empty (#1395)
|
|
43
|
+
- feat: show the h7107's room temperature (#1394)
|
|
44
|
+
- fix: lead the h7107's tile with the fan
|
|
45
|
+
- fix(scenes): cap the scene selector at 97 scenes, so lights with more still show it (#1362)
|
|
46
|
+
- fix(h7107): ask the fan for its status every minute, so its temperature stays current (#1394)
|
|
47
|
+
- fix: treat the h600c and h6008 bulbs as matter models (#1399) (@R-STR)
|
|
48
|
+
- fix(lights): switch a light off when home sets it to 0% (#1397)
|
|
49
|
+
- feat(h60b2): control each head of the tree lamp as its own light (#1395) (@ethanjnrg)
|
|
50
|
+
- fix(lights): name each zone light in the home app (@ethanjnrg)
|
|
51
|
+
- fix(lights): follow a zone light's saturation, as the main light does (@ethanjnrg)
|
|
52
|
+
- fix(lights): send a zone light's white again after a colour (@ethanjnrg)
|
|
53
|
+
- fix(lights): ask for a zone light's state after a whole-light change (@ethanjnrg)
|
|
54
|
+
- feat(h60b2): set a head's white as kelvin, on its white leds (@ethanjnrg)
|
|
55
|
+
- fix(lights): show a zone light's colour in home when it changes elsewhere (@ethanjnrg)
|
|
56
|
+
- feat(lights): switch a sync box's hdmi input from home, when govee's api lists it (#1289)
|
|
57
|
+
- ci: publish releases with node.js v26, the current lts
|
|
58
|
+
- chore(deps): dependency updates
|
|
32
59
|
|
|
33
60
|
## v11.39.1 (2026-10-18)
|
|
34
61
|
|
package/lib/connection/ble.js
CHANGED
|
@@ -420,7 +420,7 @@ export default class BLEConnection {
|
|
|
420
420
|
return await Promise.race([
|
|
421
421
|
process.platform === 'darwin'
|
|
422
422
|
? this.connectByNameDarwin(address, timeout)
|
|
423
|
-
:
|
|
423
|
+
: this.connectAfterScan(address, timeout),
|
|
424
424
|
new Promise((_, reject) => {
|
|
425
425
|
timeoutId = setTimeout(() => reject(new Error('Connection timeout')), timeout)
|
|
426
426
|
}),
|
|
@@ -434,6 +434,65 @@ export default class BLEConnection {
|
|
|
434
434
|
// never resolve. Govee devices advertise a local name whose suffix is the last
|
|
435
435
|
// two bytes of their MAC (e.g. Govee_H617A_325A for ...:32:5A), so scan and
|
|
436
436
|
// match on that instead, then connect to the discovered peripheral directly.
|
|
437
|
+
// Linux: noble only sends LE Create Connection for a peripheral it has seen
|
|
438
|
+
// in a scan since the last reset, so connecting by address alone never
|
|
439
|
+
// reaches the controller. Wait for the adapter after the reset in
|
|
440
|
+
// updateDevice(), scan until the device advertises, then connect to it.
|
|
441
|
+
async connectAfterScan(address, timeout) {
|
|
442
|
+
const wanted = address.toLowerCase()
|
|
443
|
+
await btClient.waitForPoweredOnAsync(5000).catch(() => {})
|
|
444
|
+
let peripheral
|
|
445
|
+
try {
|
|
446
|
+
peripheral = await this.scanFor(wanted, 2500)
|
|
447
|
+
} catch {
|
|
448
|
+
// Some adapters (seen on a Realtek RTL8821C) stop delivering advertising
|
|
449
|
+
// reports after an LE connection, even though scanning is reported as
|
|
450
|
+
// enabled. btClient.reset() is a no-op on the hci-socket bindings, so
|
|
451
|
+
// send a real HCI reset and scan once more.
|
|
452
|
+
this.log.debug('[BLE] %s not seen, resetting the adapter and scanning again.', address)
|
|
453
|
+
btClient._bindings?._hci?.reset?.()
|
|
454
|
+
await new Promise(r => setTimeout(r, 1000))
|
|
455
|
+
await btClient.waitForPoweredOnAsync(2000).catch(() => {})
|
|
456
|
+
peripheral = await this.scanFor(wanted, Math.max(timeout - 6000, 3000))
|
|
457
|
+
}
|
|
458
|
+
// With a weak signal the link can drop with 0x3e (connection failed to be
|
|
459
|
+
// established) a few hundred ms after LE Connection Complete. Give it a
|
|
460
|
+
// moment to settle and try again if it did not hold.
|
|
461
|
+
for (let attempt = 1; ; attempt += 1) {
|
|
462
|
+
await peripheral.connectAsync()
|
|
463
|
+
await new Promise(r => setTimeout(r, 500))
|
|
464
|
+
if (peripheral.state === 'connected' || attempt >= 3) {
|
|
465
|
+
return peripheral
|
|
466
|
+
}
|
|
467
|
+
this.log.debug('[BLE] %s dropped right after connecting, retrying (%s/3).', address, attempt + 1)
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
scanFor(wanted, scanTimeout) {
|
|
472
|
+
return new Promise((resolve, reject) => {
|
|
473
|
+
let done = false
|
|
474
|
+
let scanTimeoutId
|
|
475
|
+
function onDiscover(p) {
|
|
476
|
+
if (p.address?.toLowerCase() === wanted) {
|
|
477
|
+
finish(resolve, p)
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
function finish(fn, value) {
|
|
481
|
+
if (done) {
|
|
482
|
+
return
|
|
483
|
+
}
|
|
484
|
+
done = true
|
|
485
|
+
clearTimeout(scanTimeoutId)
|
|
486
|
+
btClient.removeListener('discover', onDiscover)
|
|
487
|
+
btClient.stopScanningAsync().catch(() => {})
|
|
488
|
+
fn(value)
|
|
489
|
+
}
|
|
490
|
+
btClient.on('discover', onDiscover)
|
|
491
|
+
btClient.startScanningAsync([], true).catch(err => finish(reject, err))
|
|
492
|
+
scanTimeoutId = setTimeout(() => finish(reject, new Error(`no advertisement from ${wanted} during scan`)), scanTimeout)
|
|
493
|
+
})
|
|
494
|
+
}
|
|
495
|
+
|
|
437
496
|
async connectByNameDarwin(address, timeout) {
|
|
438
497
|
const suffix = address.replace(/:/g, '').slice(-4).toLowerCase()
|
|
439
498
|
this.darwinPeripheralCache = this.darwinPeripheralCache ?? new Map()
|
package/lib/connection/http.js
CHANGED
|
@@ -16,7 +16,6 @@ export default class {
|
|
|
16
16
|
this.log = platform.log
|
|
17
17
|
this.password = platform.config.password
|
|
18
18
|
this.token = platform.accountToken
|
|
19
|
-
this.tokenTTR = platform.accountTokenTTR
|
|
20
19
|
this.username = platform.config.username
|
|
21
20
|
this.code = platform.config.code
|
|
22
21
|
|
|
@@ -110,18 +109,6 @@ export default class {
|
|
|
110
109
|
// Make the token available in other functions
|
|
111
110
|
this.token = res.data.client.token
|
|
112
111
|
|
|
113
|
-
// Also grab an access token specifically for the get-tap-to-run endpoint.
|
|
114
|
-
// It comes from a different server to the account login, and tap-to-runs
|
|
115
|
-
// are optional - the token is fetched again when one is next needed. So a
|
|
116
|
-
// failure here must not stop the login, or one server being down takes
|
|
117
|
-
// every cloud device with it
|
|
118
|
-
try {
|
|
119
|
-
await this.loginTTR()
|
|
120
|
-
} catch (err) {
|
|
121
|
-
this.tokenTTR = undefined
|
|
122
|
-
this.log.debug('[HTTP] %s %s.', platformLang.ttrLoginFail, parseError(err))
|
|
123
|
-
}
|
|
124
|
-
|
|
125
112
|
// Mark this request complete if in debug mode
|
|
126
113
|
this.log.debug('[HTTP] %s.', platformLang.loginSuccess)
|
|
127
114
|
|
|
@@ -148,7 +135,6 @@ export default class {
|
|
|
148
135
|
iot: iotRes.data.data.p12,
|
|
149
136
|
iotPass: iotRes.data.data.p12Pass,
|
|
150
137
|
token: res.data.client.token,
|
|
151
|
-
tokenTTR: this.tokenTTR,
|
|
152
138
|
topic: res.data.client.topic,
|
|
153
139
|
}
|
|
154
140
|
} catch (err) {
|
|
@@ -166,24 +152,6 @@ export default class {
|
|
|
166
152
|
}
|
|
167
153
|
}
|
|
168
154
|
|
|
169
|
-
async loginTTR() {
|
|
170
|
-
// The tap-to-run endpoint uses a separate, shorter-lived token obtained from
|
|
171
|
-
// a different login endpoint to the main Govee account token. This is split
|
|
172
|
-
// out so it can be refreshed on its own without a full account re-login.
|
|
173
|
-
const ttrRes = await axios({
|
|
174
|
-
url: 'https://community-api.govee.com/os/v1/login',
|
|
175
|
-
method: 'post',
|
|
176
|
-
data: {
|
|
177
|
-
email: this.username,
|
|
178
|
-
password: this.password,
|
|
179
|
-
},
|
|
180
|
-
timeout: 30000,
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
this.tokenTTR = ttrRes.data?.data?.token
|
|
184
|
-
return this.tokenTTR
|
|
185
|
-
}
|
|
186
|
-
|
|
187
155
|
async logout() {
|
|
188
156
|
try {
|
|
189
157
|
await axios({
|
|
@@ -267,15 +235,12 @@ export default class {
|
|
|
267
235
|
}
|
|
268
236
|
}
|
|
269
237
|
|
|
270
|
-
async getTapToRuns(
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if (!this.tokenTTR || this.tokenTTR === 'undefined') {
|
|
277
|
-
await this.loginTTR()
|
|
278
|
-
this.tokenTTRRefreshed = true
|
|
238
|
+
async getTapToRuns() {
|
|
239
|
+
// Make sure we do have the account token. This endpoint used to take a
|
|
240
|
+
// separate token from community-api.govee.com, but Govee now rejects that
|
|
241
|
+
// one ('authorization token is invalid') and accepts the account token.
|
|
242
|
+
if (!this.token) {
|
|
243
|
+
throw new Error(platformLang.noTokenExists)
|
|
279
244
|
}
|
|
280
245
|
|
|
281
246
|
// Build and send the request
|
|
@@ -283,7 +248,7 @@ export default class {
|
|
|
283
248
|
url: 'https://app2.govee.com/bff-app/v1/exec-plat/home',
|
|
284
249
|
method: 'get',
|
|
285
250
|
headers: {
|
|
286
|
-
'Authorization': `Bearer ${this.
|
|
251
|
+
'Authorization': `Bearer ${this.token}`,
|
|
287
252
|
'appVersion': this.appVersion,
|
|
288
253
|
'clientId': this.clientId,
|
|
289
254
|
'clientType': 1,
|
|
@@ -296,14 +261,7 @@ export default class {
|
|
|
296
261
|
|
|
297
262
|
// Check to see we got a response
|
|
298
263
|
if (!res?.data?.data?.components) {
|
|
299
|
-
|
|
300
|
-
// login and retry once before giving up
|
|
301
|
-
if (!isRetry) {
|
|
302
|
-
await this.loginTTR()
|
|
303
|
-
this.tokenTTRRefreshed = true
|
|
304
|
-
return this.getTapToRuns(true)
|
|
305
|
-
}
|
|
306
|
-
throw new Error('not a valid response')
|
|
264
|
+
throw new Error(`not a valid response (${res.data?.message || `HTTP ${res.status}`})`)
|
|
307
265
|
}
|
|
308
266
|
|
|
309
267
|
return res.data.data.components
|
|
@@ -107,6 +107,15 @@ function getCapabilitiesFromPayload(data) {
|
|
|
107
107
|
|| []
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Govee sometimes answers with an empty value for a toggle it does not know the
|
|
112
|
+
* state of (#1395). Read as a number that is 0, so it would show the toggle as
|
|
113
|
+
* off - leave the last known state alone instead.
|
|
114
|
+
*/
|
|
115
|
+
function hasToggleValue(value) {
|
|
116
|
+
return value !== undefined && value !== null && value !== ''
|
|
117
|
+
}
|
|
118
|
+
|
|
110
119
|
function getCapabilityValue(capability) {
|
|
111
120
|
if (capability?.state && Object.hasOwn(capability.state, 'value')) {
|
|
112
121
|
return capability.state.value
|
|
@@ -176,12 +185,18 @@ export function normalizeMqttMessage(message) {
|
|
|
176
185
|
case 'humidity':
|
|
177
186
|
normalized.targetHumidity = Number(value)
|
|
178
187
|
break
|
|
188
|
+
case 'hdmiSource':
|
|
189
|
+
// 1 to 4 on the sync boxes, and nothing usable before they report one
|
|
190
|
+
if (Number.isInteger(Number(value)) && Number(value) > 0) {
|
|
191
|
+
normalized.hdmiSource = Number(value)
|
|
192
|
+
}
|
|
193
|
+
break
|
|
179
194
|
case 'targetTemperature':
|
|
180
195
|
case 'sliderTemperature':
|
|
181
196
|
normalized.targetTemperature = value
|
|
182
197
|
break
|
|
183
198
|
default:
|
|
184
|
-
if (capability?.instance?.endsWith('Toggle')) {
|
|
199
|
+
if (capability?.instance?.endsWith('Toggle') && hasToggleValue(value)) {
|
|
185
200
|
normalized.toggles = normalized.toggles || {}
|
|
186
201
|
normalized.toggles[capability.instance] = Number(value) === 1
|
|
187
202
|
}
|
|
@@ -496,12 +511,18 @@ export default class {
|
|
|
496
511
|
case 'humidity':
|
|
497
512
|
normalized.targetHumidity = Number(value)
|
|
498
513
|
break
|
|
514
|
+
case 'hdmiSource':
|
|
515
|
+
// 1 to 4 on the sync boxes, and nothing usable before they report one
|
|
516
|
+
if (Number.isInteger(Number(value)) && Number(value) > 0) {
|
|
517
|
+
normalized.hdmiSource = Number(value)
|
|
518
|
+
}
|
|
519
|
+
break
|
|
499
520
|
case 'targetTemperature':
|
|
500
521
|
case 'sliderTemperature':
|
|
501
522
|
normalized.targetTemperature = value
|
|
502
523
|
break
|
|
503
524
|
default:
|
|
504
|
-
if (capability?.instance?.endsWith('Toggle')) {
|
|
525
|
+
if (capability?.instance?.endsWith('Toggle') && hasToggleValue(value)) {
|
|
505
526
|
normalized.toggles = normalized.toggles || {}
|
|
506
527
|
normalized.toggles[capability.instance] = Number(value) === 1
|
|
507
528
|
}
|
package/lib/device/fan-H7107.js
CHANGED
|
@@ -59,6 +59,11 @@ export default class extends GoveeDevice {
|
|
|
59
59
|
// Add the fan service for the fan if it doesn't already exist
|
|
60
60
|
this.service = this.accessory.getService(this.hapServ.Fanv2) || this.accessory.addService(this.hapServ.Fanv2)
|
|
61
61
|
|
|
62
|
+
// The fan is what its tile should lead with. It also carries a night light
|
|
63
|
+
// and a temperature sensor, and without saying so the Home app picks for
|
|
64
|
+
// itself and can land on one of those instead
|
|
65
|
+
this.service.setPrimaryService(true)
|
|
66
|
+
|
|
62
67
|
// Add the set handler to the fan on/off characteristic
|
|
63
68
|
this.service
|
|
64
69
|
.getCharacteristic(this.hapChar.Active)
|
|
@@ -139,6 +144,14 @@ export default class extends GoveeDevice {
|
|
|
139
144
|
this.cacheSat = this.lightService.getCharacteristic(this.hapChar.Saturation).value
|
|
140
145
|
}
|
|
141
146
|
|
|
147
|
+
// The fan's own room temperature sensor. The service is only added once the
|
|
148
|
+
// fan has sent a reading (see updateCurrentTemperature), so a fan that never
|
|
149
|
+
// reports one is not left with a tile stuck at its starting value
|
|
150
|
+
this.tempService = this.accessory.getService(this.hapServ.TemperatureSensor)
|
|
151
|
+
this.cacheTemp = this.tempService
|
|
152
|
+
? this.tempService.getCharacteristic(this.hapChar.CurrentTemperature).value
|
|
153
|
+
: undefined
|
|
154
|
+
|
|
142
155
|
// Output the customised options to the log
|
|
143
156
|
const opts = JSON.stringify({
|
|
144
157
|
hideLight: this.hideLight,
|
|
@@ -412,6 +425,35 @@ export default class extends GoveeDevice {
|
|
|
412
425
|
}
|
|
413
426
|
}
|
|
414
427
|
|
|
428
|
+
/**
|
|
429
|
+
* Show a reading from the fan's own temperature sensor.
|
|
430
|
+
*
|
|
431
|
+
* The reading holds the temperature in tenths of a degree FAHRENHEIT in its
|
|
432
|
+
* thousands, with bit 23 as the sign, so 0x0aad98 (699800) is 69.9°F, or
|
|
433
|
+
* 21.1°C. The Govee app reads this model's own sensor as Fahrenheit whatever
|
|
434
|
+
* the app's unit is set to (ThermometerSensorInfo.parseInfo, device type 297).
|
|
435
|
+
*
|
|
436
|
+
* @param {number} reading the 3-byte reading from an `aa 10` status frame
|
|
437
|
+
*/
|
|
438
|
+
updateCurrentTemperature(reading) {
|
|
439
|
+
const isNegative = (reading & 0x800000) !== 0
|
|
440
|
+
const size = Math.floor((reading & 0x7FFFFF) / 1000)
|
|
441
|
+
const tenthsF = isNegative
|
|
442
|
+
? -size
|
|
443
|
+
: size
|
|
444
|
+
const newTemp = Math.round(((tenthsF / 10 - 32) / 1.8) * 10) / 10
|
|
445
|
+
|
|
446
|
+
if (!this.tempService) {
|
|
447
|
+
this.tempService = this.accessory.addService(this.hapServ.TemperatureSensor)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (newTemp !== this.cacheTemp) {
|
|
451
|
+
this.cacheTemp = newTemp
|
|
452
|
+
this.tempService.updateCharacteristic(this.hapChar.CurrentTemperature, newTemp)
|
|
453
|
+
this.accessory.log(`${platformLang.curTemp} [${this.cacheTemp}°C]`)
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
415
457
|
externalUpdate(params) {
|
|
416
458
|
// Update the active characteristic
|
|
417
459
|
if (params.state && params.state !== this.cacheState) {
|
|
@@ -467,6 +509,18 @@ export default class extends GoveeDevice {
|
|
|
467
509
|
return
|
|
468
510
|
}
|
|
469
511
|
|
|
512
|
+
// Room temperature: aa 10 <flags> <3-byte reading>. Flag bit 7 marks a
|
|
513
|
+
// reading from the fan's own sensor (bit 0 is a linked thermometer, which
|
|
514
|
+
// is read differently), and 0xffffff means no reading
|
|
515
|
+
if (getTwoItemPosition(hexParts, 2) === '10') {
|
|
516
|
+
const flags = hexToDecimal(getTwoItemPosition(hexParts, 3))
|
|
517
|
+
const reading = hexToDecimal(`${getTwoItemPosition(hexParts, 4)}${getTwoItemPosition(hexParts, 5)}${getTwoItemPosition(hexParts, 6)}`)
|
|
518
|
+
if ((flags & 0x80) && reading !== 0xFFFFFF) {
|
|
519
|
+
this.updateCurrentTemperature(reading)
|
|
520
|
+
}
|
|
521
|
+
return
|
|
522
|
+
}
|
|
523
|
+
|
|
470
524
|
const deviceFunction = `${getTwoItemPosition(hexParts, 2)}${getTwoItemPosition(hexParts, 3)}`
|
|
471
525
|
|
|
472
526
|
switch (deviceFunction) {
|
|
@@ -494,9 +494,15 @@ export default class GoveeFanCeiling extends deviceFanCeilingBase {
|
|
|
494
494
|
}
|
|
495
495
|
}
|
|
496
496
|
|
|
497
|
+
/**
|
|
498
|
+
* ⚠️ The per-light handlers below carry their own names on purpose. Named
|
|
499
|
+
* like the parent's, they replaced the handlers its single fallback light
|
|
500
|
+
* still calls with just a value - so without an api key that light threw on
|
|
501
|
+
* every switch (#1384).
|
|
502
|
+
*/
|
|
497
503
|
setupLightService(service, name, carriesSharedControls) {
|
|
498
504
|
service.getCharacteristic(this.hapChar.On).onSet(async (value) => {
|
|
499
|
-
await this.
|
|
505
|
+
await this.internalNamedLightStateUpdate(service, name, value)
|
|
500
506
|
})
|
|
501
507
|
|
|
502
508
|
if (!carriesSharedControls) {
|
|
@@ -522,26 +528,26 @@ export default class GoveeFanCeiling extends deviceFanCeilingBase {
|
|
|
522
528
|
.getCharacteristic(this.hapChar.Brightness)
|
|
523
529
|
.setProps({ minStep: 1 })
|
|
524
530
|
.onSet(async (value) => {
|
|
525
|
-
await this.
|
|
531
|
+
await this.internalNamedBrightnessUpdate(service, name, value)
|
|
526
532
|
})
|
|
527
533
|
|
|
528
534
|
service.getCharacteristic(this.hapChar.Hue).onSet(async (value) => {
|
|
529
|
-
await this.
|
|
535
|
+
await this.internalNamedColourUpdate(service, name, value)
|
|
530
536
|
})
|
|
531
537
|
|
|
532
538
|
service.getCharacteristic(this.hapChar.Saturation).onSet(async () => {
|
|
533
|
-
await this.
|
|
539
|
+
await this.internalNamedColourUpdate(service, name, service.getCharacteristic(this.hapChar.Hue).value)
|
|
534
540
|
})
|
|
535
541
|
|
|
536
542
|
if (!service.testCharacteristic(this.hapChar.ColorTemperature)) {
|
|
537
543
|
service.addCharacteristic(this.hapChar.ColorTemperature)
|
|
538
544
|
}
|
|
539
545
|
service.getCharacteristic(this.hapChar.ColorTemperature).onSet(async (value) => {
|
|
540
|
-
await this.
|
|
546
|
+
await this.internalNamedCTUpdate(service, name, value)
|
|
541
547
|
})
|
|
542
548
|
}
|
|
543
549
|
|
|
544
|
-
async
|
|
550
|
+
async internalNamedLightStateUpdate(service, name, value) {
|
|
545
551
|
const newValue = value ? 'on' : 'off'
|
|
546
552
|
const cacheKey = `${name}:state`
|
|
547
553
|
if (this[cacheKey] === newValue) {
|
|
@@ -566,7 +572,7 @@ export default class GoveeFanCeiling extends deviceFanCeilingBase {
|
|
|
566
572
|
this.accessory.log(`${name} ${newValue}`)
|
|
567
573
|
}
|
|
568
574
|
|
|
569
|
-
async
|
|
575
|
+
async internalNamedBrightnessUpdate(service, name, value) {
|
|
570
576
|
const updateKey = generateRandomString(5)
|
|
571
577
|
this[`${name}:updateKeyBright`] = updateKey
|
|
572
578
|
await sleep(350)
|
|
@@ -579,7 +585,7 @@ export default class GoveeFanCeiling extends deviceFanCeilingBase {
|
|
|
579
585
|
// nothing. Switch this light off with its own switch instead, and leave
|
|
580
586
|
// the shared brightness where its owner had it (#1352)
|
|
581
587
|
if (value === 0) {
|
|
582
|
-
await this.
|
|
588
|
+
await this.internalNamedLightStateUpdate(service, name, false)
|
|
583
589
|
return
|
|
584
590
|
}
|
|
585
591
|
|
|
@@ -597,7 +603,7 @@ export default class GoveeFanCeiling extends deviceFanCeilingBase {
|
|
|
597
603
|
this.accessory.log(`${name} brightness [${value}%]`)
|
|
598
604
|
}
|
|
599
605
|
|
|
600
|
-
async
|
|
606
|
+
async internalNamedColourUpdate(service, name, hue) {
|
|
601
607
|
const updateKey = generateRandomString(5)
|
|
602
608
|
this[`${name}:updateKeyColour`] = updateKey
|
|
603
609
|
await sleep(300)
|
|
@@ -621,7 +627,7 @@ export default class GoveeFanCeiling extends deviceFanCeilingBase {
|
|
|
621
627
|
this.accessory.log(`${name} colour [rgb ${r} ${g} ${b}]`)
|
|
622
628
|
}
|
|
623
629
|
|
|
624
|
-
async
|
|
630
|
+
async internalNamedCTUpdate(service, name, value) {
|
|
625
631
|
const updateKey = generateRandomString(5)
|
|
626
632
|
this[`${name}:updateKeyCT`] = updateKey
|
|
627
633
|
await sleep(300)
|
|
@@ -428,6 +428,30 @@ export default class extends GoveeDevice {
|
|
|
428
428
|
return
|
|
429
429
|
}
|
|
430
430
|
|
|
431
|
+
// Humidity: aa 10 <flags> <3-byte reading>. Flag bit 7 means the reading is
|
|
432
|
+
// from the humidifier's own sensor, bit 0 from a linked thermometer. The
|
|
433
|
+
// reading holds the temperature in its thousands and the humidity in tenths
|
|
434
|
+
// of a percent below that, so 0x000212 (530) is 53.0%, and 0xffffff is none
|
|
435
|
+
if (getTwoItemPosition(hexParts, 2) === '10') {
|
|
436
|
+
const flags = hexToDecimal(getTwoItemPosition(hexParts, 3))
|
|
437
|
+
const reading = hexToDecimal(`${getTwoItemPosition(hexParts, 4)}${getTwoItemPosition(hexParts, 5)}${getTwoItemPosition(hexParts, 6)}`)
|
|
438
|
+
if ((flags & 0x81) && reading !== 0xFFFFFF) {
|
|
439
|
+
const newHumiHKValue = Math.round(((reading & 0x7FFFFF) % 1000) / 10)
|
|
440
|
+
if (newHumiHKValue !== this.cacheHumi) {
|
|
441
|
+
this.cacheHumi = newHumiHKValue
|
|
442
|
+
this.humiService.updateCharacteristic(this.hapChar.CurrentRelativeHumidity, this.cacheHumi)
|
|
443
|
+
this.accessory.log(`${platformLang.curHumi} [${this.cacheHumi}%]`)
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Reports this device sends that Home has no use for: a linked thermometer (08),
|
|
450
|
+
// timers (12), cleaning reminders (17), humidity calibration (1e) and charts (30)
|
|
451
|
+
if (['08', '12', '17', '1e', '30'].includes(getTwoItemPosition(hexParts, 2))) {
|
|
452
|
+
return
|
|
453
|
+
}
|
|
454
|
+
|
|
431
455
|
const deviceFunction = `${getTwoItemPosition(hexParts, 2)}${getTwoItemPosition(hexParts, 3)}`
|
|
432
456
|
|
|
433
457
|
switch (deviceFunction) {
|
|
@@ -471,20 +495,6 @@ export default class extends GoveeDevice {
|
|
|
471
495
|
}
|
|
472
496
|
break
|
|
473
497
|
}
|
|
474
|
-
case '1001': { // sometimes humidity value
|
|
475
|
-
const humiCheck = getTwoItemPosition(hexParts, 4)
|
|
476
|
-
if (humiCheck === '00') {
|
|
477
|
-
const newHumiHex = `${getTwoItemPosition(hexParts, 5)}${getTwoItemPosition(hexParts, 6)}`
|
|
478
|
-
const newHumiDec = Math.round(hexToDecimal(newHumiHex)) / 10 // eg 55.5%
|
|
479
|
-
const newHumiHKValue = Math.round(newHumiDec) // eg 56%
|
|
480
|
-
if (newHumiHKValue !== this.cacheHumi) {
|
|
481
|
-
this.cacheHumi = newHumiHKValue
|
|
482
|
-
this.humiService.updateCharacteristic(this.hapChar.CurrentRelativeHumidity, this.cacheHumi)
|
|
483
|
-
this.accessory.log(`${platformLang.curHumi} [${this.cacheHumi}%]`)
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
break
|
|
487
|
-
}
|
|
488
498
|
case '1800': // display off
|
|
489
499
|
case '1801': { // display on
|
|
490
500
|
const newDisplay = deviceFunction === '1801'
|