@homebridge-plugins/homebridge-govee 11.31.0 → 11.32.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/homebridge-ui/public/index.html +234 -1
- package/lib/homebridge-ui/server.js +44 -0
- 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.32.0 (Pending Release)
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- chore(github): use the shared homebridge action to deprecate past pre-releases
|
|
10
|
+
- feat(ui): add and remove devices from the config via the my devices tab (#1323)
|
|
11
|
+
- feat(ui): hide or show a device in homekit from the my devices tab
|
|
12
|
+
|
|
5
13
|
## v11.31.0 (2026-07-24)
|
|
6
14
|
|
|
7
15
|
### Added
|
|
@@ -161,11 +161,24 @@
|
|
|
161
161
|
<th scope="row">Humidity Range</th>
|
|
162
162
|
<td id="humi_range"></td>
|
|
163
163
|
</tr>
|
|
164
|
+
<tr>
|
|
165
|
+
<th scope="row">Hide From HomeKit</th>
|
|
166
|
+
<td>
|
|
167
|
+
<div class="custom-control custom-checkbox">
|
|
168
|
+
<input type="checkbox" class="custom-control-input" id="hideDeviceCheck">
|
|
169
|
+
<label class="custom-control-label" for="hideDeviceCheck">Hide this device from HomeKit</label>
|
|
170
|
+
</div>
|
|
171
|
+
</td>
|
|
172
|
+
</tr>
|
|
164
173
|
<tr>
|
|
165
174
|
<td colspan="2" style="text-align: center" id="image"></td>
|
|
166
175
|
</tr>
|
|
167
176
|
</tbody>
|
|
168
177
|
</table>
|
|
178
|
+
<div id="configBtnRow" class="text-center mt-2 mb-3" style="display: none;">
|
|
179
|
+
<button type="button" class="btn btn-primary" id="deviceConfigBtn">Add To Config</button>
|
|
180
|
+
<button type="button" class="btn btn-secondary" id="addAllBtn">Add All To Config</button>
|
|
181
|
+
</div>
|
|
169
182
|
</div>
|
|
170
183
|
</div>
|
|
171
184
|
<div id="pageSupport" class="mt-4" style="display: none;">
|
|
@@ -305,6 +318,208 @@
|
|
|
305
318
|
;(async () => {
|
|
306
319
|
try {
|
|
307
320
|
const currentConfig = await homebridge.getPluginConfig()
|
|
321
|
+
|
|
322
|
+
// ===== Device picker (add/remove config entries from the My Devices tab) =====
|
|
323
|
+
// The button state is always computed from a fresh getPluginConfig() call,
|
|
324
|
+
// so staged-but-unsaved edits made in the Settings tab are respected
|
|
325
|
+
const deviceArrayKeys = [
|
|
326
|
+
'lightDevices',
|
|
327
|
+
'switchDevices',
|
|
328
|
+
'leakDevices',
|
|
329
|
+
'thermoDevices',
|
|
330
|
+
'fanDevices',
|
|
331
|
+
'heaterDevices',
|
|
332
|
+
'dehumidifierDevices',
|
|
333
|
+
'humidifierDevices',
|
|
334
|
+
'purifierDevices',
|
|
335
|
+
'diffuserDevices',
|
|
336
|
+
'kettleDevices',
|
|
337
|
+
'iceMakerDevices',
|
|
338
|
+
]
|
|
339
|
+
let pickerAccessories = []
|
|
340
|
+
let pickerModelMap = {}
|
|
341
|
+
let removeConfirmTimer
|
|
342
|
+
|
|
343
|
+
// Compare device ids ignoring case, colons and underscores, so an entry
|
|
344
|
+
// typed by hand in a different format still counts as the same device
|
|
345
|
+
const looseId = id => (id || '').toString().toUpperCase().replace(/[^A-F0-9]+/g, '')
|
|
346
|
+
|
|
347
|
+
const getConfigBlocks = async () => {
|
|
348
|
+
const blocks = await homebridge.getPluginConfig()
|
|
349
|
+
if (!blocks.length) {
|
|
350
|
+
blocks.push({ name: 'Govee' })
|
|
351
|
+
}
|
|
352
|
+
return blocks
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const findInConfig = (config, deviceId) => {
|
|
356
|
+
const target = looseId(deviceId)
|
|
357
|
+
for (const key of deviceArrayKeys) {
|
|
358
|
+
if (Array.isArray(config[key])) {
|
|
359
|
+
const index = config[key].findIndex(entry => entry && looseId(entry.deviceId) === target)
|
|
360
|
+
if (index !== -1) {
|
|
361
|
+
return { key, index }
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return null
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const setHideCheckbox = hidden => {
|
|
369
|
+
document.getElementById('hideDeviceCheck').checked = !!hidden
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const setConfigButton = inConfig => {
|
|
373
|
+
const btn = document.getElementById('deviceConfigBtn')
|
|
374
|
+
clearTimeout(removeConfirmTimer)
|
|
375
|
+
delete btn.dataset.confirm
|
|
376
|
+
btn.dataset.state = inConfig ? 'remove' : 'add'
|
|
377
|
+
btn.classList.remove('btn-danger')
|
|
378
|
+
btn.classList.add('btn-primary')
|
|
379
|
+
btn.innerHTML = inConfig ? 'Remove From Config' : 'Add To Config'
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const addDeviceToConfig = (config, acc) => {
|
|
383
|
+
const arrayKey = pickerModelMap[acc.context.gvModel] || 'lightDevices'
|
|
384
|
+
if (!Array.isArray(config[arrayKey])) {
|
|
385
|
+
config[arrayKey] = []
|
|
386
|
+
}
|
|
387
|
+
// label is included so the entry survives the platform's clean-up of
|
|
388
|
+
// entries that contain nothing but a device id
|
|
389
|
+
config[arrayKey].push({ deviceId: acc.context.gvDeviceId, label: acc.displayName })
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
document.getElementById('deviceConfigBtn').addEventListener('click', async () => {
|
|
393
|
+
try {
|
|
394
|
+
const btn = document.getElementById('deviceConfigBtn')
|
|
395
|
+
const selectedId = document.getElementById('deviceSelect').value
|
|
396
|
+
const acc = pickerAccessories.find(x => x.context.gvDeviceId === selectedId)
|
|
397
|
+
if (!acc) {
|
|
398
|
+
return
|
|
399
|
+
}
|
|
400
|
+
const blocks = await getConfigBlocks()
|
|
401
|
+
const config = blocks[0]
|
|
402
|
+
const found = findInConfig(config, selectedId)
|
|
403
|
+
if (btn.dataset.state === 'add') {
|
|
404
|
+
// The config may have changed since the button was drawn, so
|
|
405
|
+
// re-check and never add a duplicate - just correct the button
|
|
406
|
+
if (found) {
|
|
407
|
+
setConfigButton(true)
|
|
408
|
+
setHideCheckbox(!!config[found.key][found.index].ignoreDevice)
|
|
409
|
+
homebridge.toast.info(acc.displayName + ' is already in the config.', 'Not Added')
|
|
410
|
+
return
|
|
411
|
+
}
|
|
412
|
+
addDeviceToConfig(config, acc)
|
|
413
|
+
await homebridge.updatePluginConfig(blocks)
|
|
414
|
+
setConfigButton(true)
|
|
415
|
+
setHideCheckbox(false)
|
|
416
|
+
homebridge.toast.success(acc.displayName + ' added - press Save to apply.', 'Added To Config')
|
|
417
|
+
} else {
|
|
418
|
+
if (!found) {
|
|
419
|
+
setConfigButton(false)
|
|
420
|
+
setHideCheckbox(false)
|
|
421
|
+
homebridge.toast.info(acc.displayName + ' is not in the config.', 'Not Removed')
|
|
422
|
+
return
|
|
423
|
+
}
|
|
424
|
+
const entry = config[found.key][found.index]
|
|
425
|
+
const extraKeys = Object.keys(entry).filter(k => !['deviceId', 'label'].includes(k))
|
|
426
|
+
if (extraKeys.length > 0 && btn.dataset.confirm !== 'yes') {
|
|
427
|
+
// The entry has settings beyond id and label which would be
|
|
428
|
+
// lost, so ask for a second click within five seconds
|
|
429
|
+
btn.dataset.confirm = 'yes'
|
|
430
|
+
btn.classList.remove('btn-primary')
|
|
431
|
+
btn.classList.add('btn-danger')
|
|
432
|
+
btn.innerHTML = 'Click Again To Remove (entry has settings)'
|
|
433
|
+
removeConfirmTimer = setTimeout(() => setConfigButton(true), 5000)
|
|
434
|
+
return
|
|
435
|
+
}
|
|
436
|
+
config[found.key].splice(found.index, 1)
|
|
437
|
+
if (config[found.key].length === 0) {
|
|
438
|
+
delete config[found.key]
|
|
439
|
+
}
|
|
440
|
+
await homebridge.updatePluginConfig(blocks)
|
|
441
|
+
setConfigButton(false)
|
|
442
|
+
setHideCheckbox(false)
|
|
443
|
+
homebridge.toast.success(acc.displayName + ' removed - press Save to apply.', 'Removed From Config')
|
|
444
|
+
}
|
|
445
|
+
} catch (err) {
|
|
446
|
+
homebridge.toast.error(err.message, 'Error')
|
|
447
|
+
}
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
document.getElementById('hideDeviceCheck').addEventListener('change', async (e) => {
|
|
451
|
+
try {
|
|
452
|
+
const selectedId = document.getElementById('deviceSelect').value
|
|
453
|
+
const acc = pickerAccessories.find(x => x.context.gvDeviceId === selectedId)
|
|
454
|
+
if (!acc) {
|
|
455
|
+
return
|
|
456
|
+
}
|
|
457
|
+
const blocks = await getConfigBlocks()
|
|
458
|
+
const config = blocks[0]
|
|
459
|
+
let found = findInConfig(config, selectedId)
|
|
460
|
+
if (e.target.checked) {
|
|
461
|
+
// Hiding needs a config entry to carry the flag, so create one if
|
|
462
|
+
// the device is not in the config yet
|
|
463
|
+
if (!found) {
|
|
464
|
+
addDeviceToConfig(config, acc)
|
|
465
|
+
found = findInConfig(config, selectedId)
|
|
466
|
+
}
|
|
467
|
+
config[found.key][found.index].ignoreDevice = true
|
|
468
|
+
await homebridge.updatePluginConfig(blocks)
|
|
469
|
+
setConfigButton(true)
|
|
470
|
+
homebridge.toast.success(acc.displayName + ' will be hidden from HomeKit - press Save to apply.', 'Device Hidden')
|
|
471
|
+
} else {
|
|
472
|
+
if (found) {
|
|
473
|
+
// Drop the flag rather than storing ignoreDevice false, to keep
|
|
474
|
+
// the entry clean
|
|
475
|
+
delete config[found.key][found.index].ignoreDevice
|
|
476
|
+
await homebridge.updatePluginConfig(blocks)
|
|
477
|
+
}
|
|
478
|
+
setConfigButton(!!found)
|
|
479
|
+
homebridge.toast.success(acc.displayName + ' will be shown in HomeKit - press Save to apply.', 'Device Unhidden')
|
|
480
|
+
}
|
|
481
|
+
} catch (err) {
|
|
482
|
+
homebridge.toast.error(err.message, 'Error')
|
|
483
|
+
}
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
document.getElementById('addAllBtn').addEventListener('click', async () => {
|
|
487
|
+
try {
|
|
488
|
+
const blocks = await getConfigBlocks()
|
|
489
|
+
const config = blocks[0]
|
|
490
|
+
let added = 0
|
|
491
|
+
let skipped = 0
|
|
492
|
+
pickerAccessories.forEach(acc => {
|
|
493
|
+
if (findInConfig(config, acc.context.gvDeviceId)) {
|
|
494
|
+
skipped += 1
|
|
495
|
+
return
|
|
496
|
+
}
|
|
497
|
+
addDeviceToConfig(config, acc)
|
|
498
|
+
added += 1
|
|
499
|
+
})
|
|
500
|
+
if (added > 0) {
|
|
501
|
+
await homebridge.updatePluginConfig(blocks)
|
|
502
|
+
homebridge.toast.success(
|
|
503
|
+
added + ' device(s) added, ' + skipped + ' already in the config - press Save to apply.',
|
|
504
|
+
'Added To Config',
|
|
505
|
+
)
|
|
506
|
+
} else {
|
|
507
|
+
homebridge.toast.info('All devices are already in the config.', 'Nothing To Add')
|
|
508
|
+
}
|
|
509
|
+
const selectedId = document.getElementById('deviceSelect').value
|
|
510
|
+
if (selectedId) {
|
|
511
|
+
const nowFound = findInConfig(config, selectedId)
|
|
512
|
+
setConfigButton(!!nowFound)
|
|
513
|
+
if (nowFound) {
|
|
514
|
+
setHideCheckbox(!!config[nowFound.key][nowFound.index].ignoreDevice)
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
} catch (err) {
|
|
518
|
+
homebridge.toast.error(err.message, 'Error')
|
|
519
|
+
}
|
|
520
|
+
})
|
|
521
|
+
// ===== End device picker =====
|
|
522
|
+
|
|
308
523
|
showIntro = () => {
|
|
309
524
|
homebridge.disableSaveButton?.()
|
|
310
525
|
const introContinue = document.getElementById('introContinue')
|
|
@@ -319,7 +534,9 @@
|
|
|
319
534
|
}
|
|
320
535
|
showDevices = async () => {
|
|
321
536
|
homebridge.showSpinner()
|
|
322
|
-
|
|
537
|
+
// The save button stays enabled here so entries staged by the picker
|
|
538
|
+
// buttons can be saved without switching back to the Settings tab
|
|
539
|
+
homebridge.enableSaveButton?.()
|
|
323
540
|
homebridge.hideSchemaForm()
|
|
324
541
|
document.getElementById('menuHome').classList.remove('btn-elegant')
|
|
325
542
|
document.getElementById('menuHome').classList.add('btn-primary')
|
|
@@ -342,6 +559,16 @@
|
|
|
342
559
|
: 0
|
|
343
560
|
})
|
|
344
561
|
}
|
|
562
|
+
pickerAccessories = cachedAccessories
|
|
563
|
+
if (Object.keys(pickerModelMap).length === 0) {
|
|
564
|
+
try {
|
|
565
|
+
pickerModelMap = await homebridge.request('/getModelMap')
|
|
566
|
+
} catch (err) {
|
|
567
|
+
// Without the map every added device falls back to lightDevices,
|
|
568
|
+
// which still works because the platform merges all the arrays
|
|
569
|
+
pickerModelMap = {}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
345
572
|
// Build connection overview
|
|
346
573
|
if (cachedAccessories.length > 0) {
|
|
347
574
|
const counts = { lan: 0, aws: 0, openapi: 0, ble: 0 }
|
|
@@ -461,6 +688,11 @@
|
|
|
461
688
|
? '<img src="' + context.image + '" style="width: 150px;">'
|
|
462
689
|
: ''
|
|
463
690
|
document.getElementById('deviceTable').style.display = 'inline-table'
|
|
691
|
+
const configBlocks = await getConfigBlocks()
|
|
692
|
+
const configEntry = findInConfig(configBlocks[0], context.gvDeviceId)
|
|
693
|
+
setConfigButton(!!configEntry)
|
|
694
|
+
setHideCheckbox(!!(configEntry && configBlocks[0][configEntry.key][configEntry.index].ignoreDevice))
|
|
695
|
+
document.getElementById('configBtnRow').style.display = 'block'
|
|
464
696
|
homebridge.hideSpinner()
|
|
465
697
|
}
|
|
466
698
|
deviceSelect.addEventListener('change', event => showDeviceInfo(event.target.value))
|
|
@@ -471,6 +703,7 @@
|
|
|
471
703
|
option.text = 'No Devices'
|
|
472
704
|
deviceSelect.add(option)
|
|
473
705
|
deviceSelect.disabled = true
|
|
706
|
+
document.getElementById('configBtnRow').style.display = 'none'
|
|
474
707
|
}
|
|
475
708
|
homebridge.hideSpinner()
|
|
476
709
|
}
|
|
@@ -1,8 +1,52 @@
|
|
|
1
1
|
import { HomebridgePluginUiServer } from '@homebridge/plugin-ui-utils'
|
|
2
2
|
|
|
3
|
+
import platformConsts from '../utils/constants.js'
|
|
4
|
+
|
|
5
|
+
// Which config array each model category belongs in. The platform flattens
|
|
6
|
+
// every array into one lookup by device id, so this only decides which set of
|
|
7
|
+
// options the schema editor shows next to the entry. Sensor categories without
|
|
8
|
+
// an array of their own sit best in thermoDevices (the general sensor bucket).
|
|
9
|
+
const categoryToArray = {
|
|
10
|
+
rgb: 'lightDevices',
|
|
11
|
+
switchSingle: 'switchDevices',
|
|
12
|
+
switchDouble: 'switchDevices',
|
|
13
|
+
switchTriple: 'switchDevices',
|
|
14
|
+
sensorLeak: 'leakDevices',
|
|
15
|
+
sensorThermo: 'thermoDevices',
|
|
16
|
+
sensorThermo4: 'thermoDevices',
|
|
17
|
+
sensorMonitor: 'thermoDevices',
|
|
18
|
+
sensorCO2: 'thermoDevices',
|
|
19
|
+
sensorPresence: 'thermoDevices',
|
|
20
|
+
sensorButton: 'thermoDevices',
|
|
21
|
+
sensorContact: 'thermoDevices',
|
|
22
|
+
fan: 'fanDevices',
|
|
23
|
+
heater1: 'heaterDevices',
|
|
24
|
+
heater2: 'heaterDevices',
|
|
25
|
+
humidifier: 'humidifierDevices',
|
|
26
|
+
dehumidifier: 'dehumidifierDevices',
|
|
27
|
+
purifier: 'purifierDevices',
|
|
28
|
+
diffuser: 'diffuserDevices',
|
|
29
|
+
kettle: 'kettleDevices',
|
|
30
|
+
iceMaker: 'iceMakerDevices',
|
|
31
|
+
template: 'lightDevices',
|
|
32
|
+
}
|
|
33
|
+
|
|
3
34
|
class PluginUiServer extends HomebridgePluginUiServer {
|
|
4
35
|
constructor() {
|
|
5
36
|
super()
|
|
37
|
+
|
|
38
|
+
// Used by the My Devices tab to know which config array a model belongs
|
|
39
|
+
// in, built from the same model lists the plugin itself uses
|
|
40
|
+
this.onRequest('/getModelMap', () => {
|
|
41
|
+
const modelToArray = {}
|
|
42
|
+
Object.entries(categoryToArray).forEach(([category, arrayKey]) => {
|
|
43
|
+
(platformConsts.models[category] || []).forEach((model) => {
|
|
44
|
+
modelToArray[model] = arrayKey
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
return modelToArray
|
|
48
|
+
})
|
|
49
|
+
|
|
6
50
|
this.ready()
|
|
7
51
|
}
|
|
8
52
|
}
|
package/package.json
CHANGED