homebridge-unifi-protect 5.5.4 → 6.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 +3 -3
- package/config.schema.json +17 -16
- package/dist/index.d.ts +3 -0
- package/dist/index.js +6 -6
- package/dist/index.js.map +1 -1
- package/dist/protect-camera.d.ts +58 -0
- package/dist/protect-camera.js +367 -246
- package/dist/protect-camera.js.map +1 -1
- package/dist/protect-device.d.ts +48 -0
- package/dist/protect-device.js +189 -0
- package/dist/protect-device.js.map +1 -0
- package/dist/protect-doorbell.d.ts +22 -0
- package/dist/protect-doorbell.js +75 -64
- package/dist/protect-doorbell.js.map +1 -1
- package/dist/protect-ffmpeg-record.d.ts +15 -0
- package/dist/protect-ffmpeg-record.js +48 -34
- package/dist/protect-ffmpeg-record.js.map +1 -1
- package/dist/protect-ffmpeg-stream.d.ts +15 -0
- package/dist/protect-ffmpeg-stream.js +22 -12
- package/dist/protect-ffmpeg-stream.js.map +1 -1
- package/dist/protect-ffmpeg.d.ts +42 -0
- package/dist/protect-ffmpeg.js +49 -58
- package/dist/protect-ffmpeg.js.map +1 -1
- package/dist/protect-light.d.ts +13 -0
- package/dist/protect-light.js +63 -40
- package/dist/protect-light.js.map +1 -1
- package/dist/protect-liveviews.d.ts +17 -0
- package/dist/protect-liveviews.js +117 -101
- package/dist/protect-liveviews.js.map +1 -1
- package/dist/protect-mqtt.d.ts +19 -0
- package/dist/protect-mqtt.js +26 -35
- package/dist/protect-mqtt.js.map +1 -1
- package/dist/protect-nvr-events.d.ts +30 -0
- package/dist/protect-nvr-events.js +168 -431
- package/dist/protect-nvr-events.js.map +1 -1
- package/dist/protect-nvr-systeminfo.d.ts +15 -0
- package/dist/protect-nvr-systeminfo.js +43 -49
- package/dist/protect-nvr-systeminfo.js.map +1 -1
- package/dist/protect-nvr.d.ts +48 -0
- package/dist/protect-nvr.js +327 -359
- package/dist/protect-nvr.js.map +1 -1
- package/dist/protect-options.d.ts +39 -0
- package/dist/protect-options.js +172 -6
- package/dist/protect-options.js.map +1 -1
- package/dist/protect-platform.d.ts +17 -0
- package/dist/protect-platform.js +17 -30
- package/dist/protect-platform.js.map +1 -1
- package/dist/protect-record.d.ts +33 -0
- package/dist/protect-record.js +130 -126
- package/dist/protect-record.js.map +1 -1
- package/dist/protect-rtp.d.ts +29 -0
- package/dist/protect-rtp.js +133 -16
- package/dist/protect-rtp.js.map +1 -1
- package/dist/protect-securitysystem.d.ts +18 -0
- package/dist/protect-securitysystem.js +105 -109
- package/dist/protect-securitysystem.js.map +1 -1
- package/dist/protect-sensor.d.ts +28 -0
- package/dist/protect-sensor.js +79 -97
- package/dist/protect-sensor.js.map +1 -1
- package/dist/protect-stream.d.ts +41 -0
- package/dist/protect-stream.js +298 -156
- package/dist/protect-stream.js.map +1 -1
- package/dist/protect-timeshift.d.ts +30 -0
- package/dist/protect-timeshift.js +65 -48
- package/dist/protect-timeshift.js.map +1 -1
- package/dist/protect-types.d.ts +50 -0
- package/dist/protect-types.js +22 -0
- package/dist/protect-types.js.map +1 -0
- package/dist/protect-viewer.d.ts +17 -0
- package/dist/protect-viewer.js +41 -47
- package/dist/protect-viewer.js.map +1 -1
- package/dist/settings.d.ts +22 -0
- package/dist/settings.js +30 -35
- package/dist/settings.js.map +1 -1
- package/homebridge-ui/public/index.html +715 -0
- package/homebridge-ui/server.js +156 -0
- package/package.json +15 -15
- package/dist/protect-accessory.js +0 -184
- package/dist/protect-accessory.js.map +0 -1
|
@@ -0,0 +1,715 @@
|
|
|
1
|
+
<p class="text-center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/hjdhjd/homebridge-unifi-protect/main/homebridge-protect.svg"
|
|
3
|
+
alt="homebridge-unifi-protect logo" style="width: 60%;" />
|
|
4
|
+
</p>
|
|
5
|
+
<div id="pageIntro" style="display: none;">
|
|
6
|
+
<p class="lead text-center">Thank you for installing <strong>homebridge-unifi-protect</strong></p>
|
|
7
|
+
<div class="text-center">
|
|
8
|
+
<button type="button" class="btn btn-primary" id="introLink">Continue →</button>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
<div id="menuWrapper" class="btn-group w-100 mb-0" role="group" aria-label="UI Menu" style="display: none;">
|
|
12
|
+
<button type="button" class="btn btn-primary" id="menuSettings">
|
|
13
|
+
Main Settings
|
|
14
|
+
</button>
|
|
15
|
+
<button type="button" class="btn btn-primary" id="menuProtect">
|
|
16
|
+
Feature Options
|
|
17
|
+
</button>
|
|
18
|
+
<button type="button" class="btn btn-primary mr-0" id="menuHome">
|
|
19
|
+
Support
|
|
20
|
+
</button>
|
|
21
|
+
</div>
|
|
22
|
+
<div id="disabledBanner" class="alert alert-secondary mb-0 mt-3" role="alert" style="display: none;">
|
|
23
|
+
Plugin is currently disabled
|
|
24
|
+
<button id="disabledEnable" type="button" class="btn btn-link p-0 m-0 float-right">
|
|
25
|
+
Enable
|
|
26
|
+
</button>
|
|
27
|
+
</div>
|
|
28
|
+
<div id="pageDevices" class="mt-2" style="display: none;">
|
|
29
|
+
<div id="deviceInfo">
|
|
30
|
+
<table class="table table-sm table-borderless">
|
|
31
|
+
<tr class="align-top">
|
|
32
|
+
<td rowspan="2" class="w-25">
|
|
33
|
+
<table class="table table-sm table-bordered m-0 p-0">
|
|
34
|
+
<tr>
|
|
35
|
+
<td>
|
|
36
|
+
<table class="table table-sm table-borderless m-0 p-0" id="controllersTable">
|
|
37
|
+
</table>
|
|
38
|
+
</td>
|
|
39
|
+
</tr>
|
|
40
|
+
<tr>
|
|
41
|
+
<td>
|
|
42
|
+
<table class="table table-sm table-borderless m-0 p-0" id="devicesTable">
|
|
43
|
+
</table>
|
|
44
|
+
</td>
|
|
45
|
+
</tr>
|
|
46
|
+
</table>
|
|
47
|
+
</td>
|
|
48
|
+
<td>
|
|
49
|
+
<table class="table table-sm table-borderless border-bottom m-0 p-0" id="deviceStatsTable" style="display: none;">
|
|
50
|
+
<tr>
|
|
51
|
+
<th style="width: 30%" class="m-0 p-0"><B>Model<B></th>
|
|
52
|
+
<th style="width: 25%;" class="m-0 p-0"><B>IP Address</B></th>
|
|
53
|
+
<th style="width: 20%;" class="m-0 p-0"><B>MAC Address</B></th>
|
|
54
|
+
<th style="width: 25%;" class="m-0 p-0"><B>Status</B></th>
|
|
55
|
+
</tr>
|
|
56
|
+
<tr>
|
|
57
|
+
<td id="device_model" class="m-0 p-0"></td>
|
|
58
|
+
<td id="device_address" class="m-0 p-0"></td>
|
|
59
|
+
<td id="device_mac" class="m-0 p-0"></td>
|
|
60
|
+
<td id="device_online" class="m-0 p-0"></td>
|
|
61
|
+
</tr>
|
|
62
|
+
</table>
|
|
63
|
+
</td>
|
|
64
|
+
</tr>
|
|
65
|
+
<tr>
|
|
66
|
+
<td id="configTable" width="100%">
|
|
67
|
+
</td>
|
|
68
|
+
</tr>
|
|
69
|
+
</table>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
<div id="pageSupport" class="mt-4" style="display: none;">
|
|
73
|
+
<p class="text-center lead">Thank you for using <strong>homebridge-unifi-protect</strong>.</p>
|
|
74
|
+
|
|
75
|
+
<p>Other plugins by <a target="_blank" href="https://github.com/hjdhjd">HJD</a>: </p>
|
|
76
|
+
|
|
77
|
+
<ul dir="auto">
|
|
78
|
+
<li><a target="_blank" href="https://github.com/hjdhjd/homebridge-myq">homebridge-myQ: Liftmaster and Chamberlain garage door opener support for HomeKit</a></li>
|
|
79
|
+
</ul>
|
|
80
|
+
|
|
81
|
+
<p class="text-center lead">Documentation Reference</p>
|
|
82
|
+
|
|
83
|
+
<h5>Features</h5>
|
|
84
|
+
<ul dir="auto">
|
|
85
|
+
<li>
|
|
86
|
+
Getting Started
|
|
87
|
+
<ul dir="auto">
|
|
88
|
+
<li>
|
|
89
|
+
<a target="_blank" href="#installation">Installation</a>
|
|
90
|
+
: installing this plugin, including system requirements.
|
|
91
|
+
</li>
|
|
92
|
+
<li>
|
|
93
|
+
<a target="_blank" href="#plugin-configuration">Plugin Configuration</a>
|
|
94
|
+
: how to quickly get up and running.
|
|
95
|
+
</li>
|
|
96
|
+
<li>
|
|
97
|
+
<a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-protect/blob/main/docs/BestPractices.md">Best Practices</a>
|
|
98
|
+
: best practices for getting the most of your HomeKit setup and UniFi Protect.
|
|
99
|
+
</li>
|
|
100
|
+
<li>
|
|
101
|
+
<a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-protect/blob/main/docs/Troubleshooting.md">Troubleshooting</a>
|
|
102
|
+
: run into login problems or streaming problems? Give this a read.
|
|
103
|
+
</li>
|
|
104
|
+
</ul>
|
|
105
|
+
</li>
|
|
106
|
+
<li>
|
|
107
|
+
Advanced Topics
|
|
108
|
+
<ul dir="auto">
|
|
109
|
+
<li>
|
|
110
|
+
<a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-protect/blob/main/docs/Autoconfiguration.md">Autoconfiguration</a>
|
|
111
|
+
: what it is, design choices that I've made, and why.
|
|
112
|
+
</li>
|
|
113
|
+
<li>
|
|
114
|
+
<a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-protect/blob/main/docs/FeatureOptions.md">Feature Options</a>
|
|
115
|
+
: granular options to allow you to set the camera quality individually, show or hide specific cameras, controllers, and more.
|
|
116
|
+
</li>
|
|
117
|
+
<li>
|
|
118
|
+
<a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-protect/blob/main/docs/AudioOptions.md">Audio Options</a>
|
|
119
|
+
: options to further tailor how audio is handled from Protect, such as background noise reduction.
|
|
120
|
+
</li>
|
|
121
|
+
<li>
|
|
122
|
+
<a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-protect/blob/main/docs/Doorbell.md">Doorbells</a>
|
|
123
|
+
: how UniFi Protect doorbell support works in this plugin, and how to use all the available features including doorbell messages.
|
|
124
|
+
</li>
|
|
125
|
+
<li>
|
|
126
|
+
<a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-protect/blob/main/docs/HomeKitSecureVideo.md">HomeKit Secure Video</a>
|
|
127
|
+
: how HomeKit Secure Video support works in this plugin with UniFi Protect.
|
|
128
|
+
</li>
|
|
129
|
+
<li>
|
|
130
|
+
<a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-protect/blob/main/docs/Liveviews.md">Liveview Scenes</a>
|
|
131
|
+
: use the UniFi Protect liveviews feature (available in the UniFi Protect controller webUI) to create motion-detection scenes.
|
|
132
|
+
</li>
|
|
133
|
+
<li>
|
|
134
|
+
<a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-protect/blob/main/docs/MQTT.md">MQTT</a>
|
|
135
|
+
: how to configure MQTT support.
|
|
136
|
+
</li>
|
|
137
|
+
<li>
|
|
138
|
+
<a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-protect/blob/main/docs/AdvancedOptions.md">Advanced Configuration</a>
|
|
139
|
+
: complete list of configuration options available in this plugin.
|
|
140
|
+
</li>
|
|
141
|
+
<li>
|
|
142
|
+
<a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-protect/blob/main/docs/ProtectAPI.md">Realtime API Documentation</a>
|
|
143
|
+
: documentation of how the Ubiquiti realtime updates API works and how to decode the binary protocol.
|
|
144
|
+
</li>
|
|
145
|
+
<li>
|
|
146
|
+
<a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-protect/blob/main/docs/Changelog.md">Changelog</a>
|
|
147
|
+
: changes and release history of this plugin, starting with v3.0.
|
|
148
|
+
</li>
|
|
149
|
+
</ul>
|
|
150
|
+
</li>
|
|
151
|
+
</ul>
|
|
152
|
+
|
|
153
|
+
<h5>Help/About</h5>
|
|
154
|
+
<ul>
|
|
155
|
+
<li>
|
|
156
|
+
<a href="https://discord.gg/QXqfHEW"
|
|
157
|
+
target="_blank">Discord Support Channel</a>
|
|
158
|
+
</li>
|
|
159
|
+
<li>
|
|
160
|
+
<a href="https://github.com/hjdhjd/homebridge-unifi-protect/issues/new/choose"
|
|
161
|
+
target="_blank">Create a Developer Support Request</a>
|
|
162
|
+
</li>
|
|
163
|
+
<li>
|
|
164
|
+
<a href="https://github.com/hjdhjd/homebridge-unifi-protect/blob/main/docs/Changelog.md"
|
|
165
|
+
target="_blank">View the Changelog and Release Notes</a>
|
|
166
|
+
</li>
|
|
167
|
+
</ul>
|
|
168
|
+
</div>
|
|
169
|
+
<script>
|
|
170
|
+
;
|
|
171
|
+
(async () => {
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
|
|
175
|
+
// Retrieve the current plugin configuration.
|
|
176
|
+
let currentConfig = await homebridge.getPluginConfig();
|
|
177
|
+
|
|
178
|
+
// Show an navigation bar at the top of the plugin configuration UI.
|
|
179
|
+
const showIntro = () => {
|
|
180
|
+
|
|
181
|
+
const introLink = document.getElementById("introLink");
|
|
182
|
+
|
|
183
|
+
introLink.addEventListener("click", () => {
|
|
184
|
+
|
|
185
|
+
// Show the beachball while we setup.
|
|
186
|
+
homebridge.showSpinner();
|
|
187
|
+
|
|
188
|
+
// Create our UI.
|
|
189
|
+
document.getElementById("pageIntro").style.display = "none";
|
|
190
|
+
document.getElementById("menuWrapper").style.display = "inline-flex";
|
|
191
|
+
showSettings();
|
|
192
|
+
|
|
193
|
+
// All done. Let the user interact with us.
|
|
194
|
+
homebridge.hideSpinner();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
document.getElementById("pageIntro").style.display = "block";
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Our list of Protect controllers.
|
|
201
|
+
const controllerList = [];
|
|
202
|
+
|
|
203
|
+
// Show the list of controllers we've configured.
|
|
204
|
+
const showControllers = async () => {
|
|
205
|
+
|
|
206
|
+
// Show the beachball while we setup.
|
|
207
|
+
homebridge.showSpinner();
|
|
208
|
+
homebridge.hideSchemaForm();
|
|
209
|
+
|
|
210
|
+
// Make sure we have the refreshed configuration.
|
|
211
|
+
currentConfig = await homebridge.getPluginConfig();
|
|
212
|
+
|
|
213
|
+
// Create our custom UI.
|
|
214
|
+
document.getElementById("menuHome").classList.remove("btn-elegant");
|
|
215
|
+
document.getElementById("menuHome").classList.add("btn-primary");
|
|
216
|
+
document.getElementById("menuProtect").classList.add("btn-elegant");
|
|
217
|
+
document.getElementById("menuProtect").classList.remove("btn-primary");
|
|
218
|
+
document.getElementById("menuSettings").classList.remove("btn-elegant");
|
|
219
|
+
document.getElementById("menuSettings").classList.add("btn-primary");
|
|
220
|
+
|
|
221
|
+
// Hide the legacy UI.
|
|
222
|
+
document.getElementById("pageSupport").style.display = "none";
|
|
223
|
+
document.getElementById("pageDevices").style.display = "block";
|
|
224
|
+
|
|
225
|
+
// What we're going to do is display our global options, followed by the list of controllers the user has configured.
|
|
226
|
+
// We pre-select the first controller by default for the user as a starting point.
|
|
227
|
+
|
|
228
|
+
// Create the table for the our list of controllers and global options.
|
|
229
|
+
const controllersTable = document.getElementById("controllersTable");
|
|
230
|
+
|
|
231
|
+
// Start with a clean slate.
|
|
232
|
+
controllersTable.innerHTML = "";
|
|
233
|
+
|
|
234
|
+
// Enumerate our global settings.
|
|
235
|
+
const trGlobal = document.createElement("tr");
|
|
236
|
+
|
|
237
|
+
// Create the cell for our global settings.
|
|
238
|
+
const tdGlobal = document.createElement("td");
|
|
239
|
+
tdGlobal.classList.add("m-0", "p-0");
|
|
240
|
+
|
|
241
|
+
// Create our label target.
|
|
242
|
+
const globalLabel = document.createElement("label");
|
|
243
|
+
|
|
244
|
+
globalLabel.name = "UFP Global Settings";
|
|
245
|
+
globalLabel.appendChild(document.createTextNode("Global Settings"));
|
|
246
|
+
globalLabel.style.cursor = "pointer";
|
|
247
|
+
globalLabel.classList.add("mx-0", "my-2", "p-0", "w-100");
|
|
248
|
+
|
|
249
|
+
globalLabel.addEventListener("click", event => showDevices(null));
|
|
250
|
+
|
|
251
|
+
// Add the global settings label.
|
|
252
|
+
tdGlobal.appendChild(globalLabel);
|
|
253
|
+
tdGlobal.style.fontWeight = "bold";
|
|
254
|
+
|
|
255
|
+
// Add the global cell to the table.
|
|
256
|
+
trGlobal.appendChild(tdGlobal);
|
|
257
|
+
|
|
258
|
+
// Now add it to the overall controllers table.
|
|
259
|
+
controllersTable.appendChild(trGlobal);
|
|
260
|
+
|
|
261
|
+
// Add it as another controller, for UI purposes.
|
|
262
|
+
controllerList.push(globalLabel);
|
|
263
|
+
|
|
264
|
+
// Create a row for our controllers.
|
|
265
|
+
const trController = document.createElement("tr");
|
|
266
|
+
|
|
267
|
+
// Create the cell for our controller category row.
|
|
268
|
+
const tdController = document.createElement("td");
|
|
269
|
+
tdController.classList.add("m-0", "p-0");
|
|
270
|
+
|
|
271
|
+
// Add the category name, with appropriate casing.
|
|
272
|
+
tdController.appendChild(document.createTextNode("Protect Controller" + (currentConfig[0].controllers.length > 1 ? "s" : "")));
|
|
273
|
+
tdController.style.fontWeight = "bold";
|
|
274
|
+
|
|
275
|
+
// Add the cell to the table row.
|
|
276
|
+
trController.appendChild(tdController);
|
|
277
|
+
|
|
278
|
+
// Add the table row to the table.
|
|
279
|
+
controllersTable.appendChild(trController);
|
|
280
|
+
|
|
281
|
+
for(const ufpController of currentConfig[0].controllers) {
|
|
282
|
+
|
|
283
|
+
// Create a row for this controller.
|
|
284
|
+
const trDevice = document.createElement("tr");
|
|
285
|
+
trDevice.classList.add("m-0", "p-0");
|
|
286
|
+
|
|
287
|
+
// Create a cell for our controller.
|
|
288
|
+
const tdDevice = document.createElement("td");
|
|
289
|
+
tdDevice.classList.add("m-0", "p-0" , "w-100");
|
|
290
|
+
|
|
291
|
+
const label = document.createElement("label");
|
|
292
|
+
|
|
293
|
+
label.name = ufpController.address;
|
|
294
|
+
label.appendChild(document.createTextNode(ufpController.address));
|
|
295
|
+
label.style.cursor = "pointer";
|
|
296
|
+
label.classList.add("mx-2", "my-0", "p-0", "w-100");
|
|
297
|
+
|
|
298
|
+
label.addEventListener("click", event => showDevices(ufpController));
|
|
299
|
+
|
|
300
|
+
// Add the controller label to our cell.
|
|
301
|
+
tdDevice.appendChild(label);
|
|
302
|
+
|
|
303
|
+
// Add the cell to the table row.
|
|
304
|
+
trDevice.appendChild(tdDevice);
|
|
305
|
+
|
|
306
|
+
// Add the table row to the table.
|
|
307
|
+
controllersTable.appendChild(trDevice);
|
|
308
|
+
|
|
309
|
+
controllerList.push(label);
|
|
310
|
+
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// All done. Let the user interact with us.
|
|
314
|
+
homebridge.hideSpinner();
|
|
315
|
+
showDevices(currentConfig[0].controllers[0]);
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// Show the devices attached to a controller.
|
|
319
|
+
const showDevices = async (controller) => {
|
|
320
|
+
|
|
321
|
+
// Show the beachball while we setup.
|
|
322
|
+
homebridge.showSpinner();
|
|
323
|
+
|
|
324
|
+
// Make sure we highlight the selected controller so the user knows where we are.
|
|
325
|
+
controllerList.map(x => (x.name === (controller ? controller.address : "UFP Global Settings")) ?
|
|
326
|
+
x.parentElement.classList.add("bg-info", "text-white") : x.parentElement.classList.remove("bg-info", "text-white"));
|
|
327
|
+
|
|
328
|
+
const devicesTable = document.getElementById("devicesTable");
|
|
329
|
+
let ufpDevices = [];
|
|
330
|
+
|
|
331
|
+
// If we're not accessing global settings, pull a list of devices attached to this controller.
|
|
332
|
+
if(controller) {
|
|
333
|
+
|
|
334
|
+
ufpDevices = await homebridge.request("/getDevices", { address: controller.address, password: controller.password, username: controller.username });
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Couldn't connect to the Protect controller for some reason.
|
|
338
|
+
if(controller && !ufpDevices?.length) {
|
|
339
|
+
|
|
340
|
+
devicesTable.innerHTML = "";
|
|
341
|
+
|
|
342
|
+
document.getElementById("device_model").innerHTML = "Unable to connect to the Protect controller. Check your settings for this controller in the settings tab."
|
|
343
|
+
document.getElementById("device_mac").innerHTML = "";
|
|
344
|
+
document.getElementById("device_address").innerHTML = "";
|
|
345
|
+
document.getElementById("device_online").innerHTML = "";
|
|
346
|
+
document.getElementById("deviceStatsTable").style.display = "inline-table";
|
|
347
|
+
|
|
348
|
+
homebridge.hideSpinner();
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const modelKeys = [...new Set(ufpDevices.map(x => x.modelKey))];
|
|
353
|
+
const deviceList = [];
|
|
354
|
+
|
|
355
|
+
// Start with a clean slate.
|
|
356
|
+
devicesTable.innerHTML = "";
|
|
357
|
+
|
|
358
|
+
for(const key of modelKeys) {
|
|
359
|
+
|
|
360
|
+
// Get all the devices associated with this device category.
|
|
361
|
+
const devices = ufpDevices.filter(x => x.modelKey === key);
|
|
362
|
+
|
|
363
|
+
// If it's a controller, we handle that case differently.
|
|
364
|
+
if((key === "nvr") && devices.length) {
|
|
365
|
+
|
|
366
|
+
// Change the name of the controler that we show users once we've connected with the controller.
|
|
367
|
+
controllerList.map(x => (x.name === controller.address) ? x.childNodes[0].nodeValue = devices[0].name : true);
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Create a row for this device category.
|
|
372
|
+
const trCategory = document.createElement("tr");
|
|
373
|
+
|
|
374
|
+
// Create the cell for our device category row.
|
|
375
|
+
const tdCategory = document.createElement("td");
|
|
376
|
+
tdCategory.classList.add("m-0", "p-0");
|
|
377
|
+
|
|
378
|
+
// Add the category name, with appropriate casing.
|
|
379
|
+
tdCategory.appendChild(document.createTextNode((key === "nvr") ? "Protect Controller" : (key.charAt(0).toUpperCase() + key.slice(1) + "s")));
|
|
380
|
+
tdCategory.style.fontWeight = "bold";
|
|
381
|
+
|
|
382
|
+
// Add the cell to the table row.
|
|
383
|
+
trCategory.appendChild(tdCategory);
|
|
384
|
+
|
|
385
|
+
// Add the table row to the table.
|
|
386
|
+
devicesTable.appendChild(trCategory);
|
|
387
|
+
|
|
388
|
+
for(const device of devices) {
|
|
389
|
+
|
|
390
|
+
// Create a row for this device.
|
|
391
|
+
const trDevice = document.createElement("tr");
|
|
392
|
+
trDevice.classList.add("m-0", "p-0");
|
|
393
|
+
|
|
394
|
+
// Create a cell for our device.
|
|
395
|
+
const tdDevice = document.createElement("td");
|
|
396
|
+
tdDevice.classList.add("m-0", "p-0" , "w-100");
|
|
397
|
+
|
|
398
|
+
const label = document.createElement("label");
|
|
399
|
+
|
|
400
|
+
label.name = device.id;
|
|
401
|
+
label.appendChild(document.createTextNode(device.name));
|
|
402
|
+
label.style.cursor = "pointer";
|
|
403
|
+
label.classList.add("mx-2", "my-0", "p-0", "w-100");
|
|
404
|
+
|
|
405
|
+
label.addEventListener("click", event => showDeviceInfo(device.id));
|
|
406
|
+
|
|
407
|
+
// Add the device label to our cell.
|
|
408
|
+
tdDevice.appendChild(label);
|
|
409
|
+
|
|
410
|
+
// Add the cell to the table row.
|
|
411
|
+
trDevice.appendChild(tdDevice);
|
|
412
|
+
|
|
413
|
+
// Add the table row to the table.
|
|
414
|
+
devicesTable.appendChild(trDevice);
|
|
415
|
+
|
|
416
|
+
deviceList.push(label);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const configTable = document.getElementById("configTable");
|
|
421
|
+
let configOptions = currentConfig[0].options;
|
|
422
|
+
|
|
423
|
+
// Show all the valid options configured by the user.
|
|
424
|
+
optionsList = configOptions.filter(x => x.match(/^(Enable|Disable)\.*/gi)).map(x => x.toUpperCase());
|
|
425
|
+
|
|
426
|
+
// Is a feature option globally enabled?
|
|
427
|
+
const isGlobalOptionEnabled = (featureOption, defaultValue) => {
|
|
428
|
+
|
|
429
|
+
featureOption = featureOption.toUpperCase();
|
|
430
|
+
|
|
431
|
+
// Test device-specific options.
|
|
432
|
+
return optionsList.some(x => x === ("ENABLE." + featureOption)) ? true :
|
|
433
|
+
(optionsList.some(x => x === ("DISABLE." + featureOption)) ? false : defaultValue
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Is a feature option enabled at the device or controller level.
|
|
438
|
+
const isDeviceOptionEnabled = (featureOption, mac, defaultValue) => {
|
|
439
|
+
|
|
440
|
+
featureOption = featureOption.toUpperCase();
|
|
441
|
+
mac = mac.toUpperCase();
|
|
442
|
+
|
|
443
|
+
// Test device-specific options.
|
|
444
|
+
return optionsList.some(x => x === ("ENABLE." + featureOption + "." + mac)) ? true :
|
|
445
|
+
(optionsList.some(x => x === ("DISABLE." + featureOption + "." + mac)) ? false : defaultValue
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Show feature option information for a specific device, controller, or globally.
|
|
450
|
+
const showDeviceInfo = async (deviceId) => {
|
|
451
|
+
|
|
452
|
+
homebridge.showSpinner();
|
|
453
|
+
|
|
454
|
+
// Update the selected device for visibility.
|
|
455
|
+
deviceList.map(x => (x.name === deviceId) ? x.parentElement.classList.add("bg-info", "text-white") : x.parentElement.classList.remove("bg-info", "text-white"));
|
|
456
|
+
|
|
457
|
+
// Populate the device information info pane.
|
|
458
|
+
const ufpDevice = ufpDevices.find(x => x.id === deviceId);
|
|
459
|
+
|
|
460
|
+
// Ensure we have a controller or device. The only time this won't be the case is when we're looking at global settings.
|
|
461
|
+
if(ufpDevice) {
|
|
462
|
+
|
|
463
|
+
document.getElementById("device_model").innerHTML = ufpDevice.marketName ?? ufpDevice.type;
|
|
464
|
+
document.getElementById("device_mac").innerHTML = ufpDevice.mac;
|
|
465
|
+
document.getElementById("device_address").innerHTML = ufpDevice.host ?? (ufpDevice.modelKey === "sensor" ? "Bluetooth Device" : "None");
|
|
466
|
+
document.getElementById("device_online").innerHTML = ("state" in ufpDevice) ? (ufpDevice.state.charAt(0).toUpperCase() + ufpDevice.state.slice(1).toLowerCase()) : "Connected";
|
|
467
|
+
|
|
468
|
+
document.getElementById("deviceStatsTable").style.display = "inline-table";
|
|
469
|
+
} else {
|
|
470
|
+
|
|
471
|
+
document.getElementById("deviceStatsTable").style.display = "none";
|
|
472
|
+
|
|
473
|
+
document.getElementById("device_model").innerHTML = "N/A"
|
|
474
|
+
document.getElementById("device_mac").innerHTML = "N/A";
|
|
475
|
+
document.getElementById("device_address").innerHTML = "N/A";
|
|
476
|
+
document.getElementById("device_online").innerHTML = "N/A";
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Populate the feature options selected for this device.
|
|
480
|
+
const ufpFeatures = await homebridge.request("/getOptions", { configOptions: configOptions, nvrUfp: ufpDevices[0], deviceUfp: ufpDevice });
|
|
481
|
+
const optionsDevice = ufpFeatures.options;
|
|
482
|
+
|
|
483
|
+
let newConfigTableHtml = "";
|
|
484
|
+
configTable.innerHTML = "";
|
|
485
|
+
|
|
486
|
+
for(const category of ufpFeatures.categories) {
|
|
487
|
+
|
|
488
|
+
const optionTable = document.createElement("table");
|
|
489
|
+
const thead = document.createElement("thead");
|
|
490
|
+
const tbody = document.createElement("tbody");
|
|
491
|
+
const trFirst = document.createElement("tr");
|
|
492
|
+
const th = document.createElement("th");
|
|
493
|
+
|
|
494
|
+
// Set our table options.
|
|
495
|
+
optionTable.classList.add("table", "table-borderless", "table-hover", "table-sm");
|
|
496
|
+
th.classList.add("p-0");
|
|
497
|
+
th.style.fontWeight = "bold";
|
|
498
|
+
th.colSpan = 2;
|
|
499
|
+
tbody.classList.add("table-bordered");
|
|
500
|
+
|
|
501
|
+
// Add the feature option category description.
|
|
502
|
+
th.appendChild(document.createTextNode(category.description +
|
|
503
|
+
(!ufpDevice ? " (Global)" : (ufpDevice.modelKey === "nvr" ? " (Controller-wide)" : " (Device-specific)"))));
|
|
504
|
+
|
|
505
|
+
// Add the table header to the row.
|
|
506
|
+
trFirst.appendChild(th);
|
|
507
|
+
|
|
508
|
+
// Add the table row to the table head.
|
|
509
|
+
thead.appendChild(trFirst);
|
|
510
|
+
|
|
511
|
+
// Finally, add the table head to the table.
|
|
512
|
+
optionTable.appendChild(thead);
|
|
513
|
+
|
|
514
|
+
for(const option of optionsDevice[category.name]) {
|
|
515
|
+
|
|
516
|
+
// Create the next table row.
|
|
517
|
+
const trX = document.createElement("tr");
|
|
518
|
+
trX.classList.add("align-top");
|
|
519
|
+
|
|
520
|
+
// Create a checkbox for the option.
|
|
521
|
+
const tdCheckbox = document.createElement("td");
|
|
522
|
+
|
|
523
|
+
// Create the actual checkbox for the option.
|
|
524
|
+
const checkbox = document.createElement("input");
|
|
525
|
+
|
|
526
|
+
const featureOption = category.name + (option.name.length ? ("." + option.name): "");
|
|
527
|
+
checkbox.type = "checkbox";
|
|
528
|
+
checkbox.id = featureOption;
|
|
529
|
+
checkbox.name = featureOption;
|
|
530
|
+
checkbox.value = featureOption + (!ufpDevice ? "" : ("." + ufpDevice.mac)); // LEFT OFF HERE
|
|
531
|
+
checkbox.checked = !ufpDevice ? isGlobalOptionEnabled(featureOption, option.default) :
|
|
532
|
+
isDeviceOptionEnabled(featureOption, ufpDevice.mac, option.default);
|
|
533
|
+
|
|
534
|
+
checkbox.defaultChecked = option.default;
|
|
535
|
+
checkbox.classList.add("mx-2");
|
|
536
|
+
|
|
537
|
+
// Add or remove the setting from our configuration when we've changed our state.
|
|
538
|
+
checkbox.addEventListener("change", async () => {
|
|
539
|
+
|
|
540
|
+
// Find the option in our list and delete it if it exists.
|
|
541
|
+
const optionRegex = new RegExp("^(Enable|Disable)\." + checkbox.id + (!ufpDevice ? "" : ("\." + ufpDevice.mac)) + "$", "gi")
|
|
542
|
+
const newOptions = configOptions.filter(x => !x.match(optionRegex));
|
|
543
|
+
|
|
544
|
+
// The setting is different from the default, highlight it for the user, and add it to our configuration.
|
|
545
|
+
if(checkbox.checked !== option.default) {
|
|
546
|
+
|
|
547
|
+
labelDescription.classList.add("text-info");
|
|
548
|
+
newOptions.push((checkbox.checked ? "Enable." : "Disable.") + checkbox.value);
|
|
549
|
+
} else {
|
|
550
|
+
|
|
551
|
+
// We've reset to the defaults, remove our highlighting.
|
|
552
|
+
labelDescription.classList.remove("text-info");
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Update our configuration in Homebridge.
|
|
556
|
+
currentConfig[0].options = newOptions;
|
|
557
|
+
configOptions = newOptions;
|
|
558
|
+
await homebridge.updatePluginConfig(currentConfig);
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
// Add the checkbox to the table cell.
|
|
562
|
+
tdCheckbox.appendChild(checkbox);
|
|
563
|
+
|
|
564
|
+
// Add the checkbox to the table row.
|
|
565
|
+
trX.appendChild(tdCheckbox);
|
|
566
|
+
|
|
567
|
+
const tdLabel = document.createElement("td");
|
|
568
|
+
tdLabel.classList.add("w-100");
|
|
569
|
+
|
|
570
|
+
// Create a label for the checkbox with our option description.
|
|
571
|
+
const labelDescription = document.createElement("label");
|
|
572
|
+
labelDescription.for = checkbox.id;
|
|
573
|
+
labelDescription.style.cursor = "pointer";
|
|
574
|
+
labelDescription.classList.add("user-select-none", "my-0", "py-0");
|
|
575
|
+
|
|
576
|
+
// Highlight options for the user that are different than our defaults.
|
|
577
|
+
if(checkbox.checked !== checkbox.defaultChecked) {
|
|
578
|
+
|
|
579
|
+
labelDescription.classList.add("text-info");
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Add the actual description for the option after the checkbox.
|
|
583
|
+
labelDescription.appendChild(document.createTextNode(option.description));
|
|
584
|
+
|
|
585
|
+
// Add the label to the table cell.
|
|
586
|
+
tdLabel.appendChild(labelDescription);
|
|
587
|
+
|
|
588
|
+
// Provide a cell-wide target to click on options.
|
|
589
|
+
tdLabel.addEventListener("click", () => checkbox.click());
|
|
590
|
+
|
|
591
|
+
// Add the label table cell to the table row.
|
|
592
|
+
trX.appendChild(tdLabel);
|
|
593
|
+
|
|
594
|
+
// Add the table row to the table body.
|
|
595
|
+
tbody.appendChild(trX);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Add the table body to the table.
|
|
599
|
+
optionTable.appendChild(tbody);
|
|
600
|
+
|
|
601
|
+
// Add the table to the page.
|
|
602
|
+
configTable.appendChild(optionTable);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
homebridge.hideSpinner();
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Display the feature options to the user.
|
|
609
|
+
showDeviceInfo(controller ? ufpDevices[0].id : null);
|
|
610
|
+
|
|
611
|
+
// All done. Let the user interact with us.
|
|
612
|
+
homebridge.hideSpinner();
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
// Show the support tab.
|
|
616
|
+
const showSupport = () => {
|
|
617
|
+
|
|
618
|
+
// Show the beachball while we setup.
|
|
619
|
+
homebridge.showSpinner();
|
|
620
|
+
homebridge.hideSchemaForm();
|
|
621
|
+
|
|
622
|
+
// Create our UI.
|
|
623
|
+
document.getElementById("menuHome").classList.add("btn-elegant");
|
|
624
|
+
document.getElementById("menuHome").classList.remove("btn-primary");
|
|
625
|
+
document.getElementById("menuProtect").classList.remove("btn-elegant");
|
|
626
|
+
document.getElementById("menuProtect").classList.add("btn-primary");
|
|
627
|
+
document.getElementById("menuSettings").classList.remove("btn-elegant");
|
|
628
|
+
document.getElementById("menuSettings").classList.add("btn-primary");
|
|
629
|
+
|
|
630
|
+
document.getElementById("pageSupport").style.display = "block";
|
|
631
|
+
document.getElementById("pageDevices").style.display = "none";
|
|
632
|
+
|
|
633
|
+
// All done. Let the user interact with us.
|
|
634
|
+
homebridge.hideSpinner();
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
// Show the main plugin configuration tab.
|
|
638
|
+
const showSettings = () => {
|
|
639
|
+
|
|
640
|
+
// Show the beachball while we setup.
|
|
641
|
+
homebridge.showSpinner();
|
|
642
|
+
|
|
643
|
+
// Create our UI.
|
|
644
|
+
document.getElementById("menuHome").classList.remove("btn-elegant");
|
|
645
|
+
document.getElementById("menuHome").classList.add("btn-primary");
|
|
646
|
+
document.getElementById("menuProtect").classList.remove("btn-elegant");
|
|
647
|
+
document.getElementById("menuProtect").classList.add("btn-primary");
|
|
648
|
+
document.getElementById("menuSettings").classList.add("btn-elegant");
|
|
649
|
+
document.getElementById("menuSettings").classList.remove("btn-primary");
|
|
650
|
+
|
|
651
|
+
document.getElementById("pageSupport").style.display = "none";
|
|
652
|
+
document.getElementById("pageDevices").style.display = "none";
|
|
653
|
+
|
|
654
|
+
homebridge.showSchemaForm();
|
|
655
|
+
|
|
656
|
+
// All done. Let the user interact with us.
|
|
657
|
+
homebridge.hideSpinner();
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
// Show a disabled interface.
|
|
661
|
+
const showDisabledBanner = () => {
|
|
662
|
+
|
|
663
|
+
document.getElementById("disabledBanner").style.display = "block";
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
// Toggle our enabled state.
|
|
667
|
+
const enablePlugin = async () => {
|
|
668
|
+
|
|
669
|
+
// Show the beachball while we setup.
|
|
670
|
+
homebridge.showSpinner();
|
|
671
|
+
|
|
672
|
+
// Create our UI.
|
|
673
|
+
document.getElementById("disabledBanner").style.display = "none";
|
|
674
|
+
currentConfig[0].disablePlugin = false;
|
|
675
|
+
|
|
676
|
+
await homebridge.updatePluginConfig(currentConfig)
|
|
677
|
+
await homebridge.savePluginConfig()
|
|
678
|
+
|
|
679
|
+
// All done. Let the user interact with us.
|
|
680
|
+
homebridge.hideSpinner()
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
// Add our event listeners to animate the UI.
|
|
684
|
+
menuHome.addEventListener("click", () => showSupport());
|
|
685
|
+
menuProtect.addEventListener("click", () => showControllers());
|
|
686
|
+
menuSettings.addEventListener("click", () => showSettings());
|
|
687
|
+
disabledEnable.addEventListener("click", () => enablePlugin());
|
|
688
|
+
|
|
689
|
+
if(currentConfig.length) {
|
|
690
|
+
|
|
691
|
+
document.getElementById("menuWrapper").style.display = "inline-flex"
|
|
692
|
+
showSettings();
|
|
693
|
+
|
|
694
|
+
// If the plugin's disabled, inform the user.
|
|
695
|
+
if(currentConfig[0].disablePlugin) {
|
|
696
|
+
|
|
697
|
+
showDisabledBanner();
|
|
698
|
+
}
|
|
699
|
+
} else {
|
|
700
|
+
|
|
701
|
+
currentConfig.push({ name: "UniFi Protect" });
|
|
702
|
+
await homebridge.updatePluginConfig(currentConfig);
|
|
703
|
+
showIntro();
|
|
704
|
+
}
|
|
705
|
+
} catch (err) {
|
|
706
|
+
|
|
707
|
+
// If we had an error instantiating or updating the UI, notify the user.
|
|
708
|
+
homebridge.toast.error(err.message, "Error");
|
|
709
|
+
} finally {
|
|
710
|
+
|
|
711
|
+
// Always leave the UI in a usable place for the end user.
|
|
712
|
+
homebridge.hideSpinner();
|
|
713
|
+
}
|
|
714
|
+
})();
|
|
715
|
+
</script>
|