homebridge-unifi-protect 6.7.0 → 6.9.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/README.md +1 -1
- package/config.schema.json +0 -24
- package/dist/protect-camera-package.d.ts +9 -0
- package/dist/protect-camera-package.js +120 -0
- package/dist/protect-camera-package.js.map +1 -0
- package/dist/protect-camera.d.ts +5 -12
- package/dist/protect-camera.js +48 -139
- package/dist/protect-camera.js.map +1 -1
- package/dist/protect-device.d.ts +10 -4
- package/dist/protect-device.js +82 -13
- package/dist/protect-device.js.map +1 -1
- package/dist/protect-doorbell.d.ts +4 -0
- package/dist/protect-doorbell.js +54 -0
- package/dist/protect-doorbell.js.map +1 -1
- package/dist/protect-ffmpeg-codecs.js +5 -4
- package/dist/protect-ffmpeg-codecs.js.map +1 -1
- package/dist/protect-ffmpeg-options.d.ts +2 -2
- package/dist/protect-ffmpeg-options.js +199 -149
- package/dist/protect-ffmpeg-options.js.map +1 -1
- package/dist/protect-ffmpeg-record.js +1 -1
- package/dist/protect-ffmpeg-record.js.map +1 -1
- package/dist/protect-ffmpeg.d.ts +3 -3
- package/dist/protect-ffmpeg.js.map +1 -1
- package/dist/protect-light.js +2 -0
- package/dist/protect-light.js.map +1 -1
- package/dist/protect-nvr-events.js +34 -31
- package/dist/protect-nvr-events.js.map +1 -1
- package/dist/protect-nvr-systeminfo.js +1 -1
- package/dist/protect-nvr-systeminfo.js.map +1 -1
- package/dist/protect-nvr.d.ts +2 -3
- package/dist/protect-nvr.js +22 -101
- package/dist/protect-nvr.js.map +1 -1
- package/dist/protect-options.d.ts +4 -3
- package/dist/protect-options.js +87 -87
- package/dist/protect-options.js.map +1 -1
- package/dist/protect-platform.js +3 -13
- package/dist/protect-platform.js.map +1 -1
- package/dist/protect-record.js +7 -6
- package/dist/protect-record.js.map +1 -1
- package/dist/protect-sensor.d.ts +6 -3
- package/dist/protect-sensor.js +67 -13
- package/dist/protect-sensor.js.map +1 -1
- package/dist/protect-stream.d.ts +2 -2
- package/dist/protect-stream.js +18 -27
- package/dist/protect-stream.js.map +1 -1
- package/dist/protect-timeshift.d.ts +1 -1
- package/dist/protect-timeshift.js +2 -2
- package/dist/protect-timeshift.js.map +1 -1
- package/dist/protect-viewer.d.ts +2 -1
- package/dist/protect-viewer.js +12 -0
- package/dist/protect-viewer.js.map +1 -1
- package/homebridge-ui/public/index.html +426 -36
- package/homebridge-ui/server.js +12 -12
- package/package.json +8 -8
|
@@ -178,6 +178,10 @@
|
|
|
178
178
|
// Retrieve the current plugin configuration.
|
|
179
179
|
let currentConfig = await homebridge.getPluginConfig();
|
|
180
180
|
|
|
181
|
+
// Keep a list of all the feature options and option groups.
|
|
182
|
+
let featureOptionList = {};
|
|
183
|
+
let featureOptionGroups = {};
|
|
184
|
+
|
|
181
185
|
// Show an navigation bar at the top of the plugin configuration UI.
|
|
182
186
|
const showIntro = () => {
|
|
183
187
|
|
|
@@ -243,7 +247,7 @@
|
|
|
243
247
|
}
|
|
244
248
|
|
|
245
249
|
// Initialize our informational header.
|
|
246
|
-
document.getElementById("headerInfo").innerHTML = "Feature options are applied in prioritized order, from global to device-specific options:<br><i>Global options</i> (lowest priority) → <i>Protect controller options</i> → <i>Protect device options</i> (highest priority)"
|
|
250
|
+
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) → <i class=\"text-success\">Protect controller options</i> → <i class=\"text-info\">Protect device options</i> (highest priority)"
|
|
247
251
|
|
|
248
252
|
// Enumerate our global options.
|
|
249
253
|
const trGlobal = document.createElement("tr");
|
|
@@ -321,7 +325,6 @@
|
|
|
321
325
|
controllersTable.appendChild(trDevice);
|
|
322
326
|
|
|
323
327
|
controllerList.push(label);
|
|
324
|
-
|
|
325
328
|
}
|
|
326
329
|
|
|
327
330
|
// All done. Let the user interact with us.
|
|
@@ -371,6 +374,9 @@
|
|
|
371
374
|
const modelKeys = [...new Set(ufpDevices.map(x => x.modelKey))];
|
|
372
375
|
const deviceList = [];
|
|
373
376
|
|
|
377
|
+
// The first entry returned by getDevices is always the controller.
|
|
378
|
+
const nvr = ufpDevices[0];
|
|
379
|
+
|
|
374
380
|
// Start with a clean slate.
|
|
375
381
|
devicesTable.innerHTML = "";
|
|
376
382
|
|
|
@@ -382,7 +388,7 @@
|
|
|
382
388
|
// If it's a controller, we handle that case differently.
|
|
383
389
|
if((key === "nvr") && devices.length) {
|
|
384
390
|
|
|
385
|
-
// Change the name of the
|
|
391
|
+
// Change the name of the controller that we show users once we've connected with the controller.
|
|
386
392
|
controllerList.map(x => (x.name === controller.address) ? x.childNodes[0].nodeValue = devices[0].name : true);
|
|
387
393
|
continue;
|
|
388
394
|
}
|
|
@@ -417,7 +423,7 @@
|
|
|
417
423
|
const label = document.createElement("label");
|
|
418
424
|
|
|
419
425
|
label.name = device.id;
|
|
420
|
-
label.appendChild(document.createTextNode(device.name));
|
|
426
|
+
label.appendChild(document.createTextNode(device.name ?? device.marketName));
|
|
421
427
|
label.style.cursor = "pointer";
|
|
422
428
|
label.classList.add("mx-2", "my-0", "p-0", "w-100");
|
|
423
429
|
|
|
@@ -451,31 +457,182 @@
|
|
|
451
457
|
};
|
|
452
458
|
|
|
453
459
|
// Initialize our feature option configuration.
|
|
454
|
-
updateConfigOptions(currentConfig[0].options ?? [])
|
|
460
|
+
updateConfigOptions(currentConfig[0].options ?? []);
|
|
461
|
+
|
|
462
|
+
// Is this feature option set explicitly?
|
|
463
|
+
const isOptionSet = (featureOption, deviceMac) => {
|
|
464
|
+
|
|
465
|
+
const optionRegex = new RegExp("^(?:Enable|Disable)\\." + featureOption + (!deviceMac ? "" : "\\." + deviceMac) + "$", "gi");
|
|
466
|
+
return optionsList.filter(x => optionRegex.test(x)).length ? true : false;
|
|
467
|
+
};
|
|
455
468
|
|
|
456
469
|
// Is a feature option globally enabled?
|
|
457
|
-
const isGlobalOptionEnabled = (featureOption,
|
|
470
|
+
const isGlobalOptionEnabled = (featureOption, defaultState) => {
|
|
458
471
|
|
|
459
472
|
featureOption = featureOption.toUpperCase();
|
|
460
473
|
|
|
461
474
|
// Test device-specific options.
|
|
462
475
|
return optionsList.some(x => x === ("ENABLE." + featureOption)) ? true :
|
|
463
|
-
(optionsList.some(x => x === ("DISABLE." + featureOption)) ? false :
|
|
476
|
+
(optionsList.some(x => x === ("DISABLE." + featureOption)) ? false : defaultState
|
|
464
477
|
);
|
|
465
478
|
};
|
|
466
479
|
|
|
467
|
-
// Is a feature option enabled at the device or
|
|
468
|
-
const isDeviceOptionEnabled = (featureOption, mac,
|
|
480
|
+
// Is a feature option enabled at the device or global level. This function does not traverse the scoping hierarchy.
|
|
481
|
+
const isDeviceOptionEnabled = (featureOption, mac, defaultState) => {
|
|
482
|
+
|
|
483
|
+
if(!mac) {
|
|
484
|
+
|
|
485
|
+
return isGlobalOptionEnabled(featureOption, defaultState);
|
|
486
|
+
}
|
|
469
487
|
|
|
470
488
|
featureOption = featureOption.toUpperCase();
|
|
471
489
|
mac = mac.toUpperCase();
|
|
472
490
|
|
|
473
491
|
// Test device-specific options.
|
|
474
492
|
return optionsList.some(x => x === ("ENABLE." + featureOption + "." + mac)) ? true :
|
|
475
|
-
(optionsList.some(x => x === ("DISABLE." + featureOption + "." + mac)) ? false :
|
|
493
|
+
(optionsList.some(x => x === ("DISABLE." + featureOption + "." + mac)) ? false : defaultState
|
|
476
494
|
);
|
|
477
495
|
};
|
|
478
496
|
|
|
497
|
+
// Is a value-centric feature option enabled at the device or global level. This function does not traverse the scoping hierarchy.
|
|
498
|
+
const isOptionValueSet = (featureOption, deviceMac) => {
|
|
499
|
+
|
|
500
|
+
const optionRegex = new RegExp("^Enable\\." + featureOption + (!deviceMac ? "" : "\\." + deviceMac) + "\\.([^\\.]+)$", "gi");
|
|
501
|
+
|
|
502
|
+
return optionsList.filter(x => optionRegex.test(x)).length ? true : false;
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
// Get the value of a value-centric feature option.
|
|
506
|
+
const getOptionValue = (featureOption, deviceMac) => {
|
|
507
|
+
|
|
508
|
+
const optionRegex = new RegExp("^Enable\\." + featureOption + (!deviceMac ? "" : "\\." + deviceMac) + "\\.([^\\.]+)$", "gi");
|
|
509
|
+
|
|
510
|
+
// Get the option value, if we have one.
|
|
511
|
+
for(const option of optionsList) {
|
|
512
|
+
|
|
513
|
+
const regexMatch = optionRegex.exec(option);
|
|
514
|
+
|
|
515
|
+
if(regexMatch) {
|
|
516
|
+
|
|
517
|
+
return regexMatch[1];
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return undefined;
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
// Is a feature option enabled at the device or global level. It does traverse the scoping hierarchy.
|
|
525
|
+
const isOptionEnabled = (featureOption, deviceMac) => {
|
|
526
|
+
|
|
527
|
+
const defaultState = featureOptionList[featureOption]?.default ?? true;
|
|
528
|
+
|
|
529
|
+
if(deviceMac) {
|
|
530
|
+
|
|
531
|
+
// Device level check.
|
|
532
|
+
if(isDeviceOptionEnabled(featureOption, deviceMac, defaultState) !== defaultState) {
|
|
533
|
+
|
|
534
|
+
return !defaultState;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Controller level check.
|
|
538
|
+
if(isDeviceOptionEnabled(featureOption, nvr.mac, defaultState) !== defaultState) {
|
|
539
|
+
|
|
540
|
+
return !defaultState;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Global check.
|
|
545
|
+
if(isGlobalOptionEnabled(featureOption, defaultState) !== defaultState) {
|
|
546
|
+
|
|
547
|
+
return !defaultState;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Return the default.
|
|
551
|
+
return defaultState;
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
// Return the scope level of a feature option.
|
|
555
|
+
const optionScope = (featureOption, deviceMac, defaultState, isOptionValue = false) => {
|
|
556
|
+
|
|
557
|
+
// Scope priority is always: device, NVR, global.
|
|
558
|
+
|
|
559
|
+
// If we have a value-centric feature option, our lookups are a bit different.
|
|
560
|
+
if(isOptionValue) {
|
|
561
|
+
|
|
562
|
+
if(deviceMac) {
|
|
563
|
+
|
|
564
|
+
if(isOptionValueSet(featureOption, deviceMac)) {
|
|
565
|
+
|
|
566
|
+
return "device";
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if(isOptionValueSet(featureOption, nvr.mac)) {
|
|
570
|
+
|
|
571
|
+
return "nvr";
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if(isOptionValueSet(featureOption)) {
|
|
576
|
+
|
|
577
|
+
return "global";
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return "none";
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if(deviceMac) {
|
|
584
|
+
|
|
585
|
+
// Let's see if we've set it at the device-level.
|
|
586
|
+
if((isDeviceOptionEnabled(featureOption, deviceMac, defaultState) !== defaultState) || isOptionSet(featureOption, deviceMac)) {
|
|
587
|
+
|
|
588
|
+
return "device";
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Now let's test the controller level.
|
|
592
|
+
if((isDeviceOptionEnabled(featureOption, nvr.mac, defaultState) !== defaultState) || isOptionSet(featureOption, nvr.mac)) {
|
|
593
|
+
|
|
594
|
+
return "nvr";
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Finally, let's test the global level.
|
|
599
|
+
if((isGlobalOptionEnabled(featureOption, defaultState) !== defaultState) || isOptionSet(featureOption)) {
|
|
600
|
+
|
|
601
|
+
return "global";
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Option isn't set to a non-default value.
|
|
605
|
+
return "none";
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
// Return the color hinting for a given option's scope.
|
|
609
|
+
const optionScopeColor = (featureOption, deviceMac, defaultState, isOptionValue) => {
|
|
610
|
+
|
|
611
|
+
switch(optionScope(featureOption, deviceMac, defaultState, isOptionValue)) {
|
|
612
|
+
|
|
613
|
+
case "device":
|
|
614
|
+
|
|
615
|
+
return "text-info";
|
|
616
|
+
break;
|
|
617
|
+
|
|
618
|
+
case "nvr":
|
|
619
|
+
|
|
620
|
+
return "text-success";
|
|
621
|
+
break;
|
|
622
|
+
|
|
623
|
+
case "global":
|
|
624
|
+
|
|
625
|
+
return deviceMac ? "text-warning" : "text-info";
|
|
626
|
+
break;
|
|
627
|
+
|
|
628
|
+
default:
|
|
629
|
+
|
|
630
|
+
break;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return null;
|
|
634
|
+
};
|
|
635
|
+
|
|
479
636
|
// Show feature option information for a specific device, controller, or globally.
|
|
480
637
|
const showDeviceInfo = async (deviceId) => {
|
|
481
638
|
|
|
@@ -522,6 +679,36 @@
|
|
|
522
679
|
let newConfigTableHtml = "";
|
|
523
680
|
configTable.innerHTML = "";
|
|
524
681
|
|
|
682
|
+
// Initialize the full list of options.
|
|
683
|
+
featureOptionList = {};
|
|
684
|
+
featureOptionGroups = {};
|
|
685
|
+
|
|
686
|
+
for(const category of ufpFeatures.categories) {
|
|
687
|
+
|
|
688
|
+
// Now enumerate all the feature options for a given device and add then to the full list.
|
|
689
|
+
for(const option of optionsDevice[category.name]) {
|
|
690
|
+
|
|
691
|
+
const featureOption = category.name + (option.name.length ? ("." + option.name): "");
|
|
692
|
+
|
|
693
|
+
// Add it to our full list.
|
|
694
|
+
featureOptionList[featureOption] = option;
|
|
695
|
+
|
|
696
|
+
// Cross reference the feature option group it belongs to, if any.
|
|
697
|
+
if(option.group !== undefined) {
|
|
698
|
+
|
|
699
|
+
const expandedGroup = category.name + (option.group.length ? ("." + option.group): "");
|
|
700
|
+
|
|
701
|
+
// Initialize the group entry if needed.
|
|
702
|
+
if(!featureOptionGroups[expandedGroup]) {
|
|
703
|
+
|
|
704
|
+
featureOptionGroups[expandedGroup] = [];
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
featureOptionGroups[expandedGroup].push(featureOption);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
525
712
|
for(const category of ufpFeatures.categories) {
|
|
526
713
|
|
|
527
714
|
// Only show feature option categories that are valid for this context.
|
|
@@ -540,7 +727,7 @@
|
|
|
540
727
|
optionTable.classList.add("table", "table-borderless", "table-sm", "table-hover");
|
|
541
728
|
th.classList.add("p-0");
|
|
542
729
|
th.style.fontWeight = "bold";
|
|
543
|
-
th.colSpan =
|
|
730
|
+
th.colSpan = 3;
|
|
544
731
|
tbody.classList.add("table-bordered");
|
|
545
732
|
|
|
546
733
|
// Add the feature option category description.
|
|
@@ -556,6 +743,7 @@
|
|
|
556
743
|
// Finally, add the table head to the table.
|
|
557
744
|
optionTable.appendChild(thead);
|
|
558
745
|
|
|
746
|
+
// Now enumerate all the feature options for a given device.
|
|
559
747
|
for(const option of optionsDevice[category.name]) {
|
|
560
748
|
|
|
561
749
|
// Only show feature options that are valid for this device.
|
|
@@ -565,9 +753,13 @@
|
|
|
565
753
|
continue;
|
|
566
754
|
}
|
|
567
755
|
|
|
756
|
+
// Expand the full feature option.
|
|
757
|
+
const featureOption = category.name + (option.name.length ? ("." + option.name): "");
|
|
758
|
+
|
|
568
759
|
// Create the next table row.
|
|
569
760
|
const trX = document.createElement("tr");
|
|
570
761
|
trX.classList.add("align-top");
|
|
762
|
+
trX.id = "row-" + featureOption;
|
|
571
763
|
|
|
572
764
|
// Create a checkbox for the option.
|
|
573
765
|
const tdCheckbox = document.createElement("td");
|
|
@@ -575,40 +767,68 @@
|
|
|
575
767
|
// Create the actual checkbox for the option.
|
|
576
768
|
const checkbox = document.createElement("input");
|
|
577
769
|
|
|
578
|
-
const featureOption = category.name + (option.name.length ? ("." + option.name): "");
|
|
579
770
|
checkbox.type = "checkbox";
|
|
771
|
+
checkbox.readOnly = false;
|
|
580
772
|
checkbox.id = featureOption;
|
|
581
773
|
checkbox.name = featureOption;
|
|
582
774
|
checkbox.value = featureOption + (!ufpDevice ? "" : ("." + ufpDevice.mac));
|
|
583
|
-
checkbox.checked = !ufpDevice ? isGlobalOptionEnabled(featureOption, option.default) :
|
|
584
|
-
isDeviceOptionEnabled(featureOption, ufpDevice.mac, option.default);
|
|
585
775
|
|
|
586
|
-
|
|
587
|
-
|
|
776
|
+
let initialValue = undefined;
|
|
777
|
+
let initialScope;
|
|
588
778
|
|
|
589
|
-
//
|
|
590
|
-
|
|
779
|
+
// Determine our initial option scope to show the user what's been set.
|
|
780
|
+
switch(initialScope = optionScope(featureOption, ufpDevice?.mac, option.default, ("defaultValue" in option))) {
|
|
591
781
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
const newOptions = configOptions.filter(x => !x.match(optionRegex));
|
|
782
|
+
case "global":
|
|
783
|
+
case "nvr":
|
|
595
784
|
|
|
596
|
-
|
|
597
|
-
|
|
785
|
+
// 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.
|
|
786
|
+
if(!ufpDevice) {
|
|
598
787
|
|
|
599
|
-
|
|
600
|
-
newOptions.push((checkbox.checked ? "Enable." : "Disable.") + checkbox.value);
|
|
601
|
-
} else {
|
|
788
|
+
if("defaultValue" in option) {
|
|
602
789
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
790
|
+
checkbox.checked = isOptionValueSet(featureOption);
|
|
791
|
+
initialValue = getOptionValue(checkbox.id);
|
|
792
|
+
} else {
|
|
606
793
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
794
|
+
checkbox.checked = isGlobalOptionEnabled(featureOption, option.default);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
if(checkbox.checked) {
|
|
798
|
+
|
|
799
|
+
checkbox.indeterminate = false;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
} else {
|
|
803
|
+
|
|
804
|
+
if("defaultValue" in option) {
|
|
805
|
+
|
|
806
|
+
initialValue = getOptionValue(checkbox.id, (initialScope === "nvr") ? nvr.mac : undefined);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
checkbox.readOnly = checkbox.indeterminate = true;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
break;
|
|
813
|
+
|
|
814
|
+
case "device":
|
|
815
|
+
case "none":
|
|
816
|
+
default:
|
|
817
|
+
|
|
818
|
+
if("defaultValue" in option) {
|
|
819
|
+
|
|
820
|
+
checkbox.checked = isOptionValueSet(featureOption, ufpDevice?.mac);
|
|
821
|
+
initialValue = getOptionValue(checkbox.id, ufpDevice?.mac);
|
|
822
|
+
} else {
|
|
823
|
+
|
|
824
|
+
checkbox.checked = isDeviceOptionEnabled(featureOption, ufpDevice?.mac, option.default);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
break;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
checkbox.defaultChecked = option.default;
|
|
831
|
+
checkbox.classList.add("mx-2");
|
|
612
832
|
|
|
613
833
|
// Add the checkbox to the table cell.
|
|
614
834
|
tdCheckbox.appendChild(checkbox);
|
|
@@ -618,6 +838,50 @@
|
|
|
618
838
|
|
|
619
839
|
const tdLabel = document.createElement("td");
|
|
620
840
|
tdLabel.classList.add("w-100");
|
|
841
|
+
tdLabel.colSpan = 2;
|
|
842
|
+
|
|
843
|
+
let inputValue = null;
|
|
844
|
+
|
|
845
|
+
// Add an input field if we have a value-centric feature option.
|
|
846
|
+
if(("defaultValue" in option)) {
|
|
847
|
+
|
|
848
|
+
const tdInput = document.createElement("td");
|
|
849
|
+
|
|
850
|
+
inputValue = document.createElement("input");
|
|
851
|
+
inputValue.type = "text";
|
|
852
|
+
inputValue.value = initialValue ?? option.defaultValue;
|
|
853
|
+
inputValue.size = 5;
|
|
854
|
+
inputValue.readOnly = !checkbox.checked;
|
|
855
|
+
inputValue.classList.add("mr-2");
|
|
856
|
+
|
|
857
|
+
// Add or remove the setting from our configuration when we've changed our state.
|
|
858
|
+
inputValue.addEventListener("change", async () => {
|
|
859
|
+
|
|
860
|
+
// Find the option in our list and delete it if it exists.
|
|
861
|
+
const optionRegex = new RegExp("^(?:Enable|Disable)\\." + checkbox.id + (!ufpDevice ? "" : ("\\." + ufpDevice.mac)) + "\\.[^\\.]+$", "gi");
|
|
862
|
+
const newOptions = configOptions.filter(x => !optionRegex.test(x));
|
|
863
|
+
|
|
864
|
+
if(checkbox.checked) {
|
|
865
|
+
|
|
866
|
+
newOptions.push("Enable." + checkbox.value + "." + inputValue.value);
|
|
867
|
+
} else if(checkbox.indeterminate) {
|
|
868
|
+
|
|
869
|
+
// If we're in an indeterminate state, we need to traverse the tree to get the upstream value we're inheriting.
|
|
870
|
+
inputValue.value = (ufpDevice?.mac !== nvr.mac) ? (getOptionValue(checkbox.id, nvr.mac) ?? getOptionValue(checkbox.id)) : (getOptionValue(checkbox.id) ?? option.defaultValue);
|
|
871
|
+
} else {
|
|
872
|
+
|
|
873
|
+
inputValue.value = option.defaultValue;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// Update our configuration in Homebridge.
|
|
877
|
+
currentConfig[0].options = newOptions;
|
|
878
|
+
updateConfigOptions(newOptions);
|
|
879
|
+
await homebridge.updatePluginConfig(currentConfig);
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
tdInput.appendChild(inputValue);
|
|
883
|
+
trX.appendChild(tdInput);
|
|
884
|
+
}
|
|
621
885
|
|
|
622
886
|
// Create a label for the checkbox with our option description.
|
|
623
887
|
const labelDescription = document.createElement("label");
|
|
@@ -626,11 +890,131 @@
|
|
|
626
890
|
labelDescription.classList.add("user-select-none", "my-0", "py-0");
|
|
627
891
|
|
|
628
892
|
// Highlight options for the user that are different than our defaults.
|
|
629
|
-
|
|
893
|
+
const scopeColor = optionScopeColor(featureOption, ufpDevice?.mac, option.default, ("defaultValue" in option));
|
|
630
894
|
|
|
631
|
-
|
|
895
|
+
if(scopeColor) {
|
|
896
|
+
|
|
897
|
+
labelDescription.classList.add(scopeColor);
|
|
632
898
|
}
|
|
633
899
|
|
|
900
|
+
// Add or remove the setting from our configuration when we've changed our state.
|
|
901
|
+
checkbox.addEventListener("change", async () => {
|
|
902
|
+
|
|
903
|
+
// Find the option in our list and delete it if it exists.
|
|
904
|
+
const optionRegex = new RegExp("^(?:Enable|Disable)\\." + checkbox.id + (!ufpDevice ? "" : ("\\." + ufpDevice.mac)) + "$", "gi");
|
|
905
|
+
const newOptions = configOptions.filter(x => !optionRegex.test(x));
|
|
906
|
+
|
|
907
|
+
// Figure out if we've got the option set upstream.
|
|
908
|
+
let upstreamOption = false;
|
|
909
|
+
|
|
910
|
+
// 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.
|
|
911
|
+
switch(optionScope(checkbox.id, (ufpDevice && (ufpDevice.mac !== nvr.mac)) ? nvr.mac : null, option.default, ("defaultValue" in option))) {
|
|
912
|
+
|
|
913
|
+
case "device":
|
|
914
|
+
case "nvr":
|
|
915
|
+
|
|
916
|
+
if(ufpDevice.mac !== nvr.mac) {
|
|
917
|
+
|
|
918
|
+
upstreamOption = true;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
break;
|
|
922
|
+
|
|
923
|
+
case "global":
|
|
924
|
+
|
|
925
|
+
if(ufpDevice) {
|
|
926
|
+
|
|
927
|
+
upstreamOption = true;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
break;
|
|
931
|
+
|
|
932
|
+
default:
|
|
933
|
+
|
|
934
|
+
break;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// 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.
|
|
938
|
+
if(checkbox.readOnly && (!("defaultValue" in option) || (("defaultValue" in option) && inputValue && !upstreamOption))) {
|
|
939
|
+
|
|
940
|
+
// We're truly unchecked. We need this because a checkbox can be in both an unchecked and indeterminate simultaneously,
|
|
941
|
+
// so we use the readOnly property to let us know that we've just cycled from an indeterminate state.
|
|
942
|
+
checkbox.checked = checkbox.readOnly = false;
|
|
943
|
+
} else if(!checkbox.checked) {
|
|
944
|
+
|
|
945
|
+
// 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.
|
|
946
|
+
if(upstreamOption) {
|
|
947
|
+
|
|
948
|
+
// We want to set the readOnly property as well, since it will survive a user interaction when they click the checkbox to clear out the
|
|
949
|
+
// indeterminate state. This allows us to effectively cycle between three states.
|
|
950
|
+
checkbox.readOnly = checkbox.indeterminate = true;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
if(("defaultValue" in option) && inputValue) {
|
|
954
|
+
|
|
955
|
+
inputValue.readOnly = true;
|
|
956
|
+
}
|
|
957
|
+
} else if(checkbox.checked) {
|
|
958
|
+
|
|
959
|
+
// We've explicitly checked this option.
|
|
960
|
+
checkbox.readOnly = checkbox.indeterminate = false;
|
|
961
|
+
|
|
962
|
+
if(("defaultValue" in option) && inputValue) {
|
|
963
|
+
|
|
964
|
+
inputValue.readOnly = false;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// The setting is different from the default, highlight it for the user, accounting for upstream scope, and add it to our configuration.
|
|
969
|
+
if(!checkbox.indeterminate && ((checkbox.checked !== option.default) || upstreamOption)) {
|
|
970
|
+
|
|
971
|
+
labelDescription.classList.add("text-info");
|
|
972
|
+
newOptions.push((checkbox.checked ? "Enable." : "Disable.") + checkbox.value);
|
|
973
|
+
} else {
|
|
974
|
+
|
|
975
|
+
// We've reset to the defaults, remove our highlighting.
|
|
976
|
+
labelDescription.classList.remove("text-info");
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// Update our Homebridge configuration.
|
|
980
|
+
if(("defaultValue" in option) && inputValue) {
|
|
981
|
+
|
|
982
|
+
// Inform our value-centric feature option to update Homebridge.
|
|
983
|
+
const changeEvent = new Event("change");
|
|
984
|
+
|
|
985
|
+
inputValue.dispatchEvent(changeEvent);
|
|
986
|
+
} else {
|
|
987
|
+
|
|
988
|
+
// Update our configuration in Homebridge.
|
|
989
|
+
currentConfig[0].options = newOptions;
|
|
990
|
+
updateConfigOptions(newOptions);
|
|
991
|
+
await homebridge.updatePluginConfig(currentConfig);
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// If we've reset to defaults, make sure our color coding for scope is reflected.
|
|
995
|
+
if((checkbox.checked === option.default) || checkbox.indeterminate) {
|
|
996
|
+
|
|
997
|
+
const scopeColor = optionScopeColor(featureOption, ufpDevice?.mac, option.default, ("defaultValue" in option));
|
|
998
|
+
|
|
999
|
+
if(scopeColor) {
|
|
1000
|
+
|
|
1001
|
+
labelDescription.classList.add(scopeColor);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// Adjust visibility of other feature options that depend on us.
|
|
1006
|
+
if(featureOptionGroups[checkbox.id]) {
|
|
1007
|
+
|
|
1008
|
+
const entryVisibility = isOptionEnabled(featureOption, ufpDevice?.mac) ? "" : "none";
|
|
1009
|
+
|
|
1010
|
+
// Lookup each feature option setting and set the visibility accordingly.
|
|
1011
|
+
for(const entry of featureOptionGroups[checkbox.id]) {
|
|
1012
|
+
|
|
1013
|
+
document.getElementById("row-" + entry).style.display = entryVisibility;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
});
|
|
1017
|
+
|
|
634
1018
|
// Add the actual description for the option after the checkbox.
|
|
635
1019
|
labelDescription.appendChild(document.createTextNode(option.description));
|
|
636
1020
|
|
|
@@ -643,6 +1027,12 @@
|
|
|
643
1027
|
// Add the label table cell to the table row.
|
|
644
1028
|
trX.appendChild(tdLabel);
|
|
645
1029
|
|
|
1030
|
+
// Adjust the visibility of the feature option, if it's logically grouped.
|
|
1031
|
+
if((option.group !== undefined) && !isOptionEnabled(category.name + (option.group.length ? ("." + option.group): ""), ufpDevice?.mac)) {
|
|
1032
|
+
|
|
1033
|
+
trX.style.display = "none";
|
|
1034
|
+
}
|
|
1035
|
+
|
|
646
1036
|
// Add the table row to the table body.
|
|
647
1037
|
tbody.appendChild(trX);
|
|
648
1038
|
}
|
package/homebridge-ui/server.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
/* eslint-disable new-cap */
|
|
12
12
|
"use strict";
|
|
13
13
|
|
|
14
|
-
import { featureOptionCategories, featureOptions,
|
|
14
|
+
import { featureOptionCategories, featureOptions, isOptionEnabled } from "../dist/protect-options.js";
|
|
15
15
|
import { HomebridgePluginUiServer } from "@homebridge/plugin-ui-utils";
|
|
16
16
|
import { ProtectApi } from "unifi-protect";
|
|
17
17
|
import * as fs from "node:fs";
|
|
@@ -50,40 +50,40 @@ class PluginUiServer extends HomebridgePluginUiServer {
|
|
|
50
50
|
|
|
51
51
|
bootstrap.cameras.sort((a, b) => {
|
|
52
52
|
|
|
53
|
-
const aCase = a.name.toLowerCase();
|
|
54
|
-
const bCase = b.name.toLowerCase();
|
|
53
|
+
const aCase = (a.name ?? a.marketName).toLowerCase();
|
|
54
|
+
const bCase = (b.name ?? b.marketName).toLowerCase();
|
|
55
55
|
|
|
56
56
|
return aCase > bCase ? 1 : (bCase > aCase ? -1 : 0);
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
bootstrap.chimes.sort((a, b) => {
|
|
60
60
|
|
|
61
|
-
const aCase = a.name.toLowerCase();
|
|
62
|
-
const bCase = b.name.toLowerCase();
|
|
61
|
+
const aCase = (a.name ?? a.marketName).toLowerCase();
|
|
62
|
+
const bCase = (b.name ?? b.marketName).toLowerCase();
|
|
63
63
|
|
|
64
64
|
return aCase > bCase ? 1 : (bCase > aCase ? -1 : 0);
|
|
65
65
|
});
|
|
66
66
|
|
|
67
67
|
bootstrap.lights.sort((a, b) => {
|
|
68
68
|
|
|
69
|
-
const aCase = a.name.toLowerCase();
|
|
70
|
-
const bCase = b.name.toLowerCase();
|
|
69
|
+
const aCase = (a.name ?? a.marketName).toLowerCase();
|
|
70
|
+
const bCase = (b.name ?? b.marketName).toLowerCase();
|
|
71
71
|
|
|
72
72
|
return aCase > bCase ? 1 : (bCase > aCase ? -1 : 0);
|
|
73
73
|
});
|
|
74
74
|
|
|
75
75
|
bootstrap.sensors.sort((a, b) => {
|
|
76
76
|
|
|
77
|
-
const aCase = a.name.toLowerCase();
|
|
78
|
-
const bCase = b.name.toLowerCase();
|
|
77
|
+
const aCase = (a.name ?? a.marketName).toLowerCase();
|
|
78
|
+
const bCase = (b.name ?? b.marketName).toLowerCase();
|
|
79
79
|
|
|
80
80
|
return aCase > bCase ? 1 : (bCase > aCase ? -1 : 0);
|
|
81
81
|
});
|
|
82
82
|
|
|
83
83
|
bootstrap.viewers.sort((a, b) => {
|
|
84
84
|
|
|
85
|
-
const aCase = a.name.toLowerCase();
|
|
86
|
-
const bCase = b.name.toLowerCase();
|
|
85
|
+
const aCase = (a.name ?? a.marketName).toLowerCase();
|
|
86
|
+
const bCase = (b.name ?? b.marketName).toLowerCase();
|
|
87
87
|
|
|
88
88
|
return aCase > bCase ? 1 : (bCase > aCase ? -1 : 0);
|
|
89
89
|
});
|
|
@@ -113,7 +113,7 @@ class PluginUiServer extends HomebridgePluginUiServer {
|
|
|
113
113
|
|
|
114
114
|
for(const options of featureOptions[category.name]) {
|
|
115
115
|
|
|
116
|
-
options.value =
|
|
116
|
+
options.value = isOptionEnabled(request.configOptions, request.nvrUfp, request.deviceUfp, category.name + "." + options.name, options.default);
|
|
117
117
|
optionSet[category.name].push(options);
|
|
118
118
|
}
|
|
119
119
|
}
|