homebridge-unifi-protect 7.20.0 → 7.21.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/dist/devices/protect-camera-package.js +12 -3
- package/dist/devices/protect-camera-package.js.map +1 -1
- package/dist/devices/protect-camera.d.ts +1 -1
- package/dist/devices/protect-camera.js +49 -17
- package/dist/devices/protect-camera.js.map +1 -1
- package/dist/devices/protect-device.d.ts +1 -0
- package/dist/devices/protect-device.js.map +1 -1
- package/dist/devices/protect-doorbell.js.map +1 -1
- package/dist/devices/protect-liveviews.js.map +1 -1
- package/dist/devices/protect-securitysystem.js.map +1 -1
- package/dist/protect-events.js +5 -3
- package/dist/protect-events.js.map +1 -1
- package/dist/protect-livestream.d.ts +1 -0
- package/dist/protect-livestream.js +14 -0
- package/dist/protect-livestream.js.map +1 -1
- package/dist/protect-nvr.js.map +1 -1
- package/dist/protect-options.js +14 -13
- package/dist/protect-options.js.map +1 -1
- package/dist/protect-record.d.ts +0 -2
- package/dist/protect-record.js +4 -15
- package/dist/protect-record.js.map +1 -1
- package/dist/protect-snapshot.js +2 -7
- package/dist/protect-snapshot.js.map +1 -1
- package/dist/protect-stream.d.ts +2 -0
- package/dist/protect-stream.js +41 -8
- package/dist/protect-stream.js.map +1 -1
- package/dist/protect-timeshift.js +5 -8
- package/dist/protect-timeshift.js.map +1 -1
- package/dist/protect-types.d.ts +1 -0
- package/dist/protect-types.js +1 -0
- package/dist/protect-types.js.map +1 -1
- package/homebridge-ui/public/index.html +54 -51
- package/homebridge-ui/public/lib/featureoptions.js.map +1 -1
- package/homebridge-ui/public/lib/webUi-featureoptions.mjs +3200 -549
- package/homebridge-ui/public/lib/webUi.mjs +13 -5
- package/homebridge-ui/public/ui.mjs +62 -99
- package/homebridge-ui/server.js +6 -1
- package/package.json +9 -12
|
@@ -57,7 +57,7 @@ export class webUi {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
// Show the first run user experience if
|
|
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
|
-
|
|
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
|
-
//
|
|
104
|
-
const
|
|
81
|
+
// Return the list of controllers from our plugin configuration.
|
|
82
|
+
const getControllers = () => {
|
|
105
83
|
|
|
106
|
-
const
|
|
84
|
+
const controllers = [];
|
|
107
85
|
|
|
108
|
-
//
|
|
109
|
-
ui.featureOptions.
|
|
86
|
+
// Grab the controllers from our configuration.
|
|
87
|
+
for(const controller of ui.featureOptions.currentConfig[0].controllers ?? []) {
|
|
110
88
|
|
|
111
|
-
|
|
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
|
-
|
|
141
|
-
|
|
92
|
+
return controllers;
|
|
93
|
+
};
|
|
142
94
|
|
|
143
|
-
|
|
144
|
-
|
|
95
|
+
// Return the list of devices associated with a given Protect controller.
|
|
96
|
+
const getDevices = async (selectedController) => {
|
|
145
97
|
|
|
146
|
-
|
|
98
|
+
// If we're in the global context, we have no devices.
|
|
99
|
+
if(!selectedController) {
|
|
147
100
|
|
|
148
|
-
|
|
149
|
-
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
150
103
|
|
|
151
|
-
|
|
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
|
-
|
|
154
|
-
const tdDevice = document.createElement("td");
|
|
107
|
+
if(!controller) {
|
|
155
108
|
|
|
156
|
-
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
157
111
|
|
|
158
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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
|
-
|
|
118
|
+
device.name ??= device.marketName;
|
|
119
|
+
device.serialNumber = device.mac;
|
|
120
|
+
device.sidebarGroup = device.modelKey + "s";
|
|
166
121
|
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
171
|
-
trDevice.appendChild(tdDevice);
|
|
125
|
+
const activeController = [...document.querySelectorAll("[data-navigation='controller']")].find(c => c.getAttribute("data-device-serial") === controller.address);
|
|
172
126
|
|
|
173
|
-
|
|
174
|
-
ui.featureOptions.devicesTable.appendChild(trDevice);
|
|
127
|
+
if(activeController) {
|
|
175
128
|
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
231
|
+
// Populate the device details using the new CSS Grid layout. This provides a more flexible and responsive display than the previous table layout.
|
|
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
|
package/homebridge-ui/server.js
CHANGED
|
@@ -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
|
-
|
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-unifi-protect",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.21.0",
|
|
4
4
|
"displayName": "Homebridge 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": {
|
|
@@ -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.
|
|
85
|
-
"undici": "7.
|
|
86
|
-
"unifi-protect": "4.
|
|
84
|
+
"homebridge-plugin-utils": "1.27.0",
|
|
85
|
+
"undici": "7.13.0",
|
|
86
|
+
"unifi-protect": "4.26.0"
|
|
87
87
|
},
|
|
88
88
|
"devDependencies": {
|
|
89
|
-
"@stylistic/eslint-plugin": "5.
|
|
90
|
-
"@types/node": "24.
|
|
91
|
-
"eslint": "9.
|
|
89
|
+
"@stylistic/eslint-plugin": "5.2.3",
|
|
90
|
+
"@types/node": "24.2.1",
|
|
91
|
+
"eslint": "9.33.0",
|
|
92
92
|
"homebridge": "1.11.0",
|
|
93
93
|
"shx": "0.4.0",
|
|
94
|
-
"typescript": "5.
|
|
95
|
-
"typescript-eslint": "8.
|
|
96
|
-
},
|
|
97
|
-
"optionalDependencies": {
|
|
98
|
-
"bufferutil": "4.0.9"
|
|
94
|
+
"typescript": "5.9.2",
|
|
95
|
+
"typescript-eslint": "8.39.0"
|
|
99
96
|
}
|
|
100
97
|
}
|