@homebridge-plugins/homebridge-wemo 7.5.3 → 7.6.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,6 +2,12 @@
2
2
 
3
3
  All notable changes to `@homebridge-plugins/homebridge-wemo` will be documented in this file.
4
4
 
5
+ ## v7.6.0 (Pending Release)
6
+
7
+ ### Changed
8
+
9
+ - feat(ui): add, remove and hide devices from the config via the my devices tab
10
+
5
11
  ## v7.5.3 (2026-07-18)
6
12
 
7
13
  ### Changed
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <a href="https://github.com/homebridge-plugins/homebridge-wemo"><img alt="Homebridge Verified" src="https://user-images.githubusercontent.com/43026681/126868557-d0983348-d124-4247-bea9-7dcc62849cdf.png" width="600px"></a>
2
+ <a href="https://github.com/homebridge-plugins/homebridge-wemo"><img alt="Homebridge Verified" src="https://raw.githubusercontent.com/homebridge-plugins/homebridge-wemo/latest/branding/Homebridge_x_Wemo.png" width="600px"></a>
3
3
  </p>
4
4
  <span align="center">
5
5
 
@@ -39,7 +39,7 @@ Homebridge plugin to integrate Wemo devices into HomeKit
39
39
 
40
40
  - [Installation](https://github.com/homebridge-plugins/homebridge-wemo/wiki/Installation)
41
41
  - [Configuration](https://github.com/homebridge-plugins/homebridge-wemo/wiki/Configuration)
42
- - [Beta Version](https://github.com/homebridge/homebridge/wiki/How-to-Install-Alternate-Plugin-Versions)
42
+ - [Beta Version](https://github.com/homebridge-plugins/homebridge-wemo/wiki/Beta-Version)
43
43
  - [Node Version](https://github.com/homebridge-plugins/homebridge-wemo/wiki/Node-Version)
44
44
 
45
45
  ### Features
Binary file
@@ -3,7 +3,7 @@
3
3
  "pluginType": "platform",
4
4
  "customUi": true,
5
5
  "customUiPath": "./lib/homebridge-ui",
6
- "headerDisplay": "<p align=\"center\"><img width=\"60%\" src=\"https://user-images.githubusercontent.com/43026681/126868557-d0983348-d124-4247-bea9-7dcc62849cdf.png\"></p><p align=\"center\">For help and support please visit our <a href=\"https://github.com/homebridge-plugins/homebridge-wemo/wiki\">GitHub Wiki</a>. We hope you find this plugin useful!</p>",
6
+ "headerDisplay": "<p align=\"center\"><img width=\"250px\" src=\"https://raw.githubusercontent.com/homebridge-plugins/homebridge-wemo/latest/branding/Homebridge_x_Wemo.png\"></p><p align=\"center\">For help and support please visit our <a href=\"https://github.com/homebridge-plugins/homebridge-wemo/wiki\">GitHub Wiki</a>. We hope you find this plugin useful!</p>",
7
7
  "singular": true,
8
8
  "strictValidation": true,
9
9
  "schema": {
@@ -17,6 +17,22 @@
17
17
  color: #eeeeee !important;
18
18
  font-weight: 500 !important;
19
19
  }
20
+
21
+ .list-group-item {
22
+ background-color: #333333;
23
+ border-color: #444444;
24
+ color: #eeeeee;
25
+ }
26
+
27
+ .text-muted {
28
+ color: #aaaaaa !important;
29
+ }
30
+ }
31
+ .min-w-50 {
32
+ min-width: 50px;
33
+ }
34
+ .action-desc {
35
+ font-size: 0.85rem;
20
36
  }
21
37
  select {
22
38
  background-image:
@@ -60,11 +76,46 @@
60
76
  <div id="pageDevices" class="mt-4" style="display: none;">
61
77
  <div id="deviceInfo">
62
78
  <form>
63
- <div class="form-group">
79
+ <div class="form-group mb-0">
64
80
  <select class="form-control" id="deviceSelect"></select>
65
81
  </div>
66
82
  </form>
67
- <table class="table w-100 mt-3" id="deviceTable" style="display: none;">
83
+ <ul class="list-group mt-4 mb-4 mx-0" id="configActionList" style="display: none;">
84
+ <li class="list-group-item">
85
+ <div class="d-flex justify-content-between align-items-center">
86
+ <div class="pr-2">
87
+ <div>Add All Devices</div>
88
+ <div class="text-muted action-desc">Add all devices to the config that are not already in the config.</div>
89
+ </div>
90
+ <button type="button" class="btn btn-primary m-0 ml-3 py-1 min-w-50" id="addAllBtn" aria-label="Add all devices to the config">
91
+ <i aria-hidden="true" class="fas fa-arrow-right"></i>
92
+ </button>
93
+ </div>
94
+ </li>
95
+ <li class="list-group-item">
96
+ <div class="d-flex justify-content-between align-items-center">
97
+ <div class="pr-2">
98
+ <div id="deviceConfigTitle">Add This Device</div>
99
+ <div class="text-muted action-desc" id="deviceConfigText">Add this device to the plugin config.</div>
100
+ </div>
101
+ <button type="button" class="btn btn-primary m-0 ml-3 py-1 min-w-50" id="deviceConfigBtn" aria-label="Add this device to the config">
102
+ <i aria-hidden="true" class="fas fa-arrow-right"></i>
103
+ </button>
104
+ </div>
105
+ </li>
106
+ <li class="list-group-item">
107
+ <div class="d-flex justify-content-between align-items-center">
108
+ <div class="pr-2">
109
+ <div id="hideDeviceTitle">Hide This Device</div>
110
+ <div class="text-muted action-desc" id="hideDeviceText">Hide this device from HomeKit. This adds the device to the config if needed.</div>
111
+ </div>
112
+ <button type="button" class="btn btn-primary m-0 ml-3 py-1 min-w-50" id="hideDeviceBtn" aria-label="Hide this device from HomeKit">
113
+ <i aria-hidden="true" class="fas fa-arrow-right"></i>
114
+ </button>
115
+ </div>
116
+ </li>
117
+ </ul>
118
+ <table class="table w-100 mt-0" id="deviceTable" style="display: none;">
68
119
  <thead>
69
120
  <tr class="table-active">
70
121
  <th scope="col" style="width: 40%;">Device Name</th>
@@ -210,6 +261,244 @@
210
261
  ;(async () => {
211
262
  try {
212
263
  const currentConfig = await homebridge.getPluginConfig()
264
+
265
+ // ===== Device picker (add/remove/hide config entries from the My Devices tab) =====
266
+ // The control state is always computed from a fresh getPluginConfig() call,
267
+ // so staged-but-unsaved edits made in the Settings tab are respected
268
+ const deviceArrays = [
269
+ 'makerTypes',
270
+ 'wemoInsights',
271
+ 'wemoLights',
272
+ 'wemoLinks',
273
+ 'wemoMotions',
274
+ 'wemoOutlets',
275
+ 'wemoOthers',
276
+ ]
277
+
278
+ // Which config array a device belongs in, from its urn device type
279
+ // (mirrors the platform's own switch). The platform merges every array
280
+ // into one lookup by serial, so an unknown type is safe in wemoOthers.
281
+ const urnToArray = {
282
+ 'urn:Belkin:device:bridge:1': 'wemoLinks',
283
+ 'urn:Belkin:device:controllee:1': 'wemoOutlets',
284
+ 'urn:Belkin:device:dimmer:1': 'wemoLights',
285
+ 'urn:Belkin:device:insight:1': 'wemoInsights',
286
+ 'urn:Belkin:device:lightswitch:1': 'wemoOutlets',
287
+ 'urn:Belkin:device:Maker:1': 'makerTypes',
288
+ 'urn:Belkin:device:NetCamSensor:1': 'wemoMotions',
289
+ 'urn:Belkin:device:outdoor:1': 'wemoOutlets',
290
+ 'urn:Belkin:device:sensor:1': 'wemoMotions',
291
+ }
292
+ let pickerAccessories = []
293
+ let removeConfirmTimer
294
+
295
+ // The action buttons always show the settings-page arrow; the row text
296
+ // (and the button's aria-label) says what the arrow will do
297
+ const actionArrow = '<i aria-hidden="true" class="fas fa-arrow-right"></i>'
298
+
299
+ // Compare serial numbers ignoring case and separators, so an entry typed
300
+ // by hand in a different format still counts as the same device
301
+ const looseId = id => (id || '').toString().toUpperCase().replace(/[^A-Z0-9]+/g, '')
302
+
303
+ const getConfigBlocks = async () => {
304
+ const blocks = await homebridge.getPluginConfig()
305
+ if (!blocks.length) {
306
+ blocks.push({ name: 'Wemo' })
307
+ }
308
+ return blocks
309
+ }
310
+
311
+ const findInConfig = (config, serialNumber) => {
312
+ const target = looseId(serialNumber)
313
+ for (const key of deviceArrays) {
314
+ if (Array.isArray(config[key])) {
315
+ const index = config[key].findIndex(entry => entry && looseId(entry.serialNumber) === target)
316
+ if (index !== -1) {
317
+ return { key, index }
318
+ }
319
+ }
320
+ }
321
+ return null
322
+ }
323
+
324
+ const selectedAccessory = () => {
325
+ const serial = document.getElementById('deviceSelect').value
326
+ return pickerAccessories.find(x => x.context.serialNumber === serial)
327
+ }
328
+
329
+ const setHideControl = hidden => {
330
+ const btn = document.getElementById('hideDeviceBtn')
331
+ btn.dataset.hidden = hidden ? 'yes' : 'no'
332
+ btn.innerHTML = actionArrow
333
+ btn.setAttribute('aria-label', hidden ? 'Show this device in HomeKit' : 'Hide this device from HomeKit')
334
+ document.getElementById('hideDeviceTitle').innerHTML = hidden ? 'Show This Device' : 'Hide This Device'
335
+ document.getElementById('hideDeviceText').innerHTML = hidden
336
+ ? 'Show this device in HomeKit again. The config entry is kept.'
337
+ : 'Hide this device from HomeKit. This adds the device to the config if needed.'
338
+ }
339
+
340
+ const setConfigButton = inConfig => {
341
+ const btn = document.getElementById('deviceConfigBtn')
342
+ clearTimeout(removeConfirmTimer)
343
+ delete btn.dataset.confirm
344
+ btn.dataset.state = inConfig ? 'remove' : 'add'
345
+ btn.classList.remove('btn-danger')
346
+ btn.classList.add('btn-primary')
347
+ btn.innerHTML = actionArrow
348
+ btn.setAttribute('aria-label', inConfig ? 'Remove this device from the config' : 'Add this device to the config')
349
+ document.getElementById('deviceConfigTitle').innerHTML = inConfig ? 'Remove This Device' : 'Add This Device'
350
+ document.getElementById('deviceConfigText').innerHTML = inConfig
351
+ ? 'Remove this device from the plugin config.'
352
+ : 'Add this device to the plugin config.'
353
+ }
354
+
355
+ const addDeviceToConfig = (config, acc) => {
356
+ // Link bulbs carry their hub's serial as linkSerialNumber and belong
357
+ // in wemoLights; anything with an unknown type is safe in wemoOthers
358
+ const arrayKey = acc.context.linkSerialNumber
359
+ ? 'wemoLights'
360
+ : urnToArray[acc.context.deviceType] || 'wemoOthers'
361
+ if (!Array.isArray(config[arrayKey])) {
362
+ config[arrayKey] = []
363
+ }
364
+ // label is included so the entry is identifiable in the config editor
365
+ config[arrayKey].push({ label: acc.displayName, serialNumber: acc.context.serialNumber })
366
+ }
367
+
368
+ document.getElementById('deviceConfigBtn').addEventListener('click', async () => {
369
+ try {
370
+ const btn = document.getElementById('deviceConfigBtn')
371
+ const acc = selectedAccessory()
372
+ if (!acc) {
373
+ return
374
+ }
375
+ const blocks = await getConfigBlocks()
376
+ const config = blocks[0]
377
+ const found = findInConfig(config, acc.context.serialNumber)
378
+ if (btn.dataset.state === 'add') {
379
+ // The config may have changed since the button was drawn, so
380
+ // re-check and never add a duplicate - just correct the button
381
+ if (found) {
382
+ setConfigButton(true)
383
+ setHideControl(!!config[found.key][found.index].ignoreDevice)
384
+ homebridge.toast.info(acc.displayName + ' is already in the config.', 'Not Added')
385
+ return
386
+ }
387
+ addDeviceToConfig(config, acc)
388
+ await homebridge.updatePluginConfig(blocks)
389
+ setConfigButton(true)
390
+ setHideControl(false)
391
+ homebridge.toast.success(acc.displayName + ' added - press Save to apply.', 'Added To Config')
392
+ } else {
393
+ if (!found) {
394
+ setConfigButton(false)
395
+ setHideControl(false)
396
+ homebridge.toast.info(acc.displayName + ' is not in the config.', 'Not Removed')
397
+ return
398
+ }
399
+ const entry = config[found.key][found.index]
400
+ const extraKeys = Object.keys(entry).filter(k => !['serialNumber', 'label'].includes(k))
401
+ if (extraKeys.length > 0 && btn.dataset.confirm !== 'yes') {
402
+ // The entry has settings beyond serial and label which would be
403
+ // lost, so ask for a second click within five seconds
404
+ btn.dataset.confirm = 'yes'
405
+ btn.classList.remove('btn-primary')
406
+ btn.classList.add('btn-danger')
407
+ btn.innerHTML = actionArrow
408
+ btn.setAttribute('aria-label', 'Confirm removing this device from the config')
409
+ document.getElementById('deviceConfigText').innerHTML = 'This entry has extra settings which will be lost. Click again to remove it.'
410
+ removeConfirmTimer = setTimeout(() => setConfigButton(true), 5000)
411
+ return
412
+ }
413
+ config[found.key].splice(found.index, 1)
414
+ if (config[found.key].length === 0) {
415
+ delete config[found.key]
416
+ }
417
+ await homebridge.updatePluginConfig(blocks)
418
+ setConfigButton(false)
419
+ setHideControl(false)
420
+ homebridge.toast.success(acc.displayName + ' removed - press Save to apply.', 'Removed From Config')
421
+ }
422
+ } catch (err) {
423
+ homebridge.toast.error(err.message, 'Error')
424
+ }
425
+ })
426
+
427
+ document.getElementById('hideDeviceBtn').addEventListener('click', async () => {
428
+ try {
429
+ const acc = selectedAccessory()
430
+ if (!acc) {
431
+ return
432
+ }
433
+ const blocks = await getConfigBlocks()
434
+ const config = blocks[0]
435
+ let found = findInConfig(config, acc.context.serialNumber)
436
+ const currentlyHidden = document.getElementById('hideDeviceBtn').dataset.hidden === 'yes'
437
+ if (!currentlyHidden) {
438
+ // Hiding needs a config entry to carry the flag, so create one if
439
+ // the device is not in the config yet
440
+ if (!found) {
441
+ addDeviceToConfig(config, acc)
442
+ found = findInConfig(config, acc.context.serialNumber)
443
+ }
444
+ config[found.key][found.index].ignoreDevice = true
445
+ await homebridge.updatePluginConfig(blocks)
446
+ setConfigButton(true)
447
+ setHideControl(true)
448
+ homebridge.toast.success(acc.displayName + ' will be hidden from HomeKit - press Save to apply.', 'Device Hidden')
449
+ } else {
450
+ if (found) {
451
+ // Drop the flag rather than storing ignoreDevice false, to keep
452
+ // the entry clean
453
+ delete config[found.key][found.index].ignoreDevice
454
+ await homebridge.updatePluginConfig(blocks)
455
+ }
456
+ setConfigButton(!!found)
457
+ setHideControl(false)
458
+ homebridge.toast.success(acc.displayName + ' will be shown in HomeKit - press Save to apply.', 'Device Unhidden')
459
+ }
460
+ } catch (err) {
461
+ homebridge.toast.error(err.message, 'Error')
462
+ }
463
+ })
464
+
465
+ document.getElementById('addAllBtn').addEventListener('click', async () => {
466
+ try {
467
+ const blocks = await getConfigBlocks()
468
+ const config = blocks[0]
469
+ let added = 0
470
+ let skipped = 0
471
+ pickerAccessories.forEach(acc => {
472
+ if (!acc.context?.serialNumber) {
473
+ return
474
+ }
475
+ if (findInConfig(config, acc.context.serialNumber)) {
476
+ skipped += 1
477
+ return
478
+ }
479
+ addDeviceToConfig(config, acc)
480
+ added += 1
481
+ })
482
+ if (added > 0) {
483
+ await homebridge.updatePluginConfig(blocks)
484
+ homebridge.toast.success(added + ' device(s) added, ' + skipped + ' already in the config - press Save to apply.', 'Added To Config')
485
+ } else {
486
+ homebridge.toast.info('All devices are already in the config.', 'Nothing To Add')
487
+ }
488
+ const acc = selectedAccessory()
489
+ if (acc) {
490
+ const nowFound = findInConfig(config, acc.context.serialNumber)
491
+ setConfigButton(!!nowFound)
492
+ if (nowFound) {
493
+ setHideControl(!!config[nowFound.key][nowFound.index].ignoreDevice)
494
+ }
495
+ }
496
+ } catch (err) {
497
+ homebridge.toast.error(err.message, 'Error')
498
+ }
499
+ })
500
+ // ===== End device picker =====
501
+
213
502
  showIntro = () => {
214
503
  homebridge.disableSaveButton?.()
215
504
  const introContinue = document.getElementById('introContinue')
@@ -224,7 +513,9 @@
224
513
  }
225
514
  showDevices = async () => {
226
515
  homebridge.showSpinner()
227
- homebridge.disableSaveButton?.()
516
+ // The save button stays enabled here so entries staged by the picker
517
+ // buttons can be saved without switching back to the Settings tab
518
+ homebridge.enableSaveButton?.()
228
519
  homebridge.hideSchemaForm()
229
520
  document.getElementById('menuHome').classList.remove('btn-elegant')
230
521
  document.getElementById('menuHome').classList.add('btn-primary')
@@ -247,6 +538,7 @@
247
538
  : 0
248
539
  })
249
540
  }
541
+ pickerAccessories = cachedAccessories
250
542
  const deviceSelect = document.getElementById('deviceSelect')
251
543
  deviceSelect.innerHTML = ''
252
544
  cachedAccessories.forEach(a => {
@@ -290,6 +582,11 @@
290
582
  '" style="width: 150px;">'
291
583
  : ''
292
584
  document.getElementById('deviceTable').style.display = 'inline-table'
585
+ const configBlocks = await getConfigBlocks()
586
+ const configEntry = findInConfig(configBlocks[0], context.serialNumber)
587
+ setConfigButton(!!configEntry)
588
+ setHideControl(!!(configEntry && configBlocks[0][configEntry.key][configEntry.index].ignoreDevice))
589
+ document.getElementById('configActionList').style.display = 'block'
293
590
  homebridge.hideSpinner()
294
591
  }
295
592
  deviceSelect.addEventListener('change', event => showDeviceInfo(event.target.value))
@@ -300,6 +597,7 @@
300
597
  option.text = 'No Devices'
301
598
  deviceSelect.add(option)
302
599
  deviceSelect.disabled = true
600
+ document.getElementById('configActionList').style.display = 'none'
303
601
  }
304
602
  homebridge.hideSpinner()
305
603
  }
package/lib/platform.js CHANGED
@@ -794,6 +794,9 @@ export default class {
794
794
  const context = {
795
795
  connection: deviceConf.listenerType ? listenerType : disableUPNP,
796
796
  cbURL: device.cbURL,
797
+ // The urn device type lets the UI device picker place a device in the
798
+ // right config array
799
+ deviceType: device.deviceType,
797
800
  firmware: device.firmwareVersion,
798
801
  hidden: false,
799
802
  icon: device.iconList?.icon?.url || false,
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@homebridge-plugins/homebridge-wemo",
3
3
  "alias": "Wemo",
4
4
  "type": "module",
5
- "version": "7.5.3",
5
+ "version": "7.6.0-beta.0",
6
6
  "description": "Homebridge plugin to integrate Wemo devices into HomeKit.",
7
7
  "author": {
8
8
  "name": "bwp91",