@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 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
- homebridge.disableSaveButton?.()
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
@@ -2,7 +2,7 @@
2
2
  "name": "@homebridge-plugins/homebridge-govee",
3
3
  "alias": "Govee",
4
4
  "type": "module",
5
- "version": "11.31.0",
5
+ "version": "11.32.0-beta.1",
6
6
  "description": "Homebridge plugin to integrate Govee devices into HomeKit.",
7
7
  "author": {
8
8
  "name": "bwp91",