@homebridge-plugins/homebridge-govee 11.32.2-beta.17 → 11.32.2-beta.18
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 +1 -0
- package/lib/platform.js +34 -180
- package/lib/utils/device-merge.js +251 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -36,6 +36,7 @@ All notable changes to `@homebridge-plugins/homebridge-govee` will be documented
|
|
|
36
36
|
- refactor: choose a device handler from a table, so adding a device type no longer means editing the platform
|
|
37
37
|
- refactor: move the settings parsing into its own file, away from the code that talks to devices
|
|
38
38
|
- refactor: move the work of describing a device to its handler into its own file
|
|
39
|
+
- refactor: move the rules for combining govee's device lists, and for letting go of a device that has gone, into their own file with tests
|
|
39
40
|
|
|
40
41
|
## v11.32.1 (2026-08-02)
|
|
41
42
|
|
package/lib/platform.js
CHANGED
|
@@ -18,6 +18,7 @@ import { applyUserConfig } from './utils/config-parser.js'
|
|
|
18
18
|
import platformConsts from './utils/constants.js'
|
|
19
19
|
import platformChars from './utils/custom-chars.js'
|
|
20
20
|
import { applyDeviceContext } from './utils/device-context.js'
|
|
21
|
+
import { cloudListsWereFetched, planDeviceSetup, planRedundantAccessories } from './utils/device-merge.js'
|
|
21
22
|
import { resolveDeviceType } from './utils/dispatch.js'
|
|
22
23
|
import eveChars from './utils/eve-chars.js'
|
|
23
24
|
import {
|
|
@@ -342,192 +343,45 @@ export default class {
|
|
|
342
343
|
throwOnTimeout: true,
|
|
343
344
|
})
|
|
344
345
|
|
|
345
|
-
//
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
if (!httpDevice.device.includes(':')) {
|
|
357
|
-
// Eg converts abcd1234abcd1234 to AB:CD:12:34:AB:CD:12:34
|
|
358
|
-
// For sensors with an add-on sensor like H5178
|
|
359
|
-
// Eg converts abcd1234abcd1234_1 to AB:CD:12:34:AB:CD:12:34_1
|
|
360
|
-
httpDevice.device = httpDevice.device.replace(DEVICE_ID_FORMAT_REGEX, '$&:').toUpperCase()
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
// Check it's not a user-ignored device
|
|
364
|
-
if (this.ignoredDevices.includes(httpDevice.device)) {
|
|
365
|
-
return
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// Skip Govee app device groups as these are not real devices (#1309)
|
|
369
|
-
if (['BaseGroup', 'SameModeGroup'].includes(httpDevice.sku)) {
|
|
370
|
-
this.log.debug('[%s] %s.', httpDevice.deviceName, platformLang.devIsGroup)
|
|
371
|
-
return
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
// Check it's not a matter-ignored device, if the config has been set
|
|
375
|
-
if (platformConsts.matterModels.includes(httpDevice.sku) && this.config.ignoreMatter) {
|
|
376
|
-
return
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// Sets the flag to see if we need to set up the BLE/HTTP syncs
|
|
380
|
-
if (platformConsts.models.sensorLeak.includes(httpDevice.sku)) {
|
|
381
|
-
httpSyncNeeded = true
|
|
382
|
-
}
|
|
383
|
-
if (platformConsts.models.sensorThermo.includes(httpDevice.sku)) {
|
|
384
|
-
bleSyncNeeded = true
|
|
385
|
-
httpSyncNeeded = true
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// Find any matching device from the LAN client
|
|
389
|
-
const lanDevice = lanDevices.find(el => el.device === httpDevice.device)
|
|
390
|
-
|
|
391
|
-
// Find any matching device from the OpenAPI client to merge capabilities
|
|
392
|
-
const matchingOpenApiDevice = openApiDevices.find(el => el.device === httpDevice.device)
|
|
393
|
-
const openApiMerge = matchingOpenApiDevice
|
|
394
|
-
? {
|
|
395
|
-
openApiInfo: matchingOpenApiDevice.openApiInfo,
|
|
396
|
-
properties: matchingOpenApiDevice.properties,
|
|
397
|
-
supportCmds: matchingOpenApiDevice.supportCmds,
|
|
398
|
-
}
|
|
399
|
-
: {}
|
|
400
|
-
|
|
401
|
-
if (lanDevice) {
|
|
402
|
-
// Device exists in LAN data so add the http info to the object and initialise
|
|
403
|
-
this.initialiseDevice({
|
|
404
|
-
...lanDevice,
|
|
405
|
-
httpInfo: httpDevice,
|
|
406
|
-
model: httpDevice.sku,
|
|
407
|
-
deviceName: httpDevice.deviceName,
|
|
408
|
-
isLanDevice: true,
|
|
409
|
-
...openApiMerge,
|
|
410
|
-
})
|
|
411
|
-
lanDevicesWereInitialised = true
|
|
412
|
-
lanDevice.initialised = true
|
|
413
|
-
} else {
|
|
414
|
-
// Device doesn't exist in LAN data, but try to initialise as could be other device type
|
|
415
|
-
this.initialiseDevice({
|
|
416
|
-
device: httpDevice.device,
|
|
417
|
-
deviceName: httpDevice.deviceName,
|
|
418
|
-
model: httpDevice.sku,
|
|
419
|
-
httpInfo: httpDevice,
|
|
420
|
-
...openApiMerge,
|
|
421
|
-
})
|
|
422
|
-
}
|
|
423
|
-
httpDevicesWereInitialised = true
|
|
424
|
-
})
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
if (openApiDevices && openApiDevices.length > 0) {
|
|
428
|
-
openApiDevices.forEach((openApiDevice) => {
|
|
429
|
-
// Skip Govee app device groups as these are not real devices (#1309)
|
|
430
|
-
if (['BaseGroup', 'SameModeGroup'].includes(openApiDevice.model)) {
|
|
431
|
-
this.log.debug('[%s] %s.', openApiDevice.deviceName, platformLang.devIsGroup)
|
|
432
|
-
return
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
if (this.ignoredDevices.includes(openApiDevice.device)) {
|
|
436
|
-
return
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
if (platformConsts.matterModels.includes(openApiDevice.model) && this.config.ignoreMatter) {
|
|
440
|
-
return
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
if (httpDevices.some(httpDevice => httpDevice.device === openApiDevice.device)) {
|
|
444
|
-
return
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
const lanDevice = lanDevices.find(el => el.device === openApiDevice.device)
|
|
448
|
-
|
|
449
|
-
if (lanDevice) {
|
|
450
|
-
this.initialiseDevice({
|
|
451
|
-
...lanDevice,
|
|
452
|
-
deviceName: openApiDevice.deviceName,
|
|
453
|
-
model: openApiDevice.model,
|
|
454
|
-
openApiInfo: openApiDevice.openApiInfo,
|
|
455
|
-
properties: openApiDevice.properties,
|
|
456
|
-
supportCmds: openApiDevice.supportCmds,
|
|
457
|
-
isLanDevice: true,
|
|
458
|
-
})
|
|
459
|
-
lanDevicesWereInitialised = true
|
|
460
|
-
lanDevice.initialised = true
|
|
461
|
-
} else {
|
|
462
|
-
this.initialiseDevice(openApiDevice)
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
openApiDevicesWereInitialised = true
|
|
466
|
-
})
|
|
467
|
-
}
|
|
346
|
+
// Work out what to set up. Govee describes the same device through up to
|
|
347
|
+
// three lists, and how they combine is decided away from here so it can
|
|
348
|
+
// be tested rather than hoped about
|
|
349
|
+
const plan = planDeviceSetup({
|
|
350
|
+
httpDevices,
|
|
351
|
+
lanDevices,
|
|
352
|
+
openApiDevices,
|
|
353
|
+
ignoredDevices: this.ignoredDevices,
|
|
354
|
+
ignoreMatter: this.config.ignoreMatter,
|
|
355
|
+
deviceConf: this.deviceConf,
|
|
356
|
+
})
|
|
468
357
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
if (pendingLANDevices.length > 0) {
|
|
472
|
-
// No devices from HTTP client, but LAN devices exist
|
|
473
|
-
pendingLANDevices.forEach((lanDevice) => {
|
|
474
|
-
// Check it's not a user-ignored device
|
|
475
|
-
if (this.ignoredDevices.includes(lanDevice.device)) {
|
|
476
|
-
return
|
|
477
|
-
}
|
|
358
|
+
plan.skippedGroups.forEach(name => this.log.debug('[%s] %s.', name, platformLang.devIsGroup))
|
|
359
|
+
plan.devices.forEach(device => this.initialiseDevice(device))
|
|
478
360
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
361
|
+
const {
|
|
362
|
+
bleSyncNeeded,
|
|
363
|
+
httpSyncNeeded,
|
|
364
|
+
httpDevicesWereInitialised,
|
|
365
|
+
openApiDevicesWereInitialised,
|
|
366
|
+
lanDevicesWereInitialised,
|
|
367
|
+
} = plan
|
|
486
368
|
|
|
487
|
-
|
|
488
|
-
// Since LAN does not provide a name, we will use the configured label or device id
|
|
489
|
-
this.initialiseDevice({
|
|
490
|
-
device: lanDevice.device,
|
|
491
|
-
deviceName: this.deviceConf?.[lanDevice.device]?.label || lanDevice.device.replaceAll(':', ''),
|
|
492
|
-
model: lanDevice.sku || 'HXXXX', // In case the model is not provided
|
|
493
|
-
isLanDevice: true,
|
|
494
|
-
isLanOnly: true,
|
|
495
|
-
})
|
|
496
|
-
lanDevicesWereInitialised = true
|
|
497
|
-
})
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
if (!lanDevicesWereInitialised && !httpDevicesWereInitialised && !openApiDevicesWereInitialised) {
|
|
501
|
-
// No devices either from HTTP client nor LAN client
|
|
369
|
+
if (!plan.anyInitialised) {
|
|
502
370
|
throw new Error(platformLang.noDevs)
|
|
503
371
|
}
|
|
504
372
|
|
|
505
|
-
//
|
|
506
|
-
//
|
|
507
|
-
//
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|| (this.config.ignoreMatter && platformConsts.matterModels.includes(accessory.context.gvModel))
|
|
518
|
-
|
|
519
|
-
// Also remove it if the device no longer exists in Govee, but never based on
|
|
520
|
-
// an incomplete cloud list, and never for LAN-only devices which may simply
|
|
521
|
-
// have missed a scan window (#1264)
|
|
522
|
-
const isMissing = !httpDevices.some(el => el.device === accessory.context.gvDeviceId)
|
|
523
|
-
&& !lanDevices.some(el => el.device === accessory.context.gvDeviceId)
|
|
524
|
-
&& !openApiDevices.some(el => el.device === accessory.context.gvDeviceId)
|
|
525
|
-
const isLanOnly = accessory.context.hasLanControl && !accessory.context.firmware
|
|
526
|
-
|
|
527
|
-
if (isIgnored || (isMissing && cloudListsFetched && !isLanOnly)) {
|
|
528
|
-
this.removeAccessory(accessory)
|
|
529
|
-
}
|
|
530
|
-
})
|
|
373
|
+
// Let go of any cached accessory that no longer belongs. Removing one
|
|
374
|
+
// destroys the HomeKit scenes and automations it is part of, so the rules
|
|
375
|
+
// for this live next to their tests
|
|
376
|
+
planRedundantAccessories({
|
|
377
|
+
accessories: [...devicesInHB.values()],
|
|
378
|
+
httpDevices,
|
|
379
|
+
lanDevices,
|
|
380
|
+
openApiDevices,
|
|
381
|
+
ignoredDevices: this.ignoredDevices,
|
|
382
|
+
ignoreMatter: this.config.ignoreMatter,
|
|
383
|
+
cloudListsFetched: cloudListsWereFetched(this.config, this),
|
|
384
|
+
}).forEach(accessory => this.removeAccessory(accessory))
|
|
531
385
|
|
|
532
386
|
// Set up the ble client sync needed for thermo sensor devices
|
|
533
387
|
if (bleSyncNeeded) {
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import platformConsts from './constants.js'
|
|
2
|
+
import platformLang from './lang-en.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Works out which devices to set up, and which cached ones to let go of.
|
|
6
|
+
*
|
|
7
|
+
* Govee can describe the same device through three different lists - the
|
|
8
|
+
* account's own device list, the local network scan, and the public api - and
|
|
9
|
+
* each carries something the others do not. This decides how they combine.
|
|
10
|
+
*
|
|
11
|
+
* Nothing here talks to Homebridge. It returns a plan, and the platform carries
|
|
12
|
+
* it out, which is what lets the awkward cases below be tested rather than
|
|
13
|
+
* hoped about.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const DEVICE_ID_FORMAT_REGEX = /([a-z0-9]{2})(?=[a-z0-9])/gi
|
|
17
|
+
|
|
18
|
+
// The app lets people group devices together. Those groups come back looking
|
|
19
|
+
// like devices, and are not (#1309)
|
|
20
|
+
const GROUP_SKUS = ['BaseGroup', 'SameModeGroup']
|
|
21
|
+
|
|
22
|
+
function isGroup(sku) {
|
|
23
|
+
return GROUP_SKUS.includes(sku)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Govee's own list gives ids without separators, and everything else uses them.
|
|
28
|
+
* An add-on sensor keeps its `_1` suffix.
|
|
29
|
+
*/
|
|
30
|
+
function formatDeviceId(id) {
|
|
31
|
+
return id.includes(':') ? id : id.replace(DEVICE_ID_FORMAT_REGEX, '$&:').toUpperCase()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Decide what to set up.
|
|
36
|
+
*
|
|
37
|
+
* @param {object} input the three device lists and the settings that filter them
|
|
38
|
+
* @param {Array} [input.httpDevices] what the account's device list returned
|
|
39
|
+
* @param {Array} [input.lanDevices] what the local network scan found
|
|
40
|
+
* @param {Array} [input.openApiDevices] what the public api returned
|
|
41
|
+
* @param {Array} [input.ignoredDevices] ids the owner asked to leave alone
|
|
42
|
+
* @param {boolean} [input.ignoreMatter] whether matter models are being skipped
|
|
43
|
+
* @param {object} [input.deviceConf] per-device settings, for names
|
|
44
|
+
* @returns {object} `devices` to hand to the platform in order, which periodic
|
|
45
|
+
* syncs are needed, which lists produced something, and any app groups that
|
|
46
|
+
* were skipped so the caller can log them
|
|
47
|
+
*/
|
|
48
|
+
export function planDeviceSetup({
|
|
49
|
+
httpDevices = [],
|
|
50
|
+
lanDevices = [],
|
|
51
|
+
openApiDevices = [],
|
|
52
|
+
ignoredDevices = [],
|
|
53
|
+
ignoreMatter = false,
|
|
54
|
+
deviceConf = {},
|
|
55
|
+
}) {
|
|
56
|
+
const devices = []
|
|
57
|
+
const skippedGroups = []
|
|
58
|
+
let bleSyncNeeded = false
|
|
59
|
+
let httpSyncNeeded = false
|
|
60
|
+
// Which lists produced something, since each one decides whether its own
|
|
61
|
+
// periodic sync is worth starting
|
|
62
|
+
let httpDevicesWereInitialised = false
|
|
63
|
+
let openApiDevicesWereInitialised = false
|
|
64
|
+
let lanDevicesWereInitialised = false
|
|
65
|
+
|
|
66
|
+
const isIgnored = id => ignoredDevices.includes(id)
|
|
67
|
+
const isIgnoredMatter = model => ignoreMatter && platformConsts.matterModels.includes(model)
|
|
68
|
+
|
|
69
|
+
httpDevices.forEach((httpDevice) => {
|
|
70
|
+
// Kept in place, because the lists are compared by id further down and by
|
|
71
|
+
// the caller afterwards
|
|
72
|
+
httpDevice.device = formatDeviceId(httpDevice.device)
|
|
73
|
+
|
|
74
|
+
if (isIgnored(httpDevice.device)) {
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
if (isGroup(httpDevice.sku)) {
|
|
78
|
+
skippedGroups.push(httpDevice.deviceName)
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
if (isIgnoredMatter(httpDevice.sku)) {
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// These two kinds of sensor are the reason the periodic syncs exist
|
|
86
|
+
if (platformConsts.models.sensorLeak.includes(httpDevice.sku)) {
|
|
87
|
+
httpSyncNeeded = true
|
|
88
|
+
}
|
|
89
|
+
if (platformConsts.models.sensorThermo.includes(httpDevice.sku)) {
|
|
90
|
+
bleSyncNeeded = true
|
|
91
|
+
httpSyncNeeded = true
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const lanDevice = lanDevices.find(el => el.device === httpDevice.device)
|
|
95
|
+
const matchingOpenApi = openApiDevices.find(el => el.device === httpDevice.device)
|
|
96
|
+
const openApiMerge = matchingOpenApi
|
|
97
|
+
? {
|
|
98
|
+
openApiInfo: matchingOpenApi.openApiInfo,
|
|
99
|
+
properties: matchingOpenApi.properties,
|
|
100
|
+
supportCmds: matchingOpenApi.supportCmds,
|
|
101
|
+
}
|
|
102
|
+
: {}
|
|
103
|
+
|
|
104
|
+
if (lanDevice) {
|
|
105
|
+
devices.push({
|
|
106
|
+
...lanDevice,
|
|
107
|
+
httpInfo: httpDevice,
|
|
108
|
+
model: httpDevice.sku,
|
|
109
|
+
deviceName: httpDevice.deviceName,
|
|
110
|
+
isLanDevice: true,
|
|
111
|
+
...openApiMerge,
|
|
112
|
+
})
|
|
113
|
+
lanDevicesWereInitialised = true
|
|
114
|
+
lanDevice.initialised = true
|
|
115
|
+
} else {
|
|
116
|
+
// Not on the local network, but it may still be reachable another way
|
|
117
|
+
devices.push({
|
|
118
|
+
device: httpDevice.device,
|
|
119
|
+
deviceName: httpDevice.deviceName,
|
|
120
|
+
model: httpDevice.sku,
|
|
121
|
+
httpInfo: httpDevice,
|
|
122
|
+
...openApiMerge,
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
httpDevicesWereInitialised = true
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
openApiDevices.forEach((openApiDevice) => {
|
|
129
|
+
if (isGroup(openApiDevice.model)) {
|
|
130
|
+
skippedGroups.push(openApiDevice.deviceName)
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
if (isIgnored(openApiDevice.device) || isIgnoredMatter(openApiDevice.model)) {
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
// Already handled by the loop above
|
|
137
|
+
if (httpDevices.some(httpDevice => httpDevice.device === openApiDevice.device)) {
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const lanDevice = lanDevices.find(el => el.device === openApiDevice.device)
|
|
142
|
+
if (lanDevice) {
|
|
143
|
+
devices.push({
|
|
144
|
+
...lanDevice,
|
|
145
|
+
deviceName: openApiDevice.deviceName,
|
|
146
|
+
model: openApiDevice.model,
|
|
147
|
+
openApiInfo: openApiDevice.openApiInfo,
|
|
148
|
+
properties: openApiDevice.properties,
|
|
149
|
+
supportCmds: openApiDevice.supportCmds,
|
|
150
|
+
isLanDevice: true,
|
|
151
|
+
})
|
|
152
|
+
lanDevicesWereInitialised = true
|
|
153
|
+
lanDevice.initialised = true
|
|
154
|
+
} else {
|
|
155
|
+
devices.push(openApiDevice)
|
|
156
|
+
}
|
|
157
|
+
openApiDevicesWereInitialised = true
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
// Anything found on the network that neither list knew about
|
|
161
|
+
lanDevices.filter(el => !el.initialised).forEach((lanDevice) => {
|
|
162
|
+
if (isIgnored(lanDevice.device)) {
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
// A device the loops above skipped for this reason still has an unmatched
|
|
166
|
+
// network record. Without this it would be added here and removed again by
|
|
167
|
+
// the cleanup on every restart (#1315)
|
|
168
|
+
if (isIgnoredMatter(lanDevice.sku)) {
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
devices.push({
|
|
173
|
+
device: lanDevice.device,
|
|
174
|
+
// The network scan does not give a name, so use the owner's label
|
|
175
|
+
deviceName: deviceConf?.[lanDevice.device]?.label || lanDevice.device.replaceAll(':', ''),
|
|
176
|
+
model: lanDevice.sku || 'HXXXX',
|
|
177
|
+
isLanDevice: true,
|
|
178
|
+
isLanOnly: true,
|
|
179
|
+
})
|
|
180
|
+
lanDevicesWereInitialised = true
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
devices,
|
|
185
|
+
bleSyncNeeded,
|
|
186
|
+
httpSyncNeeded,
|
|
187
|
+
skippedGroups,
|
|
188
|
+
httpDevicesWereInitialised,
|
|
189
|
+
openApiDevicesWereInitialised,
|
|
190
|
+
lanDevicesWereInitialised,
|
|
191
|
+
anyInitialised: httpDevicesWereInitialised || openApiDevicesWereInitialised || lanDevicesWereInitialised,
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Decide which cached accessories no longer belong.
|
|
197
|
+
*
|
|
198
|
+
* ⚠️ The careful part. Removing an accessory destroys the HomeKit scenes and
|
|
199
|
+
* automations it is in, and that cannot be undone from here. So "Govee did not
|
|
200
|
+
* mention it" only counts when the cloud lists were actually fetched this time
|
|
201
|
+
* - a failed request must never look like a deleted device (#1264).
|
|
202
|
+
*
|
|
203
|
+
* @param {object} input the cached accessories and what was seen this session
|
|
204
|
+
* @param {Array} [input.accessories] the accessories Homebridge already has
|
|
205
|
+
* @param {Array} [input.httpDevices] what the account's device list returned
|
|
206
|
+
* @param {Array} [input.lanDevices] what the local network scan found
|
|
207
|
+
* @param {Array} [input.openApiDevices] what the public api returned
|
|
208
|
+
* @param {Array} [input.ignoredDevices] ids the owner asked to leave alone
|
|
209
|
+
* @param {boolean} [input.ignoreMatter] whether matter models are being skipped
|
|
210
|
+
* @param {boolean} [input.cloudListsFetched] whether the cloud lists can be
|
|
211
|
+
* trusted to be complete this session
|
|
212
|
+
* @returns {Array} the accessories to remove
|
|
213
|
+
*/
|
|
214
|
+
export function planRedundantAccessories({
|
|
215
|
+
accessories = [],
|
|
216
|
+
httpDevices = [],
|
|
217
|
+
lanDevices = [],
|
|
218
|
+
openApiDevices = [],
|
|
219
|
+
ignoredDevices = [],
|
|
220
|
+
ignoreMatter = false,
|
|
221
|
+
cloudListsFetched = false,
|
|
222
|
+
}) {
|
|
223
|
+
return accessories.filter((accessory) => {
|
|
224
|
+
const id = accessory.context.gvDeviceId
|
|
225
|
+
|
|
226
|
+
// Asked to be ignored, or a matter model while matter models are ignored -
|
|
227
|
+
// otherwise the cached tile lingers even though setup skipped it (#1287)
|
|
228
|
+
const isIgnored = ignoredDevices.includes(id)
|
|
229
|
+
|| (ignoreMatter && platformConsts.matterModels.includes(accessory.context.gvModel))
|
|
230
|
+
|
|
231
|
+
const isMissing = !httpDevices.some(el => el.device === id)
|
|
232
|
+
&& !lanDevices.some(el => el.device === id)
|
|
233
|
+
&& !openApiDevices.some(el => el.device === id)
|
|
234
|
+
|
|
235
|
+
// A device only ever seen on the network may simply have missed a scan
|
|
236
|
+
const isLanOnly = accessory.context.hasLanControl && !accessory.context.firmware
|
|
237
|
+
|
|
238
|
+
return isIgnored || (isMissing && cloudListsFetched && !isLanOnly)
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Whether the cloud lists can be trusted to be complete this session. A client
|
|
244
|
+
* that was configured but failed to connect means the answer is no.
|
|
245
|
+
*/
|
|
246
|
+
export function cloudListsWereFetched(config, clients) {
|
|
247
|
+
return (!config.username || !!clients.httpClient)
|
|
248
|
+
&& (!!config.openApiDisable || !config.apiKey || !!clients.openApiClient)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export { formatDeviceId, platformLang }
|
package/package.json
CHANGED