homebridge-unifi-access 1.4.0 → 1.6.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/LICENSE.md +1 -1
- package/dist/access-controller.d.ts +3 -3
- package/dist/access-controller.js +10 -9
- package/dist/access-controller.js.map +1 -1
- package/dist/access-device.d.ts +4 -3
- package/dist/access-device.js +8 -6
- package/dist/access-device.js.map +1 -1
- package/dist/access-events.js +4 -1
- package/dist/access-events.js.map +1 -1
- package/dist/access-hub.js +1 -1
- package/dist/access-hub.js.map +1 -1
- package/dist/access-options.js +1 -1
- package/dist/access-types.js +1 -1
- package/dist/index.js +1 -1
- package/dist/settings.js +1 -1
- package/homebridge-ui/public/index.html +7 -6
- package/homebridge-ui/public/lib/featureoptions.js +50 -29
- package/homebridge-ui/public/lib/featureoptions.js.map +1 -1
- package/homebridge-ui/public/lib/webUi-featureoptions.mjs +87 -84
- package/homebridge-ui/public/lib/webUi.mjs +2 -5
- package/homebridge-ui/public/ui.mjs +11 -7
- package/homebridge-ui/server.js +1 -1
- package/package.json +13 -13
|
@@ -50,16 +50,8 @@ export class FeatureOptions {
|
|
|
50
50
|
* @returns Returns true or false, depending on the option default.
|
|
51
51
|
*/
|
|
52
52
|
defaultValue(option) {
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
return this.defaultReturnValue;
|
|
56
|
-
}
|
|
57
|
-
const value = this.defaults[option.toLowerCase()];
|
|
58
|
-
// If it's unknown to us, assume it's true.
|
|
59
|
-
if (value === undefined) {
|
|
60
|
-
return this.defaultReturnValue;
|
|
61
|
-
}
|
|
62
|
-
return value;
|
|
53
|
+
// If it's unknown to us, return the default return value.
|
|
54
|
+
return this.defaults[option.toLowerCase()] ?? this.defaultReturnValue;
|
|
63
55
|
}
|
|
64
56
|
/**
|
|
65
57
|
* Return whether the option explicitly exists in the list of configured options.
|
|
@@ -96,7 +88,7 @@ export class FeatureOptions {
|
|
|
96
88
|
* @param device - Optional device scope identifier.
|
|
97
89
|
* @param controller - Optional controller scope identifier.
|
|
98
90
|
*
|
|
99
|
-
* @returns Returns the value of a value-centric option as a floating point number
|
|
91
|
+
* @returns Returns the value of a value-centric option as a floating point number, `undefined` if it doesn't exist or couldn't be parsed, and `null` if disabled.
|
|
100
92
|
*/
|
|
101
93
|
getFloat(option, device, controller) {
|
|
102
94
|
// Parse the number and return the value.
|
|
@@ -109,7 +101,7 @@ export class FeatureOptions {
|
|
|
109
101
|
* @param device - Optional device scope identifier.
|
|
110
102
|
* @param controller - Optional controller scope identifier.
|
|
111
103
|
*
|
|
112
|
-
* @returns Returns the value of a value-centric option as an integer
|
|
104
|
+
* @returns Returns the value of a value-centric option as an integer, `undefined` if it doesn't exist or couldn't be parsed, and `null` if disabled.
|
|
113
105
|
*/
|
|
114
106
|
getInteger(option, device, controller) {
|
|
115
107
|
// Parse the number and return the value.
|
|
@@ -147,7 +139,10 @@ export class FeatureOptions {
|
|
|
147
139
|
* @returns Returns true if it is a value-centric option and false otherwise.
|
|
148
140
|
*/
|
|
149
141
|
isValue(option) {
|
|
150
|
-
|
|
142
|
+
if (!option) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
return option.toLowerCase() in this.valueOptions;
|
|
151
146
|
}
|
|
152
147
|
/**
|
|
153
148
|
* Return the scope hierarchy location of an option.
|
|
@@ -180,20 +175,24 @@ export class FeatureOptions {
|
|
|
180
175
|
* @param device - Optional device scope identifier.
|
|
181
176
|
* @param controller - Optional controller scope identifier.
|
|
182
177
|
*
|
|
183
|
-
* @returns Returns the current value associated with `option`
|
|
178
|
+
* @returns Returns the current value associated with `option` if the feature option is enabled, `null` if disabled (or not a value-centric feature option), or
|
|
179
|
+
* `undefined` if it's not specified.
|
|
184
180
|
*/
|
|
185
181
|
value(option, device, controller) {
|
|
186
182
|
// If this isn't a value-centric feature option, we're done.
|
|
187
183
|
if (!this.isValue(option)) {
|
|
188
|
-
return
|
|
184
|
+
return null;
|
|
189
185
|
}
|
|
186
|
+
// Normalize the option.
|
|
187
|
+
option = option.toLowerCase();
|
|
190
188
|
const getValue = (checkOption, checkId) => {
|
|
191
189
|
const regex = this.valueRegex(checkOption, checkId);
|
|
192
190
|
// Get the option value, if we have one.
|
|
193
191
|
for (const entry of this.configuredOptions) {
|
|
194
192
|
const regexMatch = regex.exec(entry);
|
|
195
193
|
if (regexMatch) {
|
|
196
|
-
return
|
|
194
|
+
// If the option is enabled, return the value. Otherwise, we have nothing.
|
|
195
|
+
return (regexMatch[1].toLowerCase() === "enable") ? regexMatch[2] : null;
|
|
197
196
|
}
|
|
198
197
|
}
|
|
199
198
|
return undefined;
|
|
@@ -201,6 +200,10 @@ export class FeatureOptions {
|
|
|
201
200
|
// Check to see if we have a device-level value first.
|
|
202
201
|
if (device) {
|
|
203
202
|
const value = getValue(option, device);
|
|
203
|
+
// The option must been explicitly disabled.
|
|
204
|
+
if (value === null) {
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
204
207
|
if (value) {
|
|
205
208
|
return value;
|
|
206
209
|
}
|
|
@@ -208,12 +211,25 @@ export class FeatureOptions {
|
|
|
208
211
|
// Now check to see if we have an controller-level value.
|
|
209
212
|
if (controller) {
|
|
210
213
|
const value = getValue(option, controller);
|
|
214
|
+
// The option's been explicitly disabled.
|
|
215
|
+
if (value === null) {
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
211
218
|
if (value) {
|
|
212
219
|
return value;
|
|
213
220
|
}
|
|
214
221
|
}
|
|
215
222
|
// Finally, we check for a global-level value.
|
|
216
|
-
|
|
223
|
+
const value = getValue(option);
|
|
224
|
+
if (value) {
|
|
225
|
+
return value;
|
|
226
|
+
}
|
|
227
|
+
// The option's been explicitly disabled or is disabled by default.
|
|
228
|
+
if ((value === null) || !this.defaultValue(option)) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
// Return the enabled value, or the default value if we've got nothing explicitly configured.
|
|
232
|
+
return value ?? ((this.valueOptions[option] === undefined) ? undefined : this.valueOptions[option]?.toString());
|
|
217
233
|
}
|
|
218
234
|
/**
|
|
219
235
|
* Return the list of available feature option categories.
|
|
@@ -279,6 +295,10 @@ export class FeatureOptions {
|
|
|
279
295
|
this._groups = {};
|
|
280
296
|
this.valueOptions = {};
|
|
281
297
|
for (const category of this.categories) {
|
|
298
|
+
// If the category doesn't exist, let's skip it.
|
|
299
|
+
if (!this.options[category.name]) {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
282
302
|
// Now enumerate all the feature options for a given device and add then to the full list.
|
|
283
303
|
for (const option of this.options[category.name]) {
|
|
284
304
|
// Expand the entry.
|
|
@@ -286,7 +306,9 @@ export class FeatureOptions {
|
|
|
286
306
|
// Index the default value.
|
|
287
307
|
this.defaults[entry.toLowerCase()] = option.default;
|
|
288
308
|
// Track value-centric options.
|
|
289
|
-
|
|
309
|
+
if ("defaultValue" in option) {
|
|
310
|
+
this.valueOptions[entry.toLowerCase()] = option.defaultValue;
|
|
311
|
+
}
|
|
290
312
|
// Cross reference the feature option group it belongs to, if any.
|
|
291
313
|
if (option.group !== undefined) {
|
|
292
314
|
const expandedGroup = category.name + (option.group.length ? ("." + option.group) : "");
|
|
@@ -332,11 +354,7 @@ export class FeatureOptions {
|
|
|
332
354
|
// Utility to test whether an option is set in a given scope.
|
|
333
355
|
// We return true if an option is enabled, false for disabled, undefined otherwise. For value-centric options, we return true if a value exists.
|
|
334
356
|
isOptionEnabled(option, id) {
|
|
335
|
-
|
|
336
|
-
if (this.isValue(option)) {
|
|
337
|
-
return this.exists(option, id);
|
|
338
|
-
}
|
|
339
|
-
const regex = this.optionRegex(option, id);
|
|
357
|
+
const regex = this.isValue(option) ? this.valueRegex(option, id) : this.optionRegex(option, id);
|
|
340
358
|
// Get the option value, if we have one.
|
|
341
359
|
for (const entry of this.configuredOptions) {
|
|
342
360
|
const regexMatch = regex.exec(entry);
|
|
@@ -354,14 +372,14 @@ export class FeatureOptions {
|
|
|
354
372
|
}
|
|
355
373
|
// Utility function to parse and return a numeric configuration parameter.
|
|
356
374
|
parseOptionNumeric(option, convert) {
|
|
357
|
-
//
|
|
358
|
-
if (option
|
|
359
|
-
return undefined;
|
|
375
|
+
// If the option is disabled or we don't have it configured -- we're done.
|
|
376
|
+
if (!option) {
|
|
377
|
+
return (option === null) ? null : undefined;
|
|
360
378
|
}
|
|
361
379
|
// Convert it to a number, if needed.
|
|
362
380
|
const convertedValue = convert(option);
|
|
363
381
|
// Let's validate to make sure it's really a number.
|
|
364
|
-
if (isNaN(convertedValue)
|
|
382
|
+
if (isNaN(convertedValue)) {
|
|
365
383
|
return undefined;
|
|
366
384
|
}
|
|
367
385
|
// Return the value.
|
|
@@ -369,8 +387,11 @@ export class FeatureOptions {
|
|
|
369
387
|
}
|
|
370
388
|
// Regular expression test for value-centric feature options.
|
|
371
389
|
valueRegex(option, id) {
|
|
372
|
-
//
|
|
373
|
-
|
|
390
|
+
// Escape out our option to ensure we have no inadvertent issues in matching the regular expression.
|
|
391
|
+
option = option.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
392
|
+
// This regular expression is a bit more intricate than you might think it should be due to the need to ensure we capture values at the very end of the option when
|
|
393
|
+
// the option is enabled, and that we ignore the values at the end when the option is disabled in order to correctly traverse the hierarchy.
|
|
394
|
+
return new RegExp("^(Disable|Enable)\\." + option + (!id ? "" : "\\." + id) + "(?:(?<=^Enable\\." + option + (!id ? "" : "\\." + id) + ")\\.([^\\.]*))?$", "gi");
|
|
374
395
|
}
|
|
375
396
|
}
|
|
376
397
|
//# sourceMappingURL=featureoptions.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"featureoptions.js","sourceRoot":"","sources":["../src/featureoptions.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"featureoptions.js","sourceRoot":"","sources":["../src/featureoptions.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* Copyright(C) 2017-
|
|
1
|
+
/* Copyright(C) 2017-2025, HJD (https://github.com/hjdhjd). All rights reserved.
|
|
2
2
|
*
|
|
3
3
|
* webUi-featureoptions.mjs: Device feature option webUI.
|
|
4
4
|
*/
|
|
@@ -150,14 +150,17 @@ export class webUiFeatureOptions {
|
|
|
150
150
|
this.#configTable.innerHTML = "";
|
|
151
151
|
this.webUiDeviceList = [];
|
|
152
152
|
|
|
153
|
-
// Create our
|
|
154
|
-
const
|
|
153
|
+
// Create our override styles for things like hover for our sidebar and workarounds for Homebridge UI quirks.
|
|
154
|
+
const overrideStyles = document.createElement("style");
|
|
155
|
+
|
|
156
|
+
// We want to override the default colors that Homebridge UI might apply for table cells.
|
|
157
|
+
overrideStyles.innerHTML = "td { color: unset !important }";
|
|
155
158
|
|
|
156
159
|
// We emulate the styles that Bootstrap uses when hovering over a table, accounting for both light and dark modes.
|
|
157
|
-
|
|
160
|
+
overrideStyles.innerHTML += "@media (prefers-color-scheme: dark) { .hbup-hover td:hover { background-color: #212121; color: #FFA000 !important } }" +
|
|
158
161
|
"@media (prefers-color-scheme: light) { .hbup-hover td:hover { background-color: #ECECEC; } }";
|
|
159
162
|
|
|
160
|
-
document.head.appendChild(
|
|
163
|
+
document.head.appendChild(overrideStyles);
|
|
161
164
|
|
|
162
165
|
// Add our hover styles to the controllers and devices tables.
|
|
163
166
|
controllersTable.classList.add("hbup-hover");
|
|
@@ -179,6 +182,7 @@ export class webUiFeatureOptions {
|
|
|
179
182
|
}
|
|
180
183
|
|
|
181
184
|
// Initialize our informational header.
|
|
185
|
+
document.getElementById("headerInfo").style.fontWeight = "bold";
|
|
182
186
|
document.getElementById("headerInfo").innerHTML = "Feature options are applied in prioritized order, from global to device-specific options:" +
|
|
183
187
|
"<br><i class=\"text-warning\">Global options</i> (lowest priority) → " +
|
|
184
188
|
(this.#hasControllers ? "<i class=\"text-success\">Controller options</i> → " : "") +
|
|
@@ -312,7 +316,7 @@ export class webUiFeatureOptions {
|
|
|
312
316
|
}
|
|
313
317
|
|
|
314
318
|
// The first entry returned by getDevices() must always be the controller.
|
|
315
|
-
this.#controller = this.#devices[0]?.
|
|
319
|
+
this.#controller = this.#devices[0]?.serialNumber ?? null;
|
|
316
320
|
}
|
|
317
321
|
|
|
318
322
|
// Make the UI visible.
|
|
@@ -329,7 +333,7 @@ export class webUiFeatureOptions {
|
|
|
329
333
|
this.#sidebar.showDevices(controller, this.#devices);
|
|
330
334
|
|
|
331
335
|
// Display the feature options to the user.
|
|
332
|
-
this.showDeviceOptions(controller ? this.#devices[0].
|
|
336
|
+
this.showDeviceOptions(controller ? this.#devices[0].serialNumber : "Global Options");
|
|
333
337
|
|
|
334
338
|
// All done. Let the user interact with us.
|
|
335
339
|
homebridge.hideSpinner();
|
|
@@ -345,7 +349,7 @@ export class webUiFeatureOptions {
|
|
|
345
349
|
webUiEntry.parentElement.classList.add("bg-info", "text-white") : webUiEntry.parentElement.classList.remove("bg-info", "text-white"));
|
|
346
350
|
|
|
347
351
|
// Populate the device information info pane.
|
|
348
|
-
const currentDevice = this.#devices.find(device => device.
|
|
352
|
+
const currentDevice = this.#devices.find(device => device.serialNumber === deviceId);
|
|
349
353
|
|
|
350
354
|
// Populate the details view. If there's no device specified, the context is considered global and we hide the device details view.
|
|
351
355
|
if(!currentDevice) {
|
|
@@ -382,7 +386,7 @@ export class webUiFeatureOptions {
|
|
|
382
386
|
th.classList.add("p-0");
|
|
383
387
|
th.style.fontWeight = "bold";
|
|
384
388
|
th.colSpan = 3;
|
|
385
|
-
tbody.classList.add("
|
|
389
|
+
tbody.classList.add("border");
|
|
386
390
|
|
|
387
391
|
// Add the feature option category description.
|
|
388
392
|
th.appendChild(document.createTextNode(category.description + (!currentDevice ? " (Global)" :
|
|
@@ -428,13 +432,13 @@ export class webUiFeatureOptions {
|
|
|
428
432
|
checkbox.readOnly = false;
|
|
429
433
|
checkbox.id = featureOption;
|
|
430
434
|
checkbox.name = featureOption;
|
|
431
|
-
checkbox.value = featureOption + (!currentDevice ? "" : ("." + currentDevice.
|
|
435
|
+
checkbox.value = featureOption + (!currentDevice ? "" : ("." + currentDevice.serialNumber));
|
|
432
436
|
|
|
433
437
|
let initialValue = undefined;
|
|
434
438
|
let initialScope;
|
|
435
439
|
|
|
436
440
|
// Determine our initial option scope to show the user what's been set.
|
|
437
|
-
switch(initialScope = this.#featureOptions.scope(featureOption, currentDevice?.
|
|
441
|
+
switch(initialScope = this.#featureOptions.scope(featureOption, currentDevice?.serialNumber, this.#controller)) {
|
|
438
442
|
|
|
439
443
|
case "global":
|
|
440
444
|
case "controller":
|
|
@@ -442,13 +446,11 @@ export class webUiFeatureOptions {
|
|
|
442
446
|
// 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.
|
|
443
447
|
if(!currentDevice) {
|
|
444
448
|
|
|
449
|
+
checkbox.checked = this.#featureOptions.test(featureOption);
|
|
450
|
+
|
|
445
451
|
if(this.#featureOptions.isValue(featureOption)) {
|
|
446
452
|
|
|
447
|
-
checkbox.checked = this.#featureOptions.exists(featureOption);
|
|
448
453
|
initialValue = this.#featureOptions.value(checkbox.id);
|
|
449
|
-
} else {
|
|
450
|
-
|
|
451
|
-
checkbox.checked = this.#featureOptions.test(featureOption);
|
|
452
454
|
}
|
|
453
455
|
|
|
454
456
|
if(checkbox.checked) {
|
|
@@ -472,13 +474,11 @@ export class webUiFeatureOptions {
|
|
|
472
474
|
case "none":
|
|
473
475
|
default:
|
|
474
476
|
|
|
475
|
-
|
|
477
|
+
checkbox.checked = this.#featureOptions.test(featureOption, currentDevice?.serialNumber);
|
|
476
478
|
|
|
477
|
-
|
|
478
|
-
initialValue = this.#featureOptions.value(checkbox.id, currentDevice?.serial);
|
|
479
|
-
} else {
|
|
479
|
+
if(this.#featureOptions.isValue(featureOption)) {
|
|
480
480
|
|
|
481
|
-
|
|
481
|
+
initialValue = this.#featureOptions.value(checkbox.id, currentDevice?.serialNumber);
|
|
482
482
|
}
|
|
483
483
|
|
|
484
484
|
break;
|
|
@@ -512,35 +512,10 @@ export class webUiFeatureOptions {
|
|
|
512
512
|
inputValue.type = "text";
|
|
513
513
|
inputValue.value = initialValue ?? option.defaultValue;
|
|
514
514
|
inputValue.size = 5;
|
|
515
|
-
inputValue.readOnly =
|
|
515
|
+
inputValue.readOnly = checkbox.readOnly;
|
|
516
516
|
|
|
517
517
|
// Add or remove the setting from our configuration when we've changed our state.
|
|
518
|
-
inputValue.addEventListener("change",
|
|
519
|
-
|
|
520
|
-
// Find the option in our list and delete it if it exists.
|
|
521
|
-
const optionRegex = new RegExp("^(?:Enable|Disable)\\." + checkbox.id + (!currentDevice ? "" : ("\\." + currentDevice.serial)) + "\\.[^\\.]+$", "gi");
|
|
522
|
-
const newOptions = this.#featureOptions.configuredOptions.filter(entry => !optionRegex.test(entry));
|
|
523
|
-
|
|
524
|
-
if(checkbox.checked) {
|
|
525
|
-
|
|
526
|
-
newOptions.push("Enable." + checkbox.value + "." + inputValue.value);
|
|
527
|
-
} else if(checkbox.indeterminate) {
|
|
528
|
-
|
|
529
|
-
// If we're in an indeterminate state, we need to traverse the tree to get the upstream value we're inheriting.
|
|
530
|
-
inputValue.value = (currentDevice?.serial !== this.#controller) ?
|
|
531
|
-
(this.#featureOptions.value(checkbox.id, this.#controller) ?? this.#featureOptions.value(checkbox.id)) :
|
|
532
|
-
(this.#featureOptions.value(checkbox.id) ?? option.defaultValue);
|
|
533
|
-
} else {
|
|
534
|
-
|
|
535
|
-
inputValue.value = option.defaultValue;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
// Update our configuration in Homebridge.
|
|
539
|
-
this.currentConfig[0].options = newOptions;
|
|
540
|
-
this.#featureOptions.configuredOptions = newOptions;
|
|
541
|
-
await homebridge.updatePluginConfig(this.currentConfig);
|
|
542
|
-
});
|
|
543
|
-
|
|
518
|
+
inputValue.addEventListener("change", () => checkbox.dispatchEvent(new Event("change")));
|
|
544
519
|
tdInput.appendChild(inputValue);
|
|
545
520
|
trX.appendChild(tdInput);
|
|
546
521
|
}
|
|
@@ -553,7 +528,7 @@ export class webUiFeatureOptions {
|
|
|
553
528
|
labelDescription.classList.add("user-select-none", "my-0", "py-0");
|
|
554
529
|
|
|
555
530
|
// Highlight options for the user that are different than our defaults.
|
|
556
|
-
const scopeColor = this.#featureOptions.color(featureOption, currentDevice?.
|
|
531
|
+
const scopeColor = this.#featureOptions.color(featureOption, currentDevice?.serialNumber, this.#controller);
|
|
557
532
|
|
|
558
533
|
if(scopeColor) {
|
|
559
534
|
|
|
@@ -564,19 +539,21 @@ export class webUiFeatureOptions {
|
|
|
564
539
|
checkbox.addEventListener("change", async () => {
|
|
565
540
|
|
|
566
541
|
// Find the option in our list and delete it if it exists.
|
|
567
|
-
const optionRegex = new RegExp("^(?:Enable|Disable)\\." + checkbox.id + (!currentDevice ? "" : ("\\." + currentDevice.
|
|
542
|
+
const optionRegex = new RegExp("^(?:Enable|Disable)\\." + checkbox.id + (!currentDevice ? "" : ("\\." + currentDevice.serialNumber)) +
|
|
543
|
+
"(?:\\.([^\\.]*))?$", "gi");
|
|
544
|
+
|
|
568
545
|
const newOptions = this.#featureOptions.configuredOptions.filter(entry => !optionRegex.test(entry));
|
|
569
546
|
|
|
570
547
|
// Figure out if we've got the option set upstream.
|
|
571
548
|
let upstreamOption = false;
|
|
572
549
|
|
|
573
550
|
// 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.
|
|
574
|
-
switch(this.#featureOptions.scope(checkbox.id, (currentDevice && (currentDevice.
|
|
551
|
+
switch(this.#featureOptions.scope(checkbox.id, (currentDevice && (currentDevice.serialNumber !== this.#controller)) ? this.#controller : undefined)) {
|
|
575
552
|
|
|
576
553
|
case "device":
|
|
577
554
|
case "controller":
|
|
578
555
|
|
|
579
|
-
if(currentDevice.
|
|
556
|
+
if(currentDevice.serialNumber !== this.#controller) {
|
|
580
557
|
|
|
581
558
|
upstreamOption = true;
|
|
582
559
|
}
|
|
@@ -597,15 +574,24 @@ export class webUiFeatureOptions {
|
|
|
597
574
|
break;
|
|
598
575
|
}
|
|
599
576
|
|
|
600
|
-
//
|
|
601
|
-
|
|
602
|
-
if(checkbox.readOnly && (!this.#featureOptions.isValue(featureOption) || (this.#featureOptions.isValue(featureOption) && inputValue && !upstreamOption))) {
|
|
577
|
+
// We're currently in an indetermindate state and transitioning to an unchecked state.
|
|
578
|
+
if(checkbox.readOnly) {
|
|
603
579
|
|
|
604
|
-
//
|
|
605
|
-
//
|
|
580
|
+
// The user wants to change the state to unchecked. We need this because a checkbox can be in both an unchecked and indeterminate simultaneously, so we use
|
|
581
|
+
// the readOnly property to let us know that we've just cycled from an indeterminate state.
|
|
606
582
|
checkbox.checked = checkbox.readOnly = false;
|
|
583
|
+
|
|
584
|
+
// If we have a value-centric feature option, we show the default value when we're in an indeterminate state.
|
|
585
|
+
if(this.#featureOptions.isValue(featureOption)) {
|
|
586
|
+
|
|
587
|
+
// If we're unchecked, clear out the value and make it read only. We show the system default for reference.
|
|
588
|
+
inputValue.value = option.defaultValue;
|
|
589
|
+
inputValue.readOnly = true;
|
|
590
|
+
}
|
|
607
591
|
} else if(!checkbox.checked) {
|
|
608
592
|
|
|
593
|
+
// We're currently in a checked state and transitioning to an unchecked or an indeterminate state.
|
|
594
|
+
|
|
609
595
|
// If we have an upstream option configured, we reveal a third state to show inheritance of that option and allow the user to select it.
|
|
610
596
|
if(upstreamOption) {
|
|
611
597
|
|
|
@@ -614,51 +600,64 @@ export class webUiFeatureOptions {
|
|
|
614
600
|
checkbox.readOnly = checkbox.indeterminate = true;
|
|
615
601
|
}
|
|
616
602
|
|
|
617
|
-
|
|
603
|
+
// If we're in an indeterminate state, we need to traverse the tree to get the upstream value we're inheriting.
|
|
604
|
+
if(this.#featureOptions.isValue(featureOption)) {
|
|
605
|
+
|
|
606
|
+
let newInputValue;
|
|
607
|
+
|
|
608
|
+
// If our scope is global, let's fallback on the default value.
|
|
609
|
+
// eslint-disable-next-line eqeqeq
|
|
610
|
+
if((currentDevice?.serialNumber == null) && (this.#controller == null)) {
|
|
618
611
|
|
|
612
|
+
newInputValue = option.defaultValue;
|
|
613
|
+
} else if(currentDevice?.serialNumber !== this.#controller) {
|
|
614
|
+
|
|
615
|
+
// We're at the device level - get the controller level value if it exists and fallback to the global value otherwise.
|
|
616
|
+
newInputValue = this.#featureOptions.value(checkbox.id, this.#controller) ?? this.#featureOptions.value(checkbox.id);
|
|
617
|
+
} else {
|
|
618
|
+
|
|
619
|
+
// We're at the controller level - get the global value.
|
|
620
|
+
newInputValue = this.#featureOptions.value(checkbox.id);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Our fallback if there's no value defined within the scope hierarchy is the default value.
|
|
624
|
+
inputValue.value = newInputValue ?? option.defaultValue;
|
|
619
625
|
inputValue.readOnly = true;
|
|
620
626
|
}
|
|
621
627
|
} else if(checkbox.checked) {
|
|
622
628
|
|
|
623
|
-
// We'
|
|
629
|
+
// We're currently in an unchecked state and transitioning to a checked state.
|
|
624
630
|
checkbox.readOnly = checkbox.indeterminate = false;
|
|
625
631
|
|
|
626
|
-
if(this.#featureOptions.isValue(featureOption)
|
|
632
|
+
if(this.#featureOptions.isValue(featureOption)) {
|
|
627
633
|
|
|
628
634
|
inputValue.readOnly = false;
|
|
629
635
|
}
|
|
630
636
|
}
|
|
631
637
|
|
|
632
|
-
// The
|
|
633
|
-
|
|
638
|
+
// The feature option is different from the default - highlight it for the user, accounting for the scope hierarchy, and add it to our configuration. We
|
|
639
|
+
// provide a visual queue to the user, highlighting to indicate that a non-default option has been set.
|
|
640
|
+
if(!checkbox.indeterminate && ((checkbox.checked !== option.default) ||
|
|
641
|
+
(this.#featureOptions.isValue(featureOption) && (inputValue.value.toString() !== option.defaultValue.toString())) || upstreamOption)) {
|
|
634
642
|
|
|
635
643
|
labelDescription.classList.add("text-info");
|
|
636
|
-
newOptions.push((checkbox.checked ? "Enable." : "Disable.") + checkbox.value
|
|
644
|
+
newOptions.push((checkbox.checked ? "Enable." : "Disable.") + checkbox.value +
|
|
645
|
+
(this.#featureOptions.isValue(featureOption) && checkbox.checked ? ("." + inputValue.value) : ""));
|
|
637
646
|
} else {
|
|
638
647
|
|
|
639
648
|
// We've reset to the defaults, remove our highlighting.
|
|
640
649
|
labelDescription.classList.remove("text-info");
|
|
641
650
|
}
|
|
642
651
|
|
|
643
|
-
// Update our Homebridge
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
const changeEvent = new Event("change");
|
|
648
|
-
|
|
649
|
-
inputValue.dispatchEvent(changeEvent);
|
|
650
|
-
} else {
|
|
651
|
-
|
|
652
|
-
// Update our configuration in Homebridge.
|
|
653
|
-
this.currentConfig[0].options = newOptions;
|
|
654
|
-
this.#featureOptions.configuredOptions = newOptions;
|
|
655
|
-
await homebridge.updatePluginConfig(this.currentConfig);
|
|
656
|
-
}
|
|
652
|
+
// Update our configuration in Homebridge.
|
|
653
|
+
this.currentConfig[0].options = newOptions;
|
|
654
|
+
this.#featureOptions.configuredOptions = newOptions;
|
|
655
|
+
await homebridge.updatePluginConfig(this.currentConfig);
|
|
657
656
|
|
|
658
657
|
// If we've reset to defaults, make sure our color coding for scope is reflected.
|
|
659
658
|
if((checkbox.checked === option.default) || checkbox.indeterminate) {
|
|
660
659
|
|
|
661
|
-
const scopeColor = this.#featureOptions.color(featureOption, currentDevice?.
|
|
660
|
+
const scopeColor = this.#featureOptions.color(featureOption, currentDevice?.serialNumber, this.#controller);
|
|
662
661
|
|
|
663
662
|
if(scopeColor) {
|
|
664
663
|
|
|
@@ -669,7 +668,7 @@ export class webUiFeatureOptions {
|
|
|
669
668
|
// Adjust visibility of other feature options that depend on us.
|
|
670
669
|
if(this.#featureOptions.groups[checkbox.id]) {
|
|
671
670
|
|
|
672
|
-
const entryVisibility = this.#featureOptions.test(featureOption, currentDevice?.
|
|
671
|
+
const entryVisibility = this.#featureOptions.test(featureOption, currentDevice?.serialNumber) ? "" : "none";
|
|
673
672
|
|
|
674
673
|
// Lookup each feature option setting and set the visibility accordingly.
|
|
675
674
|
for(const entry of this.#featureOptions.groups[checkbox.id]) {
|
|
@@ -692,7 +691,7 @@ export class webUiFeatureOptions {
|
|
|
692
691
|
trX.appendChild(tdLabel);
|
|
693
692
|
|
|
694
693
|
// Adjust the visibility of the feature option, if it's logically grouped.
|
|
695
|
-
if((option.group !== undefined) && !this.#featureOptions.test(category.name + (option.group.length ? ("." + option.group) : ""), currentDevice?.
|
|
694
|
+
if((option.group !== undefined) && !this.#featureOptions.test(category.name + (option.group.length ? ("." + option.group) : ""), currentDevice?.serialNumber)) {
|
|
696
695
|
|
|
697
696
|
trX.style.display = "none";
|
|
698
697
|
} else {
|
|
@@ -738,7 +737,7 @@ export class webUiFeatureOptions {
|
|
|
738
737
|
|
|
739
738
|
// Display our device details.
|
|
740
739
|
deviceFirmware.innerHTML = device.firmwareVersion;
|
|
741
|
-
deviceSerial.innerHTML = device.
|
|
740
|
+
deviceSerial.innerHTML = device.serialNumber;
|
|
742
741
|
}
|
|
743
742
|
|
|
744
743
|
// Default method for enumerating the device list in the sidebar.
|
|
@@ -785,12 +784,12 @@ export class webUiFeatureOptions {
|
|
|
785
784
|
|
|
786
785
|
const label = document.createElement("label");
|
|
787
786
|
|
|
788
|
-
label.name = device.
|
|
787
|
+
label.name = device.serialNumber;
|
|
789
788
|
label.appendChild(document.createTextNode(device.name ?? "Unknown"));
|
|
790
789
|
label.style.cursor = "pointer";
|
|
791
790
|
label.classList.add("mx-2", "my-0", "p-0", "w-100");
|
|
792
791
|
|
|
793
|
-
label.addEventListener("click", () => this.showDeviceOptions(device.
|
|
792
|
+
label.addEventListener("click", () => this.showDeviceOptions(device.serialNumber));
|
|
794
793
|
|
|
795
794
|
// Add the device label to our cell.
|
|
796
795
|
tdDevice.appendChild(label);
|
|
@@ -814,10 +813,14 @@ export class webUiFeatureOptions {
|
|
|
814
813
|
// Filter out only the components we're interested in.
|
|
815
814
|
devices = devices.map(device => ({
|
|
816
815
|
|
|
817
|
-
|
|
816
|
+
firmwareRevision: (device.services.find(service => service.constructorName ===
|
|
818
817
|
"AccessoryInformation")?.characteristics.find(characteristic => characteristic.constructorName === "FirmwareRevision")?.value ?? ""),
|
|
818
|
+
manufacturer: (device.services.find(service => service.constructorName ===
|
|
819
|
+
"AccessoryInformation")?.characteristics.find(characteristic => characteristic.constructorName === "Manufacturer")?.value ?? ""),
|
|
820
|
+
model: (device.services.find(service => service.constructorName ===
|
|
821
|
+
"AccessoryInformation")?.characteristics.find(characteristic => characteristic.constructorName === "Model")?.value ?? ""),
|
|
819
822
|
name: device.displayName,
|
|
820
|
-
|
|
823
|
+
serialNumber: (device.services.find(service => service.constructorName ===
|
|
821
824
|
"AccessoryInformation")?.characteristics.find(characteristic => characteristic.constructorName === "SerialNumber")?.value ?? "")
|
|
822
825
|
}));
|
|
823
826
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* Copyright(C) 2017-
|
|
1
|
+
/* Copyright(C) 2017-2025, HJD (https://github.com/hjdhjd). All rights reserved.
|
|
2
2
|
*
|
|
3
3
|
* webUi.mjs: Plugin webUI.
|
|
4
4
|
*/
|
|
@@ -142,11 +142,8 @@ export class webUi {
|
|
|
142
142
|
document.getElementById("menuFeatureOptions").addEventListener("click", () => this.featureOptions.show());
|
|
143
143
|
document.getElementById("menuSettings").addEventListener("click", () => this.#showSettings());
|
|
144
144
|
|
|
145
|
-
// Get the list of devices the plugin knows about.
|
|
146
|
-
const devices = await homebridge.getCachedAccessories();
|
|
147
|
-
|
|
148
145
|
// If we've got devices detected, we launch our feature option UI. Otherwise, we launch our first run UI.
|
|
149
|
-
if(this.featureOptions.currentConfig.length &&
|
|
146
|
+
if(this.featureOptions.currentConfig.length && !(await this.#processHandler(this.#firstRun.isRequired))) {
|
|
150
147
|
|
|
151
148
|
document.getElementById("menuWrapper").style.display = "inline-flex";
|
|
152
149
|
this.featureOptions.show();
|