homebridge-ratgdo 2.1.3 → 2.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,37 +1,47 @@
1
1
  /* Copyright(C) 2017-2024, HJD (https://github.com/hjdhjd). All rights reserved.
2
2
  *
3
- * ratgdo-featureoptions.mjs: Ratgdo feature option webUI.
3
+ * webui-featureoptions.mjs: Device feature option webUI.
4
4
  */
5
5
  "use strict";
6
6
 
7
- import { FeatureOptions} from "./lib/featureoptions.mjs";
7
+ import { FeatureOptions} from "./featureoptions.js";
8
8
 
9
- export class ratgdoFeatureOptions extends FeatureOptions {
9
+ export class webUiFeatureOptions {
10
10
 
11
11
  // The current plugin configuration.
12
12
  currentConfig;
13
13
 
14
- // Current configuration options selected in the webUI for a given device.
15
- #configOptions;
16
-
17
14
  // Table containing the currently displayed feature options.
18
15
  #configTable;
19
16
 
20
- // Current list of devices on a given controller, for webUI elements.
21
- #deviceList;
17
+ // The current controller context.
18
+ #controller;
19
+
20
+ // Current list of devices from the Homebridge accessory cache.
21
+ #devices;
22
22
 
23
- // Current list of Ratgdo devices from the Homebridge accessory cache.
24
- #ratgdoDevices;
23
+ // Feature options instance.
24
+ #featureOptions;
25
25
 
26
- constructor() {
26
+ // Device sidebar category name.
27
+ #sidebar;
27
28
 
28
- super();
29
+ // Enable the use of controllers.
30
+ #useControllers;
31
+
32
+ // Current list of devices on a given controller, for webUI elements.
33
+ #webuiDeviceList;
34
+
35
+ constructor({ sidebar = "Devices", useControllers = true } = {}) {
29
36
 
30
- this.configOptions = [];
31
37
  this.configTable = document.getElementById("configTable");
38
+ this.controller = null;
32
39
  this.currentConfig = [];
33
- this.deviceList = [];
34
- this.ratgdoDevices = [];
40
+ this.devices = [];
41
+ this.featureOptions = null;
42
+ this.sidebarName = sidebar;
43
+ this.useControllers = useControllers;
44
+ this.webuiDeviceList = [];
35
45
  }
36
46
 
37
47
  // Render the feature option webUI.
@@ -44,6 +54,12 @@ export class ratgdoFeatureOptions extends FeatureOptions {
44
54
  // Make sure we have the refreshed configuration.
45
55
  this.currentConfig = await homebridge.getPluginConfig();
46
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
+
47
63
  // Create our custom UI.
48
64
  document.getElementById("menuHome").classList.remove("btn-elegant");
49
65
  document.getElementById("menuHome").classList.add("btn-primary");
@@ -66,18 +82,18 @@ export class ratgdoFeatureOptions extends FeatureOptions {
66
82
  controllersTable.innerHTML = "";
67
83
  document.getElementById("devicesTable").innerHTML = "";
68
84
  this.configTable.innerHTML = "";
69
- this.deviceList = [];
85
+ this.webuiDeviceList = [];
70
86
 
71
87
  // Hide the UI until we're ready.
72
88
  document.getElementById("sidebar").style.display = "none";
73
89
  document.getElementById("headerInfo").style.display = "none";
74
90
  document.getElementById("deviceStatsTable").style.display = "none";
75
91
 
76
- // Get the list of devices the plugin knows about.
77
- const ratgdoDevices = await homebridge.getCachedAccessories();
78
-
79
92
  // Initialize our informational header.
80
- document.getElementById("headerInfo").innerHTML = "Feature options are applied in prioritized order, from global to device-specific options:<br><i class=\"text-warning\">Global options</i> (lowest priority) &rarr; <i class=\"text-info\">Ratgdo device options</i> (highest priority)";
93
+ document.getElementById("headerInfo").innerHTML = "Feature options are applied in prioritized order, from global to device-specific options:" +
94
+ "<br><i class=\"text-warning\">Global options</i> (lowest priority) &rarr; " +
95
+ (this.useControllers ? "<i class=\"text-success\">Controller options</i> &rarr; " : "") +
96
+ "<i class=\"text-info\">Device options</i> (highest priority)";
81
97
 
82
98
  // Enumerate our global options.
83
99
  const trGlobal = document.createElement("tr");
@@ -94,7 +110,7 @@ export class ratgdoFeatureOptions extends FeatureOptions {
94
110
  globalLabel.style.cursor = "pointer";
95
111
  globalLabel.classList.add("mx-2", "my-0", "p-0", "w-100");
96
112
 
97
- globalLabel.addEventListener("click", event => this.#showDevices(true));
113
+ globalLabel.addEventListener("click", () => this.#showDevices(true));
98
114
 
99
115
  // Add the global options label.
100
116
  tdGlobal.appendChild(globalLabel);
@@ -107,7 +123,7 @@ export class ratgdoFeatureOptions extends FeatureOptions {
107
123
  controllersTable.appendChild(trGlobal);
108
124
 
109
125
  // Add it as another device, for UI purposes.
110
- this.deviceList.push(globalLabel);
126
+ this.webuiDeviceList.push(globalLabel);
111
127
 
112
128
  // All done. Let the user interact with us.
113
129
  homebridge.hideSpinner();
@@ -123,18 +139,19 @@ export class ratgdoFeatureOptions extends FeatureOptions {
123
139
  homebridge.showSpinner();
124
140
 
125
141
  const devicesTable = document.getElementById("devicesTable");
126
- this.ratgdoDevices = [];
142
+ this.devices = [];
127
143
 
128
144
  // If we're not accessing global options, pull the list of devices this plugin knows about from Homebridge.
129
- this.ratgdoDevices = (await homebridge.getCachedAccessories()).map(x => ({
145
+ this.devices = (await homebridge.getCachedAccessories()).map(x => ({
130
146
  firmwareVersion: (x.services.find(service => service.constructorName ===
131
147
  "AccessoryInformation")?.characteristics.find(characteristic => characteristic.constructorName === "FirmwareRevision")?.value ?? ""),
132
- mac: (x.services.find(service => service.constructorName ===
133
- "AccessoryInformation")?.characteristics.find(characteristic => characteristic.constructorName === "SerialNumber")?.value ?? ""),
134
- name: x.displayName }));
148
+ name: x.displayName,
149
+ serial: (x.services.find(service => service.constructorName ===
150
+ "AccessoryInformation")?.characteristics.find(characteristic => characteristic.constructorName === "SerialNumber")?.value ?? "")
151
+ }));
135
152
 
136
153
  // Sort it for posterity.
137
- this.ratgdoDevices?.sort((a, b) => {
154
+ this.devices?.sort((a, b) => {
138
155
 
139
156
  const aCase = (a.name ?? "").toLowerCase();
140
157
  const bCase = (b.name ?? "").toLowerCase();
@@ -147,16 +164,13 @@ export class ratgdoFeatureOptions extends FeatureOptions {
147
164
  document.getElementById("headerInfo").style.display = "";
148
165
 
149
166
  // Wipe out the device list, except for our global entry.
150
- this.deviceList.splice(1, this.deviceList.length);
151
-
152
- // We aren't using the concept of a controller category in Ratgdo, so we set this to something innocuous.
153
- this.controller = "NA";
167
+ this.webuiDeviceList.splice(1, this.webuiDeviceList.length);
154
168
 
155
169
  // Start with a clean slate.
156
170
  devicesTable.innerHTML = "";
157
171
 
158
172
  // Show the devices list only if we have actual devices to show.
159
- if(this.ratgdoDevices?.length) {
173
+ if(this.devices?.length) {
160
174
 
161
175
  // Create a row for this device category.
162
176
  const trCategory = document.createElement("tr");
@@ -166,7 +180,7 @@ export class ratgdoFeatureOptions extends FeatureOptions {
166
180
  tdCategory.classList.add("m-0", "p-0");
167
181
 
168
182
  // Add the category name, with appropriate casing.
169
- tdCategory.appendChild(document.createTextNode("Ratgdo Devices"));
183
+ tdCategory.appendChild(document.createTextNode(this.sidebarName));
170
184
  tdCategory.style.fontWeight = "bold";
171
185
 
172
186
  // Add the cell to the table row.
@@ -175,7 +189,7 @@ export class ratgdoFeatureOptions extends FeatureOptions {
175
189
  // Add the table row to the table.
176
190
  devicesTable.appendChild(trCategory);
177
191
 
178
- for(const device of this.ratgdoDevices) {
192
+ for(const device of this.devices) {
179
193
 
180
194
  // Create a row for this device.
181
195
  const trDevice = document.createElement("tr");
@@ -187,12 +201,12 @@ export class ratgdoFeatureOptions extends FeatureOptions {
187
201
 
188
202
  const label = document.createElement("label");
189
203
 
190
- label.name = device.mac;
191
- label.appendChild(document.createTextNode(device.name ?? "Ratgdo"));
204
+ label.name = device.serial;
205
+ label.appendChild(document.createTextNode(device.name ?? "Unknown"));
192
206
  label.style.cursor = "pointer";
193
207
  label.classList.add("mx-2", "my-0", "p-0", "w-100");
194
208
 
195
- label.addEventListener("click", event => this.#showDeviceInfo(device.mac));
209
+ label.addEventListener("click", () => this.#showDeviceInfo(device.serial));
196
210
 
197
211
  // Add the device label to our cell.
198
212
  tdDevice.appendChild(label);
@@ -203,92 +217,47 @@ export class ratgdoFeatureOptions extends FeatureOptions {
203
217
  // Add the table row to the table.
204
218
  devicesTable.appendChild(trDevice);
205
219
 
206
- this.deviceList.push(label);
220
+ this.webuiDeviceList.push(label);
207
221
  }
208
222
  }
209
223
 
210
- this.configOptions = [];
211
-
212
- // Initialize our feature option configuration.
213
- this.#updateConfigOptions(this.currentConfig[0].options ?? []);
214
-
215
224
  // Display the feature options to the user.
216
- this.#showDeviceInfo(isGlobal ? "Global Options" : this.ratgdoDevices[0].mac);
225
+ this.#showDeviceInfo(isGlobal ? "Global Options" : this.devices[0].serial);
217
226
 
218
227
  // All done. Let the user interact with us.
219
228
  homebridge.hideSpinner();
220
229
  }
221
230
 
222
- // Return the serial number of a given Ratgdo device.
223
- getSerial(device) {
224
-
225
- return !device ? undefined :
226
- device.services.find(x => x.constructorName === "AccessoryInformation")?.characteristics.find(x => x.constructorName === "SerialNumber")?.value ?? "";
227
- }
228
-
229
231
  // Show feature option information for a specific device, controller, or globally.
230
232
  async #showDeviceInfo(deviceId) {
231
233
 
232
234
  homebridge.showSpinner();
233
235
 
234
236
  // Update the selected device for visibility.
235
- this.deviceList.map(x => (x.name === deviceId) ? x.parentElement.classList.add("bg-info", "text-white") : x.parentElement.classList.remove("bg-info", "text-white"));
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"));
236
239
 
237
240
  // Populate the device information info pane.
238
- const ratgdoDevice = this.ratgdoDevices.find(x => x.mac === deviceId);
241
+ const currentDevice = this.devices.find(x => x.serial === deviceId);
242
+ this.controller = currentDevice?.serial;
239
243
 
240
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.
241
- if(ratgdoDevice) {
245
+ if(currentDevice) {
242
246
 
243
- document.getElementById("device_firmware").innerHTML = ratgdoDevice.firmwareVersion;
244
- document.getElementById("device_serial").innerHTML = ratgdoDevice.mac;
247
+ document.getElementById("device_firmware").innerHTML = currentDevice.firmwareVersion;
248
+ document.getElementById("device_serial").innerHTML = currentDevice.serial;
245
249
  document.getElementById("deviceStatsTable").style.display = "";
246
250
  } else {
247
251
 
248
252
  document.getElementById("deviceStatsTable").style.display = "none";
249
- document.getElementById("device_firmware").innerHTML = "N/A"
253
+ document.getElementById("device_firmware").innerHTML = "N/A";
250
254
  document.getElementById("device_serial").innerHTML = "N/A";
251
255
  }
252
256
 
253
- // Populate the feature options selected for this device.
254
- const ratgdoFeatures = (await homebridge.request("/getOptions", { configOptions: this.configOptions, ratgdoDevice: ratgdoDevice })) ?? [];
255
- const optionsDevice = ratgdoFeatures.options;
256
-
257
257
  // Start with a clean slate.
258
- let newConfigTableHtml = "";
259
258
  this.configTable.innerHTML = "";
260
259
 
261
- // Initialize the full list of options.
262
- this.featureOptionList = {};
263
- this.featureOptionGroups = {};
264
-
265
- for(const category of ratgdoFeatures.categories) {
266
-
267
- // Now enumerate all the feature options for a given device and add then to the full list.
268
- for(const option of optionsDevice[category.name]) {
269
-
270
- const featureOption = category.name + (option.name.length ? ("." + option.name): "");
271
-
272
- // Add it to our full list.
273
- this.featureOptionList[featureOption] = option;
274
-
275
- // Cross reference the feature option group it belongs to, if any.
276
- if(option.group !== undefined) {
277
-
278
- const expandedGroup = category.name + (option.group.length ? ("." + option.group): "");
279
-
280
- // Initialize the group entry if needed.
281
- if(!this.featureOptionGroups[expandedGroup]) {
282
-
283
- this.featureOptionGroups[expandedGroup] = [];
284
- }
285
-
286
- this.featureOptionGroups[expandedGroup].push(featureOption);
287
- }
288
- }
289
- }
290
-
291
- for(const category of ratgdoFeatures.categories) {
260
+ for(const category of this.featureOptions.categories) {
292
261
 
293
262
  const optionTable = document.createElement("table");
294
263
  const thead = document.createElement("thead");
@@ -304,7 +273,7 @@ export class ratgdoFeatureOptions extends FeatureOptions {
304
273
  tbody.classList.add("table-bordered");
305
274
 
306
275
  // Add the feature option category description.
307
- th.appendChild(document.createTextNode(category.description + (!ratgdoDevice ? " (Global)" : " (Device-specific)")));
276
+ th.appendChild(document.createTextNode(category.description + (!currentDevice ? " (Global)" : " (Device-specific)")));
308
277
 
309
278
  // Add the table header to the row.
310
279
  trFirst.appendChild(th);
@@ -319,10 +288,10 @@ export class ratgdoFeatureOptions extends FeatureOptions {
319
288
  let optionsVisibleCount = 0;
320
289
 
321
290
  // Now enumerate all the feature options for a given device.
322
- for(const option of optionsDevice[category.name]) {
291
+ for(const option of this.featureOptions.options[category.name]) {
323
292
 
324
293
  // Expand the full feature option.
325
- const featureOption = category.name + (option.name.length ? ("." + option.name): "");
294
+ const featureOption = this.featureOptions.expandOption(category, option);
326
295
 
327
296
  // Create the next table row.
328
297
  const trX = document.createElement("tr");
@@ -339,27 +308,27 @@ export class ratgdoFeatureOptions extends FeatureOptions {
339
308
  checkbox.readOnly = false;
340
309
  checkbox.id = featureOption;
341
310
  checkbox.name = featureOption;
342
- checkbox.value = featureOption + (!ratgdoDevice ? "" : ("." + ratgdoDevice.mac));
311
+ checkbox.value = featureOption + (!currentDevice ? "" : ("." + currentDevice.serial));
343
312
 
344
313
  let initialValue = undefined;
345
314
  let initialScope;
346
315
 
347
316
  // Determine our initial option scope to show the user what's been set.
348
- switch(initialScope = this.optionScope(featureOption, ratgdoDevice?.mac, option.default, ("defaultValue" in option))) {
317
+ switch(initialScope = this.featureOptions.scope(featureOption, currentDevice?.serial)) {
349
318
 
350
319
  case "global":
351
320
  case "controller":
352
321
 
353
322
  // If we're looking at the global scope, show the option value. Otherwise, we show that we're inheriting a value from the scope above.
354
- if(!ratgdoDevice) {
323
+ if(!currentDevice) {
355
324
 
356
- if("defaultValue" in option) {
325
+ if(this.featureOptions.isValue(featureOption)) {
357
326
 
358
- checkbox.checked = this.isOptionValueSet(featureOption);
359
- initialValue = this.getOptionValue(checkbox.id);
327
+ checkbox.checked = this.featureOptions.exists(featureOption);
328
+ initialValue = this.featureOptions.value(checkbox.id);
360
329
  } else {
361
330
 
362
- checkbox.checked = this.isGlobalOptionEnabled(featureOption, option.default);
331
+ checkbox.checked = this.featureOptions.test(featureOption);
363
332
  }
364
333
 
365
334
  if(checkbox.checked) {
@@ -369,9 +338,9 @@ export class ratgdoFeatureOptions extends FeatureOptions {
369
338
 
370
339
  } else {
371
340
 
372
- if("defaultValue" in option) {
341
+ if(this.featureOptions.isValue(featureOption)) {
373
342
 
374
- initialValue = this.getOptionValue(checkbox.id, (initialScope === "controller") ? this.controller : undefined);
343
+ initialValue = this.featureOptions.value(checkbox.id, (initialScope === "controller") ? this.controller : undefined);
375
344
  }
376
345
 
377
346
  checkbox.readOnly = checkbox.indeterminate = true;
@@ -383,13 +352,13 @@ export class ratgdoFeatureOptions extends FeatureOptions {
383
352
  case "none":
384
353
  default:
385
354
 
386
- if("defaultValue" in option) {
355
+ if(this.featureOptions.isValue(featureOption)) {
387
356
 
388
- checkbox.checked = this.isOptionValueSet(featureOption, ratgdoDevice?.mac);
389
- initialValue = this.getOptionValue(checkbox.id, ratgdoDevice?.mac);
357
+ checkbox.checked = this.featureOptions.exists(featureOption, currentDevice?.serial);
358
+ initialValue = this.featureOptions.value(checkbox.id, currentDevice?.serial);
390
359
  } else {
391
360
 
392
- checkbox.checked = this.isDeviceOptionEnabled(featureOption, ratgdoDevice?.mac, option.default);
361
+ checkbox.checked = this.featureOptions.test(featureOption, currentDevice?.serial);
393
362
  }
394
363
 
395
364
  break;
@@ -411,7 +380,7 @@ export class ratgdoFeatureOptions extends FeatureOptions {
411
380
  let inputValue = null;
412
381
 
413
382
  // Add an input field if we have a value-centric feature option.
414
- if(("defaultValue" in option)) {
383
+ if(this.featureOptions.isValue(featureOption)) {
415
384
 
416
385
  const tdInput = document.createElement("td");
417
386
  tdInput.classList.add("mr-2");
@@ -427,8 +396,8 @@ export class ratgdoFeatureOptions extends FeatureOptions {
427
396
  inputValue.addEventListener("change", async () => {
428
397
 
429
398
  // Find the option in our list and delete it if it exists.
430
- const optionRegex = new RegExp("^(?:Enable|Disable)\\." + checkbox.id + (!ratgdoDevice ? "" : ("\\." + ratgdoDevice.mac)) + "\\.[^\\.]+$", "gi");
431
- const newOptions = this.configOptions.filter(x => !optionRegex.test(x));
399
+ const optionRegex = new RegExp("^(?:Enable|Disable)\\." + checkbox.id + (!currentDevice ? "" : ("\\." + currentDevice.serial)) + "\\.[^\\.]+$", "gi");
400
+ const newOptions = this.featureOptions.configuredOptions.filter(x => !optionRegex.test(x));
432
401
 
433
402
  if(checkbox.checked) {
434
403
 
@@ -436,7 +405,9 @@ export class ratgdoFeatureOptions extends FeatureOptions {
436
405
  } else if(checkbox.indeterminate) {
437
406
 
438
407
  // If we're in an indeterminate state, we need to traverse the tree to get the upstream value we're inheriting.
439
- inputValue.value = (ratgdoDevice?.mac !== this.controller) ? (this.getOptionValue(checkbox.id, this.controller) ?? this.getOptionValue(checkbox.id)) : (this.getOptionValue(checkbox.id) ?? option.defaultValue);
408
+ inputValue.value = (currentDevice?.serial !== this.controller) ?
409
+ (this.featureOptions.value(checkbox.id, this.controller) ?? this.featureOptions.value(checkbox.id)) :
410
+ (this.featureOptions.value(checkbox.id) ?? option.defaultValue);
440
411
  } else {
441
412
 
442
413
  inputValue.value = option.defaultValue;
@@ -444,7 +415,7 @@ export class ratgdoFeatureOptions extends FeatureOptions {
444
415
 
445
416
  // Update our configuration in Homebridge.
446
417
  this.currentConfig[0].options = newOptions;
447
- this.#updateConfigOptions(newOptions);
418
+ this.featureOptions.configuredOptions = newOptions;
448
419
  await homebridge.updatePluginConfig(this.currentConfig);
449
420
  });
450
421
 
@@ -459,7 +430,7 @@ export class ratgdoFeatureOptions extends FeatureOptions {
459
430
  labelDescription.classList.add("user-select-none", "my-0", "py-0");
460
431
 
461
432
  // Highlight options for the user that are different than our defaults.
462
- const scopeColor = this.optionScopeColor(featureOption, ratgdoDevice?.mac, option.default, ("defaultValue" in option));
433
+ const scopeColor = this.featureOptions.color(featureOption, currentDevice?.serial);
463
434
 
464
435
  if(scopeColor) {
465
436
 
@@ -470,19 +441,19 @@ export class ratgdoFeatureOptions extends FeatureOptions {
470
441
  checkbox.addEventListener("change", async () => {
471
442
 
472
443
  // Find the option in our list and delete it if it exists.
473
- const optionRegex = new RegExp("^(?:Enable|Disable)\\." + checkbox.id + (!ratgdoDevice ? "" : ("\\." + ratgdoDevice.mac)) + "$", "gi");
474
- const newOptions = this.configOptions.filter(x => !optionRegex.test(x));
444
+ const optionRegex = new RegExp("^(?:Enable|Disable)\\." + checkbox.id + (!currentDevice ? "" : ("\\." + currentDevice.serial)) + "$", "gi");
445
+ const newOptions = this.featureOptions.configuredOptions.filter(x => !optionRegex.test(x));
475
446
 
476
447
  // Figure out if we've got the option set upstream.
477
448
  let upstreamOption = false;
478
449
 
479
450
  // We explicitly want to check for the scope of the feature option above where we are now, so we can appropriately determine what we should show.
480
- switch(this.optionScope(checkbox.id, (ratgdoDevice && (ratgdoDevice.mac !== this.controller)) ? this.controller : null, option.default, ("defaultValue" in option))) {
451
+ switch(this.featureOptions.scope(checkbox.id, (currentDevice && (currentDevice.serial !== this.controller)) ? this.controller : undefined)) {
481
452
 
482
453
  case "device":
483
454
  case "controller":
484
455
 
485
- if(ratgdoDevice.mac !== this.controller) {
456
+ if(currentDevice.serial !== this.controller) {
486
457
 
487
458
  upstreamOption = true;
488
459
  }
@@ -491,7 +462,7 @@ export class ratgdoFeatureOptions extends FeatureOptions {
491
462
 
492
463
  case "global":
493
464
 
494
- if(ratgdoDevice) {
465
+ if(currentDevice) {
495
466
 
496
467
  upstreamOption = true;
497
468
  }
@@ -503,8 +474,9 @@ export class ratgdoFeatureOptions extends FeatureOptions {
503
474
  break;
504
475
  }
505
476
 
506
- // For value-centric feature options, if there's an upstream value assigned above us, we don't allow for an unchecked state as it makes no sense in that context.
507
- if(checkbox.readOnly && (!("defaultValue" in option) || (("defaultValue" in option) && inputValue && !upstreamOption))) {
477
+ // For value-centric feature options, if there's an upstream value assigned above us, we don't allow for an unchecked state as it doesn't make sense in this
478
+ // context.
479
+ if(checkbox.readOnly && (!this.featureOptions.isValue(featureOption) || (this.featureOptions.isValue(featureOption) && inputValue && !upstreamOption))) {
508
480
 
509
481
  // We're truly unchecked. We need this because a checkbox can be in both an unchecked and indeterminate simultaneously,
510
482
  // so we use the readOnly property to let us know that we've just cycled from an indeterminate state.
@@ -519,7 +491,7 @@ export class ratgdoFeatureOptions extends FeatureOptions {
519
491
  checkbox.readOnly = checkbox.indeterminate = true;
520
492
  }
521
493
 
522
- if(("defaultValue" in option) && inputValue) {
494
+ if(this.featureOptions.isValue(featureOption) && inputValue) {
523
495
 
524
496
  inputValue.readOnly = true;
525
497
  }
@@ -528,7 +500,7 @@ export class ratgdoFeatureOptions extends FeatureOptions {
528
500
  // We've explicitly checked this option.
529
501
  checkbox.readOnly = checkbox.indeterminate = false;
530
502
 
531
- if(("defaultValue" in option) && inputValue) {
503
+ if(this.featureOptions.isValue(featureOption) && inputValue) {
532
504
 
533
505
  inputValue.readOnly = false;
534
506
  }
@@ -546,7 +518,7 @@ export class ratgdoFeatureOptions extends FeatureOptions {
546
518
  }
547
519
 
548
520
  // Update our Homebridge configuration.
549
- if(("defaultValue" in option) && inputValue) {
521
+ if(this.featureOptions.isValue(featureOption) && inputValue) {
550
522
 
551
523
  // Inform our value-centric feature option to update Homebridge.
552
524
  const changeEvent = new Event("change");
@@ -556,14 +528,14 @@ export class ratgdoFeatureOptions extends FeatureOptions {
556
528
 
557
529
  // Update our configuration in Homebridge.
558
530
  this.currentConfig[0].options = newOptions;
559
- this.#updateConfigOptions(newOptions);
531
+ this.featureOptions.configuredOptions = newOptions;
560
532
  await homebridge.updatePluginConfig(this.currentConfig);
561
533
  }
562
534
 
563
535
  // If we've reset to defaults, make sure our color coding for scope is reflected.
564
536
  if((checkbox.checked === option.default) || checkbox.indeterminate) {
565
537
 
566
- const scopeColor = this.optionScopeColor(featureOption, ratgdoDevice?.mac, option.default, ("defaultValue" in option));
538
+ const scopeColor = this.featureOptions.color(featureOption, currentDevice?.serial);
567
539
 
568
540
  if(scopeColor) {
569
541
 
@@ -572,12 +544,12 @@ export class ratgdoFeatureOptions extends FeatureOptions {
572
544
  }
573
545
 
574
546
  // Adjust visibility of other feature options that depend on us.
575
- if(this.featureOptionGroups[checkbox.id]) {
547
+ if(this.featureOptions.groups[checkbox.id]) {
576
548
 
577
- const entryVisibility = this.isOptionEnabled(featureOption, ratgdoDevice?.mac) ? "" : "none";
549
+ const entryVisibility = this.featureOptions.test(featureOption, currentDevice?.serial) ? "" : "none";
578
550
 
579
551
  // Lookup each feature option setting and set the visibility accordingly.
580
- for(const entry of this.featureOptionGroups[checkbox.id]) {
552
+ for(const entry of this.featureOptions.groups[checkbox.id]) {
581
553
 
582
554
  document.getElementById("row-" + entry).style.display = entryVisibility;
583
555
  }
@@ -597,7 +569,7 @@ export class ratgdoFeatureOptions extends FeatureOptions {
597
569
  trX.appendChild(tdLabel);
598
570
 
599
571
  // Adjust the visibility of the feature option, if it's logically grouped.
600
- if((option.group !== undefined) && !this.isOptionEnabled(category.name + (option.group.length ? ("." + option.group): ""), ratgdoDevice?.mac)) {
572
+ if((option.group !== undefined) && !this.featureOptions.test(category.name + (option.group.length ? ("." + option.group) : ""), currentDevice?.serial)) {
601
573
 
602
574
  trX.style.display = "none";
603
575
  } else {
@@ -625,14 +597,4 @@ export class ratgdoFeatureOptions extends FeatureOptions {
625
597
 
626
598
  homebridge.hideSpinner();
627
599
  }
628
-
629
- // Update our configuration options.
630
- #updateConfigOptions(newConfig) {
631
-
632
- // Update our configuration.
633
- this.configOptions = newConfig;
634
-
635
- // Show all the valid options configured by the user.
636
- this.optionsList = this.configOptions.filter(x => x.match(/^(Enable|Disable)\.*/gi)).map(x => x.toUpperCase());
637
- }
638
600
  }
@@ -1,58 +1,22 @@
1
1
  /* Copyright(C) 2017-2024, HJD (https://github.com/hjdhjd). All rights reserved.
2
2
  *
3
3
  * server.js: homebridge-ratgdo webUI server API.
4
- *
5
- * This module is heavily inspired by the homebridge-config-ui-x source code and borrows from both.
6
- * Thank you oznu for your contributions to the HomeKit world.
7
4
  */
8
5
  "use strict";
9
6
 
10
- import { featureOptionCategories, featureOptions, isOptionEnabled } from "../dist/ratgdo-options.js";
7
+ import { featureOptionCategories, featureOptions } from "../dist/ratgdo-options.js";
11
8
  import { HomebridgePluginUiServer } from "@homebridge/plugin-ui-utils";
12
- import util from "node:util";
13
9
 
14
10
  class PluginUiServer extends HomebridgePluginUiServer {
15
11
 
16
- constructor () {
12
+ constructor() {
17
13
  super();
18
14
 
19
15
  // Register getOptions() with the Homebridge server API.
20
- this.#registerGetOptions();
16
+ this.onRequest("/getOptions", () => ({ categories: featureOptionCategories, options: featureOptions }));
21
17
 
22
18
  this.ready();
23
19
  }
24
-
25
- // Register the getOptions() webUI server API endpoint.
26
- #registerGetOptions() {
27
-
28
- // Return the list of options configured for a given Ratgdo device.
29
- this.onRequest("/getOptions", async(request) => {
30
-
31
- try {
32
-
33
- const optionSet = {};
34
-
35
- // Loop through all the feature option categories.
36
- for(const category of featureOptionCategories) {
37
-
38
- optionSet[category.name] = [];
39
-
40
- for(const options of featureOptions[category.name]) {
41
-
42
- options.value = isOptionEnabled(request.configOptions, request.ratgdoDevice, category.name + "." + options.name, options.default);
43
- optionSet[category.name].push(options);
44
- }
45
- }
46
-
47
- return { categories: featureOptionCategories, options: optionSet };
48
-
49
- } catch(err) {
50
-
51
- // Return nothing if we error out for some reason.
52
- return {};
53
- }
54
- });
55
- }
56
20
  }
57
21
 
58
22
  (() => new PluginUiServer())();