homebridge-unifi-protect 6.12.2 → 6.13.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.
@@ -81,13 +81,11 @@ export class ProtectFeatureOptions extends FeatureOptions {
81
81
  if(!this.currentConfig[0]?.controllers?.length) {
82
82
 
83
83
  document.getElementById("headerInfo").innerHTML = "Please configure a UniFi Protect controller to access in the main settings tab before configuring feature options."
84
+ document.getElementById("headerInfo").style.display = "";
84
85
  homebridge.hideSpinner();
85
86
  return;
86
87
  }
87
88
 
88
- // Initialize our informational header.
89
- 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) &rarr; <i class=\"text-success\">Protect controller options</i> &rarr; <i class=\"text-info\">Protect device options</i> (highest priority)"
90
-
91
89
  // Enumerate our global options.
92
90
  const trGlobal = document.createElement("tr");
93
91
 
@@ -189,7 +187,7 @@ export class ProtectFeatureOptions extends FeatureOptions {
189
187
  // If we're not accessing global options, pull a list of devices attached to this controller.
190
188
  if(controller) {
191
189
 
192
- this.ufpDevices = await homebridge.request("/getDevices", { address: controller.address, password: controller.password, username: controller.username });
190
+ this.ufpDevices = await homebridge.request("/getDevices", { address: controller.address, username: controller.username, password: controller.password });
193
191
  }
194
192
 
195
193
  // Couldn't connect to the Protect controller for some reason.
@@ -198,24 +196,21 @@ export class ProtectFeatureOptions extends FeatureOptions {
198
196
  devicesTable.innerHTML = "";
199
197
  this.configTable.innerHTML = "";
200
198
 
201
- document.getElementById("device_model").innerHTML = "Unable to connect to the Protect controller. Check your settings for this controller in the main settings tab to verify they are correct."
202
- document.getElementById("device_model").colSpan = 3;
203
- document.getElementById("device_model").style.fontWeight = "bold";
204
- document.getElementById("device_model").classList.add("text-center");
205
- document.getElementById("deviceStatsHeader").style.display = "none";
206
-
207
- document.getElementById("device_mac").innerHTML = "";
208
- document.getElementById("device_address").innerHTML = "";
209
- document.getElementById("device_online").innerHTML = "";
210
- document.getElementById("deviceStatsTable").style.display = "inline-table";
199
+ document.getElementById("headerInfo").innerHTML = "Unable to connect to the Protect controller.<br>Check your settings for this controller in the settings tab to verify they are correct.<br><code class=\"text-danger\">" + (await homebridge.request("/getErrorMessage")) + "</code>";
200
+ document.getElementById("headerInfo").style.display = "";
201
+ document.getElementById("deviceStatsTable").style.display = "none";
211
202
 
212
203
  homebridge.hideSpinner();
213
204
  return;
214
205
  }
215
206
 
207
+ // Initialize our informational header.
208
+ 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) &rarr; <i class=\"text-success\">Protect controller options</i> &rarr; <i class=\"text-info\">Protect device options</i> (highest priority)"
209
+
216
210
  // Make the UI visible.
217
- document.getElementById("sidebar").style.display = "";
218
211
  document.getElementById("headerInfo").style.display = "";
212
+ document.getElementById("sidebar").style.display = "";
213
+ document.getElementById("deviceStatsTable").style.display = "";
219
214
 
220
215
  const modelKeys = [...new Set(this.ufpDevices.map(x => x.modelKey))];
221
216
  this.deviceList = [];
