@homebridge-plugins/homebridge-resideo 3.1.4 → 3.2.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
@@ -1,3 +1,15 @@
1
+ ## v3.2.0 (Pending Release)
2
+
3
+ ### Changed
4
+
5
+ - feat(ui): add, remove and hide devices from the config via the devices tab
6
+
7
+ ## v3.1.5 (2026-07-24)
8
+
9
+ ### Changed
10
+
11
+ - fix(ui): spell out that the sign-in window wants the resideo app account, not the developer account (#930) (@dash16)
12
+
1
13
  ## v3.1.4 (2026-07-20)
2
14
 
3
15
  ### Changed
@@ -1,3 +1,19 @@
1
+ <style>
2
+ .min-w-50 {
3
+ min-width: 50px;
4
+ }
5
+ .action-desc {
6
+ font-size: 0.85rem;
7
+ }
8
+ .dark-mode .list-group-item {
9
+ background-color: #333333;
10
+ border-color: #444444;
11
+ color: #eeeeee;
12
+ }
13
+ .dark-mode .text-muted {
14
+ color: #aaaaaa !important;
15
+ }
16
+ </style>
1
17
  <p class="text-center">
2
18
  <img
3
19
  src="https://raw.githubusercontent.com/homebridge-plugins/homebridge-resideo/latest/branding/Homebridge_x_Resideo.svg"
@@ -7,8 +23,13 @@
7
23
  <p class="lead text-center">Thank you for installing <strong>homebridge-resideo</strong></p>
8
24
  <p class="lead text-center">Before continuing:</p>
9
25
  <ol>
10
- <li class="mb-3">Login / create an account at <a href="https://developer.honeywellhome.com/user" target="_blank"
11
- rel="noreferrer noopener">https://developer.honeywellhome.com/user</a>.</li>
26
+ <li class="mb-3">Login / create a <strong>developer</strong> account at <a
27
+ href="https://developer.honeywellhome.com/user" target="_blank"
28
+ rel="noreferrer noopener">https://developer.honeywellhome.com/user</a>.
29
+ <br>
30
+ <small class="text-muted">This is a separate account from the one you use in the Honeywell Home / Resideo
31
+ app. It is only used to generate the key and secret below.</small>
32
+ </li>
12
33
  <li class="mb-3">Click <strong>Create New App</strong>.</li>
13
34
  <li class="mb-3">
14
35
  Give your application a name, and enter the 'Callback URL' exactly as it is displayed below.
@@ -63,6 +84,17 @@
63
84
  <label for="inputConsumerSecret">Consumer Secret <span class="text-danger">*</span></label>
64
85
  <input type="text" class="form-control" id="inputConsumerSecret">
65
86
  </div>
87
+ <div class="alert alert-info">
88
+ <strong>Which account does the next window want?</strong>
89
+ <br>
90
+ Continuing opens a Honeywell/Resideo sign-in window. Sign in there with your
91
+ <strong>Honeywell Home / Resideo app</strong> account &mdash; the same email and password you use in the phone
92
+ app to see your thermostats. That is <em>not</em> the developer account you used above, and your browser may try
93
+ to autofill the wrong one.
94
+ <br>
95
+ <small>If that account will not sign in even though the password is right, use <strong>Forgot
96
+ password</strong> in that window and reset it &mdash; that has fixed it for others.</small>
97
+ </div>
66
98
  <div class="form-group text-center">
67
99
  <button type="button" class="btn btn-primary" id="backToIntro">&larr;</button>
68
100
  <button type="button" class="btn btn-primary" id="linkButton">Continue &rarr;</button>
@@ -72,11 +104,46 @@
72
104
  <div id="pageDevices" class="mt-4" style="display: none;">
73
105
  <div id="deviceInfo">
74
106
  <form>
75
- <div class="form-group">
107
+ <div class="form-group mb-0">
76
108
  <select class="form-control" id="deviceSelect"></select>
77
109
  </div>
78
110
  </form>
79
- <table class="table w-100" id="deviceTable" style="display: none;">
111
+ <ul class="list-group mt-4 mb-4 mx-0" id="configActionList" style="display: none;">
112
+ <li class="list-group-item">
113
+ <div class="d-flex justify-content-between align-items-center">
114
+ <div class="pr-2">
115
+ <div>Add All Devices</div>
116
+ <div class="text-muted action-desc">Add all devices to the config that are not already in the config.</div>
117
+ </div>
118
+ <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">
119
+ <i aria-hidden="true" class="fas fa-arrow-right"></i>
120
+ </button>
121
+ </div>
122
+ </li>
123
+ <li class="list-group-item">
124
+ <div class="d-flex justify-content-between align-items-center">
125
+ <div class="pr-2">
126
+ <div id="deviceConfigTitle">Add This Device</div>
127
+ <div class="text-muted action-desc" id="deviceConfigText">Add this device to the plugin config.</div>
128
+ </div>
129
+ <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">
130
+ <i aria-hidden="true" class="fas fa-arrow-right"></i>
131
+ </button>
132
+ </div>
133
+ </li>
134
+ <li class="list-group-item">
135
+ <div class="d-flex justify-content-between align-items-center">
136
+ <div class="pr-2">
137
+ <div id="hideDeviceTitle">Hide This Device</div>
138
+ <div class="text-muted action-desc" id="hideDeviceText">Hide this device from HomeKit. This adds the device to the config if needed.</div>
139
+ </div>
140
+ <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">
141
+ <i aria-hidden="true" class="fas fa-arrow-right"></i>
142
+ </button>
143
+ </div>
144
+ </li>
145
+ </ul>
146
+ <table class="table w-100 mt-0" id="deviceTable" style="display: none;">
80
147
  <thead>
81
148
  <tr class="table-active">
82
149
  <th scope="col" style="width: 40%;">Device Name</th>
@@ -165,6 +232,227 @@
165
232
  ; (async () => {
166
233
  try {
167
234
  const currentConfig = await homebridge.getPluginConfig()
235
+
236
+ // ===== Device picker (add/remove/hide config entries from the Devices tab) =====
237
+ // The control state is always computed from a fresh getPluginConfig() call,
238
+ // so staged-but-unsaved edits made in the Settings tab are respected
239
+ let pickerAccessories = []
240
+ let removeConfirmTimer
241
+
242
+ // The action buttons always show the settings-page arrow; the row text
243
+ // (and the button's aria-label) says what the arrow will do
244
+ const actionArrow = '<i aria-hidden="true" class="fas fa-arrow-right"></i>'
245
+
246
+ // The platform merges device config by exact trimmed id, so the
247
+ // picker mirrors that rather than a looser comparison
248
+ const cleanId = id => (id || '').toString().trim()
249
+
250
+ const getConfigBlocks = async () => {
251
+ const blocks = await homebridge.getPluginConfig()
252
+ if (!blocks.length) {
253
+ blocks.push({ name: 'Resideo' })
254
+ }
255
+ return blocks
256
+ }
257
+
258
+ // The devices array lives nested under options
259
+ const getDevicesArray = (config, create) => {
260
+ if (!config.options || typeof config.options !== 'object') {
261
+ if (!create) {
262
+ return null
263
+ }
264
+ config.options = {}
265
+ }
266
+ if (!Array.isArray(config.options.devices)) {
267
+ if (!create) {
268
+ return null
269
+ }
270
+ config.options.devices = []
271
+ }
272
+ return config.options.devices
273
+ }
274
+
275
+ const findInConfig = (config, deviceID) => {
276
+ const target = cleanId(deviceID)
277
+ const devices = getDevicesArray(config, false)
278
+ if (!devices) {
279
+ return null
280
+ }
281
+ const index = devices.findIndex(entry => entry && cleanId(entry.deviceID) === target)
282
+ return index === -1 ? null : { index }
283
+ }
284
+
285
+ const selectedAccessory = () => {
286
+ const uuid = document.getElementById('deviceSelect').value
287
+ return pickerAccessories.find(x => x.UUID === uuid)
288
+ }
289
+
290
+ const setHideControl = hidden => {
291
+ const btn = document.getElementById('hideDeviceBtn')
292
+ btn.dataset.hidden = hidden ? 'yes' : 'no'
293
+ btn.innerHTML = actionArrow
294
+ btn.setAttribute('aria-label', hidden ? 'Show this device in HomeKit' : 'Hide this device from HomeKit')
295
+ document.getElementById('hideDeviceTitle').innerHTML = hidden ? 'Show This Device' : 'Hide This Device'
296
+ document.getElementById('hideDeviceText').innerHTML = hidden
297
+ ? 'Show this device in HomeKit again. The config entry is kept.'
298
+ : 'Hide this device from HomeKit. This adds the device to the config if needed.'
299
+ }
300
+
301
+ const setConfigButton = inConfig => {
302
+ const btn = document.getElementById('deviceConfigBtn')
303
+ clearTimeout(removeConfirmTimer)
304
+ delete btn.dataset.confirm
305
+ btn.dataset.state = inConfig ? 'remove' : 'add'
306
+ btn.classList.remove('btn-danger')
307
+ btn.classList.add('btn-primary')
308
+ btn.innerHTML = actionArrow
309
+ btn.setAttribute('aria-label', inConfig ? 'Remove this device from the config' : 'Add this device to the config')
310
+ document.getElementById('deviceConfigTitle').innerHTML = inConfig ? 'Remove This Device' : 'Add This Device'
311
+ document.getElementById('deviceConfigText').innerHTML = inConfig
312
+ ? 'Remove this device from the plugin config.'
313
+ : 'Add this device to the plugin config.'
314
+ }
315
+
316
+ const addDeviceToConfig = (config, acc) => {
317
+ const devices = getDevicesArray(config, true)
318
+ // configDeviceName is included so the entry is identifiable in the
319
+ // config editor; it matches the current name so nothing is renamed
320
+ devices.push({ deviceID: acc.context.deviceID, configDeviceName: acc.displayName })
321
+ }
322
+
323
+ document.getElementById('deviceConfigBtn').addEventListener('click', async () => {
324
+ try {
325
+ const btn = document.getElementById('deviceConfigBtn')
326
+ const acc = selectedAccessory()
327
+ if (!acc) {
328
+ return
329
+ }
330
+ const blocks = await getConfigBlocks()
331
+ const config = blocks[0]
332
+ const found = findInConfig(config, acc.context.deviceID)
333
+ if (btn.dataset.state === 'add') {
334
+ // The config may have changed since the button was drawn, so
335
+ // re-check and never add a duplicate - just correct the button
336
+ if (found) {
337
+ setConfigButton(true)
338
+ setHideControl(!!config.options.devices[found.index].hide_device)
339
+ homebridge.toast.info(acc.displayName + ' is already in the config.', 'Not Added')
340
+ return
341
+ }
342
+ addDeviceToConfig(config, acc)
343
+ await homebridge.updatePluginConfig(blocks)
344
+ setConfigButton(true)
345
+ setHideControl(false)
346
+ homebridge.toast.success(acc.displayName + ' added - press Save to apply.', 'Added To Config')
347
+ } else {
348
+ if (!found) {
349
+ setConfigButton(false)
350
+ setHideControl(false)
351
+ homebridge.toast.info(acc.displayName + ' is not in the config.', 'Not Removed')
352
+ return
353
+ }
354
+ const entry = config.options.devices[found.index]
355
+ const extraKeys = Object.keys(entry).filter(k => !['deviceID', 'configDeviceName'].includes(k))
356
+ if (extraKeys.length > 0 && btn.dataset.confirm !== 'yes') {
357
+ // The entry has settings beyond id and name which would be
358
+ // lost, so ask for a second click within five seconds
359
+ btn.dataset.confirm = 'yes'
360
+ btn.classList.remove('btn-primary')
361
+ btn.classList.add('btn-danger')
362
+ btn.innerHTML = actionArrow
363
+ btn.setAttribute('aria-label', 'Confirm removing this device from the config')
364
+ document.getElementById('deviceConfigText').innerHTML = 'This entry has extra settings which will be lost. Click again to remove it.'
365
+ removeConfirmTimer = setTimeout(() => setConfigButton(true), 5000)
366
+ return
367
+ }
368
+ config.options.devices.splice(found.index, 1)
369
+ if (config.options.devices.length === 0) {
370
+ delete config.options.devices
371
+ }
372
+ await homebridge.updatePluginConfig(blocks)
373
+ setConfigButton(false)
374
+ setHideControl(false)
375
+ homebridge.toast.success(acc.displayName + ' removed - press Save to apply.', 'Removed From Config')
376
+ }
377
+ } catch (err) {
378
+ homebridge.toast.error(err.message, 'Error')
379
+ }
380
+ })
381
+
382
+ document.getElementById('hideDeviceBtn').addEventListener('click', async () => {
383
+ try {
384
+ const acc = selectedAccessory()
385
+ if (!acc) {
386
+ return
387
+ }
388
+ const blocks = await getConfigBlocks()
389
+ const config = blocks[0]
390
+ let found = findInConfig(config, acc.context.deviceID)
391
+ const currentlyHidden = document.getElementById('hideDeviceBtn').dataset.hidden === 'yes'
392
+ if (!currentlyHidden) {
393
+ // Hiding needs a config entry to carry the flag, so create
394
+ // one if the device is not in the config yet
395
+ if (!found) {
396
+ addDeviceToConfig(config, acc)
397
+ found = findInConfig(config, acc.context.deviceID)
398
+ }
399
+ config.options.devices[found.index].hide_device = true
400
+ await homebridge.updatePluginConfig(blocks)
401
+ setConfigButton(true)
402
+ setHideControl(true)
403
+ homebridge.toast.success(acc.displayName + ' will be hidden from HomeKit - press Save to apply.', 'Device Hidden')
404
+ } else {
405
+ if (found) {
406
+ // Drop the flag rather than storing hide_device false, to
407
+ // keep the entry clean
408
+ delete config.options.devices[found.index].hide_device
409
+ await homebridge.updatePluginConfig(blocks)
410
+ }
411
+ setConfigButton(!!found)
412
+ setHideControl(false)
413
+ homebridge.toast.success(acc.displayName + ' will be shown in HomeKit - press Save to apply.', 'Device Unhidden')
414
+ }
415
+ } catch (err) {
416
+ homebridge.toast.error(err.message, 'Error')
417
+ }
418
+ })
419
+
420
+ document.getElementById('addAllBtn').addEventListener('click', async () => {
421
+ try {
422
+ const blocks = await getConfigBlocks()
423
+ const config = blocks[0]
424
+ let added = 0
425
+ let skipped = 0
426
+ pickerAccessories.forEach(acc => {
427
+ if (!acc.context?.deviceID) {
428
+ return
429
+ }
430
+ if (findInConfig(config, acc.context.deviceID)) {
431
+ skipped += 1
432
+ return
433
+ }
434
+ addDeviceToConfig(config, acc)
435
+ added += 1
436
+ })
437
+ if (added > 0) {
438
+ await homebridge.updatePluginConfig(blocks)
439
+ homebridge.toast.success(added + ' device(s) added, ' + skipped + ' already in the config - press Save to apply.', 'Added To Config')
440
+ } else {
441
+ homebridge.toast.info('All devices are already in the config.', 'Nothing To Add')
442
+ }
443
+ const acc = selectedAccessory()
444
+ if (acc) {
445
+ const nowFound = findInConfig(config, acc.context.deviceID)
446
+ setConfigButton(!!nowFound)
447
+ if (nowFound) {
448
+ setHideControl(!!config.options.devices[nowFound.index].hide_device)
449
+ }
450
+ }
451
+ } catch (err) {
452
+ homebridge.toast.error(err.message, 'Error')
453
+ }
454
+ })
455
+ // ===== End device picker =====
168
456
  const hostname = window.location.hostname
169
457
  try {
170
458
  await homebridge.request('Start Resideo Login Server')
@@ -287,6 +575,7 @@
287
575
  : 0
288
576
  })
289
577
  }
578
+ pickerAccessories = cachedAccessories
290
579
  const deviceSelect = document.getElementById('deviceSelect')
291
580
  deviceSelect.innerHTML = ''
292
581
  cachedAccessories.forEach(a => {
@@ -304,6 +593,11 @@
304
593
  document.getElementById('model').innerHTML = context.model
305
594
  document.getElementById('firmwareRevision').innerHTML = context.firmwareRevision || 'N/A'
306
595
  document.getElementById('deviceTable').style.display = 'inline-table'
596
+ const configBlocks = await getConfigBlocks()
597
+ const configEntry = findInConfig(configBlocks[0], context.deviceID)
598
+ setConfigButton(!!configEntry)
599
+ setHideControl(!!(configEntry && configBlocks[0].options.devices[configEntry.index].hide_device))
600
+ document.getElementById('configActionList').style.display = 'block'
307
601
  homebridge.hideSpinner()
308
602
  }
309
603
  deviceSelect.addEventListener('change', event => showDeviceInfo(event.target.value))
@@ -314,6 +608,7 @@
314
608
  option.text = 'No Devices'
315
609
  deviceSelect.add(option)
316
610
  deviceSelect.disabled = true
611
+ document.getElementById('configActionList').style.display = 'none'
317
612
  }
318
613
  homebridge.hideSpinner()
319
614
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@homebridge-plugins/homebridge-resideo",
3
3
  "displayName": "Resideo",
4
4
  "type": "module",
5
- "version": "3.1.4",
5
+ "version": "3.2.0-beta.0",
6
6
  "description": "The Resideo plugin allows you to access your Resideo device(s) from HomeKit.",
7
7
  "author": {
8
8
  "name": "bwp91",
@@ -45,6 +45,7 @@
45
45
  },
46
46
  "keywords": [
47
47
  "homebridge-plugin",
48
+ "supports-matter",
48
49
  "homebridge-matter",
49
50
  "honeywell",
50
51
  "honeywell-home",