@homebridge-plugins/homebridge-meross 10.16.5 → 10.17.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-meross` will be documented in this file.
4
4
 
5
+ ## v10.17.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
  ## v10.16.5 (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-meross"><img alt="Homebridge Verified" src="https://user-images.githubusercontent.com/43026681/127397024-8b15fc07-f31b-44bd-89e3-51d738d2609a.png" width="600px"></a>
2
+ <a href="https://github.com/homebridge-plugins/homebridge-meross"><img alt="Homebridge Verified" src="https://raw.githubusercontent.com/homebridge-plugins/homebridge-meross/latest/branding/Homebridge_x_Meross.png" width="600px"></a>
3
3
  </p>
4
4
  <span align="center">
5
5
 
@@ -35,7 +35,7 @@ Homebridge plugin to integrate Meross devices into HomeKit
35
35
 
36
36
  - [Installation](https://github.com/homebridge-plugins/homebridge-meross/wiki/Installation)
37
37
  - [Configuration](https://github.com/homebridge-plugins/homebridge-meross/wiki/Configuration)
38
- - [Beta Version](https://github.com/homebridge/homebridge/wiki/How-to-Install-Alternate-Plugin-Versions)
38
+ - [Beta Version](https://github.com/homebridge-plugins/homebridge-meross/wiki/Beta-Version)
39
39
  - [Node Version](https://github.com/homebridge-plugins/homebridge-meross/wiki/Node-Version)
40
40
 
41
41
  ### Features
Binary file
@@ -5,7 +5,7 @@
5
5
  "strictValidation": true,
6
6
  "customUi": true,
7
7
  "customUiPath": "./lib/homebridge-ui",
8
- "headerDisplay": "<p align=\"center\"><img width=\"60%\" src=\"https://user-images.githubusercontent.com/43026681/127397024-8b15fc07-f31b-44bd-89e3-51d738d2609a.png\"></p><p align=\"center\">For help and support please visit our <a href=\"https://github.com/homebridge-plugins/homebridge-meross/wiki\">GitHub Wiki</a>. We hope you find this plugin useful!</p>",
8
+ "headerDisplay": "<p align=\"center\"><img width=\"250px\" src=\"https://raw.githubusercontent.com/homebridge-plugins/homebridge-meross/latest/branding/Homebridge_x_Meross.png\"></p><p align=\"center\">For help and support please visit our <a href=\"https://github.com/homebridge-plugins/homebridge-meross/wiki\">GitHub Wiki</a>. We hope you find this plugin useful!</p>",
9
9
  "schema": {
10
10
  "name": {
11
11
  "type": "string",
@@ -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:
@@ -70,11 +86,46 @@
70
86
  <div id="pageDevices" class="mt-4" style="display: none;">
71
87
  <div id="deviceInfo">
72
88
  <form>
73
- <div class="form-group">
89
+ <div class="form-group mb-0">
74
90
  <select class="form-control" id="deviceSelect"></select>
75
91
  </div>
76
92
  </form>
77
- <table class="table w-100 mt-3" id="deviceTable" style="display: none;">
93
+ <ul class="list-group mt-4 mb-4 mx-0" id="configActionList" style="display: none;">
94
+ <li class="list-group-item">
95
+ <div class="d-flex justify-content-between align-items-center">
96
+ <div class="pr-2">
97
+ <div>Add All Devices</div>
98
+ <div class="text-muted action-desc">Add all devices to the config that are not already in the config.</div>
99
+ </div>
100
+ <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">
101
+ <i aria-hidden="true" class="fas fa-arrow-right"></i>
102
+ </button>
103
+ </div>
104
+ </li>
105
+ <li class="list-group-item">
106
+ <div class="d-flex justify-content-between align-items-center">
107
+ <div class="pr-2">
108
+ <div id="deviceConfigTitle">Add This Device</div>
109
+ <div class="text-muted action-desc" id="deviceConfigText">Add this device to the plugin config.</div>
110
+ </div>
111
+ <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">
112
+ <i aria-hidden="true" class="fas fa-arrow-right"></i>
113
+ </button>
114
+ </div>
115
+ </li>
116
+ <li class="list-group-item">
117
+ <div class="d-flex justify-content-between align-items-center">
118
+ <div class="pr-2">
119
+ <div id="hideDeviceTitle">Hide This Device</div>
120
+ <div class="text-muted action-desc" id="hideDeviceText">Hide this device from HomeKit. This adds the device to the config if needed.</div>
121
+ </div>
122
+ <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">
123
+ <i aria-hidden="true" class="fas fa-arrow-right"></i>
124
+ </button>
125
+ </div>
126
+ </li>
127
+ </ul>
128
+ <table class="table w-100 mt-0" id="deviceTable" style="display: none;">
78
129
  <thead>
79
130
  <tr class="table-active">
80
131
  <th scope="col" style="width: 40%;">Device Name</th>
@@ -114,17 +165,6 @@
114
165
  <th scope="row">Firmware Version</th>
115
166
  <td id="firmware"></td>
116
167
  </tr>
117
- <tr>
118
- <th scope="row">Hide from HomeKit</th>
119
- <td>
120
- <div class="custom-control custom-checkbox">
121
- <input type="checkbox" class="custom-control-input" id="hideDevice">
122
- <label class="custom-control-label" for="hideDevice">
123
- Hide this device from HomeKit
124
- </label>
125
- </div>
126
- </td>
127
- </tr>
128
168
  </tbody>
129
169
  </table>
130
170
  </div>
@@ -242,6 +282,249 @@
242
282
  ;(async () => {
243
283
  try {
244
284
  const currentConfig = await homebridge.getPluginConfig()
285
+
286
+ // ===== Device picker (add/remove/hide config entries from the My Devices tab) =====
287
+ // The control state is always computed from a fresh getPluginConfig() call,
288
+ // so staged-but-unsaved edits made in the Settings tab are respected
289
+ const deviceArrays = [
290
+ 'singleDevices',
291
+ 'multiDevices',
292
+ 'lightDevices',
293
+ 'fanDevices',
294
+ 'diffuserDevices',
295
+ 'purifierDevices',
296
+ 'humidifierDevices',
297
+ 'thermostatDevices',
298
+ 'garageDevices',
299
+ 'rollerDevices',
300
+ 'babyDevices',
301
+ 'sensorDevices',
302
+ ]
303
+ let pickerAccessories = []
304
+ let removeConfirmTimer
305
+
306
+ // The action buttons always show the settings-page arrow; the row text
307
+ // (and the button's aria-label) says what the arrow will do
308
+ const actionArrow = '<i aria-hidden="true" class="fas fa-arrow-right"></i>'
309
+
310
+ // Compare serial numbers ignoring case and separators, so an entry typed
311
+ // by hand in a different format still counts as the same device
312
+ const looseId = id => (id || '').toString().toLowerCase().replace(/[^a-z0-9]+/g, '')
313
+
314
+ const getConfigBlocks = async () => {
315
+ const blocks = await homebridge.getPluginConfig()
316
+ if (!blocks.length) {
317
+ blocks.push({ name: 'Meross' })
318
+ }
319
+ return blocks
320
+ }
321
+
322
+ const findInConfig = (config, serialNumber) => {
323
+ const target = looseId(serialNumber)
324
+ for (const key of deviceArrays) {
325
+ if (Array.isArray(config[key])) {
326
+ const index = config[key].findIndex(entry => entry && looseId(entry.serialNumber) === target)
327
+ if (index !== -1) {
328
+ return { key, index }
329
+ }
330
+ }
331
+ }
332
+ return null
333
+ }
334
+
335
+ const selectedAccessory = () => {
336
+ const uuid = document.getElementById('deviceSelect').value
337
+ return pickerAccessories.find(x => x.UUID === uuid)
338
+ }
339
+
340
+ const setHideControl = hidden => {
341
+ const btn = document.getElementById('hideDeviceBtn')
342
+ btn.dataset.hidden = hidden ? 'yes' : 'no'
343
+ btn.innerHTML = actionArrow
344
+ btn.setAttribute('aria-label', hidden ? 'Show this device in HomeKit' : 'Hide this device from HomeKit')
345
+ document.getElementById('hideDeviceTitle').innerHTML = hidden ? 'Show This Device' : 'Hide This Device'
346
+ document.getElementById('hideDeviceText').innerHTML = hidden
347
+ ? 'Show this device in HomeKit again. The config entry is kept.'
348
+ : 'Hide this device from HomeKit. This adds the device to the config if needed.'
349
+ }
350
+
351
+ const setConfigButton = inConfig => {
352
+ const btn = document.getElementById('deviceConfigBtn')
353
+ clearTimeout(removeConfirmTimer)
354
+ delete btn.dataset.confirm
355
+ btn.dataset.state = inConfig ? 'remove' : 'add'
356
+ btn.classList.remove('btn-danger')
357
+ btn.classList.add('btn-primary')
358
+ btn.innerHTML = actionArrow
359
+ btn.setAttribute('aria-label', inConfig ? 'Remove this device from the config' : 'Add this device to the config')
360
+ document.getElementById('deviceConfigTitle').innerHTML = inConfig ? 'Remove This Device' : 'Add This Device'
361
+ document.getElementById('deviceConfigText').innerHTML = inConfig
362
+ ? 'Remove this device from the plugin config.'
363
+ : 'Add this device to the plugin config.'
364
+ }
365
+
366
+ // Which config array a model belongs in comes from the plugin's own
367
+ // model lists via the UI server; a model in none of them cannot be added
368
+ const addDeviceToConfig = async (config, acc) => {
369
+ const arrayKey = await homebridge.request('/getDeviceConfigKey', { model: acc.context.model })
370
+ if (!arrayKey) {
371
+ return false
372
+ }
373
+ if (!Array.isArray(config[arrayKey])) {
374
+ config[arrayKey] = []
375
+ }
376
+ config[arrayKey].push({ name: acc.displayName, serialNumber: looseId(acc.context.serialNumber) })
377
+ return true
378
+ }
379
+
380
+ document.getElementById('deviceConfigBtn').addEventListener('click', async () => {
381
+ try {
382
+ const btn = document.getElementById('deviceConfigBtn')
383
+ const acc = selectedAccessory()
384
+ if (!acc) {
385
+ return
386
+ }
387
+ const blocks = await getConfigBlocks()
388
+ const config = blocks[0]
389
+ const found = findInConfig(config, acc.context.serialNumber)
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
+ setHideControl(!!config[found.key][found.index].ignoreDevice)
396
+ homebridge.toast.info(acc.displayName + ' is already in the config.', 'Not Added')
397
+ return
398
+ }
399
+ if (!(await addDeviceToConfig(config, acc))) {
400
+ homebridge.toast.error('Unsupported device model: ' + acc.context.model, 'Cannot Add Device')
401
+ return
402
+ }
403
+ await homebridge.updatePluginConfig(blocks)
404
+ setConfigButton(true)
405
+ setHideControl(false)
406
+ homebridge.toast.success(acc.displayName + ' added - press Save to apply.', 'Added To Config')
407
+ } else {
408
+ if (!found) {
409
+ setConfigButton(false)
410
+ setHideControl(false)
411
+ homebridge.toast.info(acc.displayName + ' is not in the config.', 'Not Removed')
412
+ return
413
+ }
414
+ const entry = config[found.key][found.index]
415
+ const extraKeys = Object.keys(entry).filter(k => !['serialNumber', 'name'].includes(k))
416
+ if (extraKeys.length > 0 && btn.dataset.confirm !== 'yes') {
417
+ // The entry has settings beyond serial and name which would be
418
+ // lost, so ask for a second click within five seconds
419
+ btn.dataset.confirm = 'yes'
420
+ btn.classList.remove('btn-primary')
421
+ btn.classList.add('btn-danger')
422
+ btn.innerHTML = actionArrow
423
+ btn.setAttribute('aria-label', 'Confirm removing this device from the config')
424
+ document.getElementById('deviceConfigText').innerHTML = 'This entry has extra settings which will be lost. Click again to remove it.'
425
+ removeConfirmTimer = setTimeout(() => setConfigButton(true), 5000)
426
+ return
427
+ }
428
+ config[found.key].splice(found.index, 1)
429
+ if (config[found.key].length === 0) {
430
+ delete config[found.key]
431
+ }
432
+ await homebridge.updatePluginConfig(blocks)
433
+ setConfigButton(false)
434
+ setHideControl(false)
435
+ homebridge.toast.success(acc.displayName + ' removed - press Save to apply.', 'Removed From Config')
436
+ }
437
+ } catch (err) {
438
+ homebridge.toast.error(err.message, 'Error')
439
+ }
440
+ })
441
+
442
+ document.getElementById('hideDeviceBtn').addEventListener('click', async () => {
443
+ try {
444
+ const acc = selectedAccessory()
445
+ if (!acc) {
446
+ return
447
+ }
448
+ const blocks = await getConfigBlocks()
449
+ const config = blocks[0]
450
+ let found = findInConfig(config, acc.context.serialNumber)
451
+ const currentlyHidden = document.getElementById('hideDeviceBtn').dataset.hidden === 'yes'
452
+ if (!currentlyHidden) {
453
+ // Hiding needs a config entry to carry the flag, so create one if
454
+ // the device is not in the config yet
455
+ if (!found) {
456
+ if (!(await addDeviceToConfig(config, acc))) {
457
+ homebridge.toast.error('Unsupported device model: ' + acc.context.model, 'Cannot Hide Device')
458
+ return
459
+ }
460
+ found = findInConfig(config, acc.context.serialNumber)
461
+ }
462
+ config[found.key][found.index].ignoreDevice = true
463
+ await homebridge.updatePluginConfig(blocks)
464
+ setConfigButton(true)
465
+ setHideControl(true)
466
+ homebridge.toast.success(acc.displayName + ' will be hidden from HomeKit - press Save to apply.', 'Device Hidden')
467
+ } else {
468
+ if (found) {
469
+ // Drop the flag rather than storing ignoreDevice false, to keep
470
+ // the entry clean
471
+ delete config[found.key][found.index].ignoreDevice
472
+ await homebridge.updatePluginConfig(blocks)
473
+ }
474
+ setConfigButton(!!found)
475
+ setHideControl(false)
476
+ homebridge.toast.success(acc.displayName + ' will be shown in HomeKit - press Save to apply.', 'Device Unhidden')
477
+ }
478
+ } catch (err) {
479
+ homebridge.toast.error(err.message, 'Error')
480
+ }
481
+ })
482
+
483
+ document.getElementById('addAllBtn').addEventListener('click', async () => {
484
+ try {
485
+ const blocks = await getConfigBlocks()
486
+ const config = blocks[0]
487
+ let added = 0
488
+ let skipped = 0
489
+ let unsupported = 0
490
+ for (const acc of pickerAccessories) {
491
+ if (!acc.context?.serialNumber) {
492
+ continue
493
+ }
494
+ if (findInConfig(config, acc.context.serialNumber)) {
495
+ skipped += 1
496
+ continue
497
+ }
498
+ if (await addDeviceToConfig(config, acc)) {
499
+ added += 1
500
+ } else {
501
+ unsupported += 1
502
+ }
503
+ }
504
+ if (added > 0) {
505
+ await homebridge.updatePluginConfig(blocks)
506
+ const parts = [added + ' device(s) added', skipped + ' already in the config']
507
+ if (unsupported > 0) {
508
+ parts.push(unsupported + ' unsupported')
509
+ }
510
+ homebridge.toast.success(parts.join(', ') + ' - press Save to apply.', 'Added To Config')
511
+ } else {
512
+ homebridge.toast.info('All devices are already in the config.', 'Nothing To Add')
513
+ }
514
+ const acc = selectedAccessory()
515
+ if (acc) {
516
+ const nowFound = findInConfig(config, acc.context.serialNumber)
517
+ setConfigButton(!!nowFound)
518
+ if (nowFound) {
519
+ setHideControl(!!config[nowFound.key][nowFound.index].ignoreDevice)
520
+ }
521
+ }
522
+ } catch (err) {
523
+ homebridge.toast.error(err.message, 'Error')
524
+ }
525
+ })
526
+ // ===== End device picker =====
527
+
245
528
  showIntro = () => {
246
529
  homebridge.disableSaveButton?.()
247
530
  const introContinue = document.getElementById('introContinue')
@@ -256,7 +539,9 @@
256
539
  }
257
540
  showDevices = async () => {
258
541
  homebridge.showSpinner()
259
- homebridge.disableSaveButton?.()
542
+ // The save button stays enabled here so entries staged by the picker
543
+ // buttons can be saved without switching back to the Settings tab
544
+ homebridge.enableSaveButton?.()
260
545
  homebridge.hideSchemaForm()
261
546
  document.getElementById('menuHome').classList.remove('btn-elegant')
262
547
  document.getElementById('menuHome').classList.add('btn-primary')
@@ -279,6 +564,7 @@
279
564
  : 0
280
565
  })
281
566
  }
567
+ pickerAccessories = cachedAccessories
282
568
  const deviceSelect = document.getElementById('deviceSelect')
283
569
  deviceSelect.innerHTML = ''
284
570
  cachedAccessories.forEach(a => {
@@ -306,80 +592,12 @@
306
592
  document.getElementById('hardware').innerHTML = context.hardware || 'N/A'
307
593
  document.getElementById('firmware').innerHTML = context.firmware || 'N/A'
308
594
 
309
- // Handle hide device checkbox
310
- const hideCheckbox = document.getElementById('hideDevice')
311
- const deviceUUID = context.serialNumber.toLowerCase().replace(/[^a-z0-9]/g, '')
312
- const deviceArrays = [
313
- 'singleDevices', 'multiDevices', 'lightDevices', 'fanDevices',
314
- 'diffuserDevices', 'purifierDevices', 'humidifierDevices',
315
- 'thermostatDevices', 'garageDevices', 'rollerDevices',
316
- 'babyDevices', 'sensorDevices',
317
- ]
318
- const findEntry = (cfg) => {
319
- for (const key of deviceArrays) {
320
- const arr = cfg[0]?.[key]
321
- if (!Array.isArray(arr)) continue
322
- const idx = arr.findIndex(e => e.serialNumber?.toLowerCase().replace(/[^a-z0-9]/g, '') === deviceUUID)
323
- if (idx !== -1) return { key, idx, entry: arr[idx] }
324
- }
325
- return null
326
- }
327
-
328
- // Check if device is currently hidden
329
- const config = await homebridge.getPluginConfig()
330
- const existing = findEntry(config)
331
- hideCheckbox.checked = !!existing?.entry?.ignoreDevice
332
-
333
- // Remove previous event listeners by cloning the element
334
- const newCheckbox = hideCheckbox.cloneNode(true)
335
- hideCheckbox.parentNode.replaceChild(newCheckbox, hideCheckbox)
336
-
337
- // Add event listener for checkbox changes
338
- newCheckbox.addEventListener('change', async (e) => {
339
- try {
340
- homebridge.showSpinner()
341
- const cfg = await homebridge.getPluginConfig()
342
- const found = findEntry(cfg)
343
-
344
- if (e.target.checked) {
345
- if (found) {
346
- cfg[0][found.key][found.idx].ignoreDevice = true
347
- } else {
348
- const arrayKey = await homebridge.request('/getDeviceConfigKey', { model: context.model })
349
- if (!arrayKey) {
350
- homebridge.toast.error(`Unsupported device model: ${context.model}`, 'Cannot hide device')
351
- e.target.checked = false
352
- homebridge.hideSpinner()
353
- return
354
- }
355
- if (!Array.isArray(cfg[0][arrayKey])) {
356
- cfg[0][arrayKey] = []
357
- }
358
- cfg[0][arrayKey].push({
359
- name: thisAcc.displayName,
360
- serialNumber: deviceUUID,
361
- ignoreDevice: true,
362
- })
363
- }
364
- homebridge.toast.success(`${thisAcc.displayName} will be hidden from HomeKit after restart`, 'Device Hidden')
365
- } else if (found) {
366
- cfg[0][found.key][found.idx].ignoreDevice = false
367
- homebridge.toast.success(`${thisAcc.displayName} will be shown in HomeKit after restart`, 'Device Unhidden')
368
- }
369
-
370
- await homebridge.updatePluginConfig(cfg)
371
-
372
- // Switch to Settings tab to sync the form with updated config
373
- showSettings()
374
- homebridge.toast.info('Please click Save to persist your changes', 'Settings Updated')
375
- homebridge.hideSpinner()
376
- } catch (err) {
377
- homebridge.toast.error(err.message, 'Error')
378
- homebridge.hideSpinner()
379
- }
380
- })
381
-
382
595
  document.getElementById('deviceTable').style.display = 'inline-table'
596
+ const configBlocks = await getConfigBlocks()
597
+ const configEntry = findInConfig(configBlocks[0], context.serialNumber)
598
+ setConfigButton(!!configEntry)
599
+ setHideControl(!!(configEntry && configBlocks[0][configEntry.key][configEntry.index].ignoreDevice))
600
+ document.getElementById('configActionList').style.display = 'block'
383
601
  homebridge.hideSpinner()
384
602
  }
385
603
  deviceSelect.addEventListener('change', event => showDeviceInfo(event.target.value))
@@ -390,6 +608,7 @@
390
608
  option.text = 'No Devices'
391
609
  deviceSelect.add(option)
392
610
  deviceSelect.disabled = true
611
+ document.getElementById('configActionList').style.display = 'none'
393
612
  }
394
613
  homebridge.hideSpinner()
395
614
  }
package/lib/platform.js CHANGED
@@ -137,7 +137,10 @@ export default class {
137
137
  return
138
138
  }
139
139
  const entries = Object.entries(x)
140
- if (entries.length < 3) {
140
+ // An entry with just a serial number carries nothing; two keys
141
+ // (serial number plus name) is kept, as that is what the UI
142
+ // device picker creates for users to build on
143
+ if (entries.length < 2) {
141
144
  logRemove(`${key}.${id}`)
142
145
  return
143
146
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@homebridge-plugins/homebridge-meross",
3
3
  "alias": "Meross",
4
4
  "type": "module",
5
- "version": "10.16.5",
5
+ "version": "10.17.0-beta.0",
6
6
  "description": "Homebridge plugin to integrate Meross devices into HomeKit.",
7
7
  "author": {
8
8
  "name": "bwp91",