homebridge-unifi-access 0.0.1 → 1.1.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.
Files changed (49) hide show
  1. package/LICENSE.md +3 -3
  2. package/README.md +88 -3
  3. package/config.schema.json +202 -0
  4. package/dist/access-controller.d.ts +45 -0
  5. package/dist/access-controller.js +387 -0
  6. package/dist/access-controller.js.map +1 -0
  7. package/dist/access-device.d.ts +53 -0
  8. package/dist/access-device.js +362 -0
  9. package/dist/access-device.js.map +1 -0
  10. package/dist/access-events.d.ts +24 -0
  11. package/dist/access-events.js +151 -0
  12. package/dist/access-events.js.map +1 -0
  13. package/dist/access-hub.d.ts +21 -0
  14. package/dist/access-hub.js +277 -0
  15. package/dist/access-hub.js.map +1 -0
  16. package/dist/access-mqtt.d.ts +20 -0
  17. package/dist/access-mqtt.js +163 -0
  18. package/dist/access-mqtt.js.map +1 -0
  19. package/dist/access-options.d.ts +38 -0
  20. package/dist/access-options.js +167 -0
  21. package/dist/access-options.js.map +1 -0
  22. package/dist/access-platform.d.ts +16 -0
  23. package/dist/access-platform.js +103 -0
  24. package/dist/access-platform.js.map +1 -0
  25. package/dist/access-types.d.ts +11 -0
  26. package/dist/access-types.js +13 -0
  27. package/dist/access-types.js.map +1 -0
  28. package/dist/index.d.ts +3 -0
  29. package/dist/index.js +9 -5
  30. package/dist/index.js.map +1 -1
  31. package/dist/settings.d.ts +10 -0
  32. package/dist/settings.js +23 -10
  33. package/dist/settings.js.map +1 -1
  34. package/homebridge-ui/public/access-featureoptions.mjs +748 -0
  35. package/homebridge-ui/public/index.html +151 -0
  36. package/homebridge-ui/public/lib/featureoptions.mjs +201 -0
  37. package/homebridge-ui/public/ui.mjs +182 -0
  38. package/homebridge-ui/server.js +153 -0
  39. package/package.json +55 -23
  40. package/.eslintrc.json +0 -45
  41. package/dist/platform.js +0 -98
  42. package/dist/platform.js.map +0 -1
  43. package/dist/platformAccessory.js +0 -104
  44. package/dist/platformAccessory.js.map +0 -1
  45. package/src/index.ts +0 -11
  46. package/src/platform.ts +0 -116
  47. package/src/platformAccessory.ts +0 -130
  48. package/src/settings.ts +0 -9
  49. package/tsconfig.json +0 -20
