homebridge-plugin-utils 1.0.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/dist/ui/webUi.mjs CHANGED
@@ -4,22 +4,43 @@
4
4
  */
5
5
  "use strict";
6
6
 
7
+ import { webUiFeatureOptions } from "./webUi-featureoptions.mjs";
8
+
7
9
  export class webUi {
8
10
 
9
11
  // Feature options class instance.
10
- #featureOptions;
12
+ featureOptions;
11
13
 
12
- // Homebridge class instance.
13
- #homebridge;
14
+ // First run webUI callback endpoints for customization.
15
+ #firstRun;
14
16
 
15
17
  // Plugin name.
16
18
  #name;
17
19
 
18
- constructor({ name, featureOptions, homebridge } = {}) {
19
-
20
- this.homebridge = homebridge;
21
- this.featureOptions = featureOptions;
20
+ /**
21
+ * featureOptions - parameters to webUiFeatureOptions.
22
+ * firstRun - first run handlers:
23
+ * isRequired - do we need to run the first run UI workflow?
24
+ * onStart - initialization for the first run webUI to populate forms and other startup tasks.
25
+ * onSubmit - execute the first run workflow, typically a login or configuration validation of some sort.
26
+ * name - plugin name.
27
+ */
28
+ constructor({ featureOptions, firstRun = {}, name } = {}) {
29
+
30
+ // Defaults for our first run handlers.
31
+ this.firstRun = { isRequired: () => false, onStart: () => true, onSubmit: () => true };
32
+
33
+ // Figure out the options passed in to us.
34
+ this.featureOptions = new webUiFeatureOptions(featureOptions);
35
+ this.firstRun = Object.assign({}, this.firstRun, firstRun);
22
36
  this.name = name;
37
+ }
38
+
39
+ /**
40
+ * Render the webUI.
41
+ */
42
+ // Render the UI.
43
+ show() {
23
44
 
24
45
  // Fire off our UI, catching errors along the way.
25
46
  try {
@@ -28,11 +49,11 @@ export class webUi {
28
49
  } catch(err) {
29
50
 
30
51
  // If we had an error instantiating or updating the UI, notify the user.
31
- this.homebridge.toast.error(err.message, "Error");
52
+ homebridge.toast.error(err.message, "Error");
32
53
  } finally {
33
54
 
34
55
  // Always leave the UI in a usable place for the end user.
35
- this.homebridge.hideSpinner();
56
+ homebridge.hideSpinner();
36
57
  }
37
58
  }
38
59
 
@@ -41,31 +62,31 @@ export class webUi {
41
62
 
42
63
  const buttonFirstRun = document.getElementById("firstRun");
43
64
 
65
+ // Run a custom initialization handler the user may have provided.
66
+ if(!(await this.#processHandler(this.firstRun.onStart))) {
67
+
68
+ return;
69
+ }
70
+
44
71
  // First run user experience.
45
72
  buttonFirstRun.addEventListener("click", async () => {
46
73
 
47
74
  // 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();
75
+ homebridge.showSpinner();
52
76
 
53
- // Sort it for posterity.
54
- devices?.sort((a, b) => {
77
+ // Run a custom submit handler the user may have provided.
78
+ if(!(await this.#processHandler(this.firstRun.onSubmit))) {
55
79
 
56
- const aCase = (a.displayName ?? "").toLowerCase();
57
- const bCase = (b.displayName ?? "").toLowerCase();
58
-
59
- return aCase > bCase ? 1 : (bCase > aCase ? -1 : 0);
60
- });
80
+ return;
81
+ }
61
82
 
62
83
  // Create our UI.
63
84
  document.getElementById("pageFirstRun").style.display = "none";
64
85
  document.getElementById("menuWrapper").style.display = "inline-flex";
65
- this.featureOptions.showUI();
86
+ this.featureOptions.show();
66
87
 
67
88
  // All done. Let the user interact with us, although in practice, we shouldn't get here.
68
- // this.homebridge.hideSpinner();
89
+ // homebridge.hideSpinner();
69
90
  });
70
91
 
71
92
  document.getElementById("pageFirstRun").style.display = "block";
@@ -75,81 +96,89 @@ export class webUi {
75
96
  #showSettings() {
76
97
 
77
98
  // Show the beachball while we setup.
78
- this.homebridge.showSpinner();
99
+ homebridge.showSpinner();
79
100
 
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");
101
+ // Highlight the tab in our UI.
102
+ this.#toggleClasses("menuHome", "btn-elegant", "btn-primary");
103
+ this.#toggleClasses("menuFeatureOptions", "btn-elegant", "btn-primary");
104
+ this.#toggleClasses("menuSettings", "btn-primary", "btn-elegant");
87
105
 
88
106
  document.getElementById("pageSupport").style.display = "none";
89
107
  document.getElementById("pageFeatureOptions").style.display = "none";
90
108
 
91
- this.homebridge.showSchemaForm();
109
+ homebridge.showSchemaForm();
92
110
 
93
111
  // All done. Let the user interact with us.
94
- this.homebridge.hideSpinner();
112
+ homebridge.hideSpinner();
95
113
  }
96
114
 
97
115
  // Show the support tab.
98
116
  #showSupport() {
99
117
 
100
118
  // Show the beachball while we setup.
101
- this.homebridge.showSpinner();
102
- this.homebridge.hideSchemaForm();
119
+ homebridge.showSpinner();
120
+ homebridge.hideSchemaForm();
103
121
 
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");
122
+ // Highlight the tab in our UI.
123
+ this.#toggleClasses("menuHome", "btn-primary", "btn-elegant");
124
+ this.#toggleClasses("menuFeatureOptions", "btn-elegant", "btn-primary");
125
+ this.#toggleClasses("menuSettings", "btn-elegant", "btn-primary");
111
126
 
112
127
  document.getElementById("pageSupport").style.display = "block";
113
128
  document.getElementById("pageFeatureOptions").style.display = "none";
114
129
 
115
130
  // All done. Let the user interact with us.
116
- this.homebridge.hideSpinner();
131
+ homebridge.hideSpinner();
117
132
  }
118
133
 
119
134
  // Launch our webUI.
120
135
  async #launchWebUI() {
121
136
 
122
137
  // Retrieve the current plugin configuration.
123
- this.featureOptions.currentConfig = await this.homebridge.getPluginConfig();
138
+ this.featureOptions.currentConfig = await homebridge.getPluginConfig();
124
139
 
125
140
  // Add our event listeners to animate the UI.
126
141
  document.getElementById("menuHome").addEventListener("click", () => this.#showSupport());
127
- document.getElementById("menuFeatureOptions").addEventListener("click", () => this.featureOptions.showUI());
142
+ document.getElementById("menuFeatureOptions").addEventListener("click", () => this.featureOptions.show());
128
143
  document.getElementById("menuSettings").addEventListener("click", () => this.#showSettings());
129
144
 
130
145
  // Get the list of devices the plugin knows about.
131
- const devices = await this.homebridge.getCachedAccessories();
146
+ const devices = await homebridge.getCachedAccessories();
132
147
 
133
148
  // 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) {
149
+ if(this.featureOptions.currentConfig.length && devices?.length && !(await this.#processHandler(this.firstRun.isRequired))) {
135
150
 
136
151
  document.getElementById("menuWrapper").style.display = "inline-flex";
137
- this.featureOptions.showUI();
152
+ this.featureOptions.show();
153
+
138
154
  return;
139
155
  }
140
156
 
141
- // If we have no configuration, let's create one.
142
- if(!this.featureOptions.currentConfig.length) {
157
+ // If we have the name property set for the plugin configuration yet, let's do so now. If we don't have a configuration, let's initialize it as well.
158
+ (this.featureOptions.currentConfig[0] ??= { name: this.name }).name ??= this.name;
143
159
 
144
- this.featureOptions.currentConfig.push({ name: this.name });
145
- } else if(!("name" in this.featureOptions.currentConfig[0])) {
160
+ // Update the plugin configuration and launch the first run UI.
161
+ await homebridge.updatePluginConfig(this.featureOptions.currentConfig);
162
+ this.#showFirstRun();
163
+ }
146
164
 
147
- // If we haven't set the name, let's do so now.
148
- this.featureOptions.currentConfig[0].name = this.name;
165
+ // Utility to process user-provided custom handlers that can handle both synchronous and asynchronous handlers.
166
+ async #processHandler(handler) {
167
+
168
+ if(((typeof handler === "function") && !(await handler())) || ((typeof handler !== "function") && !handler)) {
169
+
170
+ return false;
149
171
  }
150
172
 
151
- // Update the plugin configuration and launch the first run UI.
152
- await this.homebridge.updatePluginConfig(this.featureOptions.currentConfig);
153
- this.#showFirstRun();
173
+ return true;
174
+ }
175
+
176
+ // Utility to toggle our classes.
177
+ #toggleClasses(id, removeClass, addClass) {
178
+
179
+ const element = document.getElementById(id);
180
+
181
+ element.classList.remove(removeClass);
182
+ element.classList.add(addClass);
154
183
  }
155
184
  }
package/dist/utils.d.ts CHANGED
@@ -1,4 +1,26 @@
1
1
  /// <reference types="node" />
2
+ /**
3
+ * @internal
4
+ *
5
+ * A utility type that recursively makes all properties of an object, including nested objects, optional. This should only be used on JSON objects only. Otherwise,
6
+ * you're going to end up with class methods marked as optional as well. Credit for this belongs to: https://github.com/joonhocho/tsdef.
7
+ *
8
+ * @template T - The type to make recursively partial.
9
+ */
10
+ export type DeepPartial<T> = {
11
+ [P in keyof T]?: T[P] extends Array<infer I> ? Array<DeepPartial<I>> : DeepPartial<T[P]>;
12
+ };
13
+ /**
14
+ * @internal
15
+ *
16
+ * A utility type that recursively makes all properties of an object, including nested objects, optional. This should only be used on JSON objects only. Otherwise,
17
+ * you're going to end up with class methods marked as optional as well. Credit for this belongs to: https://github.com/joonhocho/tsdef.
18
+ *
19
+ * @template T - The type to make recursively partial.
20
+ */
21
+ export type DeepReadonly<T> = {
22
+ readonly [P in keyof T]: T[P] extends Array<infer I> ? Array<DeepReadonly<I>> : DeepReadonly<T[P]>;
23
+ };
2
24
  export interface HomebridgePluginLogging {
3
25
  debug: (message: string, ...parameters: unknown[]) => void;
4
26
  error: (message: string, ...parameters: unknown[]) => void;
package/dist/utils.js.map CHANGED
@@ -1 +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"}
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA4CH,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;QAE3B,OAAO,KAAK,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC;IACzC,CAAC;IAED,mCAAmC;IACnC,OAAO,IAAI,CAAC;AACd,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-plugin-utils",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "displayName": "Homebridge Plugin Utilities",
5
5
  "description": "Opinionated utilities to provide common capabilities and create rich configuration webUI experiences for Homebridge plugins.",
6
6
  "author": {
@@ -40,13 +40,13 @@
40
40
  "main": "dist/index.js",
41
41
  "devDependencies": {
42
42
  "@stylistic/eslint-plugin": "2.1.0",
43
- "@types/node": "20.12.12",
43
+ "@types/node": "20.13.0",
44
44
  "eslint": "8.57.0",
45
45
  "shx": "^0.3.4",
46
46
  "typescript": "5.4.5",
47
- "typescript-eslint": "^7.9.0"
47
+ "typescript-eslint": "^7.11.0"
48
48
  },
49
49
  "dependencies": {
50
- "mqtt": "^5.6.1"
50
+ "mqtt": "^5.7.0"
51
51
  }
52
52
  }