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,600 @@
1
+ /* Copyright(C) 2017-2024, HJD (https://github.com/hjdhjd). All rights reserved.
2
+ *
3
+ * webui-featureoptions.mjs: Device feature option webUI.
4
+ */
5
+ "use strict";
6
+
7
+ import { FeatureOptions} from "./featureoptions.js";
8
+
9
+ export class webUiFeatureOptions {
10
+
11
+ // The current plugin configuration.
12
+ currentConfig;
13
+
14
+ // Table containing the currently displayed feature options.
15
+ #configTable;
16
+
17
+ // The current controller context.
18
+ #controller;
19
+
20
+ // Current list of devices from the Homebridge accessory cache.
21
+ #devices;
22
+
23
+ // Feature options instance.
24
+ #featureOptions;
25
+
26
+ // Device sidebar category name.
27
+ #sidebar;
28
+
29
+ // Enable the use of controllers.
30
+ #useControllers;
31
+
32
+ // Current list of devices on a given controller, for webUI elements.
33
+ #webuiDeviceList;
34
+
35
+ constructor({ sidebar = "Devices", useControllers = true } = {}) {
36
+
37
+ this.configTable = document.getElementById("configTable");
38
+ this.controller = null;
39
+ this.currentConfig = [];
40
+ this.devices = [];
41
+ this.featureOptions = null;
42
+ this.sidebarName = sidebar;
43
+ this.useControllers = useControllers;
44
+ this.webuiDeviceList = [];
45
+ }
46
+
47
+ // Render the feature option webUI.
48
+ async showUI() {
49
+
50
+ // Show the beachball while we setup.
51
+ homebridge.showSpinner();
52
+ homebridge.hideSchemaForm();
53
+
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
+ // Create our custom UI.
64
+ document.getElementById("menuHome").classList.remove("btn-elegant");
65
+ document.getElementById("menuHome").classList.add("btn-primary");
66
+ document.getElementById("menuFeatureOptions").classList.add("btn-elegant");
67
+ document.getElementById("menuFeatureOptions").classList.remove("btn-primary");
68
+ document.getElementById("menuSettings").classList.remove("btn-elegant");
69
+ document.getElementById("menuSettings").classList.add("btn-primary");
70
+
71
+ // Hide the legacy UI.
72
+ document.getElementById("pageSupport").style.display = "none";
73
+ document.getElementById("pageFeatureOptions").style.display = "block";
74
+
75
+ // What we're going to do is display our global options, followed by the list of devices from the Homebridge accessory cache.
76
+ // We pre-select our global options by default for the user as a starting point.
77
+
78
+ // Retrieve the table for the our list of controllers and global options.
79
+ const controllersTable = document.getElementById("controllersTable");
80
+
81
+ // Start with a clean slate.
82
+ controllersTable.innerHTML = "";
83
+ document.getElementById("devicesTable").innerHTML = "";
84
+ this.configTable.innerHTML = "";
85
+ this.webuiDeviceList = [];
86
+
87
+ // Hide the UI until we're ready.
88
+ document.getElementById("sidebar").style.display = "none";
89
+ document.getElementById("headerInfo").style.display = "none";
90
+ document.getElementById("deviceStatsTable").style.display = "none";
91
+
92
+ // Initialize our informational header.
93
+ document.getElementById("headerInfo").innerHTML = "Feature options are applied in prioritized order, from global to device-specific options:" +
94
+ "<br><i class=\"text-warning\">Global options</i> (lowest priority) &rarr; " +
95
+ (this.useControllers ? "<i class=\"text-success\">Controller options</i> &rarr; " : "") +
96
+ "<i class=\"text-info\">Device options</i> (highest priority)";
97
+
98
+ // Enumerate our global options.
99
+ const trGlobal = document.createElement("tr");
100
+
101
+ // Create the cell for our global options.
102
+ const tdGlobal = document.createElement("td");
103
+ tdGlobal.classList.add("m-0", "p-0");
104
+
105
+ // Create our label target.
106
+ const globalLabel = document.createElement("label");
107
+
108
+ globalLabel.name = "Global Options";
109
+ globalLabel.appendChild(document.createTextNode("Global Options"));
110
+ globalLabel.style.cursor = "pointer";
111
+ globalLabel.classList.add("mx-2", "my-0", "p-0", "w-100");
112
+
113
+ globalLabel.addEventListener("click", () => this.#showDevices(true));
114
+
115
+ // Add the global options label.
116
+ tdGlobal.appendChild(globalLabel);
117
+ tdGlobal.style.fontWeight = "bold";
118
+
119
+ // Add the global cell to the table.
120
+ trGlobal.appendChild(tdGlobal);
121
+
122
+ // Now add it to the overall controllers table.
123
+ controllersTable.appendChild(trGlobal);
124
+
125
+ // Add it as another device, for UI purposes.
126
+ this.webuiDeviceList.push(globalLabel);
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) {
137
+
138
+ // Show the beachball while we setup.
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
+ }));
152
+
153
+ // Sort it for posterity.
154
+ this.devices?.sort((a, b) => {
155
+
156
+ const aCase = (a.name ?? "").toLowerCase();
157
+ const bCase = (b.name ?? "").toLowerCase();
158
+
159
+ return aCase > bCase ? 1 : (bCase > aCase ? -1 : 0);
160
+ });
161
+
162
+ // Make the UI visible.
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");
181
+
182
+ // Add the category name, with appropriate casing.
183
+ tdCategory.appendChild(document.createTextNode(this.sidebarName));
184
+ tdCategory.style.fontWeight = "bold";
185
+
186
+ // Add the cell to the table row.
187
+ trCategory.appendChild(tdCategory);
188
+
189
+ // Add the table row to the table.
190
+ devicesTable.appendChild(trCategory);
191
+
192
+ for(const device of this.devices) {
193
+
194
+ // Create a row for this device.
195
+ const trDevice = document.createElement("tr");
196
+ trDevice.classList.add("m-0", "p-0");
197
+
198
+ // Create a cell for our device.
199
+ const tdDevice = document.createElement("td");
200
+ tdDevice.classList.add("m-0", "p-0", "w-100");
201
+
202
+ const label = document.createElement("label");
203
+
204
+ label.name = device.serial;
205
+ label.appendChild(document.createTextNode(device.name ?? "Unknown"));
206
+ label.style.cursor = "pointer";
207
+ label.classList.add("mx-2", "my-0", "p-0", "w-100");
208
+
209
+ label.addEventListener("click", () => this.#showDeviceInfo(device.serial));
210
+
211
+ // Add the device label to our cell.
212
+ tdDevice.appendChild(label);
213
+
214
+ // Add the cell to the table row.
215
+ trDevice.appendChild(tdDevice);
216
+
217
+ // Add the table row to the table.
218
+ devicesTable.appendChild(trDevice);
219
+
220
+ this.webuiDeviceList.push(label);
221
+ }
222
+ }
223
+
224
+ // Display the feature options to the user.
225
+ this.#showDeviceInfo(isGlobal ? "Global Options" : this.devices[0].serial);
226
+
227
+ // All done. Let the user interact with us.
228
+ homebridge.hideSpinner();
229
+ }
230
+
231
+ // Show feature option information for a specific device, controller, or globally.
232
+ async #showDeviceInfo(deviceId) {
233
+
234
+ homebridge.showSpinner();
235
+
236
+ // Update the selected device for visibility.
237
+ this.webuiDeviceList.map(x => (x.name === deviceId) ?
238
+ x.parentElement.classList.add("bg-info", "text-white") : x.parentElement.classList.remove("bg-info", "text-white"));
239
+
240
+ // Populate the device information info pane.
241
+ const currentDevice = this.devices.find(x => x.serial === deviceId);
242
+ this.controller = currentDevice?.serial;
243
+
244
+ // Ensure we have a controller or device. The only time this won't be the case is when we're looking at global options.
245
+ if(currentDevice) {
246
+
247
+ document.getElementById("device_firmware").innerHTML = currentDevice.firmwareVersion;
248
+ document.getElementById("device_serial").innerHTML = currentDevice.serial;
249
+ document.getElementById("deviceStatsTable").style.display = "";
250
+ } else {
251
+
252
+ document.getElementById("deviceStatsTable").style.display = "none";
253
+ document.getElementById("device_firmware").innerHTML = "N/A";
254
+ document.getElementById("device_serial").innerHTML = "N/A";
255
+ }
256
+
257
+ // Start with a clean slate.
258
+ this.configTable.innerHTML = "";
259
+
260
+ for(const category of this.featureOptions.categories) {
261
+
262
+ const optionTable = document.createElement("table");
263
+ const thead = document.createElement("thead");
264
+ const tbody = document.createElement("tbody");
265
+ const trFirst = document.createElement("tr");
266
+ const th = document.createElement("th");
267
+
268
+ // Set our table options.
269
+ optionTable.classList.add("table", "table-borderless", "table-sm", "table-hover");
270
+ th.classList.add("p-0");
271
+ th.style.fontWeight = "bold";
272
+ th.colSpan = 3;
273
+ tbody.classList.add("table-bordered");
274
+
275
+ // Add the feature option category description.
276
+ th.appendChild(document.createTextNode(category.description + (!currentDevice ? " (Global)" : " (Device-specific)")));
277
+
278
+ // Add the table header to the row.
279
+ trFirst.appendChild(th);
280
+
281
+ // Add the table row to the table head.
282
+ thead.appendChild(trFirst);
283
+
284
+ // Finally, add the table head to the table.
285
+ optionTable.appendChild(thead);
286
+
287
+ // Keep track of the number of options we have made available in a given category.
288
+ let optionsVisibleCount = 0;
289
+
290
+ // Now enumerate all the feature options for a given device.
291
+ for(const option of this.featureOptions.options[category.name]) {
292
+
293
+ // Expand the full feature option.
294
+ const featureOption = this.featureOptions.expandOption(category, option);
295
+
296
+ // Create the next table row.
297
+ const trX = document.createElement("tr");
298
+ trX.classList.add("align-top");
299
+ trX.id = "row-" + featureOption;
300
+
301
+ // Create a checkbox for the option.
302
+ const tdCheckbox = document.createElement("td");
303
+
304
+ // Create the actual checkbox for the option.
305
+ const checkbox = document.createElement("input");
306
+
307
+ checkbox.type = "checkbox";
308
+ checkbox.readOnly = false;
309
+ checkbox.id = featureOption;
310
+ checkbox.name = featureOption;
311
+ checkbox.value = featureOption + (!currentDevice ? "" : ("." + currentDevice.serial));
312
+
313
+ let initialValue = undefined;
314
+ let initialScope;
315
+
316
+ // Determine our initial option scope to show the user what's been set.
317
+ switch(initialScope = this.featureOptions.scope(featureOption, currentDevice?.serial)) {
318
+
319
+ case "global":
320
+ case "controller":
321
+
322
+ // If we're looking at the global scope, show the option value. Otherwise, we show that we're inheriting a value from the scope above.
323
+ if(!currentDevice) {
324
+
325
+ if(this.featureOptions.isValue(featureOption)) {
326
+
327
+ checkbox.checked = this.featureOptions.exists(featureOption);
328
+ initialValue = this.featureOptions.value(checkbox.id);
329
+ } else {
330
+
331
+ checkbox.checked = this.featureOptions.test(featureOption);
332
+ }
333
+
334
+ if(checkbox.checked) {
335
+
336
+ checkbox.indeterminate = false;
337
+ }
338
+
339
+ } else {
340
+
341
+ if(this.featureOptions.isValue(featureOption)) {
342
+
343
+ initialValue = this.featureOptions.value(checkbox.id, (initialScope === "controller") ? this.controller : undefined);
344
+ }
345
+
346
+ checkbox.readOnly = checkbox.indeterminate = true;
347
+ }
348
+
349
+ break;
350
+
351
+ case "device":
352
+ case "none":
353
+ default:
354
+
355
+ if(this.featureOptions.isValue(featureOption)) {
356
+
357
+ checkbox.checked = this.featureOptions.exists(featureOption, currentDevice?.serial);
358
+ initialValue = this.featureOptions.value(checkbox.id, currentDevice?.serial);
359
+ } else {
360
+
361
+ checkbox.checked = this.featureOptions.test(featureOption, currentDevice?.serial);
362
+ }
363
+
364
+ break;
365
+ }
366
+
367
+ checkbox.defaultChecked = option.default;
368
+ checkbox.classList.add("mx-2");
369
+
370
+ // Add the checkbox to the table cell.
371
+ tdCheckbox.appendChild(checkbox);
372
+
373
+ // Add the checkbox to the table row.
374
+ trX.appendChild(tdCheckbox);
375
+
376
+ const tdLabel = document.createElement("td");
377
+ tdLabel.classList.add("w-100");
378
+ tdLabel.colSpan = 2;
379
+
380
+ let inputValue = null;
381
+
382
+ // Add an input field if we have a value-centric feature option.
383
+ if(this.featureOptions.isValue(featureOption)) {
384
+
385
+ const tdInput = document.createElement("td");
386
+ tdInput.classList.add("mr-2");
387
+ tdInput.style.width = "10%";
388
+
389
+ inputValue = document.createElement("input");
390
+ inputValue.type = "text";
391
+ inputValue.value = initialValue ?? option.defaultValue;
392
+ inputValue.size = 5;
393
+ inputValue.readOnly = !checkbox.checked;
394
+
395
+ // Add or remove the setting from our configuration when we've changed our state.
396
+ inputValue.addEventListener("change", async () => {
397
+
398
+ // Find the option in our list and delete it if it exists.
399
+ const optionRegex = new RegExp("^(?:Enable|Disable)\\." + checkbox.id + (!currentDevice ? "" : ("\\." + currentDevice.serial)) + "\\.[^\\.]+$", "gi");
400
+ const newOptions = this.featureOptions.configuredOptions.filter(x => !optionRegex.test(x));
401
+
402
+ if(checkbox.checked) {
403
+
404
+ newOptions.push("Enable." + checkbox.value + "." + inputValue.value);
405
+ } else if(checkbox.indeterminate) {
406
+
407
+ // If we're in an indeterminate state, we need to traverse the tree to get the upstream value we're inheriting.
408
+ inputValue.value = (currentDevice?.serial !== this.controller) ?
409
+ (this.featureOptions.value(checkbox.id, this.controller) ?? this.featureOptions.value(checkbox.id)) :
410
+ (this.featureOptions.value(checkbox.id) ?? option.defaultValue);
411
+ } else {
412
+
413
+ inputValue.value = option.defaultValue;
414
+ }
415
+
416
+ // Update our configuration in Homebridge.
417
+ this.currentConfig[0].options = newOptions;
418
+ this.featureOptions.configuredOptions = newOptions;
419
+ await homebridge.updatePluginConfig(this.currentConfig);
420
+ });
421
+
422
+ tdInput.appendChild(inputValue);
423
+ trX.appendChild(tdInput);
424
+ }
425
+
426
+ // Create a label for the checkbox with our option description.
427
+ const labelDescription = document.createElement("label");
428
+ labelDescription.for = checkbox.id;
429
+ labelDescription.style.cursor = "pointer";
430
+ labelDescription.classList.add("user-select-none", "my-0", "py-0");
431
+
432
+ // Highlight options for the user that are different than our defaults.
433
+ const scopeColor = this.featureOptions.color(featureOption, currentDevice?.serial);
434
+
435
+ if(scopeColor) {
436
+
437
+ labelDescription.classList.add(scopeColor);
438
+ }
439
+
440
+ // Add or remove the setting from our configuration when we've changed our state.
441
+ checkbox.addEventListener("change", async () => {
442
+
443
+ // Find the option in our list and delete it if it exists.
444
+ const optionRegex = new RegExp("^(?:Enable|Disable)\\." + checkbox.id + (!currentDevice ? "" : ("\\." + currentDevice.serial)) + "$", "gi");
445
+ const newOptions = this.featureOptions.configuredOptions.filter(x => !optionRegex.test(x));
446
+
447
+ // Figure out if we've got the option set upstream.
448
+ let upstreamOption = false;
449
+
450
+ // We explicitly want to check for the scope of the feature option above where we are now, so we can appropriately determine what we should show.
451
+ switch(this.featureOptions.scope(checkbox.id, (currentDevice && (currentDevice.serial !== this.controller)) ? this.controller : undefined)) {
452
+
453
+ case "device":
454
+ case "controller":
455
+
456
+ if(currentDevice.serial !== this.controller) {
457
+
458
+ upstreamOption = true;
459
+ }
460
+
461
+ break;
462
+
463
+ case "global":
464
+
465
+ if(currentDevice) {
466
+
467
+ upstreamOption = true;
468
+ }
469
+
470
+ break;
471
+
472
+ default:
473
+
474
+ break;
475
+ }
476
+
477
+ // For value-centric feature options, if there's an upstream value assigned above us, we don't allow for an unchecked state as it doesn't make sense in this
478
+ // context.
479
+ if(checkbox.readOnly && (!this.featureOptions.isValue(featureOption) || (this.featureOptions.isValue(featureOption) && inputValue && !upstreamOption))) {
480
+
481
+ // We're truly unchecked. We need this because a checkbox can be in both an unchecked and indeterminate simultaneously,
482
+ // so we use the readOnly property to let us know that we've just cycled from an indeterminate state.
483
+ checkbox.checked = checkbox.readOnly = false;
484
+ } else if(!checkbox.checked) {
485
+
486
+ // If we have an upstream option configured, we reveal a third state to show inheritance of that option and allow the user to select it.
487
+ if(upstreamOption) {
488
+
489
+ // We want to set the readOnly property as well, since it will survive a user interaction when they click the checkbox to clear out the
490
+ // indeterminate state. This allows us to effectively cycle between three states.
491
+ checkbox.readOnly = checkbox.indeterminate = true;
492
+ }
493
+
494
+ if(this.featureOptions.isValue(featureOption) && inputValue) {
495
+
496
+ inputValue.readOnly = true;
497
+ }
498
+ } else if(checkbox.checked) {
499
+
500
+ // We've explicitly checked this option.
501
+ checkbox.readOnly = checkbox.indeterminate = false;
502
+
503
+ if(this.featureOptions.isValue(featureOption) && inputValue) {
504
+
505
+ inputValue.readOnly = false;
506
+ }
507
+ }
508
+
509
+ // The setting is different from the default, highlight it for the user, accounting for upstream scope, and add it to our configuration.
510
+ if(!checkbox.indeterminate && ((checkbox.checked !== option.default) || upstreamOption)) {
511
+
512
+ labelDescription.classList.add("text-info");
513
+ newOptions.push((checkbox.checked ? "Enable." : "Disable.") + checkbox.value);
514
+ } else {
515
+
516
+ // We've reset to the defaults, remove our highlighting.
517
+ labelDescription.classList.remove("text-info");
518
+ }
519
+
520
+ // Update our Homebridge configuration.
521
+ if(this.featureOptions.isValue(featureOption) && inputValue) {
522
+
523
+ // Inform our value-centric feature option to update Homebridge.
524
+ const changeEvent = new Event("change");
525
+
526
+ inputValue.dispatchEvent(changeEvent);
527
+ } else {
528
+
529
+ // Update our configuration in Homebridge.
530
+ this.currentConfig[0].options = newOptions;
531
+ this.featureOptions.configuredOptions = newOptions;
532
+ await homebridge.updatePluginConfig(this.currentConfig);
533
+ }
534
+
535
+ // If we've reset to defaults, make sure our color coding for scope is reflected.
536
+ if((checkbox.checked === option.default) || checkbox.indeterminate) {
537
+
538
+ const scopeColor = this.featureOptions.color(featureOption, currentDevice?.serial);
539
+
540
+ if(scopeColor) {
541
+
542
+ labelDescription.classList.add(scopeColor);
543
+ }
544
+ }
545
+
546
+ // Adjust visibility of other feature options that depend on us.
547
+ if(this.featureOptions.groups[checkbox.id]) {
548
+
549
+ const entryVisibility = this.featureOptions.test(featureOption, currentDevice?.serial) ? "" : "none";
550
+
551
+ // Lookup each feature option setting and set the visibility accordingly.
552
+ for(const entry of this.featureOptions.groups[checkbox.id]) {
553
+
554
+ document.getElementById("row-" + entry).style.display = entryVisibility;
555
+ }
556
+ }
557
+ });
558
+
559
+ // Add the actual description for the option after the checkbox.
560
+ labelDescription.appendChild(document.createTextNode(option.description));
561
+
562
+ // Add the label to the table cell.
563
+ tdLabel.appendChild(labelDescription);
564
+
565
+ // Provide a cell-wide target to click on options.
566
+ tdLabel.addEventListener("click", () => checkbox.click());
567
+
568
+ // Add the label table cell to the table row.
569
+ trX.appendChild(tdLabel);
570
+
571
+ // Adjust the visibility of the feature option, if it's logically grouped.
572
+ if((option.group !== undefined) && !this.featureOptions.test(category.name + (option.group.length ? ("." + option.group) : ""), currentDevice?.serial)) {
573
+
574
+ trX.style.display = "none";
575
+ } else {
576
+
577
+ // Increment the visible option count.
578
+ optionsVisibleCount++;
579
+ }
580
+
581
+ // Add the table row to the table body.
582
+ tbody.appendChild(trX);
583
+ }
584
+
585
+ // Add the table body to the table.
586
+ optionTable.appendChild(tbody);
587
+
588
+ // If we have no options visible in a given category, then hide the entire category.
589
+ if(!optionsVisibleCount) {
590
+
591
+ optionTable.style.display = "none";
592
+ }
593
+
594
+ // Add the table to the page.
595
+ this.configTable.appendChild(optionTable);
596
+ }
597
+
598
+ homebridge.hideSpinner();
599
+ }
600
+ }
@@ -0,0 +1,9 @@
1
+ /// <reference types="node" />
2
+ export interface HomebridgePluginLogging {
3
+ debug: (message: string, ...parameters: unknown[]) => void;
4
+ error: (message: string, ...parameters: unknown[]) => void;
5
+ info: (message: string, ...parameters: unknown[]) => void;
6
+ warn: (message: string, ...parameters: unknown[]) => void;
7
+ }
8
+ export declare function sleep(sleepTimer: number): Promise<NodeJS.Timeout>;
9
+ export declare function retry(operation: () => Promise<boolean>, retryInterval: number): Promise<boolean>;
package/dist/utils.js ADDED
@@ -0,0 +1,20 @@
1
+ /* Copyright(C) 2017-2024, HJD (https://github.com/hjdhjd). All rights reserved.
2
+ *
3
+ * utils.ts: Useful utility functions when writing TypeScript.
4
+ */
5
+ // Emulate a sleep function.
6
+ export function sleep(sleepTimer) {
7
+ return new Promise(resolve => setTimeout(resolve, sleepTimer));
8
+ }
9
+ // Retry an operation until we're successful.
10
+ export async function retry(operation, retryInterval) {
11
+ // Try the operation that was requested.
12
+ if (!(await operation())) {
13
+ // If the operation wasn't successful, let's sleep for the requested interval and try again.
14
+ await sleep(retryInterval);
15
+ return retry(operation, retryInterval);
16
+ }
17
+ // We were successful - we're done.
18
+ return true;
19
+ }
20
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAUH,4BAA4B;AAC5B,MAAM,UAAU,KAAK,CAAC,UAAkB;IAEtC,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC;AACjE,CAAC;AAED,6CAA6C;AAC7C,MAAM,CAAC,KAAK,UAAU,KAAK,CAAC,SAAiC,EAAE,aAAqB;IAElF,wCAAwC;IACxC,IAAG,CAAC,CAAC,MAAM,SAAS,EAAE,CAAC,EAAE,CAAC;QAExB,4FAA4F;QAC5F,MAAM,KAAK,CAAC,aAAa,CAAC,CAAC;QAC3B,OAAO,KAAK,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC;IACzC,CAAC;IAED,mCAAmC;IACnC,OAAO,IAAI,CAAC;AACd,CAAC"}
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "homebridge-plugin-utils",
3
+ "version": "1.0.0",
4
+ "displayName": "Homebridge Plugin Utilities",
5
+ "description": "Opinionated utilities to provide common capabilities and create rich configuration webUI experiences for Homebridge plugins.",
6
+ "author": {
7
+ "name": "HJD",
8
+ "url": "https://github.com/hjdhjd"
9
+ },
10
+ "homepage": "https://github.com/hjdhjd/homebridge-plugin-utils#readme",
11
+ "license": "ISC",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git://github.com/hjdhjd/homebridge-plugin-utils.git"
15
+ },
16
+ "bugs": {
17
+ "url": "http://github.com/hjdhjd/homebridge-plugin-utils/issues"
18
+ },
19
+ "type": "module",
20
+ "engines": {
21
+ "node": ">=18"
22
+ },
23
+ "keywords": [
24
+ "homebridge",
25
+ "homebridge-developer",
26
+ "homebridge-plugin-developer",
27
+ "homekit",
28
+ "homekit secure video",
29
+ "hksv",
30
+ "camera"
31
+ ],
32
+ "scripts": {
33
+ "build": "npm run clean && tsc && shx cp dist/featureoptions.js{,.map} dist/ui",
34
+ "build-ui": "shx mkdir -p dist/ui && shx cp ui/**.mjs dist/ui",
35
+ "clean": "shx rm -rf dist && npm run build-ui",
36
+ "lint": "eslint eslint.config.mjs build/**.mjs src/**.ts \"ui/**/*.@(js|mjs)\"",
37
+ "postpublish": "npm run clean",
38
+ "prepublishOnly": "npm run lint && npm run build"
39
+ },
40
+ "main": "dist/index.js",
41
+ "devDependencies": {
42
+ "@stylistic/eslint-plugin": "2.1.0",
43
+ "@types/node": "20.12.12",
44
+ "eslint": "8.57.0",
45
+ "shx": "^0.3.4",
46
+ "typescript": "5.4.5",
47
+ "typescript-eslint": "^7.9.0"
48
+ },
49
+ "dependencies": {
50
+ "mqtt": "^5.6.1"
51
+ }
52
+ }