homebridge-plugin-utils 1.1.0 → 1.2.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/build/eslint-rules.mjs +158 -12
- package/dist/featureoptions.d.ts +24 -11
- package/dist/featureoptions.js +32 -22
- package/dist/featureoptions.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mqttclient.js.map +1 -1
- package/dist/rtp.d.ts +25 -0
- package/dist/rtp.js +178 -0
- package/dist/rtp.js.map +1 -0
- package/dist/ui/featureoptions.js +32 -22
- package/dist/ui/featureoptions.js.map +1 -1
- package/dist/ui/{webui-featureoptions.mjs → webUi-featureoptions.mjs} +353 -117
- package/dist/ui/webUi.mjs +47 -33
- package/dist/utils.d.ts +22 -0
- package/dist/utils.js.map +1 -1
- package/package.json +4 -4
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* Copyright(C) 2017-2024, HJD (https://github.com/hjdhjd). All rights reserved.
|
|
2
2
|
*
|
|
3
|
-
*
|
|
3
|
+
* webUi-featureoptions.mjs: Device feature option webUI.
|
|
4
4
|
*/
|
|
5
5
|
"use strict";
|
|
6
6
|
|
|
@@ -8,58 +8,116 @@ import { FeatureOptions} from "./featureoptions.js";
|
|
|
8
8
|
|
|
9
9
|
export class webUiFeatureOptions {
|
|
10
10
|
|
|
11
|
-
// The current plugin configuration.
|
|
12
|
-
currentConfig;
|
|
13
|
-
|
|
14
11
|
// Table containing the currently displayed feature options.
|
|
15
12
|
#configTable;
|
|
16
13
|
|
|
17
14
|
// The current controller context.
|
|
18
15
|
#controller;
|
|
19
16
|
|
|
17
|
+
// The current plugin configuration.
|
|
18
|
+
currentConfig;
|
|
19
|
+
|
|
20
|
+
// Table containing the details on the currently selected device.
|
|
21
|
+
deviceStatsTable;
|
|
22
|
+
|
|
20
23
|
// Current list of devices from the Homebridge accessory cache.
|
|
21
24
|
#devices;
|
|
22
25
|
|
|
26
|
+
// Table containing the list of devices.
|
|
27
|
+
devicesTable;
|
|
28
|
+
|
|
23
29
|
// Feature options instance.
|
|
24
30
|
#featureOptions;
|
|
25
31
|
|
|
26
|
-
//
|
|
27
|
-
#
|
|
32
|
+
// Get devices handler.
|
|
33
|
+
#getDevices;
|
|
28
34
|
|
|
29
35
|
// Enable the use of controllers.
|
|
30
|
-
#
|
|
36
|
+
#hasControllers;
|
|
31
37
|
|
|
32
|
-
//
|
|
33
|
-
#
|
|
38
|
+
// Device information panel handler.
|
|
39
|
+
#infoPanel;
|
|
40
|
+
|
|
41
|
+
// Sidebar configuration parameters.
|
|
42
|
+
#sidebar;
|
|
43
|
+
|
|
44
|
+
// Options UI configuration parameters.
|
|
45
|
+
#ui;
|
|
46
|
+
|
|
47
|
+
// Current list of controllers, for webUI elements.
|
|
48
|
+
webUiControllerList;
|
|
34
49
|
|
|
35
|
-
|
|
50
|
+
// Current list of devices on a given controller, for webUI elements.
|
|
51
|
+
webUiDeviceList;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Display the feature option webUI. All webUI configuration settings are optional.
|
|
55
|
+
*
|
|
56
|
+
* getDevices - return an array of displays to be displayed.
|
|
57
|
+
* hasControllers - true (default) if the plugin hierarchically has controllers and then devices managed by each controller, rather than just devices.
|
|
58
|
+
* infoPanel - handler to display information in the device detail information panel.
|
|
59
|
+
* ui - customize which options are displayed in the feature option webUI:
|
|
60
|
+
* isController - validate whether a given device is a controller. Returns true or false.
|
|
61
|
+
* validOption - validate whether an option is valid on a given device (or controller).
|
|
62
|
+
* validCategory - validate whether a category of options is valid for a given device (or controller).
|
|
63
|
+
* sidebar - customize the sidebar for the feature option webUI:
|
|
64
|
+
* controllerLabel - label to use for the controllers category. Defaults to "Controllers".
|
|
65
|
+
* deviceLabel - label to use for the devices category. Defaults to "Devices".
|
|
66
|
+
* showDevices - handler for enumerating devices in the sidebar.
|
|
67
|
+
*/
|
|
68
|
+
constructor(options = {}) {
|
|
69
|
+
|
|
70
|
+
// Defaults for the feature option webUI sidebar.
|
|
71
|
+
this.ui = {
|
|
72
|
+
|
|
73
|
+
isController: () => false,
|
|
74
|
+
validOption: () => true,
|
|
75
|
+
validOptionCategory: () => true
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Defaults for the feature option webUI sidebar.
|
|
79
|
+
this.sidebar = {
|
|
80
|
+
|
|
81
|
+
controllerLabel: "Controllers",
|
|
82
|
+
deviceLabel: "Devices",
|
|
83
|
+
showDevices: this.#showSidebarDevices.bind(this)
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Defaults for the feature option webUI.
|
|
87
|
+
const {
|
|
88
|
+
|
|
89
|
+
getDevices = this.#getHomebridgeDevices,
|
|
90
|
+
hasControllers = true,
|
|
91
|
+
infoPanel = this.#showDeviceInfoPanel,
|
|
92
|
+
sidebar = {},
|
|
93
|
+
ui = {}
|
|
94
|
+
} = options;
|
|
36
95
|
|
|
37
96
|
this.configTable = document.getElementById("configTable");
|
|
38
97
|
this.controller = null;
|
|
39
98
|
this.currentConfig = [];
|
|
99
|
+
this.deviceStatsTable = document.getElementById("deviceStatsTable");
|
|
40
100
|
this.devices = [];
|
|
101
|
+
this.devicesTable = document.getElementById("devicesTable");
|
|
41
102
|
this.featureOptions = null;
|
|
42
|
-
this.
|
|
43
|
-
this.
|
|
44
|
-
this.
|
|
103
|
+
this.getDevices = getDevices;
|
|
104
|
+
this.hasControllers = hasControllers;
|
|
105
|
+
this.infoPanel = infoPanel;
|
|
106
|
+
this.sidebar = Object.assign({}, this.sidebar, sidebar);
|
|
107
|
+
this.ui = Object.assign({}, this.ui, ui);
|
|
108
|
+
this.webUiControllerList = [];
|
|
109
|
+
this.webUiDeviceList = [];
|
|
45
110
|
}
|
|
46
111
|
|
|
47
|
-
|
|
48
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Render the feature options webUI.
|
|
114
|
+
*/
|
|
115
|
+
async show() {
|
|
49
116
|
|
|
50
117
|
// Show the beachball while we setup.
|
|
51
118
|
homebridge.showSpinner();
|
|
52
119
|
homebridge.hideSchemaForm();
|
|
53
120
|
|
|
54
|
-
// Make sure we have the refreshed configuration.
|
|
55
|
-
this.currentConfig = await homebridge.getPluginConfig();
|
|
56
|
-
|
|
57
|
-
// Retrieve the set of feature options available to us.
|
|
58
|
-
const features = (await homebridge.request("/getOptions")) ?? [];
|
|
59
|
-
|
|
60
|
-
// Initialize our feature option configuration.
|
|
61
|
-
this.featureOptions = new FeatureOptions(features.categories, features.options, this.currentConfig[0].options ?? []);
|
|
62
|
-
|
|
63
121
|
// Create our custom UI.
|
|
64
122
|
document.getElementById("menuHome").classList.remove("btn-elegant");
|
|
65
123
|
document.getElementById("menuHome").classList.add("btn-primary");
|
|
@@ -72,27 +130,58 @@ export class webUiFeatureOptions {
|
|
|
72
130
|
document.getElementById("pageSupport").style.display = "none";
|
|
73
131
|
document.getElementById("pageFeatureOptions").style.display = "block";
|
|
74
132
|
|
|
75
|
-
//
|
|
76
|
-
|
|
133
|
+
// Make sure we have the refreshed configuration.
|
|
134
|
+
this.currentConfig = await homebridge.getPluginConfig();
|
|
135
|
+
|
|
136
|
+
// Retrieve the set of feature options available to us.
|
|
137
|
+
const features = (await homebridge.request("/getOptions")) ?? [];
|
|
138
|
+
|
|
139
|
+
// Initialize our feature option configuration.
|
|
140
|
+
this.featureOptions = new FeatureOptions(features.categories, features.options, this.currentConfig[0].options ?? []);
|
|
141
|
+
|
|
142
|
+
// We render our global options, followed by either a list of controllers (if so configured) or by a list of devices from the Homebridge accessory cache.
|
|
77
143
|
|
|
78
144
|
// Retrieve the table for the our list of controllers and global options.
|
|
79
145
|
const controllersTable = document.getElementById("controllersTable");
|
|
80
146
|
|
|
81
147
|
// Start with a clean slate.
|
|
82
148
|
controllersTable.innerHTML = "";
|
|
83
|
-
|
|
149
|
+
this.devicesTable.innerHTML = "";
|
|
84
150
|
this.configTable.innerHTML = "";
|
|
85
|
-
this.
|
|
151
|
+
this.webUiDeviceList = [];
|
|
152
|
+
|
|
153
|
+
// Create our hover style for our sidebar.
|
|
154
|
+
const sidebarHoverStyle = document.createElement("style");
|
|
155
|
+
|
|
156
|
+
// We emulate the styles that Bootstrap uses when hovering over a table, accounting for both light and dark modes.
|
|
157
|
+
sidebarHoverStyle.innerHTML = "@media (prefers-color-scheme: dark) { .hbup-hover td:hover { background-color: #212121; color: #FFA000 } }" +
|
|
158
|
+
"@media (prefers-color-scheme: light) { .hbup-hover td:hover { background-color: #ECECEC; } }";
|
|
159
|
+
|
|
160
|
+
document.head.appendChild(sidebarHoverStyle);
|
|
161
|
+
|
|
162
|
+
// Add our hover styles to the controllers and devices tables.
|
|
163
|
+
controllersTable.classList.add("hbup-hover");
|
|
164
|
+
this.devicesTable.classList.add("hbup-hover");
|
|
86
165
|
|
|
87
166
|
// Hide the UI until we're ready.
|
|
88
167
|
document.getElementById("sidebar").style.display = "none";
|
|
89
168
|
document.getElementById("headerInfo").style.display = "none";
|
|
90
169
|
document.getElementById("deviceStatsTable").style.display = "none";
|
|
91
170
|
|
|
171
|
+
// If we haven't configured any controllers, we're done.
|
|
172
|
+
if(this.hasControllers && !this.currentConfig[0]?.controllers?.length) {
|
|
173
|
+
|
|
174
|
+
document.getElementById("headerInfo").innerHTML = "Please configure a controller to access in the main settings tab before configuring feature options.";
|
|
175
|
+
document.getElementById("headerInfo").style.display = "";
|
|
176
|
+
homebridge.hideSpinner();
|
|
177
|
+
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
92
181
|
// Initialize our informational header.
|
|
93
182
|
document.getElementById("headerInfo").innerHTML = "Feature options are applied in prioritized order, from global to device-specific options:" +
|
|
94
183
|
"<br><i class=\"text-warning\">Global options</i> (lowest priority) → " +
|
|
95
|
-
(this.
|
|
184
|
+
(this.hasControllers ? "<i class=\"text-success\">Controller options</i> → " : "") +
|
|
96
185
|
"<i class=\"text-info\">Device options</i> (highest priority)";
|
|
97
186
|
|
|
98
187
|
// Enumerate our global options.
|
|
@@ -100,7 +189,8 @@ export class webUiFeatureOptions {
|
|
|
100
189
|
|
|
101
190
|
// Create the cell for our global options.
|
|
102
191
|
const tdGlobal = document.createElement("td");
|
|
103
|
-
|
|
192
|
+
|
|
193
|
+
tdGlobal.classList.add("m-0", "p-0", "w-100");
|
|
104
194
|
|
|
105
195
|
// Create our label target.
|
|
106
196
|
const globalLabel = document.createElement("label");
|
|
@@ -108,9 +198,9 @@ export class webUiFeatureOptions {
|
|
|
108
198
|
globalLabel.name = "Global Options";
|
|
109
199
|
globalLabel.appendChild(document.createTextNode("Global Options"));
|
|
110
200
|
globalLabel.style.cursor = "pointer";
|
|
111
|
-
globalLabel.classList.add("
|
|
201
|
+
globalLabel.classList.add("m-0", "p-0", "pl-1", "w-100");
|
|
112
202
|
|
|
113
|
-
globalLabel.addEventListener("click", () => this.#
|
|
203
|
+
globalLabel.addEventListener("click", () => this.#showSidebar(null));
|
|
114
204
|
|
|
115
205
|
// Add the global options label.
|
|
116
206
|
tdGlobal.appendChild(globalLabel);
|
|
@@ -122,136 +212,152 @@ export class webUiFeatureOptions {
|
|
|
122
212
|
// Now add it to the overall controllers table.
|
|
123
213
|
controllersTable.appendChild(trGlobal);
|
|
124
214
|
|
|
125
|
-
// Add it as another device, for UI purposes.
|
|
126
|
-
this.
|
|
127
|
-
|
|
128
|
-
// All done. Let the user interact with us.
|
|
129
|
-
homebridge.hideSpinner();
|
|
130
|
-
|
|
131
|
-
// Default the user on our global settings.
|
|
132
|
-
this.#showDevices(true);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Show the device list.
|
|
136
|
-
async #showDevices(isGlobal) {
|
|
215
|
+
// Add it as another controller of device, for UI purposes.
|
|
216
|
+
(this.hasControllers ? this.webUiControllerList : this.webUiDeviceList).push(globalLabel);
|
|
137
217
|
|
|
138
|
-
|
|
139
|
-
homebridge.showSpinner();
|
|
140
|
-
|
|
141
|
-
const devicesTable = document.getElementById("devicesTable");
|
|
142
|
-
this.devices = [];
|
|
143
|
-
|
|
144
|
-
// If we're not accessing global options, pull the list of devices this plugin knows about from Homebridge.
|
|
145
|
-
this.devices = (await homebridge.getCachedAccessories()).map(x => ({
|
|
146
|
-
firmwareVersion: (x.services.find(service => service.constructorName ===
|
|
147
|
-
"AccessoryInformation")?.characteristics.find(characteristic => characteristic.constructorName === "FirmwareRevision")?.value ?? ""),
|
|
148
|
-
name: x.displayName,
|
|
149
|
-
serial: (x.services.find(service => service.constructorName ===
|
|
150
|
-
"AccessoryInformation")?.characteristics.find(characteristic => characteristic.constructorName === "SerialNumber")?.value ?? "")
|
|
151
|
-
}));
|
|
218
|
+
if(this.hasControllers) {
|
|
152
219
|
|
|
153
|
-
|
|
154
|
-
|
|
220
|
+
// Create a row for our controllers.
|
|
221
|
+
const trController = document.createElement("tr");
|
|
155
222
|
|
|
156
|
-
|
|
157
|
-
|
|
223
|
+
// Disable any pointer events and hover activity.
|
|
224
|
+
trController.style.pointerEvents = "none";
|
|
158
225
|
|
|
159
|
-
|
|
160
|
-
|
|
226
|
+
// Create the cell for our controller category row.
|
|
227
|
+
const tdController = document.createElement("td");
|
|
161
228
|
|
|
162
|
-
|
|
163
|
-
document.getElementById("sidebar").style.display = "";
|
|
164
|
-
document.getElementById("headerInfo").style.display = "";
|
|
165
|
-
|
|
166
|
-
// Wipe out the device list, except for our global entry.
|
|
167
|
-
this.webuiDeviceList.splice(1, this.webuiDeviceList.length);
|
|
168
|
-
|
|
169
|
-
// Start with a clean slate.
|
|
170
|
-
devicesTable.innerHTML = "";
|
|
171
|
-
|
|
172
|
-
// Show the devices list only if we have actual devices to show.
|
|
173
|
-
if(this.devices?.length) {
|
|
174
|
-
|
|
175
|
-
// Create a row for this device category.
|
|
176
|
-
const trCategory = document.createElement("tr");
|
|
177
|
-
|
|
178
|
-
// Create the cell for our device category row.
|
|
179
|
-
const tdCategory = document.createElement("td");
|
|
180
|
-
tdCategory.classList.add("m-0", "p-0");
|
|
229
|
+
tdController.classList.add("m-0", "p-0", "pl-1", "w-100");
|
|
181
230
|
|
|
182
231
|
// Add the category name, with appropriate casing.
|
|
183
|
-
|
|
184
|
-
|
|
232
|
+
tdController.appendChild(document.createTextNode(this.sidebar.controllerLabel));
|
|
233
|
+
tdController.style.fontWeight = "bold";
|
|
185
234
|
|
|
186
235
|
// Add the cell to the table row.
|
|
187
|
-
|
|
236
|
+
trController.appendChild(tdController);
|
|
188
237
|
|
|
189
238
|
// Add the table row to the table.
|
|
190
|
-
|
|
239
|
+
controllersTable.appendChild(trController);
|
|
191
240
|
|
|
192
|
-
for(const
|
|
241
|
+
for(const controller of this.currentConfig[0].controllers) {
|
|
193
242
|
|
|
194
|
-
// Create a row for this
|
|
243
|
+
// Create a row for this controller.
|
|
195
244
|
const trDevice = document.createElement("tr");
|
|
245
|
+
|
|
196
246
|
trDevice.classList.add("m-0", "p-0");
|
|
197
247
|
|
|
198
|
-
// Create a cell for our
|
|
248
|
+
// Create a cell for our controller.
|
|
199
249
|
const tdDevice = document.createElement("td");
|
|
250
|
+
|
|
200
251
|
tdDevice.classList.add("m-0", "p-0", "w-100");
|
|
201
252
|
|
|
202
253
|
const label = document.createElement("label");
|
|
203
254
|
|
|
204
|
-
label.name =
|
|
205
|
-
label.appendChild(document.createTextNode(
|
|
255
|
+
label.name = controller.address;
|
|
256
|
+
label.appendChild(document.createTextNode(controller.address));
|
|
206
257
|
label.style.cursor = "pointer";
|
|
207
258
|
label.classList.add("mx-2", "my-0", "p-0", "w-100");
|
|
208
259
|
|
|
209
|
-
label.addEventListener("click", () => this.#
|
|
260
|
+
label.addEventListener("click", () => this.#showSidebar(controller));
|
|
210
261
|
|
|
211
|
-
// Add the
|
|
262
|
+
// Add the controller label to our cell.
|
|
212
263
|
tdDevice.appendChild(label);
|
|
213
264
|
|
|
214
265
|
// Add the cell to the table row.
|
|
215
266
|
trDevice.appendChild(tdDevice);
|
|
216
267
|
|
|
217
268
|
// Add the table row to the table.
|
|
218
|
-
|
|
269
|
+
controllersTable.appendChild(trDevice);
|
|
219
270
|
|
|
220
|
-
this.
|
|
271
|
+
this.webUiControllerList.push(label);
|
|
221
272
|
}
|
|
222
273
|
}
|
|
223
274
|
|
|
275
|
+
// All done. Let the user interact with us.
|
|
276
|
+
homebridge.hideSpinner();
|
|
277
|
+
|
|
278
|
+
// Default the user on our global settings if we have no controller.
|
|
279
|
+
this.#showSidebar(this.hasControllers ? this.currentConfig[0].controllers[0] : null);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Show the device list taking the controller context into account.
|
|
283
|
+
async #showSidebar(controller) {
|
|
284
|
+
|
|
285
|
+
// Show the beachball while we setup.
|
|
286
|
+
homebridge.showSpinner();
|
|
287
|
+
|
|
288
|
+
// Grab the list of devices we're displaying.
|
|
289
|
+
this.devices = await this.getDevices(controller);
|
|
290
|
+
|
|
291
|
+
if(this.hasControllers) {
|
|
292
|
+
|
|
293
|
+
// Make sure we highlight the selected controller so the user knows where we are.
|
|
294
|
+
this.webUiControllerList.map(webUiEntry => (webUiEntry.name === (controller ? controller.address : "Global Options")) ?
|
|
295
|
+
webUiEntry.parentElement.classList.add("bg-info", "text-white") : webUiEntry.parentElement.classList.remove("bg-info", "text-white"));
|
|
296
|
+
|
|
297
|
+
// Unable to connect to the controller for some reason.
|
|
298
|
+
if(controller && !this.devices?.length) {
|
|
299
|
+
|
|
300
|
+
this.devicesTable.innerHTML = "";
|
|
301
|
+
this.configTable.innerHTML = "";
|
|
302
|
+
|
|
303
|
+
document.getElementById("headerInfo").innerHTML = ["Unable to connect to the controller.",
|
|
304
|
+
"Check the Settings tab to verify the controller details are correct.",
|
|
305
|
+
"<code class=\"text-danger\">" + (await homebridge.request("/getErrorMessage")) + "</code>"].join("<br>");
|
|
306
|
+
document.getElementById("headerInfo").style.display = "";
|
|
307
|
+
this.deviceStatsTable.style.display = "none";
|
|
308
|
+
|
|
309
|
+
homebridge.hideSpinner();
|
|
310
|
+
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// The first entry returned by getDevices() must always be the controller.
|
|
315
|
+
this.controller = this.devices[0]?.serial ?? null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Make the UI visible.
|
|
319
|
+
document.getElementById("headerInfo").style.display = "";
|
|
320
|
+
document.getElementById("sidebar").style.display = "";
|
|
321
|
+
|
|
322
|
+
// Wipe out the device list, except for our global entry.
|
|
323
|
+
this.webUiDeviceList.splice(1, this.webUiDeviceList.length);
|
|
324
|
+
|
|
325
|
+
// Start with a clean slate.
|
|
326
|
+
this.devicesTable.innerHTML = "";
|
|
327
|
+
|
|
328
|
+
// Populate our devices sidebar.
|
|
329
|
+
this.sidebar.showDevices(controller, this.devices);
|
|
330
|
+
|
|
224
331
|
// Display the feature options to the user.
|
|
225
|
-
this
|
|
332
|
+
this.showDeviceOptions(controller ? this.devices[0].serial : "Global Options");
|
|
226
333
|
|
|
227
334
|
// All done. Let the user interact with us.
|
|
228
335
|
homebridge.hideSpinner();
|
|
229
336
|
}
|
|
230
337
|
|
|
231
338
|
// Show feature option information for a specific device, controller, or globally.
|
|
232
|
-
async
|
|
339
|
+
async showDeviceOptions(deviceId) {
|
|
233
340
|
|
|
234
341
|
homebridge.showSpinner();
|
|
235
342
|
|
|
236
343
|
// Update the selected device for visibility.
|
|
237
|
-
this.
|
|
238
|
-
|
|
344
|
+
this.webUiDeviceList.map(webUiEntry => (webUiEntry.name === deviceId) ?
|
|
345
|
+
webUiEntry.parentElement.classList.add("bg-info", "text-white") : webUiEntry.parentElement.classList.remove("bg-info", "text-white"));
|
|
239
346
|
|
|
240
347
|
// Populate the device information info pane.
|
|
241
|
-
const currentDevice = this.devices.find(
|
|
242
|
-
this.controller = currentDevice?.serial;
|
|
348
|
+
const currentDevice = this.devices.find(device => device.serial === deviceId);
|
|
243
349
|
|
|
244
|
-
//
|
|
245
|
-
if(currentDevice) {
|
|
350
|
+
// Populate the details view. If there's no device specified, the context is considered global and we hide the device details view.
|
|
351
|
+
if(!currentDevice) {
|
|
352
|
+
|
|
353
|
+
this.deviceStatsTable.style.display = "none";
|
|
354
|
+
}
|
|
246
355
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
} else {
|
|
356
|
+
this.infoPanel(currentDevice);
|
|
357
|
+
|
|
358
|
+
if(currentDevice) {
|
|
251
359
|
|
|
252
|
-
|
|
253
|
-
document.getElementById("device_firmware").innerHTML = "N/A";
|
|
254
|
-
document.getElementById("device_serial").innerHTML = "N/A";
|
|
360
|
+
this.deviceStatsTable.style.display = "";
|
|
255
361
|
}
|
|
256
362
|
|
|
257
363
|
// Start with a clean slate.
|
|
@@ -259,6 +365,12 @@ export class webUiFeatureOptions {
|
|
|
259
365
|
|
|
260
366
|
for(const category of this.featureOptions.categories) {
|
|
261
367
|
|
|
368
|
+
// Validate that we should display this feature option category. This is useful when you want to only display feature option categories for certain device types.
|
|
369
|
+
if(!this.ui.validOptionCategory(currentDevice, category)) {
|
|
370
|
+
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
|
|
262
374
|
const optionTable = document.createElement("table");
|
|
263
375
|
const thead = document.createElement("thead");
|
|
264
376
|
const tbody = document.createElement("tbody");
|
|
@@ -273,7 +385,8 @@ export class webUiFeatureOptions {
|
|
|
273
385
|
tbody.classList.add("table-bordered");
|
|
274
386
|
|
|
275
387
|
// Add the feature option category description.
|
|
276
|
-
th.appendChild(document.createTextNode(category.description + (!currentDevice ? " (Global)" :
|
|
388
|
+
th.appendChild(document.createTextNode(category.description + (!currentDevice ? " (Global)" :
|
|
389
|
+
(this.ui.isController(currentDevice) ? " (Controller-specific)" : " (Device-specific)"))));
|
|
277
390
|
|
|
278
391
|
// Add the table header to the row.
|
|
279
392
|
trFirst.appendChild(th);
|
|
@@ -290,11 +403,18 @@ export class webUiFeatureOptions {
|
|
|
290
403
|
// Now enumerate all the feature options for a given device.
|
|
291
404
|
for(const option of this.featureOptions.options[category.name]) {
|
|
292
405
|
|
|
406
|
+
// Only show feature options that are valid for this device.
|
|
407
|
+
if(!this.ui.validOption(currentDevice, option)) {
|
|
408
|
+
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
|
|
293
412
|
// Expand the full feature option.
|
|
294
413
|
const featureOption = this.featureOptions.expandOption(category, option);
|
|
295
414
|
|
|
296
415
|
// Create the next table row.
|
|
297
416
|
const trX = document.createElement("tr");
|
|
417
|
+
|
|
298
418
|
trX.classList.add("align-top");
|
|
299
419
|
trX.id = "row-" + featureOption;
|
|
300
420
|
|
|
@@ -314,7 +434,7 @@ export class webUiFeatureOptions {
|
|
|
314
434
|
let initialScope;
|
|
315
435
|
|
|
316
436
|
// Determine our initial option scope to show the user what's been set.
|
|
317
|
-
switch(initialScope = this.featureOptions.scope(featureOption, currentDevice?.serial)) {
|
|
437
|
+
switch(initialScope = this.featureOptions.scope(featureOption, currentDevice?.serial, this.controller)) {
|
|
318
438
|
|
|
319
439
|
case "global":
|
|
320
440
|
case "controller":
|
|
@@ -374,6 +494,7 @@ export class webUiFeatureOptions {
|
|
|
374
494
|
trX.appendChild(tdCheckbox);
|
|
375
495
|
|
|
376
496
|
const tdLabel = document.createElement("td");
|
|
497
|
+
|
|
377
498
|
tdLabel.classList.add("w-100");
|
|
378
499
|
tdLabel.colSpan = 2;
|
|
379
500
|
|
|
@@ -383,6 +504,7 @@ export class webUiFeatureOptions {
|
|
|
383
504
|
if(this.featureOptions.isValue(featureOption)) {
|
|
384
505
|
|
|
385
506
|
const tdInput = document.createElement("td");
|
|
507
|
+
|
|
386
508
|
tdInput.classList.add("mr-2");
|
|
387
509
|
tdInput.style.width = "10%";
|
|
388
510
|
|
|
@@ -397,7 +519,7 @@ export class webUiFeatureOptions {
|
|
|
397
519
|
|
|
398
520
|
// Find the option in our list and delete it if it exists.
|
|
399
521
|
const optionRegex = new RegExp("^(?:Enable|Disable)\\." + checkbox.id + (!currentDevice ? "" : ("\\." + currentDevice.serial)) + "\\.[^\\.]+$", "gi");
|
|
400
|
-
const newOptions = this.featureOptions.configuredOptions.filter(
|
|
522
|
+
const newOptions = this.featureOptions.configuredOptions.filter(entry => !optionRegex.test(entry));
|
|
401
523
|
|
|
402
524
|
if(checkbox.checked) {
|
|
403
525
|
|
|
@@ -425,12 +547,13 @@ export class webUiFeatureOptions {
|
|
|
425
547
|
|
|
426
548
|
// Create a label for the checkbox with our option description.
|
|
427
549
|
const labelDescription = document.createElement("label");
|
|
550
|
+
|
|
428
551
|
labelDescription.for = checkbox.id;
|
|
429
552
|
labelDescription.style.cursor = "pointer";
|
|
430
553
|
labelDescription.classList.add("user-select-none", "my-0", "py-0");
|
|
431
554
|
|
|
432
555
|
// Highlight options for the user that are different than our defaults.
|
|
433
|
-
const scopeColor = this.featureOptions.color(featureOption, currentDevice?.serial);
|
|
556
|
+
const scopeColor = this.featureOptions.color(featureOption, currentDevice?.serial, this.controller);
|
|
434
557
|
|
|
435
558
|
if(scopeColor) {
|
|
436
559
|
|
|
@@ -442,7 +565,7 @@ export class webUiFeatureOptions {
|
|
|
442
565
|
|
|
443
566
|
// Find the option in our list and delete it if it exists.
|
|
444
567
|
const optionRegex = new RegExp("^(?:Enable|Disable)\\." + checkbox.id + (!currentDevice ? "" : ("\\." + currentDevice.serial)) + "$", "gi");
|
|
445
|
-
const newOptions = this.featureOptions.configuredOptions.filter(
|
|
568
|
+
const newOptions = this.featureOptions.configuredOptions.filter(entry => !optionRegex.test(entry));
|
|
446
569
|
|
|
447
570
|
// Figure out if we've got the option set upstream.
|
|
448
571
|
let upstreamOption = false;
|
|
@@ -535,7 +658,7 @@ export class webUiFeatureOptions {
|
|
|
535
658
|
// If we've reset to defaults, make sure our color coding for scope is reflected.
|
|
536
659
|
if((checkbox.checked === option.default) || checkbox.indeterminate) {
|
|
537
660
|
|
|
538
|
-
const scopeColor = this.featureOptions.color(featureOption, currentDevice?.serial);
|
|
661
|
+
const scopeColor = this.featureOptions.color(featureOption, currentDevice?.serial, this.controller);
|
|
539
662
|
|
|
540
663
|
if(scopeColor) {
|
|
541
664
|
|
|
@@ -597,4 +720,117 @@ export class webUiFeatureOptions {
|
|
|
597
720
|
|
|
598
721
|
homebridge.hideSpinner();
|
|
599
722
|
}
|
|
723
|
+
|
|
724
|
+
// Our default device information panel handler.
|
|
725
|
+
#showDeviceInfoPanel(device) {
|
|
726
|
+
|
|
727
|
+
const deviceFirmware = document.getElementById("device_firmware") ?? {};
|
|
728
|
+
const deviceSerial = document.getElementById("device_serial") ?? {};
|
|
729
|
+
|
|
730
|
+
// No device specified, we must be in a global context.
|
|
731
|
+
if(!device) {
|
|
732
|
+
|
|
733
|
+
deviceFirmware.innerHTML = "N/A";
|
|
734
|
+
deviceSerial.innerHTML = "N/A";
|
|
735
|
+
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Display our device details.
|
|
740
|
+
deviceFirmware.innerHTML = device.firmwareVersion;
|
|
741
|
+
deviceSerial.innerHTML = device.serial;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// Default method for enumerating the device list in the sidebar.
|
|
745
|
+
async #showSidebarDevices() {
|
|
746
|
+
|
|
747
|
+
// Show the devices list only if we have actual devices to show.
|
|
748
|
+
if(!this.devices?.length) {
|
|
749
|
+
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// Create a row for this device category.
|
|
754
|
+
const trCategory = document.createElement("tr");
|
|
755
|
+
|
|
756
|
+
// Disable any pointer events and hover activity.
|
|
757
|
+
trCategory.style.pointerEvents = "none";
|
|
758
|
+
|
|
759
|
+
// Create the cell for our device category row.
|
|
760
|
+
const tdCategory = document.createElement("td");
|
|
761
|
+
|
|
762
|
+
tdCategory.classList.add("m-0", "p-0", "pl-1", "w-100");
|
|
763
|
+
|
|
764
|
+
// Add the category name, with appropriate casing.
|
|
765
|
+
tdCategory.appendChild(document.createTextNode(this.sidebar.deviceLabel));
|
|
766
|
+
tdCategory.style.fontWeight = "bold";
|
|
767
|
+
|
|
768
|
+
// Add the cell to the table row.
|
|
769
|
+
trCategory.appendChild(tdCategory);
|
|
770
|
+
|
|
771
|
+
// Add the table row to the table.
|
|
772
|
+
this.devicesTable.appendChild(trCategory);
|
|
773
|
+
|
|
774
|
+
for(const device of this.devices) {
|
|
775
|
+
|
|
776
|
+
// Create a row for this device.
|
|
777
|
+
const trDevice = document.createElement("tr");
|
|
778
|
+
|
|
779
|
+
trDevice.classList.add("m-0", "p-0");
|
|
780
|
+
|
|
781
|
+
// Create a cell for our device.
|
|
782
|
+
const tdDevice = document.createElement("td");
|
|
783
|
+
|
|
784
|
+
tdDevice.classList.add("m-0", "p-0", "w-100");
|
|
785
|
+
|
|
786
|
+
const label = document.createElement("label");
|
|
787
|
+
|
|
788
|
+
label.name = device.serial;
|
|
789
|
+
label.appendChild(document.createTextNode(device.name ?? "Unknown"));
|
|
790
|
+
label.style.cursor = "pointer";
|
|
791
|
+
label.classList.add("mx-2", "my-0", "p-0", "w-100");
|
|
792
|
+
|
|
793
|
+
label.addEventListener("click", () => this.showDeviceOptions(device.serial));
|
|
794
|
+
|
|
795
|
+
// Add the device label to our cell.
|
|
796
|
+
tdDevice.appendChild(label);
|
|
797
|
+
|
|
798
|
+
// Add the cell to the table row.
|
|
799
|
+
trDevice.appendChild(tdDevice);
|
|
800
|
+
|
|
801
|
+
// Add the table row to the table.
|
|
802
|
+
this.devicesTable.appendChild(trDevice);
|
|
803
|
+
|
|
804
|
+
this.webUiDeviceList.push(label);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Default method for retrieving the device list from the Homebridge accessory cache.
|
|
809
|
+
async #getHomebridgeDevices() {
|
|
810
|
+
|
|
811
|
+
// Retrieve the full list of cached accessories.
|
|
812
|
+
let devices = await homebridge.getCachedAccessories();
|
|
813
|
+
|
|
814
|
+
// Filter out only the components we're interested in.
|
|
815
|
+
devices = devices.map(device => ({
|
|
816
|
+
|
|
817
|
+
firmwareVersion: (device.services.find(service => service.constructorName ===
|
|
818
|
+
"AccessoryInformation")?.characteristics.find(characteristic => characteristic.constructorName === "FirmwareRevision")?.value ?? ""),
|
|
819
|
+
name: device.displayName,
|
|
820
|
+
serial: (device.services.find(service => service.constructorName ===
|
|
821
|
+
"AccessoryInformation")?.characteristics.find(characteristic => characteristic.constructorName === "SerialNumber")?.value ?? "")
|
|
822
|
+
}));
|
|
823
|
+
|
|
824
|
+
// Sort it for posterity.
|
|
825
|
+
devices.sort((a, b) => {
|
|
826
|
+
|
|
827
|
+
const aCase = (a.name ?? "").toLowerCase();
|
|
828
|
+
const bCase = (b.name ?? "").toLowerCase();
|
|
829
|
+
|
|
830
|
+
return aCase > bCase ? 1 : (bCase > aCase ? -1 : 0);
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
// Return the list.
|
|
834
|
+
return devices;
|
|
835
|
+
}
|
|
600
836
|
}
|