homebridge-unifi-access 1.3.0 → 1.5.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.
@@ -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,291 @@
1
1
  /* Copyright(C) 2017-2024, HJD (https://github.com/hjdhjd). All rights reserved.
2
2
  *
3
- * ui.mjs: HBUA webUI.
3
+ * ui.mjs: Homebridge UniFi Access webUI.
4
4
  */
5
5
  "use strict";
6
6
 
7
- // Keep a list of all the feature options and option groups. We dynamically import our modules to avoid browser caches.
8
- const featureOptions = new (await import("./access-featureoptions.mjs")).AccessFeatureOptions();
7
+ import { webUi } from "./lib/webUi.mjs";
9
8
 
10
- // Show the first run user experience if we don't have valid login credentials.
11
- function showFirstRun () {
9
+ // Execute our first run screen if we don't have valid Access login credentials and a controller.
10
+ const firstRunIsRequired = () => {
12
11
 
13
- const buttonFirstRun = document.getElementById("firstRun");
14
- const inputAddress = document.getElementById("address");
15
- const inputUsername = document.getElementById("username");
16
- const inputPassword = document.getElementById("password");
12
+ if(ui.featureOptions.currentConfig.length && ui.featureOptions.currentConfig[0].controllers?.length &&
13
+ ui.featureOptions.currentConfig[0].controllers[0]?.address?.length && ui.featureOptions.currentConfig[0].controllers[0]?.username?.length &&
14
+ ui.featureOptions.currentConfig[0].controllers[0]?.password?.length) {
15
+
16
+ return false;
17
+ }
18
+
19
+ return true;
20
+ };
21
+
22
+ // Initialize our first run screen with any information from our existing configuration.
23
+ const firstRunOnStart = () => {
24
+
25
+ // Pre-populate with anything we might already have in our configuration.
26
+ document.getElementById("address").value = ui.featureOptions.currentConfig[0].controllers?.[0]?.address ?? "";
27
+ document.getElementById("username").value = ui.featureOptions.currentConfig[0].controllers?.[0]?.username ?? "";
28
+ document.getElementById("password").value = ui.featureOptions.currentConfig[0].controllers?.[0]?.password ?? "";
29
+
30
+ return true;
31
+ };
32
+
33
+ // Validate our Access credentials.
34
+ const firstRunOnSubmit = async () => {
35
+
36
+ const address = document.getElementById("address").value;
37
+ const username = document.getElementById("username").value;
38
+ const password = document.getElementById("password").value;
17
39
  const tdLoginError = document.getElementById("loginError");
18
40
 
19
- // If we don't have any controllers configured, initialize the list.
20
- if(!featureOptions.currentConfig[0].controllers) {
41
+ tdLoginError.innerHTML = " ";
42
+
43
+ if(!address?.length || !username?.length || !password?.length) {
44
+
45
+ tdLoginError.innerHTML = "<code class=\"text-danger\">Please enter a valid UniFi Access controller address, username and password.</code>";
46
+ homebridge.hideSpinner();
21
47
 
22
- featureOptions.currentConfig[0].controllers = [ {} ];
48
+ return false;
23
49
  }
24
50
 
25
- // Pre-populate with anything we might already have in our configuration.
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 ?? "";
51
+ const udaDevices = await homebridge.request("/getDevices", { address: address, password: password, username: username });
52
+
53
+ // Couldn't connect to the Access controller for some reason.
54
+ if(!udaDevices?.length) {
55
+
56
+ tdLoginError.innerHTML = "Unable to login to the UniFi Access controller.<br>" +
57
+ "Please check your controller address, username, and password.<br><code class=\"text-danger\">" + (await homebridge.request("/getErrorMessage")) + "</code>";
58
+ homebridge.hideSpinner();
59
+
60
+ return false;
61
+ }
62
+
63
+ // Save the login credentials to our configuration.
64
+ if(!ui.featureOptions.currentConfig[0].controllers?.length) {
65
+
66
+ ui.featureOptions.currentConfig[0].controllers = [{}];
67
+ }
68
+
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;
72
+
73
+ await homebridge.updatePluginConfig(ui.featureOptions.currentConfig);
74
+
75
+ return true;
76
+ };
29
77
 
30
- // Clear login error messages when the login credentials change.
31
- inputAddress.addEventListener("input", () => {
78
+ // Return the list of devices associated with a given Access controller.
79
+ const getDevices = async (controller) => {
32
80
 
33
- tdLoginError.innerHTML = "&nbsp;";
34
- });
81
+ // If we're in the global context, we have no devices.
82
+ if(!controller) {
35
83
 
36
- inputUsername.addEventListener("input", () => {
84
+ return [];
85
+ }
86
+
87
+ // Retrieve the current list of devices from the Access controller.
88
+ let devices = await homebridge.request("/getDevices", { address: controller.address, password: controller.password, username: controller.username });
89
+
90
+ // Since the controller JSON doesn't have the same properties as the device JSON, let's make the controller JSON emulate the properties we care about.
91
+ if(devices?.length) {
92
+
93
+ /* eslint-disable camelcase */
94
+ devices[0].display_model = "controller";
95
+ devices[0].ip = devices[0].host.ip;
96
+ devices[0].is_online = true;
97
+ devices[0].mac = devices[0].host.mac;
98
+ devices[0].model = devices[0].host.device_type;
99
+ devices[0].unique_id = devices[0].host.mac;
100
+ /* eslint-enable camelcase */
101
+ }
102
+
103
+ // Add the fields that the webUI framework is looking for to render.
104
+ devices = devices.map(device => ({
37
105
 
38
- tdLoginError.innerHTML = "&nbsp;";
39
- });
106
+ ...device,
107
+ serial: device.mac.replace(/:/g, "").toUpperCase()
108
+ }));
40
109
 
41
- inputPassword.addEventListener("input", () => {
110
+ return devices;
111
+ };
42
112
 
43
- tdLoginError.innerHTML = "&nbsp;";
44
- });
113
+ // Return whether a given device is a controller.
114
+ const isController = (device) => device.modelKey === "controller";
45
115
 
46
- // First run user experience.
47
- buttonFirstRun.addEventListener("click", async () => {
116
+ // Show the list of Access devices associated with a controller, grouped by model.
117
+ const showSidebarDevices = (controller, devices) => {
48
118
 
49
- // Show the beachball while we setup.
50
- homebridge.showSpinner();
119
+ // Workaround for the time being to reduce the number of models we see to just the currently supported ones.
120
+ const modelKeys =
121
+ [...new Set(devices.filter(device => ["controller"].includes(device.display_model) || device.capabilities.includes("is_hub")).map(device => device.display_model))];
51
122
 
52
- const address = inputAddress.value;
53
- const username = inputUsername.value;
54
- const password = inputPassword.value;
123
+ // Start with a clean slate.
124
+ ui.featureOptions.devicesTable.innerHTML = "";
55
125
 
56
- tdLoginError.innerHTML = "&nbsp;";
126
+ for(const key of modelKeys) {
57
127
 
58
- if(!address?.length || !username?.length || !password?.length) {
128
+ // Get all the devices associated with this device category.
129
+ const modelDevices = devices.filter(x => x.display_model === key);
59
130
 
60
- tdLoginError.appendChild(document.createTextNode("Please enter a valid UniFi Access controller address, username and password."));
61
- homebridge.hideSpinner();
62
- return;
131
+ // Nothing in this category, let's keep going.
132
+ if(!modelDevices.length) {
133
+
134
+ continue;
63
135
  }
64
136
 
65
- const udaDevices = await homebridge.request("/getDevices", { address: address, username: username, password: password });
137
+ // If it's a controller, we handle that case differently.
138
+ if(key === "controller") {
66
139
 
67
- // Couldn't connect to the Access controller for some reason.
68
- if(!udaDevices?.length) {
140
+ // Change the name of the controller that we show users once we've connected with the controller.
141
+ ui.featureOptions.webUiControllerList.map(x => (x.name === controller.address) ? x.childNodes[0].nodeValue = modelDevices[0].host.hostname : true);
69
142
 
70
- tdLoginError.innerHTML = "Unable to login to the UniFi Access controller.<br>Please check your controller address, username, and password.<br><code class=\"text-danger\">" + (await homebridge.request("/getErrorMessage")) + "</code>";
71
- homebridge.hideSpinner();
72
- return;
143
+ continue;
73
144
  }
74
145
 
75
- // Save the login credentials to our configuration.
76
- featureOptions.currentConfig[0].controllers[0].address = address;
77
- featureOptions.currentConfig[0].controllers[0].username = username;
78
- featureOptions.currentConfig[0].controllers[0].password = password;
146
+ // Create a row for this device category.
147
+ const trCategory = document.createElement("tr");
79
148
 
80
- await homebridge.updatePluginConfig(featureOptions.currentConfig);
149
+ // Disable any pointer events and hover activity.
150
+ trCategory.style.pointerEvents = "none";
81
151
 
82
- // Create our UI.
83
- document.getElementById("pageFirstRun").style.display = "none";
84
- document.getElementById("menuWrapper").style.display = "inline-flex";
85
- featureOptions.showUI();
86
- });
152
+ // Create the cell for our device category row.
153
+ const tdCategory = document.createElement("td");
87
154
 
88
- document.getElementById("pageFirstRun").style.display = "block";
89
- }
155
+ tdCategory.classList.add("m-0", "p-0", "pl-1", "w-100");
90
156
 
91
- // Show the main plugin configuration tab.
92
- function showSettings () {
157
+ // Add the category name, with appropriate casing.
158
+ tdCategory.appendChild(document.createTextNode((key.charAt(0).toUpperCase() + key.slice(1) + "s")));
159
+ tdCategory.style.fontWeight = "bold";
93
160
 
94
- // Show the beachball while we setup.
95
- homebridge.showSpinner();
161
+ // Add the cell to the table row.
162
+ trCategory.appendChild(tdCategory);
96
163
 
97
- // Create our UI.
98
- document.getElementById("menuHome").classList.remove("btn-elegant");
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");
164
+ // Add the table row to the table.
165
+ ui.featureOptions.devicesTable.appendChild(trCategory);
104
166
 
105
- document.getElementById("pageSupport").style.display = "none";
106
- document.getElementById("pageFeatureOptions").style.display = "none";
167
+ for(const device of modelDevices) {
107
168
 
108
- homebridge.showSchemaForm();
169
+ // Create a row for this device.
170
+ const trDevice = document.createElement("tr");
109
171
 
110
- // All done. Let the user interact with us.
111
- homebridge.hideSpinner();
112
- }
172
+ trDevice.classList.add("m-0", "p-0");
113
173
 
114
- // Show the support tab.
115
- function showSupport() {
174
+ // Create a cell for our device.
175
+ const tdDevice = document.createElement("td");
116
176
 
117
- // Show the beachball while we setup.
118
- homebridge.showSpinner();
119
- homebridge.hideSchemaForm();
177
+ tdDevice.classList.add("m-0", "p-0" , "w-100");
120
178
 
121
- // Create our UI.
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");
179
+ const label = document.createElement("label");
128
180
 
129
- document.getElementById("pageSupport").style.display = "block";
130
- document.getElementById("pageFeatureOptions").style.display = "none";
181
+ label.name = device.serial;
182
+ label.appendChild(document.createTextNode(device.alias ?? device.display_model));
183
+ label.style.cursor = "pointer";
184
+ label.classList.add("mx-2", "my-0", "p-0", "w-100");
131
185
 
132
- // All done. Let the user interact with us.
133
- homebridge.hideSpinner();
134
- }
186
+ label.addEventListener("click", () => ui.featureOptions.showDeviceOptions(device.serial));
135
187
 
136
- // Launch our webUI.
137
- async function launchWebUI() {
188
+ // Add the device label to our cell.
189
+ tdDevice.appendChild(label);
138
190
 
139
- // Retrieve the current plugin configuration.
140
- featureOptions.currentConfig = await homebridge.getPluginConfig();
191
+ // Add the cell to the table row.
192
+ trDevice.appendChild(tdDevice);
141
193
 
142
- // Add our event listeners to animate the UI.
143
- menuHome.addEventListener("click", () => showSupport());
144
- menuFeatureOptions.addEventListener("click", () => featureOptions.showUI());
145
- menuSettings.addEventListener("click", () => showSettings());
194
+ // Add the table row to the table.
195
+ ui.featureOptions.devicesTable.appendChild(trDevice);
146
196
 
147
- // If we've got a valid Access controller, username, and password configured, we launch our feature option UI. Otherwise, we launch our first run UI.
148
- 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) {
197
+ ui.featureOptions.webUiDeviceList.push(label);
198
+ }
199
+ }
200
+ };
149
201
 
150
- document.getElementById("menuWrapper").style.display = "inline-flex";
151
- featureOptions.showUI();
152
- return;
202
+ // Only show feature options that are valid for the capabilities of this device.
203
+ const validOption = (device, option) => {
204
+
205
+ if(device && (device.display_model !== "controller") && (
206
+ (option.hasFeature && (!device.capabilities || !option.hasFeature.some(x => device.capabilities[x]))) ||
207
+ (option.hasProperty && !option.hasProperty.some(x => x in device)) ||
208
+ (option.modelKey && (option.modelKey !== "all") && !option.modelKey.includes(device.display_model)))) {
209
+
210
+ return false;
211
+ }
212
+
213
+ return true;
214
+ };
215
+
216
+ // Only show feature option categories that are valid for a particular device type.
217
+ const validOptionCategory = (device, category) => {
218
+
219
+ if(device && (device.display_model !== "controller") && !category.modelKey.some(model => ["all", device.display_model].includes(model))) {
220
+
221
+ return false;
153
222
  }
154
223
 
155
- // If we have no configuration, let's create one.
156
- if(!featureOptions.currentConfig.length) {
224
+ return true;
225
+ };
157
226
 
158
- featureOptions.currentConfig.push({ controllers: [ {} ], name: "UniFi Access" });
159
- } else if(!("name" in featureOptions.currentConfig[0])) {
227
+ // Show the details for this device.
228
+ const showAccessDetails = (device) => {
229
+
230
+ // No device specified, we must be in a global context.
231
+ if(!device) {
232
+
233
+ document.getElementById("device_model").classList.remove("text-center");
234
+ document.getElementById("device_model").colSpan = 1;
235
+ document.getElementById("device_model").style.fontWeight = "normal";
236
+ document.getElementById("device_model").innerHTML = "N/A";
237
+ document.getElementById("device_mac").innerHTML = "N/A";
238
+ document.getElementById("device_address").innerHTML = "N/A";
239
+ document.getElementById("device_online").innerHTML = "N/A";
240
+
241
+ return;
242
+ }
160
243
 
161
- // If we haven't set the name, let's do so now.
162
- featureOptions.currentConfig[0].name = "UniFi Access";
244
+ // Populate the device details.
245
+ document.getElementById("device_model").classList.remove("text-center");
246
+ document.getElementById("device_model").colSpan = 1;
247
+ document.getElementById("device_model").style.fontWeight = "normal";
248
+ document.getElementById("device_model").innerHTML = device.model ?? device.display_model;
249
+ document.getElementById("device_mac").innerHTML = device.mac.replace(/:/g, "").toUpperCase();
250
+ document.getElementById("device_address").innerHTML = device.ip;
251
+ document.getElementById("device_online").innerHTML = device.is_online ? "Connected" : "Disconnected";
252
+ };
253
+
254
+ // Parameters for our feature options webUI.
255
+ const featureOptionsParams = {
256
+
257
+ getDevices: getDevices,
258
+ hasControllers: true,
259
+ infoPanel: showAccessDetails,
260
+ sidebar: {
261
+
262
+ controllerLabel: "Access Controllers",
263
+ deviceLabel: "Access Devices",
264
+ showDevices: showSidebarDevices
265
+ },
266
+ ui: {
267
+
268
+ isController: isController,
269
+ validOption: validOption,
270
+ validOptionCategory: validOptionCategory
163
271
  }
272
+ };
164
273
 
165
- // Update the plugin configuration and launch the first run UI.
166
- await homebridge.updatePluginConfig(featureOptions.currentConfig);
167
- showFirstRun();
168
- }
274
+ // Parameters for our plugin webUI.
275
+ const webUiParams = {
169
276
 
170
- // Fire off our UI, catching errors along the way.
171
- try {
277
+ featureOptions: featureOptionsParams,
278
+ firstRun: {
172
279
 
173
- launchWebUI();
174
- } catch(err) {
280
+ isRequired: firstRunIsRequired,
281
+ onStart: firstRunOnStart,
282
+ onSubmit: firstRunOnSubmit
283
+ },
284
+ name: "UniFi Access"
285
+ };
175
286
 
176
- // If we had an error instantiating or updating the UI, notify the user.
177
- homebridge.toast.error(err.message, "Error");
178
- } finally {
287
+ // Instantiate the webUI.
288
+ const ui = new webUi(webUiParams);
179
289
 
180
- // Always leave the UI in a usable place for the end user.
181
- homebridge.hideSpinner();
182
- }
290
+ // Display the webUI.
291
+ ui.show();