homebridge-unifi-protect 6.8.1 → 6.9.1
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/README.md +3 -16
- 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 +43 -134
- package/dist/protect-camera.js.map +1 -1
- package/dist/protect-device.d.ts +7 -1
- package/dist/protect-device.js +56 -5
- 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-nvr-events.js +28 -29
- 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 +84 -86
- 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 +65 -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 +236 -39
- package/homebridge-ui/server.js +2 -2
- package/package.json +7 -7
|
@@ -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
|
|
|
@@ -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.
|
|
@@ -454,32 +457,32 @@
|
|
|
454
457
|
};
|
|
455
458
|
|
|
456
459
|
// Initialize our feature option configuration.
|
|
457
|
-
updateConfigOptions(currentConfig[0].options ?? [])
|
|
460
|
+
updateConfigOptions(currentConfig[0].options ?? []);
|
|
458
461
|
|
|
459
462
|
// Is this feature option set explicitly?
|
|
460
463
|
const isOptionSet = (featureOption, deviceMac) => {
|
|
461
464
|
|
|
462
|
-
const optionRegex = new RegExp("^(Enable|Disable)
|
|
463
|
-
return optionsList.filter(x =>
|
|
465
|
+
const optionRegex = new RegExp("^(?:Enable|Disable)\\." + featureOption + (!deviceMac ? "" : "\\." + deviceMac) + "$", "gi");
|
|
466
|
+
return optionsList.filter(x => optionRegex.test(x)).length ? true : false;
|
|
464
467
|
};
|
|
465
468
|
|
|
466
469
|
// Is a feature option globally enabled?
|
|
467
|
-
const isGlobalOptionEnabled = (featureOption,
|
|
470
|
+
const isGlobalOptionEnabled = (featureOption, defaultState) => {
|
|
468
471
|
|
|
469
472
|
featureOption = featureOption.toUpperCase();
|
|
470
473
|
|
|
471
474
|
// Test device-specific options.
|
|
472
475
|
return optionsList.some(x => x === ("ENABLE." + featureOption)) ? true :
|
|
473
|
-
(optionsList.some(x => x === ("DISABLE." + featureOption)) ? false :
|
|
476
|
+
(optionsList.some(x => x === ("DISABLE." + featureOption)) ? false : defaultState
|
|
474
477
|
);
|
|
475
478
|
};
|
|
476
479
|
|
|
477
|
-
// Is a feature option enabled at the device or global level.
|
|
478
|
-
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) => {
|
|
479
482
|
|
|
480
483
|
if(!mac) {
|
|
481
484
|
|
|
482
|
-
return isGlobalOptionEnabled(featureOption,
|
|
485
|
+
return isGlobalOptionEnabled(featureOption, defaultState);
|
|
483
486
|
}
|
|
484
487
|
|
|
485
488
|
featureOption = featureOption.toUpperCase();
|
|
@@ -487,60 +490,113 @@
|
|
|
487
490
|
|
|
488
491
|
// Test device-specific options.
|
|
489
492
|
return optionsList.some(x => x === ("ENABLE." + featureOption + "." + mac)) ? true :
|
|
490
|
-
(optionsList.some(x => x === ("DISABLE." + featureOption + "." + mac)) ? false :
|
|
493
|
+
(optionsList.some(x => x === ("DISABLE." + featureOption + "." + mac)) ? false : defaultState
|
|
491
494
|
);
|
|
492
495
|
};
|
|
493
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
|
+
|
|
494
524
|
// Is a feature option enabled at the device or global level. It does traverse the scoping hierarchy.
|
|
495
|
-
const isOptionEnabled = (featureOption, deviceMac
|
|
525
|
+
const isOptionEnabled = (featureOption, deviceMac) => {
|
|
526
|
+
|
|
527
|
+
const defaultState = featureOptionList[featureOption]?.default ?? true;
|
|
496
528
|
|
|
497
529
|
if(deviceMac) {
|
|
498
530
|
|
|
499
531
|
// Device level check.
|
|
500
|
-
if(isDeviceOptionEnabled(featureOption, deviceMac,
|
|
532
|
+
if(isDeviceOptionEnabled(featureOption, deviceMac, defaultState) !== defaultState) {
|
|
501
533
|
|
|
502
|
-
return !
|
|
534
|
+
return !defaultState;
|
|
503
535
|
}
|
|
504
536
|
|
|
505
537
|
// Controller level check.
|
|
506
|
-
if(isDeviceOptionEnabled(featureOption, nvr.mac,
|
|
538
|
+
if(isDeviceOptionEnabled(featureOption, nvr.mac, defaultState) !== defaultState) {
|
|
507
539
|
|
|
508
|
-
return !
|
|
540
|
+
return !defaultState;
|
|
509
541
|
}
|
|
510
542
|
}
|
|
511
543
|
|
|
512
544
|
// Global check.
|
|
513
|
-
if(isGlobalOptionEnabled(featureOption,
|
|
545
|
+
if(isGlobalOptionEnabled(featureOption, defaultState) !== defaultState) {
|
|
514
546
|
|
|
515
|
-
return !
|
|
547
|
+
return !defaultState;
|
|
516
548
|
}
|
|
517
549
|
|
|
518
550
|
// Return the default.
|
|
519
|
-
return
|
|
551
|
+
return defaultState;
|
|
520
552
|
};
|
|
521
553
|
|
|
522
554
|
// Return the scope level of a feature option.
|
|
523
|
-
const optionScope = (featureOption, deviceMac,
|
|
555
|
+
const optionScope = (featureOption, deviceMac, defaultState, isOptionValue = false) => {
|
|
524
556
|
|
|
525
557
|
// Scope priority is always: device, NVR, global.
|
|
526
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
|
+
|
|
527
583
|
if(deviceMac) {
|
|
528
584
|
|
|
529
585
|
// Let's see if we've set it at the device-level.
|
|
530
|
-
if((isDeviceOptionEnabled(featureOption, deviceMac,
|
|
586
|
+
if((isDeviceOptionEnabled(featureOption, deviceMac, defaultState) !== defaultState) || isOptionSet(featureOption, deviceMac)) {
|
|
531
587
|
|
|
532
588
|
return "device";
|
|
533
589
|
}
|
|
534
590
|
|
|
535
591
|
// Now let's test the controller level.
|
|
536
|
-
if((isDeviceOptionEnabled(featureOption, nvr.mac,
|
|
592
|
+
if((isDeviceOptionEnabled(featureOption, nvr.mac, defaultState) !== defaultState) || isOptionSet(featureOption, nvr.mac)) {
|
|
537
593
|
|
|
538
594
|
return "nvr";
|
|
539
595
|
}
|
|
540
596
|
}
|
|
541
597
|
|
|
542
598
|
// Finally, let's test the global level.
|
|
543
|
-
if((isGlobalOptionEnabled(featureOption,
|
|
599
|
+
if((isGlobalOptionEnabled(featureOption, defaultState) !== defaultState) || isOptionSet(featureOption)) {
|
|
544
600
|
|
|
545
601
|
return "global";
|
|
546
602
|
}
|
|
@@ -550,9 +606,9 @@
|
|
|
550
606
|
};
|
|
551
607
|
|
|
552
608
|
// Return the color hinting for a given option's scope.
|
|
553
|
-
const optionScopeColor = (featureOption, deviceMac,
|
|
609
|
+
const optionScopeColor = (featureOption, deviceMac, defaultState, isOptionValue) => {
|
|
554
610
|
|
|
555
|
-
switch(optionScope(featureOption, deviceMac,
|
|
611
|
+
switch(optionScope(featureOption, deviceMac, defaultState, isOptionValue)) {
|
|
556
612
|
|
|
557
613
|
case "device":
|
|
558
614
|
|
|
@@ -623,6 +679,36 @@
|
|
|
623
679
|
let newConfigTableHtml = "";
|
|
624
680
|
configTable.innerHTML = "";
|
|
625
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
|
+
|
|
626
712
|
for(const category of ufpFeatures.categories) {
|
|
627
713
|
|
|
628
714
|
// Only show feature option categories that are valid for this context.
|
|
@@ -641,7 +727,7 @@
|
|
|
641
727
|
optionTable.classList.add("table", "table-borderless", "table-sm", "table-hover");
|
|
642
728
|
th.classList.add("p-0");
|
|
643
729
|
th.style.fontWeight = "bold";
|
|
644
|
-
th.colSpan =
|
|
730
|
+
th.colSpan = 3;
|
|
645
731
|
tbody.classList.add("table-bordered");
|
|
646
732
|
|
|
647
733
|
// Add the feature option category description.
|
|
@@ -667,9 +753,13 @@
|
|
|
667
753
|
continue;
|
|
668
754
|
}
|
|
669
755
|
|
|
756
|
+
// Expand the full feature option.
|
|
757
|
+
const featureOption = category.name + (option.name.length ? ("." + option.name): "");
|
|
758
|
+
|
|
670
759
|
// Create the next table row.
|
|
671
760
|
const trX = document.createElement("tr");
|
|
672
761
|
trX.classList.add("align-top");
|
|
762
|
+
trX.id = "row-" + featureOption;
|
|
673
763
|
|
|
674
764
|
// Create a checkbox for the option.
|
|
675
765
|
const tdCheckbox = document.createElement("td");
|
|
@@ -677,14 +767,17 @@
|
|
|
677
767
|
// Create the actual checkbox for the option.
|
|
678
768
|
const checkbox = document.createElement("input");
|
|
679
769
|
|
|
680
|
-
const featureOption = category.name + (option.name.length ? ("." + option.name): "");
|
|
681
770
|
checkbox.type = "checkbox";
|
|
682
771
|
checkbox.readOnly = false;
|
|
683
772
|
checkbox.id = featureOption;
|
|
684
773
|
checkbox.name = featureOption;
|
|
685
774
|
checkbox.value = featureOption + (!ufpDevice ? "" : ("." + ufpDevice.mac));
|
|
686
775
|
|
|
687
|
-
|
|
776
|
+
let initialValue = undefined;
|
|
777
|
+
let initialScope;
|
|
778
|
+
|
|
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))) {
|
|
688
781
|
|
|
689
782
|
case "global":
|
|
690
783
|
case "nvr":
|
|
@@ -692,7 +785,14 @@
|
|
|
692
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.
|
|
693
786
|
if(!ufpDevice) {
|
|
694
787
|
|
|
695
|
-
|
|
788
|
+
if("defaultValue" in option) {
|
|
789
|
+
|
|
790
|
+
checkbox.checked = isOptionValueSet(featureOption);
|
|
791
|
+
initialValue = getOptionValue(checkbox.id);
|
|
792
|
+
} else {
|
|
793
|
+
|
|
794
|
+
checkbox.checked = isGlobalOptionEnabled(featureOption, option.default);
|
|
795
|
+
}
|
|
696
796
|
|
|
697
797
|
if(checkbox.checked) {
|
|
698
798
|
|
|
@@ -701,6 +801,11 @@
|
|
|
701
801
|
|
|
702
802
|
} else {
|
|
703
803
|
|
|
804
|
+
if("defaultValue" in option) {
|
|
805
|
+
|
|
806
|
+
initialValue = getOptionValue(checkbox.id, (initialScope === "nvr") ? nvr.mac : undefined);
|
|
807
|
+
}
|
|
808
|
+
|
|
704
809
|
checkbox.readOnly = checkbox.indeterminate = true;
|
|
705
810
|
}
|
|
706
811
|
|
|
@@ -709,7 +814,16 @@
|
|
|
709
814
|
case "device":
|
|
710
815
|
case "none":
|
|
711
816
|
default:
|
|
712
|
-
|
|
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
|
+
|
|
713
827
|
break;
|
|
714
828
|
}
|
|
715
829
|
|
|
@@ -724,6 +838,51 @@
|
|
|
724
838
|
|
|
725
839
|
const tdLabel = document.createElement("td");
|
|
726
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
|
+
tdInput.classList.add("mr-2");
|
|
850
|
+
tdInput.style.width = "10%";
|
|
851
|
+
|
|
852
|
+
inputValue = document.createElement("input");
|
|
853
|
+
inputValue.type = "text";
|
|
854
|
+
inputValue.value = initialValue ?? option.defaultValue;
|
|
855
|
+
inputValue.size = 5;
|
|
856
|
+
inputValue.readOnly = !checkbox.checked;
|
|
857
|
+
|
|
858
|
+
// Add or remove the setting from our configuration when we've changed our state.
|
|
859
|
+
inputValue.addEventListener("change", async () => {
|
|
860
|
+
|
|
861
|
+
// Find the option in our list and delete it if it exists.
|
|
862
|
+
const optionRegex = new RegExp("^(?:Enable|Disable)\\." + checkbox.id + (!ufpDevice ? "" : ("\\." + ufpDevice.mac)) + "\\.[^\\.]+$", "gi");
|
|
863
|
+
const newOptions = configOptions.filter(x => !optionRegex.test(x));
|
|
864
|
+
|
|
865
|
+
if(checkbox.checked) {
|
|
866
|
+
|
|
867
|
+
newOptions.push("Enable." + checkbox.value + "." + inputValue.value);
|
|
868
|
+
} else if(checkbox.indeterminate) {
|
|
869
|
+
|
|
870
|
+
// If we're in an indeterminate state, we need to traverse the tree to get the upstream value we're inheriting.
|
|
871
|
+
inputValue.value = (ufpDevice?.mac !== nvr.mac) ? (getOptionValue(checkbox.id, nvr.mac) ?? getOptionValue(checkbox.id)) : (getOptionValue(checkbox.id) ?? option.defaultValue);
|
|
872
|
+
} else {
|
|
873
|
+
|
|
874
|
+
inputValue.value = option.defaultValue;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Update our configuration in Homebridge.
|
|
878
|
+
currentConfig[0].options = newOptions;
|
|
879
|
+
updateConfigOptions(newOptions);
|
|
880
|
+
await homebridge.updatePluginConfig(currentConfig);
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
tdInput.appendChild(inputValue);
|
|
884
|
+
trX.appendChild(tdInput);
|
|
885
|
+
}
|
|
727
886
|
|
|
728
887
|
// Create a label for the checkbox with our option description.
|
|
729
888
|
const labelDescription = document.createElement("label");
|
|
@@ -732,7 +891,7 @@
|
|
|
732
891
|
labelDescription.classList.add("user-select-none", "my-0", "py-0");
|
|
733
892
|
|
|
734
893
|
// Highlight options for the user that are different than our defaults.
|
|
735
|
-
const scopeColor = optionScopeColor(featureOption, ufpDevice?.mac, option.default);
|
|
894
|
+
const scopeColor = optionScopeColor(featureOption, ufpDevice?.mac, option.default, ("defaultValue" in option));
|
|
736
895
|
|
|
737
896
|
if(scopeColor) {
|
|
738
897
|
|
|
@@ -743,14 +902,14 @@
|
|
|
743
902
|
checkbox.addEventListener("change", async () => {
|
|
744
903
|
|
|
745
904
|
// Find the option in our list and delete it if it exists.
|
|
746
|
-
const optionRegex = new RegExp("^(Enable|Disable)
|
|
747
|
-
const newOptions = configOptions.filter(x => !
|
|
905
|
+
const optionRegex = new RegExp("^(?:Enable|Disable)\\." + checkbox.id + (!ufpDevice ? "" : ("\\." + ufpDevice.mac)) + "$", "gi");
|
|
906
|
+
const newOptions = configOptions.filter(x => !optionRegex.test(x));
|
|
748
907
|
|
|
749
908
|
// Figure out if we've got the option set upstream.
|
|
750
909
|
let upstreamOption = false;
|
|
751
910
|
|
|
752
911
|
// 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.
|
|
753
|
-
switch(optionScope(checkbox.id, (ufpDevice && (ufpDevice.mac !== nvr.mac)) ? nvr.mac : null, option.default)) {
|
|
912
|
+
switch(optionScope(checkbox.id, (ufpDevice && (ufpDevice.mac !== nvr.mac)) ? nvr.mac : null, option.default, ("defaultValue" in option))) {
|
|
754
913
|
|
|
755
914
|
case "device":
|
|
756
915
|
case "nvr":
|
|
@@ -776,7 +935,8 @@
|
|
|
776
935
|
break;
|
|
777
936
|
}
|
|
778
937
|
|
|
779
|
-
if
|
|
938
|
+
// 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.
|
|
939
|
+
if(checkbox.readOnly && (!("defaultValue" in option) || (("defaultValue" in option) && inputValue && !upstreamOption))) {
|
|
780
940
|
|
|
781
941
|
// We're truly unchecked. We need this because a checkbox can be in both an unchecked and indeterminate simultaneously,
|
|
782
942
|
// so we use the readOnly property to let us know that we've just cycled from an indeterminate state.
|
|
@@ -791,10 +951,19 @@
|
|
|
791
951
|
checkbox.readOnly = checkbox.indeterminate = true;
|
|
792
952
|
}
|
|
793
953
|
|
|
954
|
+
if(("defaultValue" in option) && inputValue) {
|
|
955
|
+
|
|
956
|
+
inputValue.readOnly = true;
|
|
957
|
+
}
|
|
794
958
|
} else if(checkbox.checked) {
|
|
795
959
|
|
|
796
960
|
// We've explicitly checked this option.
|
|
797
961
|
checkbox.readOnly = checkbox.indeterminate = false;
|
|
962
|
+
|
|
963
|
+
if(("defaultValue" in option) && inputValue) {
|
|
964
|
+
|
|
965
|
+
inputValue.readOnly = false;
|
|
966
|
+
}
|
|
798
967
|
}
|
|
799
968
|
|
|
800
969
|
// The setting is different from the default, highlight it for the user, accounting for upstream scope, and add it to our configuration.
|
|
@@ -808,21 +977,43 @@
|
|
|
808
977
|
labelDescription.classList.remove("text-info");
|
|
809
978
|
}
|
|
810
979
|
|
|
811
|
-
// Update our configuration
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
980
|
+
// Update our Homebridge configuration.
|
|
981
|
+
if(("defaultValue" in option) && inputValue) {
|
|
982
|
+
|
|
983
|
+
// Inform our value-centric feature option to update Homebridge.
|
|
984
|
+
const changeEvent = new Event("change");
|
|
985
|
+
|
|
986
|
+
inputValue.dispatchEvent(changeEvent);
|
|
987
|
+
} else {
|
|
988
|
+
|
|
989
|
+
// Update our configuration in Homebridge.
|
|
990
|
+
currentConfig[0].options = newOptions;
|
|
991
|
+
updateConfigOptions(newOptions);
|
|
992
|
+
await homebridge.updatePluginConfig(currentConfig);
|
|
993
|
+
}
|
|
815
994
|
|
|
816
995
|
// If we've reset to defaults, make sure our color coding for scope is reflected.
|
|
817
996
|
if((checkbox.checked === option.default) || checkbox.indeterminate) {
|
|
818
997
|
|
|
819
|
-
const scopeColor = optionScopeColor(featureOption, ufpDevice?.mac, option.default);
|
|
998
|
+
const scopeColor = optionScopeColor(featureOption, ufpDevice?.mac, option.default, ("defaultValue" in option));
|
|
820
999
|
|
|
821
1000
|
if(scopeColor) {
|
|
822
1001
|
|
|
823
1002
|
labelDescription.classList.add(scopeColor);
|
|
824
1003
|
}
|
|
825
1004
|
}
|
|
1005
|
+
|
|
1006
|
+
// Adjust visibility of other feature options that depend on us.
|
|
1007
|
+
if(featureOptionGroups[checkbox.id]) {
|
|
1008
|
+
|
|
1009
|
+
const entryVisibility = isOptionEnabled(featureOption, ufpDevice?.mac) ? "" : "none";
|
|
1010
|
+
|
|
1011
|
+
// Lookup each feature option setting and set the visibility accordingly.
|
|
1012
|
+
for(const entry of featureOptionGroups[checkbox.id]) {
|
|
1013
|
+
|
|
1014
|
+
document.getElementById("row-" + entry).style.display = entryVisibility;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
826
1017
|
});
|
|
827
1018
|
|
|
828
1019
|
// Add the actual description for the option after the checkbox.
|
|
@@ -837,6 +1028,12 @@
|
|
|
837
1028
|
// Add the label table cell to the table row.
|
|
838
1029
|
trX.appendChild(tdLabel);
|
|
839
1030
|
|
|
1031
|
+
// Adjust the visibility of the feature option, if it's logically grouped.
|
|
1032
|
+
if((option.group !== undefined) && !isOptionEnabled(category.name + (option.group.length ? ("." + option.group): ""), ufpDevice?.mac)) {
|
|
1033
|
+
|
|
1034
|
+
trX.style.display = "none";
|
|
1035
|
+
}
|
|
1036
|
+
|
|
840
1037
|
// Add the table row to the table body.
|
|
841
1038
|
tbody.appendChild(trX);
|
|
842
1039
|
}
|
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";
|
|
@@ -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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-unifi-protect",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.9.1",
|
|
4
4
|
"displayName": "Homebridge UniFi Protect",
|
|
5
5
|
"description": "Homebridge UniFi Protect plugin providing complete HomeKit integration for the UniFi Protect ecosystem with full support for most features including autoconfiguration, motion detection, multiple controllers, and realtime updates.",
|
|
6
6
|
"author": {
|
|
@@ -68,15 +68,15 @@
|
|
|
68
68
|
"@homebridge/plugin-ui-utils": "^0.0.19",
|
|
69
69
|
"ffmpeg-for-homebridge": "^0.1.4",
|
|
70
70
|
"mqtt": "4.3.7",
|
|
71
|
-
"unifi-protect": "^4.
|
|
71
|
+
"unifi-protect": "^4.4.0",
|
|
72
72
|
"ws": "^8.13.0"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
|
-
"@types/node": "^20.
|
|
76
|
-
"@types/ws": "^8.5.
|
|
77
|
-
"@typescript-eslint/eslint-plugin": "^5.
|
|
78
|
-
"@typescript-eslint/parser": "^5.
|
|
79
|
-
"eslint": "^8.
|
|
75
|
+
"@types/node": "^20.3.1",
|
|
76
|
+
"@types/ws": "^8.5.5",
|
|
77
|
+
"@typescript-eslint/eslint-plugin": "^5.60.0",
|
|
78
|
+
"@typescript-eslint/parser": "^5.60.0",
|
|
79
|
+
"eslint": "^8.43.0",
|
|
80
80
|
"homebridge": "=1.6.1",
|
|
81
81
|
"nodemon": "^2.0.22",
|
|
82
82
|
"rimraf": "^5.0.1",
|