homebridge-unifi-protect 7.20.1 → 7.22.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.
Files changed (63) hide show
  1. package/dist/devices/protect-camera-package.js +14 -6
  2. package/dist/devices/protect-camera-package.js.map +1 -1
  3. package/dist/devices/protect-camera.d.ts +3 -2
  4. package/dist/devices/protect-camera.js +121 -61
  5. package/dist/devices/protect-camera.js.map +1 -1
  6. package/dist/devices/protect-chime.js +3 -2
  7. package/dist/devices/protect-chime.js.map +1 -1
  8. package/dist/devices/protect-device.js +19 -22
  9. package/dist/devices/protect-device.js.map +1 -1
  10. package/dist/devices/protect-doorbell.d.ts +4 -2
  11. package/dist/devices/protect-doorbell.js +37 -22
  12. package/dist/devices/protect-doorbell.js.map +1 -1
  13. package/dist/devices/protect-light.js +6 -6
  14. package/dist/devices/protect-light.js.map +1 -1
  15. package/dist/devices/protect-liveviews.js +8 -26
  16. package/dist/devices/protect-liveviews.js.map +1 -1
  17. package/dist/devices/protect-nvr-systeminfo.d.ts +0 -2
  18. package/dist/devices/protect-nvr-systeminfo.js +13 -47
  19. package/dist/devices/protect-nvr-systeminfo.js.map +1 -1
  20. package/dist/devices/protect-securitysystem.js +17 -36
  21. package/dist/devices/protect-securitysystem.js.map +1 -1
  22. package/dist/devices/protect-sensor.d.ts +2 -2
  23. package/dist/devices/protect-sensor.js +16 -16
  24. package/dist/devices/protect-sensor.js.map +1 -1
  25. package/dist/devices/protect-viewer.js +8 -8
  26. package/dist/devices/protect-viewer.js.map +1 -1
  27. package/dist/protect-events.js +16 -20
  28. package/dist/protect-events.js.map +1 -1
  29. package/dist/protect-livestream.d.ts +1 -0
  30. package/dist/protect-livestream.js +23 -3
  31. package/dist/protect-livestream.js.map +1 -1
  32. package/dist/protect-nvr.d.ts +5 -5
  33. package/dist/protect-nvr.js +36 -34
  34. package/dist/protect-nvr.js.map +1 -1
  35. package/dist/protect-options.d.ts +4 -4
  36. package/dist/protect-options.js +16 -15
  37. package/dist/protect-options.js.map +1 -1
  38. package/dist/protect-platform.d.ts +1 -1
  39. package/dist/protect-platform.js +8 -14
  40. package/dist/protect-platform.js.map +1 -1
  41. package/dist/protect-record.d.ts +1 -4
  42. package/dist/protect-record.js +13 -28
  43. package/dist/protect-record.js.map +1 -1
  44. package/dist/protect-snapshot.js.map +1 -1
  45. package/dist/protect-stream.js +39 -56
  46. package/dist/protect-stream.js.map +1 -1
  47. package/dist/protect-timeshift.d.ts +0 -2
  48. package/dist/protect-timeshift.js +12 -23
  49. package/dist/protect-timeshift.js.map +1 -1
  50. package/dist/protect-types.d.ts +1 -0
  51. package/dist/protect-types.js +1 -0
  52. package/dist/protect-types.js.map +1 -1
  53. package/dist/settings.d.ts +4 -4
  54. package/dist/settings.js +5 -5
  55. package/dist/settings.js.map +1 -1
  56. package/homebridge-ui/public/index.html +54 -51
  57. package/homebridge-ui/public/lib/featureoptions.js +7 -8
  58. package/homebridge-ui/public/lib/featureoptions.js.map +1 -1
  59. package/homebridge-ui/public/lib/webUi-featureoptions.mjs +3484 -558
  60. package/homebridge-ui/public/lib/webUi.mjs +13 -5
  61. package/homebridge-ui/public/ui.mjs +62 -99
  62. package/homebridge-ui/server.js +6 -1
  63. package/package.json +10 -13