@@ -314,7 +309,6 @@ export class ProtectFeatureOptions extends FeatureOptions {
314
309
  // Ensure we have a controller or device. The only time this won't be the case is when we're looking at global options.
315
310
  if(ufpDevice) {
316
311
 
317
- document.getElementById("deviceStatsHeader").style.display = "";
318
312
  document.getElementById("device_model").classList.remove("text-center");
319
313
  document.getElementById("device_model").colSpan = 1;
320
314
  document.getElementById("device_model").style.fontWeight = "normal";
@@ -322,13 +316,10 @@ export class ProtectFeatureOptions extends FeatureOptions {
322
316
  document.getElementById("device_mac").innerHTML = ufpDevice.mac;
323
317
  document.getElementById("device_address").innerHTML = ufpDevice.host ?? (ufpDevice.modelKey === "sensor" ? "Bluetooth Device" : "None");
324
318
  document.getElementById("device_online").innerHTML = ("state" in ufpDevice) ? (ufpDevice.state.charAt(0).toUpperCase() + ufpDevice.state.slice(1).toLowerCase()) : "Connected";
325
-
326
- document.getElementById("deviceStatsTable").style.display = "inline-table";
319
+ document.getElementById("deviceStatsTable").style.display = "";
327
320
  } else {
328
321
 
329
322
  document.getElementById("deviceStatsTable").style.display = "none";
330
-
331
- document.getElementById("deviceStatsHeader").style.display = "";
332
323
  document.getElementById("device_model").classList.remove("text-center");
333
324
  document.getElementById("device_model").colSpan = 1;
334
325
  document.getElementById("device_model").style.fontWeight = "normal";
@@ -9,49 +9,85 @@ import { ProtectFeatureOptions } from "./protect-featureoptions.mjs";
9
9
  // Keep a list of all the feature options and option groups.
10
10
  const featureOptions = new ProtectFeatureOptions();
11
11
 
12
- // Toggle our enabled state.
13
- async function enablePlugin() {
12
+ // Show the first run user experience if we don't have valid login credentials.
13
+ function showFirstRun () {
14
14
 
15
- // Show the beachball while we setup.
16
- homebridge.showSpinner();
15
+ const buttonFirstRun = document.getElementById("firstRun");
16
+ const inputAddress = document.getElementById("address");
17
+ const inputUsername = document.getElementById("username");
18
+ const inputPassword = document.getElementById("password");
19
+ const tdLoginError = document.getElementById("loginError");
17
20
 
18
- // Create our UI.
19
- document.getElementById("disabledBanner").style.display = "none";
20
- featureOptions.currentConfig[0].disablePlugin = false;
21
+ // If we don't have any controllers configured, initialize the list.
22
+ if(!featureOptions.currentConfig[0].controllers) {
21
23
 
22
- await homebridge.updatePluginConfig(featureOptions.currentConfig)
23
- await homebridge.savePluginConfig()
24
+ featureOptions.currentConfig[0].controllers = [ {} ];
25
+ }
24
26
 
25
- // All done. Let the user interact with us.
26
- homebridge.hideSpinner()
27
- }
27
+ // Pre-populate with anything we might already have in our configuration.
28
+ inputAddress.value = featureOptions.currentConfig[0].controllers[0].address ?? "";
29
+ inputUsername.value = featureOptions.currentConfig[0].controllers[0].username ?? "";
30
+ inputPassword.value = featureOptions.currentConfig[0].controllers[0].password ?? "";
28
31
 
29
- // Show a disabled interface.
30
- function showDisabledBanner() {
32
+ // Clear login error messages when the login credentials change.
33
+ inputAddress.addEventListener("input", () => {
31
34
 
32
- document.getElementById("disabledBanner").style.display = "block";
33
- }
35
+ tdLoginError.innerHTML = "&nbsp;";
36
+ });
34
37
 
35
- // Show an navigation bar at the top of the plugin configuration UI.
36
- function showIntro () {
38
+ inputUsername.addEventListener("input", () => {
37
39
 
38
- const introLink = document.getElementById("introLink");
40
+ tdLoginError.innerHTML = "&nbsp;";
41
+ });
42
+
43
+ inputPassword.addEventListener("input", () => {
44
+
45
+ tdLoginError.innerHTML = "&nbsp;";
46
+ });
39
47
 
40
- introLink.addEventListener("click", () => {
48
+ // First run user experience.
49
+ buttonFirstRun.addEventListener("click", async () => {
41
50
 
42
51
  // Show the beachball while we setup.
43
52
  homebridge.showSpinner();
44
53
 
54
+ const address = inputAddress.value;
55
+ const username = inputUsername.value;
56
+ const password = inputPassword.value;
57
+
58
+ tdLoginError.innerHTML = "&nbsp;";
59
+
60
+ if(!address?.length || !username?.length || !password?.length) {
61
+
62
+ tdLoginError.appendChild(document.createTextNode("Please enter a valid UniFi Protect controller address, username and password."));
63
+ homebridge.hideSpinner();
64
+ return;
65
+ }
66
+
67
+ const ufpDevices = await homebridge.request("/getDevices", { address: address, username: username, password: password });
68
+
69
+ // Couldn't connect to the Protect controller for some reason.
70
+ if(!ufpDevices?.length) {
71
+
72
+ tdLoginError.innerHTML = "Unable to login to the UniFi Protect controller.<br>Please check your controller address, username, and password.<br><code class=\"text-danger\">" + (await homebridge.request("/getErrorMessage")) + "</code>";
73
+ homebridge.hideSpinner();
74
+ return;
75
+ }
76
+
77
+ // Save the login credentials to our configuration.
78
+ featureOptions.currentConfig[0].controllers[0].address = address;
79
+ featureOptions.currentConfig[0].controllers[0].username = username;
80
+ featureOptions.currentConfig[0].controllers[0].password = password;
81
+
82
+ await homebridge.updatePluginConfig(featureOptions.currentConfig);
83
+
45
84
  // Create our UI.
46
- document.getElementById("pageIntro").style.display = "none";
85
+ document.getElementById("pageFirstRun").style.display = "none";
47
86
  document.getElementById("menuWrapper").style.display = "inline-flex";
48
- showSettings();
49
-
50
- // All done. Let the user interact with us.
51
- homebridge.hideSpinner();
87
+ featureOptions.showUI();
52
88
  });
53
89
 
54
- document.getElementById("pageIntro").style.display = "block";
90
+ document.getElementById("pageFirstRun").style.display = "block";
55
91
  }
56
92
 
57
93
  // Show the main plugin configuration tab.
@@ -109,31 +145,35 @@ async function launchWebUI() {
109
145
  menuHome.addEventListener("click", () => showSupport());
110
146
  menuFeatureOptions.addEventListener("click", () => featureOptions.showUI());
111
147
  menuSettings.addEventListener("click", () => showSettings());
112
- disabledEnable.addEventListener("click", () => enablePlugin());
113
148
 
114
- if(featureOptions.currentConfig.length) {
149
+ // If we've got a valid Protect controller, username, and password configured, we launch our feature option UI. Otherwise, we launch our first run UI.
150
+ if(featureOptions.currentConfig.length && featureOptions.currentConfig[0].controllers?.length && featureOptions.currentConfig[0].controllers[0]?.address?.length && featureOptions.currentConfig[0].controllers[0]?.username?.length && featureOptions.currentConfig[0].controllers[0]?.password?.length) {
115
151
 
116
- document.getElementById("menuWrapper").style.display = "inline-flex"
117
- showSettings();
152
+ document.getElementById("menuWrapper").style.display = "inline-flex";
153
+ featureOptions.showUI();
154
+ return;
155
+ }
118
156
 
119
- // If the plugin's disabled, inform the user.
120
- if(featureOptions.currentConfig[0].disablePlugin) {
157
+ // If we have no configuration, let's create one.
158
+ if(!featureOptions.currentConfig.length) {
121
159
 
122
- showDisabledBanner();
123
- }
124
- } else {
160
+ featureOptions.currentConfig.push({ controllers: [ {} ], name: "UniFi Protect" });
161
+ } else if(!("name" in featureOptions.currentConfig[0])) {
125
162
 
126
- featureOptions.currentConfig.push({ name: "UniFi Protect" });
127
- await homebridge.updatePluginConfig(featureOptions.currentConfig);
128
- showIntro();
163
+ // If we haven't set the name, let's do so now.
164
+ featureOptions.currentConfig[0].name = "UniFi Protect";
129
165
  }
166
+
167
+ // Update the plugin configuration and launch the first run UI.
168
+ await homebridge.updatePluginConfig(featureOptions.currentConfig);
169
+ showFirstRun();
130
170
  }
131
171
 
132
172
  // Fire off our UI, catching errors along the way.
133
173
  try {
134
174
 
135
175
  launchWebUI();
136
- } catch (err) {
176
+ } catch(err) {
137
177
 
138
178
  // If we had an error instantiating or updating the UI, notify the user.
139
179
  homebridge.toast.error(err.message, "Error");
@@ -1,33 +1,80 @@
1
1
  /* Copyright(C) 2017-2023, HJD (https://github.com/hjdhjd). All rights reserved.
2
2
  *
3
- * server.js: Homebridge camera streaming delegate implementation for Protect.
3
+ * server.js: homebridge-unifi-protect webUI server API.
4
4
  *
5
5
  * This module is heavily inspired by the homebridge-config-ui-x source code and borrows from both.
6
6
  * Thank you oznu for your contributions to the HomeKit world.
7
7
  */
8
- /* eslint-disable no-undef */
9
- /* eslint-disable @typescript-eslint/no-var-requires */
10
- /* jshint node: true,esversion: 9, -W014, -W033 */
11
- /* eslint-disable new-cap */
12
8
  "use strict";
13
9
 
14
10
  import { featureOptionCategories, featureOptions, isOptionEnabled } from "../dist/protect-options.js";
15
11
  import { HomebridgePluginUiServer } from "@homebridge/plugin-ui-utils";
16
12
  import { ProtectApi } from "unifi-protect";
17
- import * as fs from "node:fs";
13
+ import util from "node:util";
18
14
 
19
15
  class PluginUiServer extends HomebridgePluginUiServer {
20
16
 
17
+ errorInfo;
18
+
21
19
  constructor () {
22
20
  super();
23
21
 
22
+ this.errorInfo = "";
23
+
24
+ // Register getErrorMessage() with the Homebridge server API.
25
+ this.#registerGetErrorMessage();
26
+
27
+ // Register getDevices() with the Homebridge server API.
28
+ this.#registerGetDevices();
29
+
30
+ // Register getOptions() with the Homebridge server API.
31
+ this.#registerGetOptions();
32
+
33
+ this.ready();
34
+ }
35
+
36
+ // Register the getErrorMessage() webUI server API endpoint.
37
+ #registerGetErrorMessage() {
38
+
39
+ // Return the list of Protect devices.
40
+ this.onRequest("/getErrorMessage", async () => {
41
+
42
+ try {
43
+
44
+ return this.errorInfo;
45
+ } catch(err) {
46
+
47
+ console.log(err);
48
+
49
+ // Return nothing if we error out for some reason.
50
+ return "";
51
+ }
52
+ });
53
+ }
54
+
55
+ // Register the getDevices() webUI server API endpoint.
56
+ #registerGetDevices() {
57
+
24
58
  // Return the list of Protect devices.
25
59
  this.onRequest("/getDevices", async (controller) => {
26
60
 
27
61
  try {
28
62
 
63
+ const log = {
64
+
65
+ debug: (message, parameters) => {},
66
+ error: (message, parameters = []) => {
67
+
68
+ // Save the error to inform the user in the webUI.
69
+ this.errorInfo = util.format(message, ...parameters);
70
+ console.log(this.errorInfo);
71
+ },
72
+ info: (message, parameters) => {},
73
+ warn: (message, parameters = []) => console.log(util.format(message, ...parameters))
74
+ };
75
+
29
76
  // Connect to the Protect controller.
30
- const ufpApi = new ProtectApi();
77
+ const ufpApi = new ProtectApi(log);
31
78
 
32
79
  if(!(await ufpApi.login(controller.address, controller.username, controller.password))) {
33
80
 
@@ -91,13 +138,16 @@ class PluginUiServer extends HomebridgePluginUiServer {
91
138
  return [ ufpApi.bootstrap.nvr, ...ufpApi.bootstrap.cameras, ...ufpApi.bootstrap.chimes, ...ufpApi.bootstrap.lights, ...ufpApi.bootstrap.sensors, ...ufpApi.bootstrap.viewers ];
92
139
  } catch(err) {
93
140
 
94
- console.log("ERRORING OUT FOR " + controller.address);
95
141
  console.log(err);
96
142
 
97
143
  // Return nothing if we error out for some reason.
98
144
  return [];
99
145
  }
100
146
  });
147
+ }
148
+
149
+ // Register the getOptions() webUI server API endpoint.
150
+ #registerGetOptions() {
101
151
 
102
152
  // Return the list of options configured for a given Protect device.
103
153
  this.onRequest("/getOptions", async(request) => {
@@ -126,8 +176,6 @@ class PluginUiServer extends HomebridgePluginUiServer {
126
176
  return {};
127
177
  }
128
178
  });
129
-
130
- this.ready();
131
179
  }
132
180
  }
133
181
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-unifi-protect",
3
- "version": "6.12.2",
3
+ "version": "6.13.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": {
@@ -82,15 +82,15 @@
82
82
  "ws": "8.13.0"
83
83
  },
84
84
  "devDependencies": {
85
- "@types/node": "20.5.1",
86
- "@types/readable-stream": "4.0.1",
85
+ "@types/node": "20.5.6",
86
+ "@types/readable-stream": "4.0.2",
87
87
  "@types/ws": "8.5.5",
88
- "@typescript-eslint/eslint-plugin": "6.4.0",
89
- "@typescript-eslint/parser": "6.4.0",
90
- "eslint": "8.47.0",
88
+ "@typescript-eslint/eslint-plugin": "6.4.1",
89
+ "@typescript-eslint/parser": "6.4.1",
90
+ "eslint": "8.48.0",
91
91
  "homebridge": "1.6.1",
92
92
  "nodemon": "3.0.1",
93
93
  "rimraf": "5.0.1",
94
- "typescript": "5.1.6"
94
+ "typescript": "5.2.2"
95
95
  }
96
96
  }