@@ -0,0 +1,151 @@
1
+ <p class="text-center">
2
+ <img src="https://raw.githubusercontent.com/hjdhjd/homebridge-unifi-access/main/images/homebridge-unifi-access.svg" alt="homebridge-unifi-access logo" class="w-50" />
3
+ </p>
4
+ <div id="pageFirstRun" style="display: none;">
5
+ <div class="text-center">
6
+ <p>Please enter the address of your UniFi Access controller (e.g. unvr.local or 10.0.0.1) and the login credentials for a <strong>local user</strong> to get started with <strong>homebridge-unifi-access</strong>.</p>
7
+ <table class="table table-sm table-borderless">
8
+ <tr>
9
+ <td>
10
+ <input type="test" placeholder="Access controller hostname or IP address" size="40" id="address"></input>
11
+ </td>
12
+ </tr>
13
+ <tr>
14
+ <td>
15
+ <input type="username" placeholder="Access controller username" size="40" id="username"></input>
16
+ </td>
17
+ </tr>
18
+ <tr>
19
+ <td>
20
+ <input type="password" placeholder="Access controller password" size="40" id="password"></input>
21
+ </td>
22
+ </tr>
23
+ <tr>
24
+ <td class="m-0 p-2 text-center font-weight-bold text-danger" id="loginError">
25
+ &nbsp;
26
+ </td>
27
+ </tr>
28
+ </table>
29
+ <br>
30
+ <button type="button" class="btn btn-primary" id="firstRun">Login to UniFi Access &rarr;</button>
31
+ <br>
32
+ To optimize performance and responsiveness, please make this plugin a <a target="_blank" href="https://github.com/homebridge/homebridge/wiki/Child-Bridges">child bridge</a> once you've completed configuration.
33
+ </div>
34
+ </div>
35
+ <div id="menuWrapper" class="btn-group w-100 mb-0" role="group" aria-label="UI Menu" style="display: none;">
36
+ <button type="button" class="btn btn-primary" id="menuSettings">Settings</button>
37
+ <button type="button" class="btn btn-primary" id="menuFeatureOptions">Feature Options</button>
38
+ <button type="button" class="btn btn-primary mr-0" id="menuHome">Support</button>
39
+ </div>
40
+ <div id="pageFeatureOptions" class="mt-2" style="display: none;">
41
+ <div id="deviceInfo">
42
+ <table class="table table-sm table-borderless">
43
+ <tr class="align-center">
44
+ <td id="headerInfo" colspan="2" class="m-0 p-2 text-center font-weight-bold"></td>
45
+ </tr>
46
+ <tr class="align-top">
47
+ <td rowspan="3" class="w-25">
48
+ <table id="sidebar" class="table table-sm table-bordered m-0 p-0">
49
+ <tr>
50
+ <td>
51
+ <table class="table table-sm table-borderless m-0 p-0" id="controllersTable"></table>
52
+ </td>
53
+ </tr>
54
+ <tr>
55
+ <td>
56
+ <table class="table table-sm table-borderless m-0 p-0" id="devicesTable"></table>
57
+ </td>
58
+ </tr>
59
+ </table>
60
+ </td>
61
+ <td id="deviceStatsTable">
62
+ <table class="table table-sm table-borderless border-bottom m-0 p-0">
63
+ <tr id="deviceStatsHeader">
64
+ <th class="m-0 p-0" style="width: 30%;"><B>Model<B></th>
65
+ <th class="m-0 p-0 w-25"><B>IP Address</B></th>
66
+ <th class="m-0 p-0" style="width: 25%;"><B>MAC Address</B></th>
67
+ <th class="m-0 p-0 w-25"><B>Status</B></th>
68
+ </tr>
69
+ <tr>
70
+ <td id="device_model" class="m-0 p-0"></td>
71
+ <td id="device_address" class="m-0 p-0"></td>
72
+ <td id="device_mac" class="m-0 p-0"></td>
73
+ <td id="device_online" class="m-0 p-0"></td>
74
+ </tr>
75
+ </table>
76
+ </td>
77
+ </tr>
78
+ <tr>
79
+ <td id="configTable" class="w-100"></td>
80
+ </tr>
81
+ </table>
82
+ </div>
83
+ </div>
84
+ <div id="pageSupport" class="mt-4" style="display: none;">
85
+ <h5>Introduction</h5>
86
+ <p class="px-4">I hope you enjoy <a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-access">homebridge-unifi-access</a> as much as I enjoy developing it. All my projects are labors of love. If you'd like to show your appreciation - <a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-access">star this project on Github</A> and do some good in your community, either financially or with your time: a food bank, an animal shelter (two of my passions), or whatever resonates with you that can give something back to the world around you.</p>
87
+
88
+ <div class="px-4">
89
+ Other plugins by <a target="_blank" href="https://github.com/hjdhjd">HJD</a>:
90
+
91
+ <ul dir="auto">
92
+ <ul dir="auto">
93
+ <li><a target="_blank" href="https://github.com/hjdhjd/homebridge-myq">homebridge-myQ: Liftmaster and Chamberlain garage door and gate opener support for HomeKit</a></li>
94
+ <li><a target="_blank" href="https://github.com/hjdhjd/homebridge-ratgdo">homebridge-ratgdo: Ratgdo (non-myQ Liftmaster and Chamberlain) garage door and gate opener support for HomeKit</a></li> </ul>
95
+ <li><a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-protect">homebridge-unifi-protect: Complete HomeKit integration for the entire UniFi Protect ecosystem</a></li>
96
+ </ul>
97
+ </div>
98
+
99
+ <h5>Getting Started</h5>
100
+ <ul dir="auto">
101
+ <li>
102
+ <a target="_blank" href="#installation">Installation</a>
103
+ : installing this plugin, including system requirements.
104
+ </li>
105
+ <li>
106
+ <a target="_blank" href="#plugin-configuration">Plugin Configuration</a>
107
+ : how to quickly get up and running.
108
+ </li>
109
+ </ul>
110
+
111
+ <h5>Advanced Topics</h5>
112
+ <ul dir="auto">
113
+ <li>
114
+ <a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-access/blob/main/docs/FeatureOptions.md">Feature Options</a>: granular options to allow you to set the camera quality individually, show or hide specific cameras, controllers, and more.
115
+ </li>
116
+ <li>
117
+ <a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-access/blob/main/docs/MQTT.md">MQTT</a>: how to configure MQTT support.
118
+ </li>
119
+ <li>
120
+ <a target="_blank" href="https://github.com/hjdhjd/unifi-access">UniFi Access native API Documentation</a>: documentation of how the undocumented native Ubiquiti UniFi Access API.
121
+ </li>
122
+ <li>
123
+ <a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-access/blob/main/docs/Changelog.md">Changelog</a>: changes and release history of this plugin.
124
+ </li>
125
+ </ul>
126
+
127
+ <h5>Support</h5>
128
+ <ul>
129
+ <li>
130
+ <a target="_blank" href="https://discord.gg/QXqfHEW">Discord Support Channel</a>
131
+ </li>
132
+ <li>
133
+ <a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-access/issues/new/choose">Create a Developer Support Request</a>
134
+ </li>
135
+ <li>
136
+ <a target="_blank" href="https://github.com/hjdhjd/homebridge-unifi-access/blob/main/docs/Changelog.md">View the Changelog and Release Notes</a>
137
+ </li>
138
+ </ul>
139
+ </div>
140
+ <noscript>
141
+ <h2>A JavaScript-enabled web browser is required to use this webUI.</h2>
142
+ </noscript>
143
+ <script>
144
+ <!--
145
+ (async () => {
146
+
147
+ // We use dynamic imports because web browsers often attempt to cache modules and we always want to pull a fresh copy, in case they've been updated.
148
+ await import("./ui.mjs");
149
+ })();
150
+ // -->
151
+ </script>
@@ -0,0 +1,201 @@
1
+ /* Copyright(C) 2017-2024, HJD (https://github.com/hjdhjd). All rights reserved.
2
+ *
3
+ * featureoptions.mjs: Feature option webUI base class.
4
+ */
5
+ "use strict";
6
+
7
+ export class FeatureOptions {
8
+
9
+ controller;
10
+
11
+ featureOptionGroups;
12
+ featureOptionList;
13
+ optionsList;
14
+
15
+ constructor() {
16
+
17
+ this.featureOptionGroups = {};
18
+ this.featureOptionList = {};
19
+ this.controller = null;
20
+ this.optionsList = [];
21
+ }
22
+
23
+ // Abstract method to be implemented by subclasses to render the feature option webUI.
24
+ async showUI() {
25
+ }
26
+
27
+ // Is this feature option set explicitly?
28
+ isOptionSet(featureOption, deviceMac) {
29
+
30
+ const optionRegex = new RegExp("^(?:Enable|Disable)\\." + featureOption + (!deviceMac ? "" : "\\." + deviceMac) + "$", "gi");
31
+ return this.optionsList.filter(x => optionRegex.test(x)).length ? true : false;
32
+ }
33
+
34
+ // Is a feature option globally enabled?
35
+ isGlobalOptionEnabled(featureOption, defaultState) {
36
+
37
+ featureOption = featureOption.toUpperCase();
38
+
39
+ // Test device-specific options.
40
+ return this.optionsList.some(x => x === ("ENABLE." + featureOption)) ? true :
41
+ (this.optionsList.some(x => x === ("DISABLE." + featureOption)) ? false : defaultState
42
+ );
43
+ }
44
+
45
+ // Is a feature option enabled at the device or global level. This function does not traverse the scoping hierarchy.
46
+ isDeviceOptionEnabled(featureOption, mac, defaultState) {
47
+
48
+ if(!mac) {
49
+
50
+ return this.isGlobalOptionEnabled(featureOption, defaultState);
51
+ }
52
+
53
+ featureOption = featureOption.toUpperCase();
54
+ mac = mac.toUpperCase();
55
+
56
+ // Test device-specific options.
57
+ return this.optionsList.some(x => x === ("ENABLE." + featureOption + "." + mac)) ? true :
58
+ (this.optionsList.some(x => x === ("DISABLE." + featureOption + "." + mac)) ? false : defaultState
59
+ );
60
+ }
61
+
62
+ // Is a value-centric feature option enabled at the device or global level. This function does not traverse the scoping hierarchy.
63
+ isOptionValueSet(featureOption, deviceMac) {
64
+
65
+ const optionRegex = new RegExp("^Enable\\." + featureOption + (!deviceMac ? "" : "\\." + deviceMac) + "\\.([^\\.]+)$", "gi");
66
+
67
+
68
+ return this.optionsList.some(x => optionRegex.test(x));
69
+ }
70
+
71
+ // Get the value of a value-centric feature option.
72
+ getOptionValue(featureOption, deviceMac) {
73
+
74
+ const optionRegex = new RegExp("^Enable\\." + featureOption + (!deviceMac ? "" : "\\." + deviceMac) + "\\.([^\\.]+)$", "gi");
75
+
76
+ // Get the option value, if we have one.
77
+ for(const option of this.optionsList) {
78
+
79
+ const regexMatch = optionRegex.exec(option);
80
+
81
+ if(regexMatch) {
82
+
83
+ return regexMatch[1];
84
+ }
85
+ }
86
+
87
+ return undefined;
88
+ }
89
+
90
+ // Is a feature option enabled at the device or global level. It does traverse the scoping hierarchy.
91
+ isOptionEnabled(featureOption, deviceMac) {
92
+
93
+ const defaultState = this.featureOptionList[featureOption]?.default ?? true;
94
+
95
+ if(deviceMac) {
96
+
97
+ // Device level check.
98
+ if(this.isDeviceOptionEnabled(featureOption, deviceMac, defaultState) !== defaultState) {
99
+
100
+ return !defaultState;
101
+ }
102
+
103
+ // Controller level check.
104
+ if(this.isDeviceOptionEnabled(featureOption, this.controller, defaultState) !== defaultState) {
105
+
106
+ return !defaultState;
107
+ }
108
+ }
109
+
110
+ // Global check.
111
+ if(this.isGlobalOptionEnabled(featureOption, defaultState) !== defaultState) {
112
+
113
+ return !defaultState;
114
+ }
115
+
116
+ // Return the default.
117
+ return defaultState;
118
+ };
119
+
120
+ // Return the scope level of a feature option.
121
+ optionScope(featureOption, deviceMac, defaultState, isOptionValue = false) {
122
+
123
+ // Scope priority is always: device, controller, global.
124
+
125
+ // If we have a value-centric feature option, our lookups are a bit different.
126
+ if(isOptionValue) {
127
+
128
+ if(deviceMac) {
129
+
130
+ if(this.isOptionValueSet(featureOption, deviceMac)) {
131
+
132
+ return "device";
133
+ }
134
+
135
+ if(this.isOptionValueSet(featureOption, this.controller)) {
136
+
137
+ return "controller";
138
+ }
139
+ }
140
+
141
+ if(this.isOptionValueSet(featureOption)) {
142
+
143
+ return "global";
144
+ }
145
+
146
+ return "none";
147
+ }
148
+
149
+ if(deviceMac) {
150
+
151
+ // Let's see if we've set it at the device-level.
152
+ if((this.isDeviceOptionEnabled(featureOption, deviceMac, defaultState) !== defaultState) || this.isOptionSet(featureOption, deviceMac)) {
153
+
154
+ return "device";
155
+ }
156
+
157
+ // Now let's test the controller level.
158
+ if((this.isDeviceOptionEnabled(featureOption, this.controller, defaultState) !== defaultState) || this.isOptionSet(featureOption, this.controller)) {
159
+
160
+ return "controller";
161
+ }
162
+ }
163
+
164
+ // Finally, let's test the global level.
165
+ if((this.isGlobalOptionEnabled(featureOption, defaultState) !== defaultState) || this.isOptionSet(featureOption)) {
166
+
167
+ return "global";
168
+ }
169
+
170
+ // Option isn't set to a non-default value.
171
+ return "none";
172
+ };
173
+
174
+ // Return the color hinting for a given option's scope.
175
+ optionScopeColor(featureOption, deviceMac, defaultState, isOptionValue) {
176
+
177
+ switch(this.optionScope(featureOption, deviceMac, defaultState, isOptionValue)) {
178
+
179
+ case "device":
180
+
181
+ return "text-info";
182
+ break;
183
+
184
+ case "controller":
185
+
186
+ return "text-success";
187
+ break;
188
+
189
+ case "global":
190
+
191
+ return deviceMac ? "text-warning" : "text-info";
192
+ break;
193
+
194
+ default:
195
+
196
+ break;
197
+ }
198
+
199
+ return null;
200
+ };
201
+ }
@@ -0,0 +1,182 @@
1
+ /* Copyright(C) 2017-2024, HJD (https://github.com/hjdhjd). All rights reserved.
2
+ *
3
+ * ui.mjs: HBUA webUI.
4
+ */
5
+ "use strict";
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();
9
+
10
+ // Show the first run user experience if we don't have valid login credentials.
11
+ function showFirstRun () {
12
+
13
+ const buttonFirstRun = document.getElementById("firstRun");
14
+ const inputAddress = document.getElementById("address");
15
+ const inputUsername = document.getElementById("username");
16
+ const inputPassword = document.getElementById("password");
17
+ const tdLoginError = document.getElementById("loginError");
18
+
19
+ // If we don't have any controllers configured, initialize the list.
20
+ if(!featureOptions.currentConfig[0].controllers) {
21
+
22
+ featureOptions.currentConfig[0].controllers = [ {} ];
23
+ }
24
+
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 ?? "";
29
+
30
+ // Clear login error messages when the login credentials change.
31
+ inputAddress.addEventListener("input", () => {
32
+
33
+ tdLoginError.innerHTML = "&nbsp;";
34
+ });
35
+
36
+ inputUsername.addEventListener("input", () => {
37
+
38
+ tdLoginError.innerHTML = "&nbsp;";
39
+ });
40
+
41
+ inputPassword.addEventListener("input", () => {
42
+
43
+ tdLoginError.innerHTML = "&nbsp;";
44
+ });
45
+
46
+ // First run user experience.
47
+ buttonFirstRun.addEventListener("click", async () => {
48
+
49
+ // Show the beachball while we setup.
50
+ homebridge.showSpinner();
51
+
52
+ const address = inputAddress.value;
53
+ const username = inputUsername.value;
54
+ const password = inputPassword.value;
55
+
56
+ tdLoginError.innerHTML = "&nbsp;";
57
+
58
+ if(!address?.length || !username?.length || !password?.length) {
59
+
60
+ tdLoginError.appendChild(document.createTextNode("Please enter a valid UniFi Access controller address, username and password."));
61
+ homebridge.hideSpinner();
62
+ return;
63
+ }
64
+
65
+ const udaDevices = await homebridge.request("/getDevices", { address: address, username: username, password: password });
66
+
67
+ // Couldn't connect to the Access controller for some reason.
68
+ if(!udaDevices?.length) {
69
+
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;
73
+ }
74
+
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;
79
+
80
+ await homebridge.updatePluginConfig(featureOptions.currentConfig);
81
+
82
+ // Create our UI.
83
+ document.getElementById("pageFirstRun").style.display = "none";
84
+ document.getElementById("menuWrapper").style.display = "inline-flex";
85
+ featureOptions.showUI();
86
+ });
87
+
88
+ document.getElementById("pageFirstRun").style.display = "block";
89
+ }
90
+
91
+ // Show the main plugin configuration tab.
92
+ function showSettings () {
93
+
94
+ // Show the beachball while we setup.
95
+ homebridge.showSpinner();
96
+
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");
104
+
105
+ document.getElementById("pageSupport").style.display = "none";
106
+ document.getElementById("pageFeatureOptions").style.display = "none";
107
+
108
+ homebridge.showSchemaForm();
109
+
110
+ // All done. Let the user interact with us.
111
+ homebridge.hideSpinner();
112
+ }
113
+
114
+ // Show the support tab.
115
+ function showSupport() {
116
+
117
+ // Show the beachball while we setup.
118
+ homebridge.showSpinner();
119
+ homebridge.hideSchemaForm();
120
+
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");
128
+
129
+ document.getElementById("pageSupport").style.display = "block";
130
+ document.getElementById("pageFeatureOptions").style.display = "none";
131
+
132
+ // All done. Let the user interact with us.
133
+ homebridge.hideSpinner();
134
+ }
135
+
136
+ // Launch our webUI.
137
+ async function launchWebUI() {
138
+
139
+ // Retrieve the current plugin configuration.
140
+ featureOptions.currentConfig = await homebridge.getPluginConfig();
141
+
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());
146
+
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) {
149
+
150
+ document.getElementById("menuWrapper").style.display = "inline-flex";
151
+ featureOptions.showUI();
152
+ return;
153
+ }
154
+
155
+ // If we have no configuration, let's create one.
156
+ if(!featureOptions.currentConfig.length) {
157
+
158
+ featureOptions.currentConfig.push({ controllers: [ {} ], name: "UniFi Access" });
159
+ } else if(!("name" in featureOptions.currentConfig[0])) {
160
+
161
+ // If we haven't set the name, let's do so now.
162
+ featureOptions.currentConfig[0].name = "UniFi Access";
163
+ }
164
+
165
+ // Update the plugin configuration and launch the first run UI.
166
+ await homebridge.updatePluginConfig(featureOptions.currentConfig);
167
+ showFirstRun();
168
+ }
169
+
170
+ // Fire off our UI, catching errors along the way.
171
+ try {
172
+
173
+ launchWebUI();
174
+ } catch(err) {
175
+
176
+ // If we had an error instantiating or updating the UI, notify the user.
177
+ homebridge.toast.error(err.message, "Error");
178
+ } finally {
179
+
180
+ // Always leave the UI in a usable place for the end user.
181
+ homebridge.hideSpinner();
182
+ }
@@ -0,0 +1,153 @@
1
+ /* Copyright(C) 2017-2024, HJD (https://github.com/hjdhjd). All rights reserved.
2
+ *
3
+ * server.js: homebridge-unifi-access webUI server API.
4
+ *
5
+ * This module is heavily inspired by the homebridge-config-ui-x source code and borrows from both.
6
+ * Thank you oznu for your contributions to the HomeKit world.
7
+ */
8
+ "use strict";
9
+
10
+ import { featureOptionCategories, featureOptions, isOptionEnabled } from "../dist/access-options.js";
11
+ import { AccessApi } from "unifi-access";
12
+ import { HomebridgePluginUiServer } from "@homebridge/plugin-ui-utils";
13
+ import util from "node:util";
14
+
15
+ class PluginUiServer extends HomebridgePluginUiServer {
16
+
17
+ errorInfo;
18
+
19
+ constructor () {
20
+ super();
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 most recent error message generated by the Access API.
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
+
58
+ // Return the list of Access devices.
59
+ this.onRequest("/getDevices", async (controller) => {
60
+
61
+ try {
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
+ if(!!parameters?.[Symbol.iterator]) {
70
+
71
+ this.errorInfo = util.format(message, ...parameters);
72
+ } else {
73
+
74
+ this.errorInfo = util.format(message, parameters);
75
+ }
76
+
77
+ console.error(this.errorInfo);
78
+ },
79
+ info: (message, parameters) => {},
80
+ warn: (message, parameters = []) => {}
81
+ };
82
+
83
+ // Connect to the Access controller.
84
+ const udaApi = new AccessApi(log);
85
+
86
+ if(!(await udaApi.login(controller.address, controller.username, controller.password))) {
87
+
88
+ return [];
89
+ }
90
+
91
+ // Bootstrap the controller. It will emit a message once it's received the bootstrap JSON, or you can alternatively wait for the Promise to resolve.
92
+ if(!(await udaApi.getBootstrap())) {
93
+
94
+ return [];
95
+ }
96
+
97
+ const devices = udaApi.devices.filter(x => x.is_adopted && x.is_managed);
98
+
99
+ devices.sort((a, b) => {
100
+
101
+ const aCase = (a.name ?? a.model).toLowerCase();
102
+ const bCase = (b.name ?? b.model).toLowerCase();
103
+
104
+ return aCase > bCase ? 1 : (bCase > aCase ? -1 : 0);
105
+ });
106
+
107
+ return [ udaApi.controller, ...devices ];
108
+ } catch(err) {
109
+
110
+ console.log(err);
111
+
112
+ // Return nothing if we error out for some reason.
113
+ return [];
114
+ }
115
+ });
116
+ }
117
+
118
+ // Register the getOptions() webUI server API endpoint.
119
+ #registerGetOptions() {
120
+
121
+ // Return the list of options configured for a given Access device.
122
+ this.onRequest("/getOptions", async(request) => {
123
+
124
+ try {
125
+
126
+ const optionSet = {};
127
+
128
+ // Loop through all the feature option categories.
129
+ for(const category of featureOptionCategories) {
130
+
131
+ optionSet[category.name] = [];
132
+
133
+ for(const options of featureOptions[category.name]) {
134
+
135
+ options.value = isOptionEnabled(request.configOptions, request.controllerUda, request.deviceUda, category.name + "." + options.name, options.default);
136
+ optionSet[category.name].push(options);
137
+ }
138
+ }
139
+
140
+ return { categories: featureOptionCategories, options: optionSet };
141
+
142
+ } catch(err) {
143
+
144
+ console.log(err);
145
+
146
+ // Return nothing if we error out for some reason.
147
+ return {};
148
+ }
149
+ });
150
+ }
151
+ }
152
+
153
+ (() => new PluginUiServer())();