homebridge-unifi-access 1.9.1 → 1.10.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.
- package/dist/access-controller.d.ts +7 -6
- package/dist/access-controller.js +96 -126
- package/dist/access-controller.js.map +1 -1
- package/dist/access-device.d.ts +15 -1
- package/dist/access-device.js +17 -42
- package/dist/access-device.js.map +1 -1
- package/dist/access-events.js +21 -33
- package/dist/access-events.js.map +1 -1
- package/dist/access-hub.d.ts +22 -6
- package/dist/access-hub.js +333 -62
- package/dist/access-hub.js.map +1 -1
- package/dist/access-options.d.ts +3 -2
- package/dist/access-options.js +19 -3
- package/dist/access-options.js.map +1 -1
- package/dist/access-platform.d.ts +1 -1
- package/dist/access-platform.js +5 -9
- package/dist/access-platform.js.map +1 -1
- package/dist/access-types.d.ts +9 -0
- package/dist/access-types.js +9 -0
- package/dist/access-types.js.map +1 -1
- package/homebridge-ui/public/index.html +54 -51
- package/homebridge-ui/public/lib/featureoptions.js +7 -8
- package/homebridge-ui/public/lib/featureoptions.js.map +1 -1
- package/homebridge-ui/public/lib/webUi-featureoptions.mjs +3484 -558
- package/homebridge-ui/public/lib/webUi.mjs +13 -5
- package/homebridge-ui/public/ui.mjs +76 -105
- package/package.json +11 -11
|
@@ -57,7 +57,7 @@ export class webUi {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
// Show the first run user experience if
|
|
60
|
+
// Show the first run user experience if needed.
|
|
61
61
|
async #showFirstRun() {
|
|
62
62
|
|
|
63
63
|
const buttonFirstRun = document.getElementById("firstRun");
|
|
@@ -68,6 +68,9 @@ export class webUi {
|
|
|
68
68
|
return;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// We disable saving any settings until we configure the plugin.
|
|
72
|
+
homebridge.disableSaveButton();
|
|
73
|
+
|
|
71
74
|
// First run user experience.
|
|
72
75
|
buttonFirstRun.addEventListener("click", async () => {
|
|
73
76
|
|
|
@@ -80,10 +83,13 @@ export class webUi {
|
|
|
80
83
|
return;
|
|
81
84
|
}
|
|
82
85
|
|
|
83
|
-
// Create our UI.
|
|
86
|
+
// Create our UI and allow users to save the configuration.
|
|
84
87
|
document.getElementById("pageFirstRun").style.display = "none";
|
|
85
88
|
document.getElementById("menuWrapper").style.display = "inline-flex";
|
|
86
|
-
|
|
89
|
+
|
|
90
|
+
await this.featureOptions.show();
|
|
91
|
+
|
|
92
|
+
homebridge.enableSaveButton();
|
|
87
93
|
|
|
88
94
|
// All done. Let the user interact with us, although in practice, we shouldn't get here.
|
|
89
95
|
// homebridge.hideSpinner();
|
|
@@ -97,6 +103,7 @@ export class webUi {
|
|
|
97
103
|
|
|
98
104
|
// Show the beachball while we setup.
|
|
99
105
|
homebridge.showSpinner();
|
|
106
|
+
this.featureOptions.hide();
|
|
100
107
|
|
|
101
108
|
// Highlight the tab in our UI.
|
|
102
109
|
this.#toggleClasses("menuHome", "btn-elegant", "btn-primary");
|
|
@@ -118,6 +125,7 @@ export class webUi {
|
|
|
118
125
|
// Show the beachball while we setup.
|
|
119
126
|
homebridge.showSpinner();
|
|
120
127
|
homebridge.hideSchemaForm();
|
|
128
|
+
this.featureOptions.hide();
|
|
121
129
|
|
|
122
130
|
// Highlight the tab in our UI.
|
|
123
131
|
this.#toggleClasses("menuHome", "btn-primary", "btn-elegant");
|
|
@@ -139,14 +147,14 @@ export class webUi {
|
|
|
139
147
|
|
|
140
148
|
// Add our event listeners to animate the UI.
|
|
141
149
|
document.getElementById("menuHome").addEventListener("click", () => this.#showSupport());
|
|
142
|
-
document.getElementById("menuFeatureOptions").addEventListener("click", () => this.featureOptions.show());
|
|
150
|
+
document.getElementById("menuFeatureOptions").addEventListener("click", async () => await this.featureOptions.show());
|
|
143
151
|
document.getElementById("menuSettings").addEventListener("click", () => this.#showSettings());
|
|
144
152
|
|
|
145
153
|
// If we've got devices detected, we launch our feature option UI. Otherwise, we launch our first run UI.
|
|
146
154
|
if(this.featureOptions.currentConfig.length && !(await this.#processHandler(this.#firstRun.isRequired))) {
|
|
147
155
|
|
|
148
156
|
document.getElementById("menuWrapper").style.display = "inline-flex";
|
|
149
|
-
this.featureOptions.show();
|
|
157
|
+
await this.featureOptions.show();
|
|
150
158
|
|
|
151
159
|
return;
|
|
152
160
|
}
|
|
@@ -75,17 +75,42 @@ const firstRunOnSubmit = async () => {
|
|
|
75
75
|
return true;
|
|
76
76
|
};
|
|
77
77
|
|
|
78
|
+
// Return whether a given device is a controller.
|
|
79
|
+
const isController = (device) => device.display_model === "controller";
|
|
80
|
+
|
|
81
|
+
// Return the list of controllers from our plugin configuration.
|
|
82
|
+
const getControllers = () => {
|
|
83
|
+
|
|
84
|
+
const controllers = [];
|
|
85
|
+
|
|
86
|
+
// Grab the controllers from our configuration.
|
|
87
|
+
for(const controller of ui.featureOptions.currentConfig[0].controllers ?? []) {
|
|
88
|
+
|
|
89
|
+
controllers.push({ name: controller.address, serialNumber: controller.address });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return controllers;
|
|
93
|
+
};
|
|
94
|
+
|
|
78
95
|
// Return the list of devices associated with a given Access controller.
|
|
79
|
-
const getDevices = async (
|
|
96
|
+
const getDevices = async (selectedController) => {
|
|
80
97
|
|
|
81
98
|
// If we're in the global context, we have no devices.
|
|
99
|
+
if(!selectedController) {
|
|
100
|
+
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Find the entry in our plugin configuration.
|
|
105
|
+
const controller = (ui.featureOptions.currentConfig[0].controllers ?? []).find(c => c.address === selectedController.serialNumber);
|
|
106
|
+
|
|
82
107
|
if(!controller) {
|
|
83
108
|
|
|
84
109
|
return [];
|
|
85
110
|
}
|
|
86
111
|
|
|
87
|
-
// Retrieve the current list of devices from the
|
|
88
|
-
|
|
112
|
+
// Retrieve the current list of devices from the Protect controller.
|
|
113
|
+
const devices = await homebridge.request("/getDevices", { address: controller.address, password: controller.password, username: controller.username });
|
|
89
114
|
|
|
90
115
|
// 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
116
|
if(devices?.length) {
|
|
@@ -100,103 +125,41 @@ const getDevices = async (controller) => {
|
|
|
100
125
|
/* eslint-enable camelcase */
|
|
101
126
|
}
|
|
102
127
|
|
|
103
|
-
// Add the fields that the webUI framework is looking for to render.
|
|
104
|
-
devices = devices.map(device => ({
|
|
105
|
-
|
|
106
|
-
...device,
|
|
107
|
-
serialNumber: device.mac.replace(/:/g, "").toUpperCase() + ((device.device_type === "UAH-Ent") ? "-" + device.source_id.toUpperCase() : "")
|
|
108
|
-
}));
|
|
109
|
-
|
|
110
|
-
return devices;
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
// Return whether a given device is a controller.
|
|
114
|
-
const isController = (device) => device.display_model === "controller";
|
|
115
|
-
|
|
116
|
-
// Show the list of Access devices associated with a controller, grouped by model.
|
|
117
|
-
const showSidebarDevices = (controller, devices) => {
|
|
118
|
-
|
|
119
128
|
// Workaround for the time being to reduce the number of models we see to just the currently supported ones.
|
|
120
|
-
const modelKeys = [...new Set(
|
|
121
|
-
.
|
|
122
|
-
|
|
123
|
-
// Start with a clean slate.
|
|
124
|
-
ui.featureOptions.devicesTable.innerHTML = "";
|
|
125
|
-
|
|
126
|
-
for(const key of modelKeys) {
|
|
129
|
+
const modelKeys = [...new Set(
|
|
130
|
+
devices.filter(device => ["controller"].includes(device.display_model) || device.capabilities.includes("is_hub") || device.capabilities.includes("is_reader"))
|
|
131
|
+
.map(device => (device.device_type === "UAH-Ent") ? device.model : device.display_model))];
|
|
127
132
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
// Nothing in this category, let's keep going.
|
|
132
|
-
if(!modelDevices.length) {
|
|
133
|
+
// Add the fields that the webUI framework is looking for to render.
|
|
134
|
+
for(const device of devices) {
|
|
133
135
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
+
device.name ??= device.alias ?? device.display_model;
|
|
137
|
+
device.serialNumber = device.mac.replace(/:/g, "").toUpperCase() + ((device.device_type === "UAH-Ent") ? "-" + device.source_id.toUpperCase() : "");
|
|
136
138
|
|
|
137
|
-
|
|
138
|
-
if(key === "controller") {
|
|
139
|
+
const model = (device.device_type === "UAH-Ent") ? device.model : device.display_model;
|
|
139
140
|
|
|
140
|
-
|
|
141
|
-
ui.featureOptions.webUiControllerList.map(x => (x.name === controller.address) ? x.childNodes[0].nodeValue = modelDevices[0].host.hostname : true);
|
|
141
|
+
if(!modelKeys.includes(model)) {
|
|
142
142
|
|
|
143
|
-
|
|
143
|
+
device.sidebarGroup = "hidden";
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
|
|
147
|
-
const trCategory = document.createElement("tr");
|
|
148
|
-
|
|
149
|
-
// Disable any pointer events and hover activity.
|
|
150
|
-
trCategory.style.pointerEvents = "none";
|
|
151
|
-
|
|
152
|
-
// Create the cell for our device category row.
|
|
153
|
-
const tdCategory = document.createElement("td");
|
|
154
|
-
|
|
155
|
-
tdCategory.classList.add("m-0", "p-0", "pl-1", "w-100");
|
|
156
|
-
|
|
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";
|
|
160
|
-
|
|
161
|
-
// Add the cell to the table row.
|
|
162
|
-
trCategory.appendChild(tdCategory);
|
|
163
|
-
|
|
164
|
-
// Add the table row to the table.
|
|
165
|
-
ui.featureOptions.devicesTable.appendChild(trCategory);
|
|
146
|
+
device.sidebarGroup ??= device.capabilities.includes("is_hub") ? "Hubs" : "Readers";
|
|
166
147
|
|
|
167
|
-
|
|
148
|
+
// We update the name of the controller that we show users once we've connected with the controller and have it's name.
|
|
149
|
+
if(isController(device)) {
|
|
168
150
|
|
|
169
|
-
|
|
170
|
-
const trDevice = document.createElement("tr");
|
|
151
|
+
device.sidebarGroup = "controllers";
|
|
171
152
|
|
|
172
|
-
|
|
153
|
+
const activeController = [...document.querySelectorAll("[data-navigation='controller']")].find(c => c.getAttribute("data-device-serial") === controller.address);
|
|
173
154
|
|
|
174
|
-
|
|
175
|
-
const tdDevice = document.createElement("td");
|
|
155
|
+
if(activeController) {
|
|
176
156
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const label = document.createElement("label");
|
|
180
|
-
|
|
181
|
-
label.name = device.serialNumber;
|
|
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");
|
|
185
|
-
|
|
186
|
-
label.addEventListener("click", () => ui.featureOptions.showDeviceOptions(device.serialNumber));
|
|
187
|
-
|
|
188
|
-
// Add the device label to our cell.
|
|
189
|
-
tdDevice.appendChild(label);
|
|
190
|
-
|
|
191
|
-
// Add the cell to the table row.
|
|
192
|
-
trDevice.appendChild(tdDevice);
|
|
193
|
-
|
|
194
|
-
// Add the table row to the table.
|
|
195
|
-
ui.featureOptions.devicesTable.appendChild(trDevice);
|
|
196
|
-
|
|
197
|
-
ui.featureOptions.webUiDeviceList.push(label);
|
|
157
|
+
activeController.textContent = device.host.hostname;
|
|
158
|
+
}
|
|
198
159
|
}
|
|
199
160
|
}
|
|
161
|
+
|
|
162
|
+
return devices;
|
|
200
163
|
};
|
|
201
164
|
|
|
202
165
|
// Only show feature options that are valid for the capabilities of this device.
|
|
@@ -216,7 +179,9 @@ const validOption = (device, option) => {
|
|
|
216
179
|
// Only show feature option categories that are valid for a particular device type.
|
|
217
180
|
const validOptionCategory = (device, category) => {
|
|
218
181
|
|
|
219
|
-
if(device && (device.display_model !== "controller") &&
|
|
182
|
+
if(device && (device.display_model !== "controller") && (
|
|
183
|
+
!category.modelKey.some(model => [ "all", device.display_model ].includes(model)) ||
|
|
184
|
+
(category.hasCapability && (!device.capabilities || !category.hasCapability.some(x => device.capabilities.includes(x)))))) {
|
|
220
185
|
|
|
221
186
|
return false;
|
|
222
187
|
}
|
|
@@ -227,41 +192,47 @@ const validOptionCategory = (device, category) => {
|
|
|
227
192
|
// Show the details for this device.
|
|
228
193
|
const showAccessDetails = (device) => {
|
|
229
194
|
|
|
195
|
+
const deviceStatsContainer = document.getElementById("deviceStatsContainer");
|
|
196
|
+
|
|
230
197
|
// No device specified, we must be in a global context.
|
|
231
198
|
if(!device) {
|
|
232
199
|
|
|
233
|
-
|
|
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";
|
|
200
|
+
deviceStatsContainer.innerHTML = "";
|
|
240
201
|
|
|
241
202
|
return;
|
|
242
203
|
}
|
|
243
204
|
|
|
244
|
-
// Populate the device details.
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
205
|
+
// Populate the device details using the new CSS Grid layout. This provides a more flexible and responsive display than the previous table layout.
|
|
206
|
+
deviceStatsContainer.innerHTML =
|
|
207
|
+
"<div class=\"device-stats-grid\">" +
|
|
208
|
+
"<div class=\"stat-item\">" +
|
|
209
|
+
"<span class=\"stat-label\">Model</span>" +
|
|
210
|
+
"<span class=\"stat-value\">" + (device.model ?? device.display_model) + "</span>" +
|
|
211
|
+
"</div>" +
|
|
212
|
+
"<div class=\"stat-item\">" +
|
|
213
|
+
"<span class=\"stat-label\">MAC Address</span>" +
|
|
214
|
+
"<span class=\"stat-value font-monospace\">" + device.serialNumber + "</span>" +
|
|
215
|
+
"</div>" +
|
|
216
|
+
"<div class=\"stat-item\">" +
|
|
217
|
+
"<span class=\"stat-label\">IP Address</span>" +
|
|
218
|
+
"<span class=\"stat-value font-monospace\">" + device.ip + "</span>" +
|
|
219
|
+
"</div>" +
|
|
220
|
+
"<div class=\"stat-item\">" +
|
|
221
|
+
"<span class=\"stat-label\">Status</span>" +
|
|
222
|
+
"<span class=\"stat-value\">" + (device.is_online ? "Connected" : "Disconnected") + "</span>" +
|
|
223
|
+
"</div>" +
|
|
224
|
+
"</div>";
|
|
252
225
|
};
|
|
253
226
|
|
|
254
227
|
// Parameters for our feature options webUI.
|
|
255
228
|
const featureOptionsParams = {
|
|
256
229
|
|
|
230
|
+
getControllers: getControllers,
|
|
257
231
|
getDevices: getDevices,
|
|
258
|
-
hasControllers: true,
|
|
259
232
|
infoPanel: showAccessDetails,
|
|
260
233
|
sidebar: {
|
|
261
234
|
|
|
262
|
-
controllerLabel: "Access Controllers"
|
|
263
|
-
deviceLabel: "Access Devices",
|
|
264
|
-
showDevices: showSidebarDevices
|
|
235
|
+
controllerLabel: "Access Controllers"
|
|
265
236
|
},
|
|
266
237
|
ui: {
|
|
267
238
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-unifi-access",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"displayName": "
|
|
3
|
+
"version": "1.10.0",
|
|
4
|
+
"displayName": "UniFi Access",
|
|
5
5
|
"description": "Homebridge UniFi Access plugin providing complete HomeKit integration for the UniFi Access ecosystem with full support for most features including autoconfiguration, motion detection, multiple controllers, and realtime updates.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "HJD",
|
|
@@ -55,17 +55,17 @@
|
|
|
55
55
|
},
|
|
56
56
|
"main": "dist/index.js",
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@homebridge/plugin-ui-utils": "2.0
|
|
59
|
-
"homebridge-plugin-utils": "1.
|
|
60
|
-
"unifi-access": "
|
|
58
|
+
"@homebridge/plugin-ui-utils": "2.1.0",
|
|
59
|
+
"homebridge-plugin-utils": "1.29.3",
|
|
60
|
+
"unifi-access": "1.5.0"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
-
"@stylistic/eslint-plugin": "
|
|
64
|
-
"@types/node": "
|
|
65
|
-
"eslint": "9.
|
|
66
|
-
"homebridge": "1.
|
|
63
|
+
"@stylistic/eslint-plugin": "5.5.0",
|
|
64
|
+
"@types/node": "24.10.0",
|
|
65
|
+
"eslint": "9.39.1",
|
|
66
|
+
"homebridge": "1.11.1",
|
|
67
67
|
"shx": "^0.4.0",
|
|
68
|
-
"typescript": "5.
|
|
69
|
-
"typescript-eslint": "^8.
|
|
68
|
+
"typescript": "5.9.3",
|
|
69
|
+
"typescript-eslint": "^8.46.3"
|
|
70
70
|
}
|
|
71
71
|
}
|