homebridge-unifi-access 1.9.2 → 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.
@@ -57,7 +57,7 @@ export class webUi {
57
57
  }
58
58
  }
59
59
 
60
- // Show the first run user experience if we don't have valid login credentials.
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
- this.featureOptions.show();
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 (controller) => {
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 Access controller.
88
- let devices = await homebridge.request("/getDevices", { address: controller.address, password: controller.password, username: controller.username });
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(devices.filter(device => ["controller"].includes(device.display_model) || device.capabilities.includes("is_hub"))
121
- .map(device => (device.device_type === "UAH-Ent") ? device.model : device.display_model))];
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
- // Get all the devices associated with this device category.
129
- const modelDevices = devices.filter(x => ((x.device_type === "UAH-Ent") ? x.model : x.display_model) === key);
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
- continue;
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
- // If it's a controller, we handle that case differently.
138
- if(key === "controller") {
139
+ const model = (device.device_type === "UAH-Ent") ? device.model : device.display_model;
139
140
 
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);
141
+ if(!modelKeys.includes(model)) {
142
142
 
143
- continue;
143
+ device.sidebarGroup = "hidden";
144
144
  }
145
145
 
146
- // Create a row for this device category.
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
- for(const device of modelDevices) {
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
- // Create a row for this device.
170
- const trDevice = document.createElement("tr");
151
+ device.sidebarGroup = "controllers";
171
152
 
172
- trDevice.classList.add("m-0", "p-0");
153
+ const activeController = [...document.querySelectorAll("[data-navigation='controller']")].find(c => c.getAttribute("data-device-serial") === controller.address);
173
154
 
174
- // Create a cell for our device.
175
- const tdDevice = document.createElement("td");
155
+ if(activeController) {
176
156
 
177
- tdDevice.classList.add("m-0", "p-0" , "w-100");
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") && !category.modelKey.some(model => ["all", device.display_model].includes(model))) {
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
- 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";
200
+ deviceStatsContainer.innerHTML = "";
240
201
 
241
202
  return;
242
203
  }
243
204
 
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";
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.9.2",
4
- "displayName": "Homebridge UniFi Access",
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.2",
59
- "homebridge-plugin-utils": "1.18.0",
60
- "unifi-access": "^1.3.0"
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": "4.4.0",
64
- "@types/node": "22.15.23",
65
- "eslint": "9.27.0",
66
- "homebridge": "1.9.0",
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.8.3",
69
- "typescript-eslint": "^8.33.0"
68
+ "typescript": "5.9.3",
69
+ "typescript-eslint": "^8.46.3"
70
70
  }
71
71
  }