homebridge-plugin-utils 1.0.0 → 1.2.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.
@@ -1,6 +1,6 @@
1
1
  /* Copyright(C) 2017-2024, HJD (https://github.com/hjdhjd). All rights reserved.
2
2
  *
3
- * webui-featureoptions.mjs: Device feature option webUI.
3
+ * webUi-featureoptions.mjs: Device feature option webUI.
4
4
  */
5
5
  "use strict";
6
6
 
@@ -8,58 +8,116 @@ import { FeatureOptions} from "./featureoptions.js";
8
8
 
9
9
  export class webUiFeatureOptions {
10
10
 
11
- // The current plugin configuration.
12
- currentConfig;
13
-
14
11
  // Table containing the currently displayed feature options.
15
12
  #configTable;
16
13
 
17
14
  // The current controller context.
18
15
  #controller;
19
16
 
17
+ // The current plugin configuration.
18
+ currentConfig;
19
+
20
+ // Table containing the details on the currently selected device.
21
+ deviceStatsTable;
22
+
20
23
  // Current list of devices from the Homebridge accessory cache.
21
24
  #devices;
22
25
 
26
+ // Table containing the list of devices.
27
+ devicesTable;
28
+
23
29
  // Feature options instance.
24
30
  #featureOptions;
25
31
 
26
- // Device sidebar category name.
27
- #sidebar;
32
+ // Get devices handler.
33
+ #getDevices;
28
34
 
29
35
  // Enable the use of controllers.
30
- #useControllers;
36
+ #hasControllers;
31
37
 
32
- // Current list of devices on a given controller, for webUI elements.
33
- #webuiDeviceList;
38
+ // Device information panel handler.
39
+ #infoPanel;
40
+
41
+ // Sidebar configuration parameters.
42
+ #sidebar;
43
+
44
+ // Options UI configuration parameters.
45
+ #ui;
46
+
47
+ // Current list of controllers, for webUI elements.
48
+ webUiControllerList;
34
49
 
35
- constructor({ sidebar = "Devices", useControllers = true } = {}) {
50
+ // Current list of devices on a given controller, for webUI elements.
51
+ webUiDeviceList;
52
+
53
+ /**
54
+ * Display the feature option webUI. All webUI configuration settings are optional.
55
+ *
56
+ * getDevices - return an array of displays to be displayed.
57
+ * hasControllers - true (default) if the plugin hierarchically has controllers and then devices managed by each controller, rather than just devices.
58
+ * infoPanel - handler to display information in the device detail information panel.
59
+ * ui - customize which options are displayed in the feature option webUI:
60
+ * isController - validate whether a given device is a controller. Returns true or false.
61
+ * validOption - validate whether an option is valid on a given device (or controller).
62
+ * validCategory - validate whether a category of options is valid for a given device (or controller).
63
+ * sidebar - customize the sidebar for the feature option webUI:
64
+ * controllerLabel - label to use for the controllers category. Defaults to "Controllers".
65
+ * deviceLabel - label to use for the devices category. Defaults to "Devices".
66
+ * showDevices - handler for enumerating devices in the sidebar.
67
+ */
68
+ constructor(options = {}) {
69
+
70
+ // Defaults for the feature option webUI sidebar.
71
+ this.ui = {
72
+
73
+ isController: () => false,
74
+ validOption: () => true,
75
+ validOptionCategory: () => true
76
+ };
77
+
78
+ // Defaults for the feature option webUI sidebar.
79
+ this.sidebar = {
80
+
81
+ controllerLabel: "Controllers",
82
+ deviceLabel: "Devices",
83
+ showDevices: this.#showSidebarDevices.bind(this)
84
+ };
85
+
86
+ // Defaults for the feature option webUI.
87
+ const {
88
+
89
+ getDevices = this.#getHomebridgeDevices,
90
+ hasControllers = true,
91
+ infoPanel = this.#showDeviceInfoPanel,
92
+ sidebar = {},
93
+ ui = {}
94
+ } = options;
36
95
 
37
96
  this.configTable = document.getElementById("configTable");
38
97
  this.controller = null;
39
98
  this.currentConfig = [];
99
+ this.deviceStatsTable = document.getElementById("deviceStatsTable");
40
100
  this.devices = [];
101
+ this.devicesTable = document.getElementById("devicesTable");
41
102
  this.featureOptions = null;
42
- this.sidebarName = sidebar;
43
- this.useControllers = useControllers;
44
- this.webuiDeviceList = [];
103
+ this.getDevices = getDevices;
104
+ this.hasControllers = hasControllers;
105
+ this.infoPanel = infoPanel;
106
+ this.sidebar = Object.assign({}, this.sidebar, sidebar);
107
+ this.ui = Object.assign({}, this.ui, ui);
108
+ this.webUiControllerList = [];
109
+ this.webUiDeviceList = [];
45
110
  }
46
111
 
47
- // Render the feature option webUI.
48
- async showUI() {
112
+ /**
113
+ * Render the feature options webUI.
114
+ */
115
+ async show() {
49
116
 
50
117
  // Show the beachball while we setup.
51
118
  homebridge.showSpinner();
52
119
  homebridge.hideSchemaForm();
53
120
 
54
- // Make sure we have the refreshed configuration.
55
- this.currentConfig = await homebridge.getPluginConfig();
56
-
57
- // Retrieve the set of feature options available to us.
58
- const features = (await homebridge.request("/getOptions")) ?? [];
59
-
60
- // Initialize our feature option configuration.
61
- this.featureOptions = new FeatureOptions(features.categories, features.options, this.currentConfig[0].options ?? []);
62
-
63
121
  // Create our custom UI.
64
122
  document.getElementById("menuHome").classList.remove("btn-elegant");
65
123
  document.getElementById("menuHome").classList.add("btn-primary");
@@ -72,27 +130,58 @@ export class webUiFeatureOptions {
72
130
  document.getElementById("pageSupport").style.display = "none";
73
131
  document.getElementById("pageFeatureOptions").style.display = "block";
74
132
 
75
- // What we're going to do is display our global options, followed by the list of devices from the Homebridge accessory cache.
76
- // We pre-select our global options by default for the user as a starting point.
133
+ // Make sure we have the refreshed configuration.
134
+ this.currentConfig = await homebridge.getPluginConfig();
135
+
136
+ // Retrieve the set of feature options available to us.
137
+ const features = (await homebridge.request("/getOptions")) ?? [];
138
+
139
+ // Initialize our feature option configuration.
140
+ this.featureOptions = new FeatureOptions(features.categories, features.options, this.currentConfig[0].options ?? []);
141
+
142
+ // We render our global options, followed by either a list of controllers (if so configured) or by a list of devices from the Homebridge accessory cache.
77
143
 
78
144
  // Retrieve the table for the our list of controllers and global options.
79
145
  const controllersTable = document.getElementById("controllersTable");
80
146
 
81
147
  // Start with a clean slate.
82
148
  controllersTable.innerHTML = "";
83
- document.getElementById("devicesTable").innerHTML = "";
149
+ this.devicesTable.innerHTML = "";
84
150
  this.configTable.innerHTML = "";
85
- this.webuiDeviceList = [];
151
+ this.webUiDeviceList = [];
152
+
153
+ // Create our hover style for our sidebar.
154
+ const sidebarHoverStyle = document.createElement("style");
155
+
156
+ // We emulate the styles that Bootstrap uses when hovering over a table, accounting for both light and dark modes.
157
+ sidebarHoverStyle.innerHTML = "@media (prefers-color-scheme: dark) { .hbup-hover td:hover { background-color: #212121; color: #FFA000 } }" +
158
+ "@media (prefers-color-scheme: light) { .hbup-hover td:hover { background-color: #ECECEC; } }";
159
+
160
+ document.head.appendChild(sidebarHoverStyle);
161
+
162
+ // Add our hover styles to the controllers and devices tables.
163
+ controllersTable.classList.add("hbup-hover");
164
+ this.devicesTable.classList.add("hbup-hover");
86
165
 
87
166
  // Hide the UI until we're ready.
88
167
  document.getElementById("sidebar").style.display = "none";
89
168
  document.getElementById("headerInfo").style.display = "none";
90
169
  document.getElementById("deviceStatsTable").style.display = "none";
91
170
 
171
+ // If we haven't configured any controllers, we're done.
172
+ if(this.hasControllers && !this.currentConfig[0]?.controllers?.length) {
173
+
174
+ document.getElementById("headerInfo").innerHTML = "Please configure a controller to access in the main settings tab before configuring feature options.";
175
+ document.getElementById("headerInfo").style.display = "";
176
+ homebridge.hideSpinner();
177
+
178
+ return;
179
+ }
180
+
92
181
  // Initialize our informational header.
93
182
  document.getElementById("headerInfo").innerHTML = "Feature options are applied in prioritized order, from global to device-specific options:" +
94
183
  "<br><i class=\"text-warning\">Global options</i> (lowest priority) &rarr; " +
95
- (this.useControllers ? "<i class=\"text-success\">Controller options</i> &rarr; " : "") +
184
+ (this.hasControllers ? "<i class=\"text-success\">Controller options</i> &rarr; " : "") +
96
185
  "<i class=\"text-info\">Device options</i> (highest priority)";
97
186
 
98
187
  // Enumerate our global options.
@@ -100,7 +189,8 @@ export class webUiFeatureOptions {
100
189
 
101
190
  // Create the cell for our global options.
102
191
  const tdGlobal = document.createElement("td");
103
- tdGlobal.classList.add("m-0", "p-0");
192
+
193
+ tdGlobal.classList.add("m-0", "p-0", "w-100");
104
194
 
105
195
  // Create our label target.
106
196
  const globalLabel = document.createElement("label");
@@ -108,9 +198,9 @@ export class webUiFeatureOptions {
108
198
  globalLabel.name = "Global Options";
109
199
  globalLabel.appendChild(document.createTextNode("Global Options"));
110
200
  globalLabel.style.cursor = "pointer";
111
- globalLabel.classList.add("mx-2", "my-0", "p-0", "w-100");
201
+ globalLabel.classList.add("m-0", "p-0", "pl-1", "w-100");
112
202
 
113
- globalLabel.addEventListener("click", () => this.#showDevices(true));
203
+ globalLabel.addEventListener("click", () => this.#showSidebar(null));
114
204
 
115
205
  // Add the global options label.
116
206
  tdGlobal.appendChild(globalLabel);
@@ -122,136 +212,152 @@ export class webUiFeatureOptions {
122
212
  // Now add it to the overall controllers table.
123
213
  controllersTable.appendChild(trGlobal);
124
214
 
125
- // Add it as another device, for UI purposes.
126
- this.webuiDeviceList.push(globalLabel);
127
-
128
- // All done. Let the user interact with us.
129
- homebridge.hideSpinner();
130
-
131
- // Default the user on our global settings.
132
- this.#showDevices(true);
133
- }
134
-
135
- // Show the device list.
136
- async #showDevices(isGlobal) {
215
+ // Add it as another controller of device, for UI purposes.
216
+ (this.hasControllers ? this.webUiControllerList : this.webUiDeviceList).push(globalLabel);
137
217
 
138
- // Show the beachball while we setup.
139
- homebridge.showSpinner();
140
-
141
- const devicesTable = document.getElementById("devicesTable");
142
- this.devices = [];
143
-
144
- // If we're not accessing global options, pull the list of devices this plugin knows about from Homebridge.
145
- this.devices = (await homebridge.getCachedAccessories()).map(x => ({
146
- firmwareVersion: (x.services.find(service => service.constructorName ===
147
- "AccessoryInformation")?.characteristics.find(characteristic => characteristic.constructorName === "FirmwareRevision")?.value ?? ""),
148
- name: x.displayName,
149
- serial: (x.services.find(service => service.constructorName ===
150
- "AccessoryInformation")?.characteristics.find(characteristic => characteristic.constructorName === "SerialNumber")?.value ?? "")
151
- }));
218
+ if(this.hasControllers) {
152
219
 
153
- // Sort it for posterity.
154
- this.devices?.sort((a, b) => {
220
+ // Create a row for our controllers.
221
+ const trController = document.createElement("tr");
155
222
 
156
- const aCase = (a.name ?? "").toLowerCase();
157
- const bCase = (b.name ?? "").toLowerCase();
223
+ // Disable any pointer events and hover activity.
224
+ trController.style.pointerEvents = "none";
158
225
 
159
- return aCase > bCase ? 1 : (bCase > aCase ? -1 : 0);
160
- });
226
+ // Create the cell for our controller category row.
227
+ const tdController = document.createElement("td");
161
228
 
162
- // Make the UI visible.
163
- document.getElementById("sidebar").style.display = "";
164
- document.getElementById("headerInfo").style.display = "";
165
-
166
- // Wipe out the device list, except for our global entry.
167
- this.webuiDeviceList.splice(1, this.webuiDeviceList.length);
168
-
169
- // Start with a clean slate.
170
- devicesTable.innerHTML = "";
171
-
172
- // Show the devices list only if we have actual devices to show.
173
- if(this.devices?.length) {
174
-
175
- // Create a row for this device category.
176
- const trCategory = document.createElement("tr");
177
-
178
- // Create the cell for our device category row.
179
- const tdCategory = document.createElement("td");
180
- tdCategory.classList.add("m-0", "p-0");
229
+ tdController.classList.add("m-0", "p-0", "pl-1", "w-100");
181
230
 
182
231
  // Add the category name, with appropriate casing.
183
- tdCategory.appendChild(document.createTextNode(this.sidebarName));
184
- tdCategory.style.fontWeight = "bold";
232
+ tdController.appendChild(document.createTextNode(this.sidebar.controllerLabel));
233
+ tdController.style.fontWeight = "bold";
185
234
 
186
235
  // Add the cell to the table row.
187
- trCategory.appendChild(tdCategory);
236
+ trController.appendChild(tdController);
188
237
 
189
238
  // Add the table row to the table.
190
- devicesTable.appendChild(trCategory);
239
+ controllersTable.appendChild(trController);
191
240
 
192
- for(const device of this.devices) {
241
+ for(const controller of this.currentConfig[0].controllers) {
193
242
 
194
- // Create a row for this device.
243
+ // Create a row for this controller.
195
244
  const trDevice = document.createElement("tr");
245
+
196
246
  trDevice.classList.add("m-0", "p-0");
197
247
 
198
- // Create a cell for our device.
248
+ // Create a cell for our controller.
199
249
  const tdDevice = document.createElement("td");
250
+
200
251
  tdDevice.classList.add("m-0", "p-0", "w-100");
201
252
 
202
253
  const label = document.createElement("label");
203
254
 
204
- label.name = device.serial;
205
- label.appendChild(document.createTextNode(device.name ?? "Unknown"));
255
+ label.name = controller.address;
256
+ label.appendChild(document.createTextNode(controller.address));
206
257
  label.style.cursor = "pointer";
207
258
  label.classList.add("mx-2", "my-0", "p-0", "w-100");
208
259
 
209
- label.addEventListener("click", () => this.#showDeviceInfo(device.serial));
260
+ label.addEventListener("click", () => this.#showSidebar(controller));
210
261
 
211
- // Add the device label to our cell.
262
+ // Add the controller label to our cell.
212
263
  tdDevice.appendChild(label);
213
264
 
214
265
  // Add the cell to the table row.
215
266
  trDevice.appendChild(tdDevice);
216
267
 
217
268
  // Add the table row to the table.
218
- devicesTable.appendChild(trDevice);
269
+ controllersTable.appendChild(trDevice);
219
270
 
220
- this.webuiDeviceList.push(label);
271
+ this.webUiControllerList.push(label);
221
272
  }
222
273
  }
223
274
 
275
+ // All done. Let the user interact with us.
276
+ homebridge.hideSpinner();
277
+
278
+ // Default the user on our global settings if we have no controller.
279
+ this.#showSidebar(this.hasControllers ? this.currentConfig[0].controllers[0] : null);
280
+ }
281
+
282
+ // Show the device list taking the controller context into account.
283
+ async #showSidebar(controller) {
284
+
285
+ // Show the beachball while we setup.
286
+ homebridge.showSpinner();
287
+
288
+ // Grab the list of devices we're displaying.
289
+ this.devices = await this.getDevices(controller);
290
+
291
+ if(this.hasControllers) {
292
+
293
+ // Make sure we highlight the selected controller so the user knows where we are.
294
+ this.webUiControllerList.map(webUiEntry => (webUiEntry.name === (controller ? controller.address : "Global Options")) ?
295
+ webUiEntry.parentElement.classList.add("bg-info", "text-white") : webUiEntry.parentElement.classList.remove("bg-info", "text-white"));
296
+
297
+ // Unable to connect to the controller for some reason.
298
+ if(controller && !this.devices?.length) {
299
+
300
+ this.devicesTable.innerHTML = "";
301
+ this.configTable.innerHTML = "";
302
+
303
+ document.getElementById("headerInfo").innerHTML = ["Unable to connect to the controller.",
304
+ "Check the Settings tab to verify the controller details are correct.",
305
+ "<code class=\"text-danger\">" + (await homebridge.request("/getErrorMessage")) + "</code>"].join("<br>");
306
+ document.getElementById("headerInfo").style.display = "";
307
+ this.deviceStatsTable.style.display = "none";
308
+
309
+ homebridge.hideSpinner();
310
+
311
+ return;
312
+ }
313
+
314
+ // The first entry returned by getDevices() must always be the controller.
315
+ this.controller = this.devices[0]?.serial ?? null;
316
+ }
317
+
318
+ // Make the UI visible.
319
+ document.getElementById("headerInfo").style.display = "";
320
+ document.getElementById("sidebar").style.display = "";
321
+
322
+ // Wipe out the device list, except for our global entry.
323
+ this.webUiDeviceList.splice(1, this.webUiDeviceList.length);
324
+
325
+ // Start with a clean slate.
326
+ this.devicesTable.innerHTML = "";
327
+
328
+ // Populate our devices sidebar.
329
+ this.sidebar.showDevices(controller, this.devices);
330
+
224
331
  // Display the feature options to the user.
225
- this.#showDeviceInfo(isGlobal ? "Global Options" : this.devices[0].serial);
332
+ this.showDeviceOptions(controller ? this.devices[0].serial : "Global Options");
226
333
 
227
334
  // All done. Let the user interact with us.
228
335
  homebridge.hideSpinner();
229
336
  }
230
337
 
231
338
  // Show feature option information for a specific device, controller, or globally.
232
- async #showDeviceInfo(deviceId) {
339
+ async showDeviceOptions(deviceId) {
233
340
 
234
341
  homebridge.showSpinner();
235
342
 
236
343
  // Update the selected device for visibility.
237
- this.webuiDeviceList.map(x => (x.name === deviceId) ?
238
- x.parentElement.classList.add("bg-info", "text-white") : x.parentElement.classList.remove("bg-info", "text-white"));
344
+ this.webUiDeviceList.map(webUiEntry => (webUiEntry.name === deviceId) ?
345
+ webUiEntry.parentElement.classList.add("bg-info", "text-white") : webUiEntry.parentElement.classList.remove("bg-info", "text-white"));
239
346
 
240
347
  // Populate the device information info pane.
241
- const currentDevice = this.devices.find(x => x.serial === deviceId);
242
- this.controller = currentDevice?.serial;
348
+ const currentDevice = this.devices.find(device => device.serial === deviceId);
243
349
 
244
- // Ensure we have a controller or device. The only time this won't be the case is when we're looking at global options.
245
- if(currentDevice) {
350
+ // Populate the details view. If there's no device specified, the context is considered global and we hide the device details view.
351
+ if(!currentDevice) {
352
+
353
+ this.deviceStatsTable.style.display = "none";
354
+ }
246
355
 
247
- document.getElementById("device_firmware").innerHTML = currentDevice.firmwareVersion;
248
- document.getElementById("device_serial").innerHTML = currentDevice.serial;
249
- document.getElementById("deviceStatsTable").style.display = "";
250
- } else {
356
+ this.infoPanel(currentDevice);
357
+
358
+ if(currentDevice) {
251
359
 
252
- document.getElementById("deviceStatsTable").style.display = "none";
253
- document.getElementById("device_firmware").innerHTML = "N/A";
254
- document.getElementById("device_serial").innerHTML = "N/A";
360
+ this.deviceStatsTable.style.display = "";
255
361
  }
256
362
 
257
363
  // Start with a clean slate.
@@ -259,6 +365,12 @@ export class webUiFeatureOptions {
259
365
 
260
366
  for(const category of this.featureOptions.categories) {
261
367
 
368
+ // Validate that we should display this feature option category. This is useful when you want to only display feature option categories for certain device types.
369
+ if(!this.ui.validOptionCategory(currentDevice, category)) {
370
+
371
+ continue;
372
+ }
373
+
262
374
  const optionTable = document.createElement("table");
263
375
  const thead = document.createElement("thead");
264
376
  const tbody = document.createElement("tbody");
@@ -273,7 +385,8 @@ export class webUiFeatureOptions {
273
385
  tbody.classList.add("table-bordered");
274
386
 
275
387
  // Add the feature option category description.
276
- th.appendChild(document.createTextNode(category.description + (!currentDevice ? " (Global)" : " (Device-specific)")));
388
+ th.appendChild(document.createTextNode(category.description + (!currentDevice ? " (Global)" :
389
+ (this.ui.isController(currentDevice) ? " (Controller-specific)" : " (Device-specific)"))));
277
390
 
278
391
  // Add the table header to the row.
279
392
  trFirst.appendChild(th);
@@ -290,11 +403,18 @@ export class webUiFeatureOptions {
290
403
  // Now enumerate all the feature options for a given device.
291
404
  for(const option of this.featureOptions.options[category.name]) {
292
405
 
406
+ // Only show feature options that are valid for this device.
407
+ if(!this.ui.validOption(currentDevice, option)) {
408
+
409
+ continue;
410
+ }
411
+
293
412
  // Expand the full feature option.
294
413
  const featureOption = this.featureOptions.expandOption(category, option);
295
414
 
296
415
  // Create the next table row.
297
416
  const trX = document.createElement("tr");
417
+
298
418
  trX.classList.add("align-top");
299
419
  trX.id = "row-" + featureOption;
300
420
 
@@ -314,7 +434,7 @@ export class webUiFeatureOptions {
314
434
  let initialScope;
315
435
 
316
436
  // Determine our initial option scope to show the user what's been set.
317
- switch(initialScope = this.featureOptions.scope(featureOption, currentDevice?.serial)) {
437
+ switch(initialScope = this.featureOptions.scope(featureOption, currentDevice?.serial, this.controller)) {
318
438
 
319
439
  case "global":
320
440
  case "controller":
@@ -374,6 +494,7 @@ export class webUiFeatureOptions {
374
494
  trX.appendChild(tdCheckbox);
375
495
 
376
496
  const tdLabel = document.createElement("td");
497
+
377
498
  tdLabel.classList.add("w-100");
378
499
  tdLabel.colSpan = 2;
379
500
 
@@ -383,6 +504,7 @@ export class webUiFeatureOptions {
383
504
  if(this.featureOptions.isValue(featureOption)) {
384
505
 
385
506
  const tdInput = document.createElement("td");
507
+
386
508
  tdInput.classList.add("mr-2");
387
509
  tdInput.style.width = "10%";
388
510
 
@@ -397,7 +519,7 @@ export class webUiFeatureOptions {
397
519
 
398
520
  // Find the option in our list and delete it if it exists.
399
521
  const optionRegex = new RegExp("^(?:Enable|Disable)\\." + checkbox.id + (!currentDevice ? "" : ("\\." + currentDevice.serial)) + "\\.[^\\.]+$", "gi");
400
- const newOptions = this.featureOptions.configuredOptions.filter(x => !optionRegex.test(x));
522
+ const newOptions = this.featureOptions.configuredOptions.filter(entry => !optionRegex.test(entry));
401
523
 
402
524
  if(checkbox.checked) {
403
525
 
@@ -425,12 +547,13 @@ export class webUiFeatureOptions {
425
547
 
426
548
  // Create a label for the checkbox with our option description.
427
549
  const labelDescription = document.createElement("label");
550
+
428
551
  labelDescription.for = checkbox.id;
429
552
  labelDescription.style.cursor = "pointer";
430
553
  labelDescription.classList.add("user-select-none", "my-0", "py-0");
431
554
 
432
555
  // Highlight options for the user that are different than our defaults.
433
- const scopeColor = this.featureOptions.color(featureOption, currentDevice?.serial);
556
+ const scopeColor = this.featureOptions.color(featureOption, currentDevice?.serial, this.controller);
434
557
 
435
558
  if(scopeColor) {
436
559
 
@@ -442,7 +565,7 @@ export class webUiFeatureOptions {
442
565
 
443
566
  // Find the option in our list and delete it if it exists.
444
567
  const optionRegex = new RegExp("^(?:Enable|Disable)\\." + checkbox.id + (!currentDevice ? "" : ("\\." + currentDevice.serial)) + "$", "gi");
445
- const newOptions = this.featureOptions.configuredOptions.filter(x => !optionRegex.test(x));
568
+ const newOptions = this.featureOptions.configuredOptions.filter(entry => !optionRegex.test(entry));
446
569
 
447
570
  // Figure out if we've got the option set upstream.
448
571
  let upstreamOption = false;
@@ -535,7 +658,7 @@ export class webUiFeatureOptions {
535
658
  // If we've reset to defaults, make sure our color coding for scope is reflected.
536
659
  if((checkbox.checked === option.default) || checkbox.indeterminate) {
537
660
 
538
- const scopeColor = this.featureOptions.color(featureOption, currentDevice?.serial);
661
+ const scopeColor = this.featureOptions.color(featureOption, currentDevice?.serial, this.controller);
539
662
 
540
663
  if(scopeColor) {
541
664
 
@@ -597,4 +720,117 @@ export class webUiFeatureOptions {
597
720
 
598
721
  homebridge.hideSpinner();
599
722
  }
723
+
724
+ // Our default device information panel handler.
725
+ #showDeviceInfoPanel(device) {
726
+
727
+ const deviceFirmware = document.getElementById("device_firmware") ?? {};
728
+ const deviceSerial = document.getElementById("device_serial") ?? {};
729
+
730
+ // No device specified, we must be in a global context.
731
+ if(!device) {
732
+
733
+ deviceFirmware.innerHTML = "N/A";
734
+ deviceSerial.innerHTML = "N/A";
735
+
736
+ return;
737
+ }
738
+
739
+ // Display our device details.
740
+ deviceFirmware.innerHTML = device.firmwareVersion;
741
+ deviceSerial.innerHTML = device.serial;
742
+ }
743
+
744
+ // Default method for enumerating the device list in the sidebar.
745
+ async #showSidebarDevices() {
746
+
747
+ // Show the devices list only if we have actual devices to show.
748
+ if(!this.devices?.length) {
749
+
750
+ return;
751
+ }
752
+
753
+ // Create a row for this device category.
754
+ const trCategory = document.createElement("tr");
755
+
756
+ // Disable any pointer events and hover activity.
757
+ trCategory.style.pointerEvents = "none";
758
+
759
+ // Create the cell for our device category row.
760
+ const tdCategory = document.createElement("td");
761
+
762
+ tdCategory.classList.add("m-0", "p-0", "pl-1", "w-100");
763
+
764
+ // Add the category name, with appropriate casing.
765
+ tdCategory.appendChild(document.createTextNode(this.sidebar.deviceLabel));
766
+ tdCategory.style.fontWeight = "bold";
767
+
768
+ // Add the cell to the table row.
769
+ trCategory.appendChild(tdCategory);
770
+
771
+ // Add the table row to the table.
772
+ this.devicesTable.appendChild(trCategory);
773
+
774
+ for(const device of this.devices) {
775
+
776
+ // Create a row for this device.
777
+ const trDevice = document.createElement("tr");
778
+
779
+ trDevice.classList.add("m-0", "p-0");
780
+
781
+ // Create a cell for our device.
782
+ const tdDevice = document.createElement("td");
783
+
784
+ tdDevice.classList.add("m-0", "p-0", "w-100");
785
+
786
+ const label = document.createElement("label");
787
+
788
+ label.name = device.serial;
789
+ label.appendChild(document.createTextNode(device.name ?? "Unknown"));
790
+ label.style.cursor = "pointer";
791
+ label.classList.add("mx-2", "my-0", "p-0", "w-100");
792
+
793
+ label.addEventListener("click", () => this.showDeviceOptions(device.serial));
794
+
795
+ // Add the device label to our cell.
796
+ tdDevice.appendChild(label);
797
+
798
+ // Add the cell to the table row.
799
+ trDevice.appendChild(tdDevice);
800
+
801
+ // Add the table row to the table.
802
+ this.devicesTable.appendChild(trDevice);
803
+
804
+ this.webUiDeviceList.push(label);
805
+ }
806
+ }
807
+
808
+ // Default method for retrieving the device list from the Homebridge accessory cache.
809
+ async #getHomebridgeDevices() {
810
+
811
+ // Retrieve the full list of cached accessories.
812
+ let devices = await homebridge.getCachedAccessories();
813
+
814
+ // Filter out only the components we're interested in.
815
+ devices = devices.map(device => ({
816
+
817
+ firmwareVersion: (device.services.find(service => service.constructorName ===
818
+ "AccessoryInformation")?.characteristics.find(characteristic => characteristic.constructorName === "FirmwareRevision")?.value ?? ""),
819
+ name: device.displayName,
820
+ serial: (device.services.find(service => service.constructorName ===
821
+ "AccessoryInformation")?.characteristics.find(characteristic => characteristic.constructorName === "SerialNumber")?.value ?? "")
822
+ }));
823
+
824
+ // Sort it for posterity.
825
+ devices.sort((a, b) => {
826
+
827
+ const aCase = (a.name ?? "").toLowerCase();
828
+ const bCase = (b.name ?? "").toLowerCase();
829
+
830
+ return aCase > bCase ? 1 : (bCase > aCase ? -1 : 0);
831
+ });
832
+
833
+ // Return the list.
834
+ return devices;
835
+ }
600
836
  }