@@ -57,7 +57,7 @@ export class webUi {
57
57
  }
58
58
  }
59
59
 
60
- // Show the first run user experience if we don't have valid login credentials.
60
+ // Show the first run user experience if needed.
61
61
  async #showFirstRun() {
62
62
 
63
63
  const buttonFirstRun = document.getElementById("firstRun");
@@ -68,6 +68,9 @@ export class webUi {
68
68
  return;
69
69
  }
70
70
 
71
+ // We disable saving any settings until we configure the plugin.
72
+ homebridge.disableSaveButton();
73
+
71
74
  // First run user experience.
72
75
  buttonFirstRun.addEventListener("click", async () => {
73
76
 
@@ -80,10 +83,13 @@ export class webUi {
80
83
  return;
81
84
  }
82
85
 
83
- // Create our UI.
86
+ // Create our UI and allow users to save the configuration.
84
87
  document.getElementById("pageFirstRun").style.display = "none";
85
88
  document.getElementById("menuWrapper").style.display = "inline-flex";
86
- this.featureOptions.show();
89
+
90
+ await this.featureOptions.show();
91
+
92
+ homebridge.enableSaveButton();
87
93
 
88
94
  // All done. Let the user interact with us, although in practice, we shouldn't get here.
89
95
  // homebridge.hideSpinner();
@@ -97,6 +103,7 @@ export class webUi {
97
103
 
98
104
  // Show the beachball while we setup.
99
105
  homebridge.showSpinner();
106
+ this.featureOptions.hide();
100
107
 
101
108
  // Highlight the tab in our UI.
102
109
  this.#toggleClasses("menuHome", "btn-elegant", "btn-primary");
@@ -118,6 +125,7 @@ export class webUi {
118
125
  // Show the beachball while we setup.
119
126
  homebridge.showSpinner();
120
127
  homebridge.hideSchemaForm();
128
+ this.featureOptions.hide();
121
129
 
122
130
  // Highlight the tab in our UI.
123
131
  this.#toggleClasses("menuHome", "btn-primary", "btn-elegant");
@@ -139,14 +147,14 @@ export class webUi {
139
147
 
140
148
  // Add our event listeners to animate the UI.
141
149
  document.getElementById("menuHome").addEventListener("click", () => this.#showSupport());
142
- document.getElementById("menuFeatureOptions").addEventListener("click", () => this.featureOptions.show());
150
+ document.getElementById("menuFeatureOptions").addEventListener("click", async () => await this.featureOptions.show());
143
151
  document.getElementById("menuSettings").addEventListener("click", () => this.#showSettings());
144
152
 
145
153
  // If we've got devices detected, we launch our feature option UI. Otherwise, we launch our first run UI.
146
154
  if(this.featureOptions.currentConfig.length && !(await this.#processHandler(this.#firstRun.isRequired))) {
147
155
 
148
156
  document.getElementById("menuWrapper").style.display = "inline-flex";
149
- this.featureOptions.show();
157
+ await this.featureOptions.show();
150
158
 
151
159
  return;
152
160
  }
@@ -75,107 +75,63 @@ const firstRunOnSubmit = async () => {
75
75
  return true;
76
76
  };
77
77
 
78
- // Return the list of devices associated with a given Protect controller.
79
- const getDevices = async (controller) => {
80
-
81
- // If we're in the global context, we have no devices.
82
- if(!controller) {
83
-
84
- return [];
85
- }
86
-
87
- // Retrieve the current list of devices from the Protect controller.
88
- let devices = await homebridge.request("/getDevices", { address: controller.address, password: controller.password, username: controller.username });
89
-
90
- // Add the fields that the webUI framework is looking for to render.
91
- devices = devices.map(device => ({
92
-
93
- ...device,
94
- serialNumber: device.mac
95
- }));
96
-
97
- return devices;
98
- };
99
-
100
78
  // Return whether a given device is a controller.
101
79
  const isController = (device) => device.modelKey === "nvr";
102
80
 
103
- // Show the list of Protect devices associated with a controller, grouped by model.
104
- const showSidebarDevices = (controller, devices) => {
81
+ // Return the list of controllers from our plugin configuration.
82
+ const getControllers = () => {
105
83
 
106
- const modelKeys = [...new Set(devices.map(x => x.modelKey))];
84
+ const controllers = [];
107
85
 
108
- // Start with a clean slate.
109
- ui.featureOptions.devicesTable.innerHTML = "";
86
+ // Grab the controllers from our configuration.
87
+ for(const controller of ui.featureOptions.currentConfig[0].controllers ?? []) {
110
88
 
111
- for(const key of modelKeys) {
112
-
113
- // Get all the devices associated with this device category.
114
- const modelDevices = devices.filter(x => x.modelKey === key);
115
-
116
- // If it's a controller, we handle that case differently.
117
- if((key === "nvr") && modelDevices.length) {
118
-
119
- // Change the name of the controller that we show users once we've connected with the controller.
120
- ui.featureOptions.webUiControllerList.map(x => (x.name === controller.address) ? x.childNodes[0].nodeValue = modelDevices[0].name : true);
121
-
122
- continue;
123
- }
124
-
125
- // Create a row for this device category.
126
- const trCategory = document.createElement("tr");
127
-
128
- // Disable any pointer events and hover activity.
129
- trCategory.style.pointerEvents = "none";
130
-
131
- // Create the cell for our device category row.
132
- const tdCategory = document.createElement("td");
133
-
134
- tdCategory.classList.add("m-0", "p-0", "pl-1", "w-100");
135
-
136
- // Add the category name, with appropriate casing.
137
- tdCategory.appendChild(document.createTextNode((key.charAt(0).toUpperCase() + key.slice(1) + "s")));
138
- tdCategory.style.fontWeight = "bold";
89
+ controllers.push({ name: controller.address, serialNumber: controller.address });
90
+ }
139
91
 
140
- // Add the cell to the table row.
141
- trCategory.appendChild(tdCategory);
92
+ return controllers;
93
+ };
142
94
 
143
- // Add the table row to the table.
144
- ui.featureOptions.devicesTable.appendChild(trCategory);
95
+ // Return the list of devices associated with a given Protect controller.
96
+ const getDevices = async (selectedController) => {
145
97
 
146
- for(const device of modelDevices) {
98
+ // If we're in the global context, we have no devices.
99
+ if(!selectedController) {
147
100
 
148
- // Create a row for this device.
149
- const trDevice = document.createElement("tr");
101
+ return [];
102
+ }
150
103
 
151
- trDevice.classList.add("m-0", "p-0");
104
+ // Find the entry in our plugin configuration.
105
+ const controller = (ui.featureOptions.currentConfig[0].controllers ?? []).find(c => c.address === selectedController.serialNumber);
152
106
 
153
- // Create a cell for our device.
154
- const tdDevice = document.createElement("td");
107
+ if(!controller) {
155
108
 
156
- tdDevice.classList.add("m-0", "p-0" , "w-100");
109
+ return [];
110
+ }
157
111
 
158
- const label = document.createElement("label");
112
+ // Retrieve the current list of devices from the Protect controller.
113
+ const devices = await homebridge.request("/getDevices", { address: controller.address, password: controller.password, username: controller.username });
159
114
 
160
- label.name = device.serialNumber;
161
- label.appendChild(document.createTextNode(device.name ?? device.marketName));
162
- label.style.cursor = "pointer";
163
- label.classList.add("mx-2", "my-0", "p-0", "w-100");
115
+ // Add the fields that the webUI framework is looking for to render.
116
+ for(const device of devices) {
164
117
 
165
- label.addEventListener("click", () => ui.featureOptions.showDeviceOptions(device.serialNumber));
118
+ device.name ??= device.marketName;
119
+ device.serialNumber = device.mac;
120
+ device.sidebarGroup = device.modelKey + "s";
166
121
 
167
- // Add the device label to our cell.
168
- tdDevice.appendChild(label);
122
+ // We update the name of the controller that we show users once we've connected with the controller and have it's name.
123
+ if(isController(device)) {
169
124
 
170
- // Add the cell to the table row.
171
- trDevice.appendChild(tdDevice);
125
+ const activeController = [...document.querySelectorAll("[data-navigation='controller']")].find(c => c.getAttribute("data-device-serial") === controller.address);
172
126
 
173
- // Add the table row to the table.
174
- ui.featureOptions.devicesTable.appendChild(trDevice);
127
+ if(activeController) {
175
128
 
176
- ui.featureOptions.webUiDeviceList.push(label);
129
+ activeController.textContent = device.name;
130
+ }
177
131
  }
178
132
  }
133
+
134
+ return devices;
179
135
  };
180
136
 
181
137
  // Only show feature options that are valid for the capabilities of this device.
@@ -245,7 +201,7 @@ const validOptionCategory = (device, category) => {
245
201
  }
246
202
 
247
203
  // Only show device categories we're explicitly interested in.
248
- if(!category.modelKey?.some(model => ["all", device.modelKey].includes(model))) {
204
+ if(!category.modelKey?.some(model => [ "all", device.modelKey ].includes(model))) {
249
205
 
250
206
  return false;
251
207
  }
@@ -262,44 +218,51 @@ const validOptionCategory = (device, category) => {
262
218
  // Show the details for this device.
263
219
  const showProtectDetails = (device) => {
264
220
 
221
+ const deviceStatsContainer = document.getElementById("deviceStatsContainer");
222
+
265
223
  // No device specified, we must be in a global context.
266
224
  if(!device) {
267
225
 
268
- document.getElementById("device_model").classList.remove("text-center");
269
- document.getElementById("device_model").colSpan = 1;
270
- document.getElementById("device_model").style.fontWeight = "normal";
271
- document.getElementById("device_model").innerHTML = "N/A";
272
- document.getElementById("device_mac").innerHTML = "N/A";
273
- document.getElementById("device_address").innerHTML = "N/A";
274
- document.getElementById("device_online").innerHTML = "N/A";
226
+ deviceStatsContainer.innerHTML = "";
275
227
 
276
228
  return;
277
229
  }
278
230
 
279
- // Populate the device details.
280
- document.getElementById("device_model").classList.remove("text-center");
281
- document.getElementById("device_model").colSpan = 1;
282
- document.getElementById("device_model").style.fontWeight = "normal";
283
- document.getElementById("device_model").innerHTML = device.marketName ?? device.type;
284
- document.getElementById("device_mac").innerHTML = device.mac;
285
- document.getElementById("device_address").innerHTML = device.host ?? (device.modelKey === "sensor" ? "Bluetooth Device" : "None");
286
- document.getElementById("device_online").innerHTML = ("state" in device) ? (device.state.charAt(0).toUpperCase() + device.state.slice(1).toLowerCase()) : "Connected";
231
+ // Populate the infopanel.
232
+ deviceStatsContainer.innerHTML =
233
+ "<div class=\"device-stats-grid\">" +
234
+ "<div class=\"stat-item\">" +
235
+ "<span class=\"stat-label\">Model</span>" +
236
+ "<span class=\"stat-value\">" + (device.marketName ?? device.type) + "</span>" +
237
+ "</div>" +
238
+ "<div class=\"stat-item\">" +
239
+ "<span class=\"stat-label\">MAC Address</span>" +
240
+ "<span class=\"stat-value font-monospace\">" + device.mac + "</span>" +
241
+ "</div>" +
242
+ "<div class=\"stat-item\">" +
243
+ "<span class=\"stat-label\">IP Address</span>" +
244
+ "<span class=\"stat-value font-monospace\">" + (device.host ?? (device.modelKey === "sensor" ? "Bluetooth Device" : "None")) + "</span>" +
245
+ "</div>" +
246
+ "<div class=\"stat-item\">" +
247
+ "<span class=\"stat-label\">Status</span>" +
248
+ "<span class=\"stat-value\">" + (("state" in device) ? (device.state.charAt(0).toUpperCase() + device.state.slice(1).toLowerCase()) : "Connected") + "</span>" +
249
+ "</div>" +
250
+ "</div>";
287
251
  };
288
252
 
289
253
  // Parameters for our feature options webUI.
290
254
  const featureOptionsParams = {
291
255
 
256
+ getControllers: getControllers,
292
257
  getDevices: getDevices,
293
- hasControllers: true,
294
258
  infoPanel: showProtectDetails,
295
259
  sidebar: {
296
260
 
297
- controllerLabel: "Protect Controllers",
298
- deviceLabel: "Protect Devices",
299
- showDevices: showSidebarDevices
261
+ controllerLabel: "Protect Controllers"
300
262
  },
301
263
  ui: {
302
264
 
265
+ controllerRetryEnableDelayMs: 20000,
303
266
  isController: isController,
304
267
  validOption: validOption,
305
268
  validOptionCategory: validOptionCategory
@@ -44,6 +44,8 @@ class PluginUiServer extends HomebridgePluginUiServer {
44
44
  // Register the getDevices() webUI server API endpoint.
45
45
  #registerGetDevices() {
46
46
 
47
+ let ufpApi;
48
+
47
49
  // Return the list of Protect devices.
48
50
  this.onRequest("/getDevices", async (controller) => {
49
51
 
@@ -65,7 +67,7 @@ class PluginUiServer extends HomebridgePluginUiServer {
65
67
  };
66
68
 
67
69
  // Connect to the Protect controller.
68
- const ufpApi = new ProtectApi(log);
70
+ ufpApi = new ProtectApi(log);
69
71
 
70
72
  if(!(await ufpApi.login(controller.address, controller.username, controller.password))) {
71
73
 
@@ -135,6 +137,9 @@ class PluginUiServer extends HomebridgePluginUiServer {
135
137
 
136
138
  // Return nothing if we error out for some reason.
137
139
  return [];
140
+ } finally {
141
+
142
+ ufpApi?.logout();
138
143
  }
139
144
  });
140
145
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "homebridge-unifi-protect",
3
- "version": "7.20.1",
4
- "displayName": "Homebridge UniFi Protect",
3
+ "version": "7.22.0",
4
+ "displayName": "UniFi Protect",
5
5
  "description": "Homebridge UniFi Protect plugin providing complete HomeKit integration for the entire UniFi Protect ecosystem with full support for most features including HomeKit Secure Video, multiple controllers, blazing fast performance, and much more.",
6
6
  "author": {
7
7
  "name": "HJD",
@@ -81,20 +81,17 @@
81
81
  "dependencies": {
82
82
  "@homebridge/plugin-ui-utils": "2.1.0",
83
83
  "ffmpeg-for-homebridge": "2.1.7",
84
- "homebridge-plugin-utils": "1.26.0",
85
- "undici": "7.11.0",
86
- "unifi-protect": "4.25.0"
84
+ "homebridge-plugin-utils": "1.29.1",
85
+ "undici": "7.15.0",
86
+ "unifi-protect": "4.27.0"
87
87
  },
88
88
  "devDependencies": {
89
- "@stylistic/eslint-plugin": "5.1.0",
90
- "@types/node": "24.0.13",
91
- "eslint": "9.31.0",
89
+ "@stylistic/eslint-plugin": "5.3.1",
90
+ "@types/node": "24.3.0",
91
+ "eslint": "9.34.0",
92
92
  "homebridge": "1.11.0",
93
93
  "shx": "0.4.0",
94
- "typescript": "5.8.3",
95
- "typescript-eslint": "8.36.0"
96
- },
97
- "optionalDependencies": {
98
- "bufferutil": "4.0.9"
94
+ "typescript": "5.9.2",
95
+ "typescript-eslint": "8.41.0"
99
96
  }
100
97
  }