homebridge-plugin-utils 1.0.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,366 @@
1
+ export class FeatureOptions {
2
+ _categories;
3
+ _configuredOptions;
4
+ _groups;
5
+ _options;
6
+ defaultReturnValue;
7
+ defaults;
8
+ valueOptions;
9
+ // Create a feature option instance.
10
+ constructor(categories, options, configuredOptions) {
11
+ // Initialize our defaults.
12
+ this._categories = [];
13
+ this._configuredOptions = [];
14
+ this._groups = {};
15
+ this._options = {};
16
+ this.defaultReturnValue = false;
17
+ this.defaults = {};
18
+ this.valueOptions = {};
19
+ this.categories = categories ?? [];
20
+ this.configuredOptions = configuredOptions;
21
+ this.options = options ?? {};
22
+ }
23
+ color(option, device) {
24
+ switch (this.scope(option, device)) {
25
+ case "device":
26
+ return "text-info";
27
+ case "controller":
28
+ return "text-success";
29
+ case "global":
30
+ return device ? "text-warning" : "text-info";
31
+ default:
32
+ return "";
33
+ }
34
+ }
35
+ /**
36
+ * Return the default value for an option.
37
+ *
38
+ * @param option - Feature option to check.
39
+ *
40
+ * @returns Returns true or false, depending on the option default.
41
+ */
42
+ defaultValue(option) {
43
+ // Value-centric feature options don't have default values.
44
+ if (this.isValue(option)) {
45
+ return this.defaultReturnValue;
46
+ }
47
+ const value = this.defaults[option.toLowerCase()];
48
+ // If it's unknown to us, assume it's true.
49
+ if (value === undefined) {
50
+ return this.defaultReturnValue;
51
+ }
52
+ return value;
53
+ }
54
+ /**
55
+ * Return whether the option explicitly exists in the list of configured options.
56
+ *
57
+ * @param option - Feature option to check.
58
+ * @param id - Optional device or controller scope identifier to check.
59
+ *
60
+ * @returns Returns true if the option has been explicitly configured, false otherwise.
61
+ */
62
+ exists(option, id) {
63
+ const regex = this.isValue(option) ? this.valueRegex(option, id) : this.optionRegex(option, id);
64
+ return this.configuredOptions.some(x => regex.test(x));
65
+ }
66
+ /**
67
+ * Return a fully formed feature option string.
68
+ *
69
+ * @param category - Feature option category entry or category name string.
70
+ * @param option - Feature option entry of option name string.
71
+ *
72
+ * @returns Returns a fully formed feature option in the form of `category.option`.
73
+ */
74
+ expandOption(category, option) {
75
+ const categoryName = (typeof category === "string") ? category : category.name;
76
+ const optionName = (typeof option === "string") ? option : option.name;
77
+ if (!categoryName || !categoryName.length) {
78
+ return "";
79
+ }
80
+ return (!optionName || !optionName.length) ? categoryName : categoryName + "." + optionName;
81
+ }
82
+ /**
83
+ * Parse a floating point feature option value.
84
+ *
85
+ * @param value - Value to parse.
86
+ *
87
+ * @returns Returns a floating point number from a string, or `undefined` if it couldn't be parsed.
88
+ */
89
+ getFloat(value) {
90
+ // We don't have the value configured -- we're done.
91
+ if (value === undefined) {
92
+ return undefined;
93
+ }
94
+ // Parse the number and return the value.
95
+ return this.parseOptionNumeric(value, parseFloat);
96
+ }
97
+ /**
98
+ * Parse an integer feature option value.
99
+ *
100
+ * @param value - Value to parse.
101
+ *
102
+ * @returns Returns an integer from a string, or `undefined` if it couldn't be parsed.
103
+ */
104
+ getInteger(value) {
105
+ // We don't have the value configured -- we're done.
106
+ if (value === undefined) {
107
+ return undefined;
108
+ }
109
+ // Parse the number and return the value.
110
+ return this.parseOptionNumeric(value, parseInt);
111
+ }
112
+ /**
113
+ * Return whether an option has been set in either the device or controller scope context.
114
+ *
115
+ * @param option - Feature option to check.
116
+ *
117
+ * @returns Returns true if the option is set at the device or controller level and false otherwise.
118
+ */
119
+ isScopeDevice(option, device) {
120
+ const value = this.exists(option, device);
121
+ // Return the value if it's set, or the default value for this option.
122
+ return (value !== undefined) ? value : this.defaultValue(option);
123
+ }
124
+ /**
125
+ * Return whether an option has been set in the global scope context.
126
+ *
127
+ * @param option - Feature option to check.
128
+ *
129
+ * @returns Returns true if the option is set globally and false otherwise.
130
+ */
131
+ isScopeGlobal(option) {
132
+ const value = this.exists(option);
133
+ // Return the value if it's set, or the default value for this option.
134
+ return (value !== undefined) ? value : this.defaultValue(option);
135
+ }
136
+ /**
137
+ * Return whether an option is value-centric or not.
138
+ *
139
+ * @param option - Feature option entry or string to check.
140
+ *
141
+ * @returns Returns true if it is a value-centric option and false otherwise.
142
+ */
143
+ isValue(option) {
144
+ return this.valueOptions[option?.toLowerCase()] === true;
145
+ }
146
+ /**
147
+ * Return the scope hierarchy location of an option.
148
+ *
149
+ * @param option - Feature option to check.
150
+ * @param device - Optional device scope identifier.
151
+ * @param controller - Optional controller scope identifier.
152
+ *
153
+ * @returns Returns an object containing the location in the scope hierarchy of an `option` as well as the current value associated with the option.
154
+ */
155
+ scope(option, device, controller) {
156
+ return this.getOptionInfo(option, device, controller).scope;
157
+ }
158
+ /**
159
+ * Return the current state of a feature option, traversing the scope hierarchy.
160
+ *
161
+ * @param option - Feature option to check.
162
+ * @param device - Optional device scope identifier.
163
+ * @param controller - Optional controller scope identifier.
164
+ *
165
+ * @returns Returns true if the option is enabled, and false otherwise.
166
+ */
167
+ test(option, device, controller) {
168
+ return this.getOptionInfo(option, device, controller).value;
169
+ }
170
+ /**
171
+ * Return the value associated with a value-centric feature option, traversing the scope hierarchy.
172
+ *
173
+ * @param option - Feature option to check.
174
+ * @param device - Optional device scope identifier.
175
+ * @param controller - Optional controller scope identifier.
176
+ *
177
+ * @returns Returns the current value associated with `option` or `undefined` if none.
178
+ */
179
+ value(option, device, controller) {
180
+ const getValue = (checkOption, checkId) => {
181
+ const regex = this.valueRegex(checkOption, checkId);
182
+ // Get the option value, if we have one.
183
+ for (const entry of this.configuredOptions) {
184
+ const regexMatch = regex.exec(entry);
185
+ if (regexMatch) {
186
+ return regexMatch[1];
187
+ }
188
+ }
189
+ return undefined;
190
+ };
191
+ // Check to see if we have a device-level value first.
192
+ if (device) {
193
+ const value = getValue(option, device);
194
+ if (value) {
195
+ return value;
196
+ }
197
+ }
198
+ // Now check to see if we have an controller-level value.
199
+ if (controller) {
200
+ const value = getValue(option, controller);
201
+ if (value) {
202
+ return value;
203
+ }
204
+ }
205
+ // Finally, we check for a global-level value.
206
+ return getValue(option);
207
+ }
208
+ /**
209
+ * Return the list of available feature option categories.
210
+ *
211
+ * @returns Returns the current list of available feature option categories.
212
+ */
213
+ get categories() {
214
+ return this._categories;
215
+ }
216
+ /**
217
+ * Set the list of available feature option categories.
218
+ *
219
+ * @param options - Array of available feature options.
220
+ */
221
+ set categories(category) {
222
+ this._categories = category;
223
+ }
224
+ /**
225
+ * Return the list of currently configured feature options.
226
+ *
227
+ * @returns Returns the currently configured list of feature options.
228
+ */
229
+ get configuredOptions() {
230
+ return this._configuredOptions;
231
+ }
232
+ /**
233
+ * Set the list of currently configured feature options.
234
+ *
235
+ * @param options - Array of configured feature options.
236
+ */
237
+ set configuredOptions(options) {
238
+ this._configuredOptions = options ?? [];
239
+ }
240
+ /**
241
+ * Return the list of available feature option groups.
242
+ *
243
+ * @returns Returns the current list of available feature option groups.
244
+ */
245
+ get groups() {
246
+ return this._groups;
247
+ }
248
+ /**
249
+ * Return the list of available feature options.
250
+ *
251
+ * @returns Returns the current list of available feature options.
252
+ */
253
+ get options() {
254
+ return this._options;
255
+ }
256
+ /**
257
+ * Set the list of available feature options.
258
+ *
259
+ * @param options - Array of available feature options.
260
+ */
261
+ set options(options) {
262
+ this._options = options ?? {};
263
+ // Regenerate our defaults.
264
+ this.generateDefaults();
265
+ }
266
+ // Build our list of default values for our feature options.
267
+ generateDefaults() {
268
+ this.defaults = {};
269
+ this._groups = {};
270
+ this.valueOptions = {};
271
+ for (const category of this.categories) {
272
+ // Now enumerate all the feature options for a given device and add then to the full list.
273
+ for (const option of this.options[category.name]) {
274
+ // Expand the entry.
275
+ const entry = this.expandOption(category, option);
276
+ // Index the default value.
277
+ this.defaults[entry.toLowerCase()] = option.default;
278
+ // Track value-centric options.
279
+ this.valueOptions[entry.toLowerCase()] = "defaultValue" in option;
280
+ // Cross reference the feature option group it belongs to, if any.
281
+ if (option.group !== undefined) {
282
+ const expandedGroup = category.name + (option.group.length ? ("." + option.group) : "");
283
+ // Initialize the group entry if needed and add the entry.
284
+ (this._groups[expandedGroup] ??= []).push(entry);
285
+ }
286
+ }
287
+ }
288
+ }
289
+ // Utility function to return the setting of a particular option and it's position in the scoping hierarchy.
290
+ getOptionInfo(option, device, controller) {
291
+ // There are a couple of ways to enable and disable options. The rules of the road are:
292
+ //
293
+ // 1. Explicitly disabling, or enabling an option on the controller propogates to all the devices that are managed by that controller. Why might you want to do this?
294
+ // Because...
295
+ //
296
+ // 2. Explicitly disabling, or enabling an option on a device always override the above. This means that it's possible to disable an option for a controller, and all
297
+ // the devices that are managed by it, and then override that behavior on a single device that it's managing.
298
+ // Check to see if we have a device-level option first.
299
+ if (device && this.exists(option, device)) {
300
+ const value = this.isOptionEnabled(option, device);
301
+ if (value !== undefined) {
302
+ return { scope: "device", value: value };
303
+ }
304
+ }
305
+ // Now check to see if we have an controller-level option.
306
+ if (controller && this.exists(option, controller)) {
307
+ const value = this.isOptionEnabled(option, controller);
308
+ if (value !== undefined) {
309
+ return { scope: "controller", value: value };
310
+ }
311
+ }
312
+ // Finally, we check for a global-level value.
313
+ if (this.exists(option)) {
314
+ const value = this.isOptionEnabled(option);
315
+ if (value !== undefined) {
316
+ return { scope: "global", value: value };
317
+ }
318
+ }
319
+ // The option hasn't been set at any scope, return our default value.
320
+ return { scope: "none", value: this.defaultValue(option) };
321
+ }
322
+ // Utility to test whether an option is set in a given scope.
323
+ // We return true if an option is enabled, false for disabled, undefined otherwise. For value-centric options, we return true if a value exists.
324
+ isOptionEnabled(option, id) {
325
+ // Deal with value-centric options uniquely.
326
+ if (this.isValue(option)) {
327
+ return this.exists(option, id);
328
+ }
329
+ const regex = this.optionRegex(option, id);
330
+ // Get the option value, if we have one.
331
+ for (const entry of this.configuredOptions) {
332
+ const regexMatch = regex.exec(entry);
333
+ if (regexMatch) {
334
+ return regexMatch[1].toLowerCase() === "enable";
335
+ }
336
+ }
337
+ return undefined;
338
+ }
339
+ // Regular expression test for feature options.
340
+ optionRegex(option, id) {
341
+ // This regular expression is a bit more intricate than you might think it should be due to the need to ensure we capture values at the very end of the option. We
342
+ // also need to escape out our option to ensure we have no inadvertent issues in matching the regular expression.
343
+ return new RegExp("^(Enable|Disable)\\." + option.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + (!id ? "" : "\\." + id) + "$", "gi");
344
+ }
345
+ // Utility function to parse and return a numeric configuration parameter.
346
+ parseOptionNumeric(option, convert) {
347
+ // We don't have the option configured -- we're done.
348
+ if (option === undefined) {
349
+ return undefined;
350
+ }
351
+ // Convert it to a number, if needed.
352
+ const convertedValue = convert(option);
353
+ // Let's validate to make sure it's really a number.
354
+ if (isNaN(convertedValue) || (convertedValue < 0)) {
355
+ return undefined;
356
+ }
357
+ // Return the value.
358
+ return convertedValue;
359
+ }
360
+ // Regular expression test for value-centric feature options.
361
+ valueRegex(option, id) {
362
+ // This regular expression is a bit more intricate than you might think it should be due to the need to ensure we capture values at the very end of the option.
363
+ return new RegExp("^Enable\\." + option.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + (!id ? "" : "\\." + id) + "\\.([^\\.]+)$", "gi");
364
+ }
365
+ }
366
+ //# sourceMappingURL=featureoptions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"featureoptions.js","sourceRoot":"","sources":["../src/featureoptions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,155 @@
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
+ export class webUi {
8
+
9
+ // Feature options class instance.
10
+ #featureOptions;
11
+
12
+ // Homebridge class instance.
13
+ #homebridge;
14
+
15
+ // Plugin name.
16
+ #name;
17
+
18
+ constructor({ name, featureOptions, homebridge } = {}) {
19
+
20
+ this.homebridge = homebridge;
21
+ this.featureOptions = featureOptions;
22
+ this.name = name;
23
+
24
+ // Fire off our UI, catching errors along the way.
25
+ try {
26
+
27
+ this.#launchWebUI();
28
+ } catch(err) {
29
+
30
+ // If we had an error instantiating or updating the UI, notify the user.
31
+ this.homebridge.toast.error(err.message, "Error");
32
+ } finally {
33
+
34
+ // Always leave the UI in a usable place for the end user.
35
+ this.homebridge.hideSpinner();
36
+ }
37
+ }
38
+
39
+ // Show the first run user experience if we don't have valid login credentials.
40
+ async #showFirstRun() {
41
+
42
+ const buttonFirstRun = document.getElementById("firstRun");
43
+
44
+ // First run user experience.
45
+ buttonFirstRun.addEventListener("click", async () => {
46
+
47
+ // Show the beachball while we setup.
48
+ this.homebridge.showSpinner();
49
+
50
+ // Get the list of devices the plugin knows about.
51
+ const devices = await this.homebridge.getCachedAccessories();
52
+
53
+ // Sort it for posterity.
54
+ devices?.sort((a, b) => {
55
+
56
+ const aCase = (a.displayName ?? "").toLowerCase();
57
+ const bCase = (b.displayName ?? "").toLowerCase();
58
+
59
+ return aCase > bCase ? 1 : (bCase > aCase ? -1 : 0);
60
+ });
61
+
62
+ // Create our UI.
63
+ document.getElementById("pageFirstRun").style.display = "none";
64
+ document.getElementById("menuWrapper").style.display = "inline-flex";
65
+ this.featureOptions.showUI();
66
+
67
+ // All done. Let the user interact with us, although in practice, we shouldn't get here.
68
+ // this.homebridge.hideSpinner();
69
+ });
70
+
71
+ document.getElementById("pageFirstRun").style.display = "block";
72
+ }
73
+
74
+ // Show the main plugin configuration tab.
75
+ #showSettings() {
76
+
77
+ // Show the beachball while we setup.
78
+ this.homebridge.showSpinner();
79
+
80
+ // Create our UI.
81
+ document.getElementById("menuHome").classList.remove("btn-elegant");
82
+ document.getElementById("menuHome").classList.add("btn-primary");
83
+ document.getElementById("menuFeatureOptions").classList.remove("btn-elegant");
84
+ document.getElementById("menuFeatureOptions").classList.add("btn-primary");
85
+ document.getElementById("menuSettings").classList.add("btn-elegant");
86
+ document.getElementById("menuSettings").classList.remove("btn-primary");
87
+
88
+ document.getElementById("pageSupport").style.display = "none";
89
+ document.getElementById("pageFeatureOptions").style.display = "none";
90
+
91
+ this.homebridge.showSchemaForm();
92
+
93
+ // All done. Let the user interact with us.
94
+ this.homebridge.hideSpinner();
95
+ }
96
+
97
+ // Show the support tab.
98
+ #showSupport() {
99
+
100
+ // Show the beachball while we setup.
101
+ this.homebridge.showSpinner();
102
+ this.homebridge.hideSchemaForm();
103
+
104
+ // Create our UI.
105
+ document.getElementById("menuHome").classList.add("btn-elegant");
106
+ document.getElementById("menuHome").classList.remove("btn-primary");
107
+ document.getElementById("menuFeatureOptions").classList.remove("btn-elegant");
108
+ document.getElementById("menuFeatureOptions").classList.add("btn-primary");
109
+ document.getElementById("menuSettings").classList.remove("btn-elegant");
110
+ document.getElementById("menuSettings").classList.add("btn-primary");
111
+
112
+ document.getElementById("pageSupport").style.display = "block";
113
+ document.getElementById("pageFeatureOptions").style.display = "none";
114
+
115
+ // All done. Let the user interact with us.
116
+ this.homebridge.hideSpinner();
117
+ }
118
+
119
+ // Launch our webUI.
120
+ async #launchWebUI() {
121
+
122
+ // Retrieve the current plugin configuration.
123
+ this.featureOptions.currentConfig = await this.homebridge.getPluginConfig();
124
+
125
+ // Add our event listeners to animate the UI.
126
+ document.getElementById("menuHome").addEventListener("click", () => this.#showSupport());
127
+ document.getElementById("menuFeatureOptions").addEventListener("click", () => this.featureOptions.showUI());
128
+ document.getElementById("menuSettings").addEventListener("click", () => this.#showSettings());
129
+
130
+ // Get the list of devices the plugin knows about.
131
+ const devices = await this.homebridge.getCachedAccessories();
132
+
133
+ // If we've got devices detected, we launch our feature option UI. Otherwise, we launch our first run UI.
134
+ if(this.featureOptions.currentConfig.length && devices?.length) {
135
+
136
+ document.getElementById("menuWrapper").style.display = "inline-flex";
137
+ this.featureOptions.showUI();
138
+ return;
139
+ }
140
+
141
+ // If we have no configuration, let's create one.
142
+ if(!this.featureOptions.currentConfig.length) {
143
+
144
+ this.featureOptions.currentConfig.push({ name: this.name });
145
+ } else if(!("name" in this.featureOptions.currentConfig[0])) {
146
+
147
+ // If we haven't set the name, let's do so now.
148
+ this.featureOptions.currentConfig[0].name = this.name;
149
+ }
150
+
151
+ // Update the plugin configuration and launch the first run UI.
152
+ await this.homebridge.updatePluginConfig(this.featureOptions.currentConfig);
153
+ this.#showFirstRun();
154
+ }
155
+ }