homebridge-unifi-protect 6.22.0 → 7.0.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 +29 -29
- package/config.schema.json +5 -15
- package/dist/devices/protect-camera-package.d.ts +0 -2
- package/dist/devices/protect-camera-package.js +4 -12
- package/dist/devices/protect-camera-package.js.map +1 -1
- package/dist/devices/protect-camera.d.ts +2 -3
- package/dist/devices/protect-camera.js +35 -124
- package/dist/devices/protect-camera.js.map +1 -1
- package/dist/devices/protect-chime.js +2 -6
- package/dist/devices/protect-chime.js.map +1 -1
- package/dist/devices/protect-device.d.ts +10 -6
- package/dist/devices/protect-device.js +23 -22
- package/dist/devices/protect-device.js.map +1 -1
- package/dist/devices/protect-doorbell.d.ts +0 -2
- package/dist/devices/protect-doorbell.js +41 -60
- package/dist/devices/protect-doorbell.js.map +1 -1
- package/dist/devices/protect-light.js +1 -3
- package/dist/devices/protect-light.js.map +1 -1
- package/dist/devices/protect-liveviews.js +2 -1
- package/dist/devices/protect-liveviews.js.map +1 -1
- package/dist/devices/protect-nvr-systeminfo.js +1 -1
- package/dist/devices/protect-nvr-systeminfo.js.map +1 -1
- package/dist/devices/protect-securitysystem.js +4 -4
- package/dist/devices/protect-securitysystem.js.map +1 -1
- package/dist/devices/protect-sensor.js +2 -6
- package/dist/devices/protect-sensor.js.map +1 -1
- package/dist/devices/protect-viewer.js +1 -1
- package/dist/devices/protect-viewer.js.map +1 -1
- package/dist/ffmpeg/protect-ffmpeg-codecs.js.map +1 -1
- package/dist/ffmpeg/protect-ffmpeg-exec.js +1 -1
- package/dist/ffmpeg/protect-ffmpeg-exec.js.map +1 -1
- package/dist/ffmpeg/protect-ffmpeg-options.js +2 -19
- package/dist/ffmpeg/protect-ffmpeg-options.js.map +1 -1
- package/dist/ffmpeg/protect-ffmpeg-record.js +2 -1
- package/dist/ffmpeg/protect-ffmpeg-record.js.map +1 -1
- package/dist/ffmpeg/protect-ffmpeg-stream.js +14 -8
- package/dist/ffmpeg/protect-ffmpeg-stream.js.map +1 -1
- package/dist/ffmpeg/protect-ffmpeg.d.ts +2 -2
- package/dist/ffmpeg/protect-ffmpeg.js.map +1 -1
- package/dist/protect-events.d.ts +1 -3
- package/dist/protect-events.js +38 -43
- package/dist/protect-events.js.map +1 -1
- package/dist/protect-livestream.d.ts +12 -0
- package/dist/protect-livestream.js +63 -0
- package/dist/protect-livestream.js.map +1 -0
- package/dist/protect-nvr.d.ts +7 -11
- package/dist/protect-nvr.js +15 -61
- package/dist/protect-nvr.js.map +1 -1
- package/dist/protect-options.d.ts +8 -23
- package/dist/protect-options.js +4 -128
- package/dist/protect-options.js.map +1 -1
- package/dist/protect-platform.d.ts +2 -4
- package/dist/protect-platform.js +3 -28
- package/dist/protect-platform.js.map +1 -1
- package/dist/protect-record.d.ts +4 -3
- package/dist/protect-record.js +23 -29
- package/dist/protect-record.js.map +1 -1
- package/dist/protect-snapshot.d.ts +2 -2
- package/dist/protect-snapshot.js +7 -3
- package/dist/protect-snapshot.js.map +1 -1
- package/dist/protect-stream.d.ts +3 -4
- package/dist/protect-stream.js +116 -98
- package/dist/protect-stream.js.map +1 -1
- package/dist/protect-timeshift.d.ts +16 -10
- package/dist/protect-timeshift.js +88 -51
- package/dist/protect-timeshift.js.map +1 -1
- package/dist/protect-types.d.ts +0 -7
- package/dist/protect-types.js +0 -1
- package/dist/protect-types.js.map +1 -1
- package/dist/settings.d.ts +2 -1
- package/dist/settings.js +4 -2
- package/dist/settings.js.map +1 -1
- package/homebridge-ui/public/index.html +42 -21
- package/homebridge-ui/public/lib/featureoptions.js +376 -0
- package/homebridge-ui/public/lib/featureoptions.js.map +1 -0
- package/homebridge-ui/public/lib/webUi-featureoptions.mjs +836 -0
- package/homebridge-ui/public/lib/webUi.mjs +184 -0
- package/homebridge-ui/public/ui.mjs +213 -124
- package/homebridge-ui/server.js +13 -52
- package/package.json +22 -21
- package/dist/protect-mqtt.d.ts +0 -20
- package/dist/protect-mqtt.js +0 -177
- package/dist/protect-mqtt.js.map +0 -1
- package/dist/protect-rtp.d.ts +0 -26
- package/dist/protect-rtp.js +0 -180
- package/dist/protect-rtp.js.map +0 -1
- package/homebridge-ui/public/lib/featureoptions.mjs +0 -201
- package/homebridge-ui/public/protect-featureoptions.mjs +0 -736
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/* Copyright(C) 2017-2024, HJD (https://github.com/hjdhjd). All rights reserved.
|
|
2
|
+
*
|
|
3
|
+
* webUi.mjs: Plugin webUI.
|
|
4
|
+
*/
|
|
5
|
+
"use strict";
|
|
6
|
+
|
|
7
|
+
import { webUiFeatureOptions } from "./webUi-featureoptions.mjs";
|
|
8
|
+
|
|
9
|
+
export class webUi {
|
|
10
|
+
|
|
11
|
+
// Feature options class instance.
|
|
12
|
+
featureOptions;
|
|
13
|
+
|
|
14
|
+
// First run webUI callback endpoints for customization.
|
|
15
|
+
#firstRun;
|
|
16
|
+
|
|
17
|
+
// Plugin name.
|
|
18
|
+
#name;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* featureOptions - parameters to webUiFeatureOptions.
|
|
22
|
+
* firstRun - first run handlers:
|
|
23
|
+
* isRequired - do we need to run the first run UI workflow?
|
|
24
|
+
* onStart - initialization for the first run webUI to populate forms and other startup tasks.
|
|
25
|
+
* onSubmit - execute the first run workflow, typically a login or configuration validation of some sort.
|
|
26
|
+
* name - plugin name.
|
|
27
|
+
*/
|
|
28
|
+
constructor({ featureOptions, firstRun = {}, name } = {}) {
|
|
29
|
+
|
|
30
|
+
// Defaults for our first run handlers.
|
|
31
|
+
this.firstRun = { isRequired: () => false, onStart: () => true, onSubmit: () => true };
|
|
32
|
+
|
|
33
|
+
// Figure out the options passed in to us.
|
|
34
|
+
this.featureOptions = new webUiFeatureOptions(featureOptions);
|
|
35
|
+
this.firstRun = Object.assign({}, this.firstRun, firstRun);
|
|
36
|
+
this.name = name;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Render the webUI.
|
|
41
|
+
*/
|
|
42
|
+
// Render the UI.
|
|
43
|
+
show() {
|
|
44
|
+
|
|
45
|
+
// Fire off our UI, catching errors along the way.
|
|
46
|
+
try {
|
|
47
|
+
|
|
48
|
+
this.#launchWebUI();
|
|
49
|
+
} catch(err) {
|
|
50
|
+
|
|
51
|
+
// If we had an error instantiating or updating the UI, notify the user.
|
|
52
|
+
homebridge.toast.error(err.message, "Error");
|
|
53
|
+
} finally {
|
|
54
|
+
|
|
55
|
+
// Always leave the UI in a usable place for the end user.
|
|
56
|
+
homebridge.hideSpinner();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Show the first run user experience if we don't have valid login credentials.
|
|
61
|
+
async #showFirstRun() {
|
|
62
|
+
|
|
63
|
+
const buttonFirstRun = document.getElementById("firstRun");
|
|
64
|
+
|
|
65
|
+
// Run a custom initialization handler the user may have provided.
|
|
66
|
+
if(!(await this.#processHandler(this.firstRun.onStart))) {
|
|
67
|
+
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// First run user experience.
|
|
72
|
+
buttonFirstRun.addEventListener("click", async () => {
|
|
73
|
+
|
|
74
|
+
// Show the beachball while we setup.
|
|
75
|
+
homebridge.showSpinner();
|
|
76
|
+
|
|
77
|
+
// Run a custom submit handler the user may have provided.
|
|
78
|
+
if(!(await this.#processHandler(this.firstRun.onSubmit))) {
|
|
79
|
+
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Create our UI.
|
|
84
|
+
document.getElementById("pageFirstRun").style.display = "none";
|
|
85
|
+
document.getElementById("menuWrapper").style.display = "inline-flex";
|
|
86
|
+
this.featureOptions.show();
|
|
87
|
+
|
|
88
|
+
// All done. Let the user interact with us, although in practice, we shouldn't get here.
|
|
89
|
+
// homebridge.hideSpinner();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
document.getElementById("pageFirstRun").style.display = "block";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Show the main plugin configuration tab.
|
|
96
|
+
#showSettings() {
|
|
97
|
+
|
|
98
|
+
// Show the beachball while we setup.
|
|
99
|
+
homebridge.showSpinner();
|
|
100
|
+
|
|
101
|
+
// Highlight the tab in our UI.
|
|
102
|
+
this.#toggleClasses("menuHome", "btn-elegant", "btn-primary");
|
|
103
|
+
this.#toggleClasses("menuFeatureOptions", "btn-elegant", "btn-primary");
|
|
104
|
+
this.#toggleClasses("menuSettings", "btn-primary", "btn-elegant");
|
|
105
|
+
|
|
106
|
+
document.getElementById("pageSupport").style.display = "none";
|
|
107
|
+
document.getElementById("pageFeatureOptions").style.display = "none";
|
|
108
|
+
|
|
109
|
+
homebridge.showSchemaForm();
|
|
110
|
+
|
|
111
|
+
// All done. Let the user interact with us.
|
|
112
|
+
homebridge.hideSpinner();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Show the support tab.
|
|
116
|
+
#showSupport() {
|
|
117
|
+
|
|
118
|
+
// Show the beachball while we setup.
|
|
119
|
+
homebridge.showSpinner();
|
|
120
|
+
homebridge.hideSchemaForm();
|
|
121
|
+
|
|
122
|
+
// Highlight the tab in our UI.
|
|
123
|
+
this.#toggleClasses("menuHome", "btn-primary", "btn-elegant");
|
|
124
|
+
this.#toggleClasses("menuFeatureOptions", "btn-elegant", "btn-primary");
|
|
125
|
+
this.#toggleClasses("menuSettings", "btn-elegant", "btn-primary");
|
|
126
|
+
|
|
127
|
+
document.getElementById("pageSupport").style.display = "block";
|
|
128
|
+
document.getElementById("pageFeatureOptions").style.display = "none";
|
|
129
|
+
|
|
130
|
+
// All done. Let the user interact with us.
|
|
131
|
+
homebridge.hideSpinner();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Launch our webUI.
|
|
135
|
+
async #launchWebUI() {
|
|
136
|
+
|
|
137
|
+
// Retrieve the current plugin configuration.
|
|
138
|
+
this.featureOptions.currentConfig = await homebridge.getPluginConfig();
|
|
139
|
+
|
|
140
|
+
// Add our event listeners to animate the UI.
|
|
141
|
+
document.getElementById("menuHome").addEventListener("click", () => this.#showSupport());
|
|
142
|
+
document.getElementById("menuFeatureOptions").addEventListener("click", () => this.featureOptions.show());
|
|
143
|
+
document.getElementById("menuSettings").addEventListener("click", () => this.#showSettings());
|
|
144
|
+
|
|
145
|
+
// Get the list of devices the plugin knows about.
|
|
146
|
+
const devices = await homebridge.getCachedAccessories();
|
|
147
|
+
|
|
148
|
+
// If we've got devices detected, we launch our feature option UI. Otherwise, we launch our first run UI.
|
|
149
|
+
if(this.featureOptions.currentConfig.length && devices?.length && !(await this.#processHandler(this.firstRun.isRequired))) {
|
|
150
|
+
|
|
151
|
+
document.getElementById("menuWrapper").style.display = "inline-flex";
|
|
152
|
+
this.featureOptions.show();
|
|
153
|
+
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// If we have the name property set for the plugin configuration yet, let's do so now. If we don't have a configuration, let's initialize it as well.
|
|
158
|
+
(this.featureOptions.currentConfig[0] ??= { name: this.name }).name ??= this.name;
|
|
159
|
+
|
|
160
|
+
// Update the plugin configuration and launch the first run UI.
|
|
161
|
+
await homebridge.updatePluginConfig(this.featureOptions.currentConfig);
|
|
162
|
+
this.#showFirstRun();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Utility to process user-provided custom handlers that can handle both synchronous and asynchronous handlers.
|
|
166
|
+
async #processHandler(handler) {
|
|
167
|
+
|
|
168
|
+
if(((typeof handler === "function") && !(await handler())) || ((typeof handler !== "function") && !handler)) {
|
|
169
|
+
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Utility to toggle our classes.
|
|
177
|
+
#toggleClasses(id, removeClass, addClass) {
|
|
178
|
+
|
|
179
|
+
const element = document.getElementById(id);
|
|
180
|
+
|
|
181
|
+
element.classList.remove(removeClass);
|
|
182
|
+
element.classList.add(addClass);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -1,182 +1,271 @@
|
|
|
1
1
|
/* Copyright(C) 2017-2024, HJD (https://github.com/hjdhjd). All rights reserved.
|
|
2
2
|
*
|
|
3
|
-
* ui.mjs:
|
|
3
|
+
* ui.mjs: Homebridge UniFi Protect webUI.
|
|
4
4
|
*/
|
|
5
|
+
|
|
5
6
|
"use strict";
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
import { webUi } from "./lib/webUi.mjs";
|
|
9
|
+
|
|
10
|
+
// Execute our first run screen if we don't have valid Protect login credentials and a controller.
|
|
11
|
+
const firstRunIsRequired = () => {
|
|
12
|
+
|
|
13
|
+
if(ui.featureOptions.currentConfig.length && ui.featureOptions.currentConfig[0].controllers?.length &&
|
|
14
|
+
ui.featureOptions.currentConfig[0].controllers[0]?.address?.length && ui.featureOptions.currentConfig[0].controllers[0]?.username?.length &&
|
|
15
|
+
ui.featureOptions.currentConfig[0].controllers[0]?.password?.length) {
|
|
16
|
+
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Initialize our first run screen with any information from our existing configuration.
|
|
24
|
+
const firstRunOnStart = () => {
|
|
25
|
+
|
|
26
|
+
// Pre-populate with anything we might already have in our configuration.
|
|
27
|
+
document.getElementById("address").value = ui.featureOptions.currentConfig[0].controllers?.[0]?.address ?? "";
|
|
28
|
+
document.getElementById("username").value = ui.featureOptions.currentConfig[0].controllers?.[0]?.username ?? "";
|
|
29
|
+
document.getElementById("password").value = ui.featureOptions.currentConfig[0].controllers?.[0]?.password ?? "";
|
|
30
|
+
|
|
31
|
+
return true;
|
|
32
|
+
};
|
|
9
33
|
|
|
10
|
-
//
|
|
11
|
-
|
|
34
|
+
// Validate our Protect credentials.
|
|
35
|
+
const firstRunOnSubmit = async () => {
|
|
12
36
|
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const inputPassword = document.getElementById("password");
|
|
37
|
+
const address = document.getElementById("address").value;
|
|
38
|
+
const username = document.getElementById("username").value;
|
|
39
|
+
const password = document.getElementById("password").value;
|
|
17
40
|
const tdLoginError = document.getElementById("loginError");
|
|
18
41
|
|
|
19
|
-
|
|
20
|
-
|
|
42
|
+
tdLoginError.innerHTML = " ";
|
|
43
|
+
|
|
44
|
+
if(!address?.length || !username?.length || !password?.length) {
|
|
21
45
|
|
|
22
|
-
|
|
46
|
+
tdLoginError.innerHTML = "<code class=\"text-danger\">Please enter a valid UniFi Protect controller address, username and password.</code>";
|
|
47
|
+
homebridge.hideSpinner();
|
|
48
|
+
|
|
49
|
+
return false;
|
|
23
50
|
}
|
|
24
51
|
|
|
25
|
-
|
|
26
|
-
inputAddress.value = featureOptions.currentConfig[0].controllers[0].address ?? "";
|
|
27
|
-
inputUsername.value = featureOptions.currentConfig[0].controllers[0].username ?? "";
|
|
28
|
-
inputPassword.value = featureOptions.currentConfig[0].controllers[0].password ?? "";
|
|
52
|
+
const ufpDevices = await homebridge.request("/getDevices", { address: address, username: username, password: password });
|
|
29
53
|
|
|
30
|
-
//
|
|
31
|
-
|
|
54
|
+
// Couldn't connect to the Protect controller for some reason.
|
|
55
|
+
if(!ufpDevices?.length) {
|
|
32
56
|
|
|
33
|
-
tdLoginError.innerHTML = "
|
|
34
|
-
|
|
57
|
+
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>";
|
|
58
|
+
homebridge.hideSpinner();
|
|
35
59
|
|
|
36
|
-
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
37
62
|
|
|
38
|
-
|
|
39
|
-
|
|
63
|
+
// Save the login credentials to our configuration.
|
|
64
|
+
if(!ui.featureOptions.currentConfig[0].controllers?.length) {
|
|
40
65
|
|
|
41
|
-
|
|
66
|
+
ui.featureOptions.currentConfig[0].controllers = [{}];
|
|
67
|
+
}
|
|
42
68
|
|
|
43
|
-
|
|
44
|
-
|
|
69
|
+
ui.featureOptions.currentConfig[0].controllers[0].address = address;
|
|
70
|
+
ui.featureOptions.currentConfig[0].controllers[0].username = username;
|
|
71
|
+
ui.featureOptions.currentConfig[0].controllers[0].password = password;
|
|
45
72
|
|
|
46
|
-
|
|
47
|
-
buttonFirstRun.addEventListener("click", async () => {
|
|
73
|
+
await homebridge.updatePluginConfig(ui.featureOptions.currentConfig);
|
|
48
74
|
|
|
49
|
-
|
|
50
|
-
|
|
75
|
+
return true;
|
|
76
|
+
};
|
|
51
77
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const password = inputPassword.value;
|
|
78
|
+
// Return the list of devices associated with a given Protect controller.
|
|
79
|
+
const getDevices = async (controller) => {
|
|
55
80
|
|
|
56
|
-
|
|
81
|
+
// If we're in the global context, we have no devices.
|
|
82
|
+
if(!controller) {
|
|
57
83
|
|
|
58
|
-
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
59
86
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
87
|
+
// Retrieve the current list of devices from the Protect controller.
|
|
88
|
+
let devices = await homebridge.request("/getDevices", { address: controller.address, username: controller.username, password: controller.password });
|
|
89
|
+
|
|
90
|
+
// Add the fields that the webUI framework is looking for to render.
|
|
91
|
+
devices = devices.map(device => ({
|
|
92
|
+
|
|
93
|
+
...device,
|
|
94
|
+
serial: device.mac
|
|
95
|
+
}));
|
|
96
|
+
|
|
97
|
+
return devices;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Return whether a given device is a controller.
|
|
101
|
+
const isController = (device) => device.modelKey === "nvr";
|
|
102
|
+
|
|
103
|
+
// Show the list of Protect devices associated with a controller, grouped by model.
|
|
104
|
+
const showSidebarDevices = (controller, devices) => {
|
|
105
|
+
|
|
106
|
+
const modelKeys = [...new Set(devices.map(x => x.modelKey))];
|
|
64
107
|
|
|
65
|
-
|
|
108
|
+
// Start with a clean slate.
|
|
109
|
+
ui.featureOptions.devicesTable.innerHTML = "";
|
|
66
110
|
|
|
67
|
-
|
|
68
|
-
if(!ufpDevices?.length) {
|
|
111
|
+
for(const key of modelKeys) {
|
|
69
112
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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;
|
|
73
123
|
}
|
|
74
124
|
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
featureOptions.currentConfig[0].controllers[0].username = username;
|
|
78
|
-
featureOptions.currentConfig[0].controllers[0].password = password;
|
|
125
|
+
// Create a row for this device category.
|
|
126
|
+
const trCategory = document.createElement("tr");
|
|
79
127
|
|
|
80
|
-
|
|
128
|
+
// Disable any pointer events and hover activity.
|
|
129
|
+
trCategory.style.pointerEvents = "none";
|
|
81
130
|
|
|
82
|
-
// Create our
|
|
83
|
-
document.
|
|
84
|
-
document.getElementById("menuWrapper").style.display = "inline-flex";
|
|
85
|
-
featureOptions.showUI();
|
|
86
|
-
});
|
|
131
|
+
// Create the cell for our device category row.
|
|
132
|
+
const tdCategory = document.createElement("td");
|
|
87
133
|
|
|
88
|
-
|
|
89
|
-
}
|
|
134
|
+
tdCategory.classList.add("m-0", "p-0", "pl-1", "w-100");
|
|
90
135
|
|
|
91
|
-
//
|
|
92
|
-
|
|
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";
|
|
93
139
|
|
|
94
|
-
|
|
95
|
-
|
|
140
|
+
// Add the cell to the table row.
|
|
141
|
+
trCategory.appendChild(tdCategory);
|
|
96
142
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
document.getElementById("menuHome").classList.add("btn-primary");
|
|
100
|
-
document.getElementById("menuFeatureOptions").classList.remove("btn-elegant");
|
|
101
|
-
document.getElementById("menuFeatureOptions").classList.add("btn-primary");
|
|
102
|
-
document.getElementById("menuSettings").classList.add("btn-elegant");
|
|
103
|
-
document.getElementById("menuSettings").classList.remove("btn-primary");
|
|
143
|
+
// Add the table row to the table.
|
|
144
|
+
ui.featureOptions.devicesTable.appendChild(trCategory);
|
|
104
145
|
|
|
105
|
-
|
|
106
|
-
document.getElementById("pageFeatureOptions").style.display = "none";
|
|
146
|
+
for(const device of modelDevices) {
|
|
107
147
|
|
|
108
|
-
|
|
148
|
+
// Create a row for this device.
|
|
149
|
+
const trDevice = document.createElement("tr");
|
|
109
150
|
|
|
110
|
-
|
|
111
|
-
homebridge.hideSpinner();
|
|
112
|
-
}
|
|
151
|
+
trDevice.classList.add("m-0", "p-0");
|
|
113
152
|
|
|
114
|
-
//
|
|
115
|
-
|
|
153
|
+
// Create a cell for our device.
|
|
154
|
+
const tdDevice = document.createElement("td");
|
|
116
155
|
|
|
117
|
-
|
|
118
|
-
homebridge.showSpinner();
|
|
119
|
-
homebridge.hideSchemaForm();
|
|
156
|
+
tdDevice.classList.add("m-0", "p-0" , "w-100");
|
|
120
157
|
|
|
121
|
-
|
|
122
|
-
document.getElementById("menuHome").classList.add("btn-elegant");
|
|
123
|
-
document.getElementById("menuHome").classList.remove("btn-primary");
|
|
124
|
-
document.getElementById("menuFeatureOptions").classList.remove("btn-elegant");
|
|
125
|
-
document.getElementById("menuFeatureOptions").classList.add("btn-primary");
|
|
126
|
-
document.getElementById("menuSettings").classList.remove("btn-elegant");
|
|
127
|
-
document.getElementById("menuSettings").classList.add("btn-primary");
|
|
158
|
+
const label = document.createElement("label");
|
|
128
159
|
|
|
129
|
-
|
|
130
|
-
|
|
160
|
+
label.name = device.serial;
|
|
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");
|
|
131
164
|
|
|
132
|
-
|
|
133
|
-
homebridge.hideSpinner();
|
|
134
|
-
}
|
|
165
|
+
label.addEventListener("click", () => ui.featureOptions.showDeviceOptions(device.serial));
|
|
135
166
|
|
|
136
|
-
//
|
|
137
|
-
|
|
167
|
+
// Add the device label to our cell.
|
|
168
|
+
tdDevice.appendChild(label);
|
|
138
169
|
|
|
139
|
-
|
|
140
|
-
|
|
170
|
+
// Add the cell to the table row.
|
|
171
|
+
trDevice.appendChild(tdDevice);
|
|
141
172
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
menuFeatureOptions.addEventListener("click", () => featureOptions.showUI());
|
|
145
|
-
menuSettings.addEventListener("click", () => showSettings());
|
|
173
|
+
// Add the table row to the table.
|
|
174
|
+
ui.featureOptions.devicesTable.appendChild(trDevice);
|
|
146
175
|
|
|
147
|
-
|
|
148
|
-
|
|
176
|
+
ui.featureOptions.webUiDeviceList.push(label);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
149
180
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
181
|
+
// Only show feature options that are valid for the capabilities of this device.
|
|
182
|
+
const validOption = (device, option) => {
|
|
183
|
+
|
|
184
|
+
if(device && (device.modelKey !== "nvr") && (
|
|
185
|
+
(option.hasFeature && (!device.featureFlags || !option.hasFeature.some(x => device.featureFlags[x]))) ||
|
|
186
|
+
(option.hasProperty && !option.hasProperty.some(x => x in device)) ||
|
|
187
|
+
(option.modelKey && (option.modelKey !== "all") && !option.modelKey.includes(device.modelKey)) ||
|
|
188
|
+
(option.hasSmartObjectType && device.featureFlags?.smartDetectTypes && !option.hasSmartObjectType.some(x => device.featureFlags.smartDetectTypes.includes(x))))) {
|
|
189
|
+
|
|
190
|
+
return false;
|
|
153
191
|
}
|
|
154
192
|
|
|
155
|
-
|
|
156
|
-
|
|
193
|
+
return true;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// Only show feature option categories that are valid for a particular device type.
|
|
197
|
+
const validOptionCategory = (device, category) => {
|
|
157
198
|
|
|
158
|
-
|
|
159
|
-
} else if(!("name" in featureOptions.currentConfig[0])) {
|
|
199
|
+
if(device && (device.modelKey !== "nvr") && !category.modelKey.some(model => ["all", device.modelKey].includes(model))) {
|
|
160
200
|
|
|
161
|
-
|
|
162
|
-
featureOptions.currentConfig[0].name = "UniFi Protect";
|
|
201
|
+
return false;
|
|
163
202
|
}
|
|
164
203
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
showFirstRun();
|
|
168
|
-
}
|
|
204
|
+
return true;
|
|
205
|
+
};
|
|
169
206
|
|
|
170
|
-
//
|
|
171
|
-
|
|
207
|
+
// Show the details for this device.
|
|
208
|
+
const showProtectDetails = (device) => {
|
|
172
209
|
|
|
173
|
-
|
|
174
|
-
|
|
210
|
+
// No device specified, we must be in a global context.
|
|
211
|
+
if(!device) {
|
|
175
212
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
213
|
+
document.getElementById("device_model").classList.remove("text-center");
|
|
214
|
+
document.getElementById("device_model").colSpan = 1;
|
|
215
|
+
document.getElementById("device_model").style.fontWeight = "normal";
|
|
216
|
+
document.getElementById("device_model").innerHTML = "N/A"
|
|
217
|
+
document.getElementById("device_mac").innerHTML = "N/A";
|
|
218
|
+
document.getElementById("device_address").innerHTML = "N/A";
|
|
219
|
+
document.getElementById("device_online").innerHTML = "N/A";
|
|
179
220
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Populate the device details.
|
|
225
|
+
document.getElementById("device_model").classList.remove("text-center");
|
|
226
|
+
document.getElementById("device_model").colSpan = 1;
|
|
227
|
+
document.getElementById("device_model").style.fontWeight = "normal";
|
|
228
|
+
document.getElementById("device_model").innerHTML = device.marketName ?? device.type;
|
|
229
|
+
document.getElementById("device_mac").innerHTML = device.mac;
|
|
230
|
+
document.getElementById("device_address").innerHTML = device.host ?? (device.modelKey === "sensor" ? "Bluetooth Device" : "None");
|
|
231
|
+
document.getElementById("device_online").innerHTML = ("state" in device) ? (device.state.charAt(0).toUpperCase() + device.state.slice(1).toLowerCase()) : "Connected";
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// Parameters for our feature options webUI.
|
|
235
|
+
const featureOptionsParams = {
|
|
236
|
+
|
|
237
|
+
getDevices: getDevices,
|
|
238
|
+
hasControllers: true,
|
|
239
|
+
infoPanel: showProtectDetails,
|
|
240
|
+
sidebar: {
|
|
241
|
+
|
|
242
|
+
controllerLabel: "Protect Controllers",
|
|
243
|
+
deviceLabel: "Protect Devices",
|
|
244
|
+
showDevices: showSidebarDevices
|
|
245
|
+
},
|
|
246
|
+
ui: {
|
|
247
|
+
|
|
248
|
+
isController: isController,
|
|
249
|
+
validOption: validOption,
|
|
250
|
+
validOptionCategory: validOptionCategory
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// Parameters for our plugin webUI.
|
|
255
|
+
const webUiParams = {
|
|
256
|
+
|
|
257
|
+
featureOptions: featureOptionsParams,
|
|
258
|
+
firstRun: {
|
|
259
|
+
|
|
260
|
+
isRequired: firstRunIsRequired,
|
|
261
|
+
onStart: firstRunOnStart,
|
|
262
|
+
onSubmit: firstRunOnSubmit
|
|
263
|
+
},
|
|
264
|
+
name: "UniFi Protect"
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// Instantiate the webUI.
|
|
268
|
+
const ui = new webUi(webUiParams);
|
|
269
|
+
|
|
270
|
+
// Display the webUI.
|
|
271
|
+
ui.show();
|