homebridge-unifi-protect 7.8.2 → 7.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.
@@ -312,7 +312,7 @@ export class webUiFeatureOptions {
312
312
  }
313
313
 
314
314
  // The first entry returned by getDevices() must always be the controller.
315
- this.#controller = this.#devices[0]?.serial ?? null;
315
+ this.#controller = this.#devices[0]?.serialNumber ?? null;
316
316
  }
317
317
 
318
318
  // Make the UI visible.
@@ -329,7 +329,7 @@ export class webUiFeatureOptions {
329
329
  this.#sidebar.showDevices(controller, this.#devices);
330
330
 
331
331
  // Display the feature options to the user.
332
- this.showDeviceOptions(controller ? this.#devices[0].serial : "Global Options");
332
+ this.showDeviceOptions(controller ? this.#devices[0].serialNumber : "Global Options");
333
333
 
334
334
  // All done. Let the user interact with us.
335
335
  homebridge.hideSpinner();
@@ -345,7 +345,7 @@ export class webUiFeatureOptions {
345
345
  webUiEntry.parentElement.classList.add("bg-info", "text-white") : webUiEntry.parentElement.classList.remove("bg-info", "text-white"));
346
346
 
347
347
  // Populate the device information info pane.
348
- const currentDevice = this.#devices.find(device => device.serial === deviceId);
348
+ const currentDevice = this.#devices.find(device => device.serialNumber === deviceId);
349
349
 
350
350
  // Populate the details view. If there's no device specified, the context is considered global and we hide the device details view.
351
351
  if(!currentDevice) {
@@ -428,13 +428,13 @@ export class webUiFeatureOptions {
428
428
  checkbox.readOnly = false;
429
429
  checkbox.id = featureOption;
430
430
  checkbox.name = featureOption;
431
- checkbox.value = featureOption + (!currentDevice ? "" : ("." + currentDevice.serial));
431
+ checkbox.value = featureOption + (!currentDevice ? "" : ("." + currentDevice.serialNumber));
432
432
 
433
433
  let initialValue = undefined;
434
434
  let initialScope;
435
435
 
436
436
  // Determine our initial option scope to show the user what's been set.
437
- switch(initialScope = this.#featureOptions.scope(featureOption, currentDevice?.serial, this.#controller)) {
437
+ switch(initialScope = this.#featureOptions.scope(featureOption, currentDevice?.serialNumber, this.#controller)) {
438
438
 
439
439
  case "global":
440
440
  case "controller":
@@ -442,13 +442,11 @@ export class webUiFeatureOptions {
442
442
  // 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
443
  if(!currentDevice) {
444
444
 
445
+ checkbox.checked = this.#featureOptions.test(featureOption);
446
+
445
447
  if(this.#featureOptions.isValue(featureOption)) {
446
448
 
447
- checkbox.checked = this.#featureOptions.exists(featureOption);
448
449
  initialValue = this.#featureOptions.value(checkbox.id);
449
- } else {
450
-
451
- checkbox.checked = this.#featureOptions.test(featureOption);
452
450
  }
453
451
 
454
452
  if(checkbox.checked) {
@@ -472,13 +470,11 @@ export class webUiFeatureOptions {
472
470
  case "none":
473
471
  default:
474
472
 
475
- if(this.#featureOptions.isValue(featureOption)) {
473
+ checkbox.checked = this.#featureOptions.test(featureOption, currentDevice?.serialNumber);
476
474
 
477
- checkbox.checked = this.#featureOptions.exists(featureOption, currentDevice?.serial);
478
- initialValue = this.#featureOptions.value(checkbox.id, currentDevice?.serial);
479
- } else {
475
+ if(this.#featureOptions.isValue(featureOption)) {
480
476
 
481
- checkbox.checked = this.#featureOptions.test(featureOption, currentDevice?.serial);
477
+ initialValue = this.#featureOptions.value(checkbox.id, currentDevice?.serialNumber);
482
478
  }
483
479
 
484
480
  break;
@@ -512,35 +508,10 @@ export class webUiFeatureOptions {
512
508
  inputValue.type = "text";
513
509
  inputValue.value = initialValue ?? option.defaultValue;
514
510
  inputValue.size = 5;
515
- inputValue.readOnly = !checkbox.checked;
511
+ inputValue.readOnly = checkbox.readOnly;
516
512
 
517
513
  // Add or remove the setting from our configuration when we've changed our state.
518
- inputValue.addEventListener("change", async () => {
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
-
514
+ inputValue.addEventListener("change", () => checkbox.dispatchEvent(new Event("change")));
544
515
  tdInput.appendChild(inputValue);
545
516
  trX.appendChild(tdInput);
546
517
  }
@@ -553,7 +524,7 @@ export class webUiFeatureOptions {
553
524
  labelDescription.classList.add("user-select-none", "my-0", "py-0");
554
525
 
555
526
  // Highlight options for the user that are different than our defaults.
556
- const scopeColor = this.#featureOptions.color(featureOption, currentDevice?.serial, this.#controller);
527
+ const scopeColor = this.#featureOptions.color(featureOption, currentDevice?.serialNumber, this.#controller);
557
528
 
558
529
  if(scopeColor) {
559
530
 
@@ -564,19 +535,21 @@ export class webUiFeatureOptions {
564
535
  checkbox.addEventListener("change", async () => {
565
536
 
566
537
  // Find the option in our list and delete it if it exists.
567
- const optionRegex = new RegExp("^(?:Enable|Disable)\\." + checkbox.id + (!currentDevice ? "" : ("\\." + currentDevice.serial)) + "$", "gi");
538
+ const optionRegex = new RegExp("^(?:Enable|Disable)\\." + checkbox.id + (!currentDevice ? "" : ("\\." + currentDevice.serialNumber)) +
539
+ "(?:\\.([^\\.]*))?$", "gi");
540
+
568
541
  const newOptions = this.#featureOptions.configuredOptions.filter(entry => !optionRegex.test(entry));
569
542
 
570
543
  // Figure out if we've got the option set upstream.
571
544
  let upstreamOption = false;
572
545
 
573
546
  // 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.serial !== this.#controller)) ? this.#controller : undefined)) {
547
+ switch(this.#featureOptions.scope(checkbox.id, (currentDevice && (currentDevice.serialNumber !== this.#controller)) ? this.#controller : undefined)) {
575
548
 
576
549
  case "device":
577
550
  case "controller":
578
551
 
579
- if(currentDevice.serial !== this.#controller) {
552
+ if(currentDevice.serialNumber !== this.#controller) {
580
553
 
581
554
  upstreamOption = true;
582
555
  }
@@ -597,15 +570,24 @@ export class webUiFeatureOptions {
597
570
  break;
598
571
  }
599
572
 
600
- // 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
601
- // context.
602
- if(checkbox.readOnly && (!this.#featureOptions.isValue(featureOption) || (this.#featureOptions.isValue(featureOption) && inputValue && !upstreamOption))) {
573
+ // We're currently in an indetermindate state and transitioning to an unchecked state.
574
+ if(checkbox.readOnly) {
603
575
 
604
- // We're truly unchecked. We need this because a checkbox can be in both an unchecked and indeterminate simultaneously,
605
- // so we use the readOnly property to let us know that we've just cycled from an indeterminate state.
576
+ // 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
577
+ // the readOnly property to let us know that we've just cycled from an indeterminate state.
606
578
  checkbox.checked = checkbox.readOnly = false;
579
+
580
+ // If we have a value-centric feature option, we show the default value when we're in an indeterminate state.
581
+ if(this.#featureOptions.isValue(featureOption)) {
582
+
583
+ // If we're unchecked, clear out the value and make it read only. We show the system default for reference.
584
+ inputValue.value = option.defaultValue;
585
+ inputValue.readOnly = true;
586
+ }
607
587
  } else if(!checkbox.checked) {
608
588
 
589
+ // We're currently in a checked state and transitioning to an unchecked or an indeterminate state.
590
+
609
591
  // 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
592
  if(upstreamOption) {
611
593
 
@@ -614,51 +596,64 @@ export class webUiFeatureOptions {
614
596
  checkbox.readOnly = checkbox.indeterminate = true;
615
597
  }
616
598
 
617
- if(this.#featureOptions.isValue(featureOption) && inputValue) {
599
+ // If we're in an indeterminate state, we need to traverse the tree to get the upstream value we're inheriting.
600
+ if(this.#featureOptions.isValue(featureOption)) {
601
+
602
+ let newInputValue;
603
+
604
+ // If our scope is global, let's fallback on the default value.
605
+ // eslint-disable-next-line eqeqeq
606
+ if((currentDevice?.serialNumber == null) && (this.#controller == null)) {
618
607
 
608
+ newInputValue = option.defaultValue;
609
+ } else if(currentDevice?.serialNumber !== this.#controller) {
610
+
611
+ // We're at the device level - get the controller level value if it exists and fallback to the global value otherwise.
612
+ newInputValue = this.#featureOptions.value(checkbox.id, this.#controller) ?? this.#featureOptions.value(checkbox.id);
613
+ } else {
614
+
615
+ // We're at the controller level - get the global value.
616
+ newInputValue = this.#featureOptions.value(checkbox.id);
617
+ }
618
+
619
+ // Our fallback if there's no value defined within the scope hierarchy is the default value.
620
+ inputValue.value = newInputValue ?? option.defaultValue;
619
621
  inputValue.readOnly = true;
620
622
  }
621
623
  } else if(checkbox.checked) {
622
624
 
623
- // We've explicitly checked this option.
625
+ // We're currently in an unchecked state and transitioning to a checked state.
624
626
  checkbox.readOnly = checkbox.indeterminate = false;
625
627
 
626
- if(this.#featureOptions.isValue(featureOption) && inputValue) {
628
+ if(this.#featureOptions.isValue(featureOption)) {
627
629
 
628
630
  inputValue.readOnly = false;
629
631
  }
630
632
  }
631
633
 
632
- // The setting is different from the default, highlight it for the user, accounting for upstream scope, and add it to our configuration.
633
- if(!checkbox.indeterminate && ((checkbox.checked !== option.default) || upstreamOption)) {
634
+ // 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
635
+ // provide a visual queue to the user, highlighting to indicate that a non-default option has been set.
636
+ if(!checkbox.indeterminate && ((checkbox.checked !== option.default) ||
637
+ (this.#featureOptions.isValue(featureOption) && (inputValue.value.toString() !== option.defaultValue.toString())) || upstreamOption)) {
634
638
 
635
639
  labelDescription.classList.add("text-info");
636
- newOptions.push((checkbox.checked ? "Enable." : "Disable.") + checkbox.value);
640
+ newOptions.push((checkbox.checked ? "Enable." : "Disable.") + checkbox.value +
641
+ (this.#featureOptions.isValue(featureOption) && checkbox.checked ? ("." + inputValue.value) : ""));
637
642
  } else {
638
643
 
639
644
  // We've reset to the defaults, remove our highlighting.
640
645
  labelDescription.classList.remove("text-info");
641
646
  }
642
647
 
643
- // Update our Homebridge configuration.
644
- if(this.#featureOptions.isValue(featureOption) && inputValue) {
645
-
646
- // Inform our value-centric feature option to update Homebridge.
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
- }
648
+ // Update our configuration in Homebridge.
649
+ this.currentConfig[0].options = newOptions;
650
+ this.#featureOptions.configuredOptions = newOptions;
651
+ await homebridge.updatePluginConfig(this.currentConfig);
657
652
 
658
653
  // If we've reset to defaults, make sure our color coding for scope is reflected.
659
654
  if((checkbox.checked === option.default) || checkbox.indeterminate) {
660
655
 
661
- const scopeColor = this.#featureOptions.color(featureOption, currentDevice?.serial, this.#controller);
656
+ const scopeColor = this.#featureOptions.color(featureOption, currentDevice?.serialNumber, this.#controller);
662
657
 
663
658
  if(scopeColor) {
664
659
 
@@ -669,7 +664,7 @@ export class webUiFeatureOptions {
669
664
  // Adjust visibility of other feature options that depend on us.
670
665
  if(this.#featureOptions.groups[checkbox.id]) {
671
666
 
672
- const entryVisibility = this.#featureOptions.test(featureOption, currentDevice?.serial) ? "" : "none";
667
+ const entryVisibility = this.#featureOptions.test(featureOption, currentDevice?.serialNumber) ? "" : "none";
673
668
 
674
669
  // Lookup each feature option setting and set the visibility accordingly.
675
670
  for(const entry of this.#featureOptions.groups[checkbox.id]) {
@@ -692,7 +687,7 @@ export class webUiFeatureOptions {
692
687
  trX.appendChild(tdLabel);
693
688
 
694
689
  // 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?.serial)) {
690
+ if((option.group !== undefined) && !this.#featureOptions.test(category.name + (option.group.length ? ("." + option.group) : ""), currentDevice?.serialNumber)) {
696
691
 
697
692
  trX.style.display = "none";
698
693
  } else {
@@ -738,7 +733,7 @@ export class webUiFeatureOptions {
738
733
 
739
734
  // Display our device details.
740
735
  deviceFirmware.innerHTML = device.firmwareVersion;
741
- deviceSerial.innerHTML = device.serial;
736
+ deviceSerial.innerHTML = device.serialNumber;
742
737
  }
743
738
 
744
739
  // Default method for enumerating the device list in the sidebar.
@@ -785,12 +780,12 @@ export class webUiFeatureOptions {
785
780
 
786
781
  const label = document.createElement("label");
787
782
 
788
- label.name = device.serial;
783
+ label.name = device.serialNumber;
789
784
  label.appendChild(document.createTextNode(device.name ?? "Unknown"));
790
785
  label.style.cursor = "pointer";
791
786
  label.classList.add("mx-2", "my-0", "p-0", "w-100");
792
787
 
793
- label.addEventListener("click", () => this.showDeviceOptions(device.serial));
788
+ label.addEventListener("click", () => this.showDeviceOptions(device.serialNumber));
794
789
 
795
790
  // Add the device label to our cell.
796
791
  tdDevice.appendChild(label);
@@ -814,10 +809,14 @@ export class webUiFeatureOptions {
814
809
  // Filter out only the components we're interested in.
815
810
  devices = devices.map(device => ({
816
811
 
817
- firmwareVersion: (device.services.find(service => service.constructorName ===
812
+ firmwareRevision: (device.services.find(service => service.constructorName ===
818
813
  "AccessoryInformation")?.characteristics.find(characteristic => characteristic.constructorName === "FirmwareRevision")?.value ?? ""),
814
+ manufacturer: (device.services.find(service => service.constructorName ===
815
+ "AccessoryInformation")?.characteristics.find(characteristic => characteristic.constructorName === "Manufacturer")?.value ?? ""),
816
+ model: (device.services.find(service => service.constructorName ===
817
+ "AccessoryInformation")?.characteristics.find(characteristic => characteristic.constructorName === "Model")?.value ?? ""),
819
818
  name: device.displayName,
820
- serial: (device.services.find(service => service.constructorName ===
819
+ serialNumber: (device.services.find(service => service.constructorName ===
821
820
  "AccessoryInformation")?.characteristics.find(characteristic => characteristic.constructorName === "SerialNumber")?.value ?? "")
822
821
  }));
823
822
 
@@ -91,7 +91,7 @@ const getDevices = async (controller) => {
91
91
  devices = devices.map(device => ({
92
92
 
93
93
  ...device,
94
- serial: device.mac
94
+ serialNumber: device.mac
95
95
  }));
96
96
 
97
97
  return devices;
@@ -157,12 +157,12 @@ const showSidebarDevices = (controller, devices) => {
157
157
 
158
158
  const label = document.createElement("label");
159
159
 
160
- label.name = device.serial;
160
+ label.name = device.serialNumber;
161
161
  label.appendChild(document.createTextNode(device.name ?? device.marketName));
162
162
  label.style.cursor = "pointer";
163
163
  label.classList.add("mx-2", "my-0", "p-0", "w-100");
164
164
 
165
- label.addEventListener("click", () => ui.featureOptions.showDeviceOptions(device.serial));
165
+ label.addEventListener("click", () => ui.featureOptions.showDeviceOptions(device.serialNumber));
166
166
 
167
167
  // Add the device label to our cell.
168
168
  tdDevice.appendChild(label);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-unifi-protect",
3
- "version": "7.8.2",
3
+ "version": "7.9.0",
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": {
@@ -75,22 +75,21 @@
75
75
  },
76
76
  "main": "dist/index.js",
77
77
  "dependencies": {
78
- "@homebridge/plugin-ui-utils": "1.0.3",
78
+ "@homebridge/plugin-ui-utils": "2.0.0",
79
79
  "ffmpeg-for-homebridge": "^2.1.7",
80
- "homebridge-plugin-utils": "^1.10.2",
81
- "unifi-protect": "^4.16.0",
80
+ "homebridge-plugin-utils": "^1.11.3",
81
+ "unifi-protect": "^4.17.0",
82
82
  "ws": "8.18.0"
83
83
  },
84
84
  "devDependencies": {
85
- "@stylistic/eslint-plugin": "2.9.0",
86
- "@types/node": "22.7.7",
87
- "@types/readable-stream": "4.0.15",
88
- "@types/ws": "8.5.12",
89
- "eslint": "9.13.0",
85
+ "@stylistic/eslint-plugin": "2.11.0",
86
+ "@types/node": "22.10.1",
87
+ "@types/ws": "8.5.13",
88
+ "eslint": "9.16.0",
90
89
  "homebridge": "1.8.4",
91
90
  "shx": "0.3.4",
92
- "typescript": "5.6.3",
93
- "typescript-eslint": "8.10.0"
91
+ "typescript": "5.7.2",
92
+ "typescript-eslint": "8.17.0"
94
93
  },
95
94
  "optionalDependencies": {
96
95
  "bufferutil": "4.0.8"