@homebridge-plugins/homebridge-govee 11.31.0-beta.0 → 11.32.0-beta.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 CHANGED
@@ -2,7 +2,14 @@
2
2
 
3
3
  All notable changes to `@homebridge-plugins/homebridge-govee` will be documented in this file.
4
4
 
5
- ## v11.31.0 (Pending Release)
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
+
12
+ ## v11.31.0 (2026-07-24)
6
13
 
7
14
  ### Added
8
15
 
@@ -166,6 +166,10 @@
166
166
  </tr>
167
167
  </tbody>
168
168
  </table>
169
+ <div id="configBtnRow" class="text-center mt-2 mb-3" style="display: none;">
170
+ <button type="button" class="btn btn-primary" id="deviceConfigBtn">Add To Config</button>
171
+ <button type="button" class="btn btn-secondary" id="addAllBtn">Add All To Config</button>
172
+ </div>
169
173
  </div>
170
174
  </div>
171
175
  <div id="pageSupport" class="mt-4" style="display: none;">
@@ -305,6 +309,160 @@
305
309
  ;(async () => {
306
310
  try {
307
311
  const currentConfig = await homebridge.getPluginConfig()
312
+
313
+ // ===== Device picker (add/remove config entries from the My Devices tab) =====
314
+ // The button state is always computed from a fresh getPluginConfig() call,
315
+ // so staged-but-unsaved edits made in the Settings tab are respected
316
+ const deviceArrayKeys = [
317
+ 'lightDevices',
318
+ 'switchDevices',
319
+ 'leakDevices',
320
+ 'thermoDevices',
321
+ 'fanDevices',
322
+ 'heaterDevices',
323
+ 'dehumidifierDevices',
324
+ 'humidifierDevices',
325
+ 'purifierDevices',
326
+ 'diffuserDevices',
327
+ 'kettleDevices',
328
+ 'iceMakerDevices',
329
+ ]
330
+ let pickerAccessories = []
331
+ let pickerModelMap = {}
332
+ let removeConfirmTimer
333
+
334
+ // Compare device ids ignoring case, colons and underscores, so an entry
335
+ // typed by hand in a different format still counts as the same device
336
+ const looseId = id => (id || '').toString().toUpperCase().replace(/[^A-F0-9]+/g, '')
337
+
338
+ const getConfigBlocks = async () => {
339
+ const blocks = await homebridge.getPluginConfig()
340
+ if (!blocks.length) {
341
+ blocks.push({ name: 'Govee' })
342
+ }
343
+ return blocks
344
+ }
345
+
346
+ const findInConfig = (config, deviceId) => {
347
+ const target = looseId(deviceId)
348
+ for (const key of deviceArrayKeys) {
349
+ if (Array.isArray(config[key])) {
350
+ const index = config[key].findIndex(entry => entry && looseId(entry.deviceId) === target)
351
+ if (index !== -1) {
352
+ return { key, index }
353
+ }
354
+ }
355
+ }
356
+ return null
357
+ }
358
+
359
+ const setConfigButton = inConfig => {
360
+ const btn = document.getElementById('deviceConfigBtn')
361
+ clearTimeout(removeConfirmTimer)
362
+ delete btn.dataset.confirm
363
+ btn.dataset.state = inConfig ? 'remove' : 'add'
364
+ btn.classList.remove('btn-danger')
365
+ btn.classList.add('btn-primary')
366
+ btn.innerHTML = inConfig ? 'Remove From Config' : 'Add To Config'
367
+ }
368
+
369
+ const addDeviceToConfig = (config, acc) => {
370
+ const arrayKey = pickerModelMap[acc.context.gvModel] || 'lightDevices'
371
+ if (!Array.isArray(config[arrayKey])) {
372
+ config[arrayKey] = []
373
+ }
374
+ // label is included so the entry survives the platform's clean-up of
375
+ // entries that contain nothing but a device id
376
+ config[arrayKey].push({ deviceId: acc.context.gvDeviceId, label: acc.displayName })
377
+ }
378
+
379
+ document.getElementById('deviceConfigBtn').addEventListener('click', async () => {
380
+ try {
381
+ const btn = document.getElementById('deviceConfigBtn')
382
+ const selectedId = document.getElementById('deviceSelect').value
383
+ const acc = pickerAccessories.find(x => x.context.gvDeviceId === selectedId)
384
+ if (!acc) {
385
+ return
386
+ }
387
+ const blocks = await getConfigBlocks()
388
+ const config = blocks[0]
389
+ const found = findInConfig(config, selectedId)
390
+ if (btn.dataset.state === 'add') {
391
+ // The config may have changed since the button was drawn, so
392
+ // re-check and never add a duplicate - just correct the button
393
+ if (found) {
394
+ setConfigButton(true)
395
+ homebridge.toast.info(acc.displayName + ' is already in the config.', 'Not Added')
396
+ return
397
+ }
398
+ addDeviceToConfig(config, acc)
399
+ await homebridge.updatePluginConfig(blocks)
400
+ setConfigButton(true)
401
+ homebridge.toast.success(acc.displayName + ' added - press Save to apply.', 'Added To Config')
402
+ } else {
403
+ if (!found) {
404
+ setConfigButton(false)
405
+ homebridge.toast.info(acc.displayName + ' is not in the config.', 'Not Removed')
406
+ return
407
+ }
408
+ const entry = config[found.key][found.index]
409
+ const extraKeys = Object.keys(entry).filter(k => !['deviceId', 'label'].includes(k))
410
+ if (extraKeys.length > 0 && btn.dataset.confirm !== 'yes') {
411
+ // The entry has settings beyond id and label which would be
412
+ // lost, so ask for a second click within five seconds
413
+ btn.dataset.confirm = 'yes'
414
+ btn.classList.remove('btn-primary')
415
+ btn.classList.add('btn-danger')
416
+ btn.innerHTML = 'Click Again To Remove (entry has settings)'
417
+ removeConfirmTimer = setTimeout(() => setConfigButton(true), 5000)
418
+ return
419
+ }
420
+ config[found.key].splice(found.index, 1)
421
+ if (config[found.key].length === 0) {
422
+ delete config[found.key]
423
+ }
424
+ await homebridge.updatePluginConfig(blocks)
425
+ setConfigButton(false)
426
+ homebridge.toast.success(acc.displayName + ' removed - press Save to apply.', 'Removed From Config')
427
+ }
428
+ } catch (err) {
429
+ homebridge.toast.error(err.message, 'Error')
430
+ }
431
+ })
432
+
433
+ document.getElementById('addAllBtn').addEventListener('click', async () => {
434
+ try {
435
+ const blocks = await getConfigBlocks()
436
+ const config = blocks[0]
437
+ let added = 0
438
+ let skipped = 0
439
+ pickerAccessories.forEach(acc => {
440
+ if (findInConfig(config, acc.context.gvDeviceId)) {
441
+ skipped += 1
442
+ return
443
+ }
444
+ addDeviceToConfig(config, acc)
445
+ added += 1
446
+ })
447
+ if (added > 0) {
448
+ await homebridge.updatePluginConfig(blocks)
449
+ homebridge.toast.success(
450
+ added + ' device(s) added, ' + skipped + ' already in the config - press Save to apply.',
451
+ 'Added To Config',
452
+ )
453
+ } else {
454
+ homebridge.toast.info('All devices are already in the config.', 'Nothing To Add')
455
+ }
456
+ const selectedId = document.getElementById('deviceSelect').value
457
+ if (selectedId) {
458
+ setConfigButton(!!findInConfig(config, selectedId))
459
+ }
460
+ } catch (err) {
461
+ homebridge.toast.error(err.message, 'Error')
462
+ }
463
+ })
464
+ // ===== End device picker =====
465
+
308
466
  showIntro = () => {
309
467
  homebridge.disableSaveButton?.()
310
468
  const introContinue = document.getElementById('introContinue')
@@ -319,7 +477,9 @@
319
477
  }
320
478
  showDevices = async () => {
321
479
  homebridge.showSpinner()
322
- homebridge.disableSaveButton?.()
480
+ // The save button stays enabled here so entries staged by the picker
481
+ // buttons can be saved without switching back to the Settings tab
482
+ homebridge.enableSaveButton?.()
323
483
  homebridge.hideSchemaForm()
324
484
  document.getElementById('menuHome').classList.remove('btn-elegant')
325
485
  document.getElementById('menuHome').classList.add('btn-primary')
@@ -342,6 +502,16 @@
342
502
  : 0
343
503
  })
344
504
  }
505
+ pickerAccessories = cachedAccessories
506
+ if (Object.keys(pickerModelMap).length === 0) {
507
+ try {
508
+ pickerModelMap = await homebridge.request('/getModelMap')
509
+ } catch (err) {
510
+ // Without the map every added device falls back to lightDevices,
511
+ // which still works because the platform merges all the arrays
512
+ pickerModelMap = {}
513
+ }
514
+ }
345
515
  // Build connection overview
346
516
  if (cachedAccessories.length > 0) {
347
517
  const counts = { lan: 0, aws: 0, openapi: 0, ble: 0 }
@@ -461,6 +631,9 @@
461
631
  ? '<img src="' + context.image + '" style="width: 150px;">'
462
632
  : ''
463
633
  document.getElementById('deviceTable').style.display = 'inline-table'
634
+ const configBlocks = await getConfigBlocks()
635
+ setConfigButton(!!findInConfig(configBlocks[0], context.gvDeviceId))
636
+ document.getElementById('configBtnRow').style.display = 'block'
464
637
  homebridge.hideSpinner()
465
638
  }
466
639
  deviceSelect.addEventListener('change', event => showDeviceInfo(event.target.value))
@@ -471,6 +644,7 @@
471
644
  option.text = 'No Devices'
472
645
  deviceSelect.add(option)
473
646
  deviceSelect.disabled = true
647
+ document.getElementById('configBtnRow').style.display = 'none'
474
648
  }
475
649
  homebridge.hideSpinner()
476
650
  }
@@ -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-beta.0",
5
+ "version": "11.32.0-beta.0",
6
6
  "description": "Homebridge plugin to integrate Govee devices into HomeKit.",
7
7
  "author": {
8
8
  "name": "bwp91",