@homebridge-plugins/homebridge-resideo 3.0.3-beta.16 → 3.0.3-beta.18

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.
@@ -131,6 +131,13 @@
131
131
  </div>
132
132
  <script type="module">
133
133
  import { webUi } from "./lib/webUi.mjs";
134
+ import { OAuthModule } from "./lib/oauth-module.mjs";
135
+ import { ConfigModule } from "./lib/config-module.mjs";
136
+ import { UIUtils } from "./lib/ui-utils.mjs";
137
+
138
+ // Initialize modules
139
+ const oauthModule = new OAuthModule();
140
+ const configModule = new ConfigModule();
134
141
 
135
142
  // Initialize the webUI with homebridge-plugin-utils
136
143
  const ui = new webUi({
@@ -152,163 +159,62 @@
152
159
  }
153
160
  },
154
161
  firstRun: {
155
- isRequired: () => {
156
- // Check if we have complete credentials to skip first-run setup
157
- const currentConfig = homebridge.getPluginConfig();
158
- if (!Array.isArray(currentConfig) || !currentConfig[0]) {
159
- return true;
160
- }
161
-
162
- const config = currentConfig[0];
163
- const credentials = config?.credentials;
164
-
165
- // Check for valid credentials - we need at least consumerKey, consumerSecret and refreshToken
166
- // accessToken can be regenerated from refreshToken, so it's not strictly required for skipping first-run
167
- if (!credentials) {
168
- return true;
169
- }
170
-
171
- const hasMinimumCredentials = credentials.consumerKey &&
172
- credentials.consumerKey.trim() !== '' &&
173
- credentials.consumerSecret &&
174
- credentials.consumerSecret.trim() !== '' &&
175
- credentials.refreshToken &&
176
- credentials.refreshToken.trim() !== '';
177
-
178
- // Return true if first-run is required (missing minimum credentials), false if can skip
179
- return !hasMinimumCredentials;
180
- },
162
+ isRequired: () => configModule.isFirstRunRequired(),
181
163
  onStart: () => {
182
- // Simple first run initialization - no legacy config handling
164
+ // Clean first run initialization
183
165
  return true;
184
166
  },
185
167
  onSubmit: async () => {
186
- // Handle first run setup with clean config creation
187
- const key = document.getElementById('firstRunKey')?.value?.trim();
188
- const secret = document.getElementById('firstRunSecret')?.value?.trim();
189
- const errorDiv = document.getElementById('loginError');
190
-
191
- // Clear any previous errors
192
- if (errorDiv) {
193
- errorDiv.innerHTML = '&nbsp;';
194
- }
195
-
196
- // Validate input fields
197
- if (!key || !secret) {
198
- if (errorDiv) {
199
- errorDiv.innerHTML = '<div class="alert alert-danger"><strong>Error:</strong> Please enter both Consumer Key and Consumer Secret.</div>';
200
- }
201
- return false;
202
- }
168
+ // Handle first run setup with modular approach
169
+ const key = UIUtils.getFieldValue('firstRunKey');
170
+ const secret = UIUtils.getFieldValue('firstRunSecret');
171
+
172
+ UIUtils.clearMessage('loginError');
173
+ UIUtils.storeButtonText('firstRun');
174
+ UIUtils.setButtonLoading('firstRun', true);
203
175
 
204
- // Basic validation for credential format
205
- if (key.length < 10 || secret.length < 10) {
206
- if (errorDiv) {
207
- errorDiv.innerHTML = '<div class="alert alert-danger"><strong>Error:</strong> Consumer Key and Secret appear to be too short. Please verify your credentials.</div>';
208
- }
176
+ // Validate credentials
177
+ const validation = oauthModule.validateCredentials(key, secret);
178
+ if (!validation.valid) {
179
+ UIUtils.showError('loginError', validation.message);
180
+ UIUtils.setButtonLoading('firstRun', false);
209
181
  return false;
210
182
  }
211
183
 
212
184
  try {
213
- // Create clean config structure for first version
214
- const currentConfig = [{
215
- platform: 'Resideo',
216
- name: 'Resideo',
217
- credentials: {
218
- consumerKey: key,
219
- consumerSecret: secret
220
- }
221
- }];
222
-
223
- await homebridge.updatePluginConfig(currentConfig);
185
+ // Create initial config
186
+ const initialConfig = configModule.createInitialConfig(key, secret);
187
+ const updateResult = await configModule.updateConfig(initialConfig);
224
188
 
225
- // Now trigger the OAuth flow
226
- await homebridge.request('Start Resideo Login Server');
189
+ if (!updateResult.success) {
190
+ throw new Error(updateResult.error || 'Failed to update configuration');
191
+ }
192
+
193
+ // Start OAuth flow
194
+ const popup = await oauthModule.startOAuthFlow(key, secret);
227
195
 
228
- // Open OAuth popup
229
- const hostname = window.location.hostname;
230
- const w = 450;
231
- const h = 700;
232
- const y = window.top.outerHeight / 2 + window.top.screenY - (h / 2);
233
- const x = window.top.outerWidth / 2 + window.top.screenX - (w / 2);
234
- const urlToOpen = 'http://' + hostname + ':8585/start?' +
235
- 'key=' + encodeURIComponent(key) +
236
- '&secret=' + encodeURIComponent(secret) +
237
- '&host=' + encodeURIComponent(hostname);
196
+ // Verify OAuth completion
197
+ const result = await oauthModule.verifyOAuthCompletion(popup);
238
198
 
239
- const popup = window.open(
240
- urlToOpen, 'resideo-auth',
241
- 'toolbar=no, location=no, directories=no, status=no, menubar=no scrollbars=no, resizable=no, copyhistory=no, ' +
242
- 'width=' + w + ', height=' + h + ', top=' + y + ', left=' + x,
243
- );
244
-
245
- return new Promise((resolve) => {
246
- const checkClosed = setInterval(() => {
247
- if (popup.closed) {
248
- clearInterval(checkClosed);
249
-
250
- // Enhanced credential verification with multiple checks and longer timeout
251
- let verificationAttempts = 0;
252
- const maxAttempts = 10; // Check up to 10 times
253
-
254
- const verifyCredentials = () => {
255
- verificationAttempts++;
256
-
257
- // Check if authentication completed successfully
258
- const updatedConfig = homebridge.getPluginConfig();
259
- const credentials = Array.isArray(updatedConfig) && updatedConfig[0]?.credentials;
260
-
261
- // We need at least consumerKey, consumerSecret, and refreshToken for success
262
- const hasMinimumAuth = credentials?.consumerKey &&
263
- credentials?.consumerSecret &&
264
- credentials?.refreshToken;
265
-
266
- const hasCompleteAuth = hasMinimumAuth && credentials?.accessToken;
267
-
268
- if (hasCompleteAuth) {
269
- if (errorDiv) {
270
- errorDiv.innerHTML = '<div class="alert alert-success"><strong>Success!</strong> Your account has been linked successfully.</div>';
271
- }
272
- // Force UI refresh to update isRequired state
273
- setTimeout(() => {
274
- resolve(true);
275
- }, 100);
276
- } else if (hasMinimumAuth && verificationAttempts >= maxAttempts) {
277
- // We have minimum credentials but no access token after max attempts
278
- // This is actually acceptable since access token can be refreshed
279
- if (errorDiv) {
280
- errorDiv.innerHTML = '<div class="alert alert-success"><strong>Success!</strong> Your account has been linked successfully.</div>';
281
- }
282
- setTimeout(() => {
283
- resolve(true);
284
- }, 100);
285
- } else if (verificationAttempts >= maxAttempts) {
286
- if (errorDiv) {
287
- errorDiv.innerHTML = '<div class="alert alert-warning"><strong>Authentication incomplete.</strong> Please try the setup process again or check your Resideo developer credentials.</div>';
288
- }
289
- resolve(false);
290
- } else {
291
- // Continue checking
292
- setTimeout(verifyCredentials, 1000);
293
- }
294
- };
295
-
296
- // Start verification after a short delay
297
- setTimeout(verifyCredentials, 500);
298
- }
299
- }, 1000);
300
- });
301
- } catch (error) {
302
- console.error('Resideo setup error:', error);
303
- if (errorDiv) {
304
- if (error.message.includes('port')) {
305
- errorDiv.innerHTML = '<div class="alert alert-danger"><strong>Error:</strong> OAuth server port conflict. Please wait a moment and try again.</div>';
306
- } else if (error.message.includes('network') || error.message.includes('fetch')) {
307
- errorDiv.innerHTML = '<div class="alert alert-danger"><strong>Error:</strong> Network error. Please check your connection and try again.</div>';
308
- } else {
309
- errorDiv.innerHTML = `<div class="alert alert-danger"><strong>Error:</strong> Setup failed. ${error.message || 'Please try again.'}</div>`;
310
- }
199
+ if (result.success) {
200
+ UIUtils.showSuccess('loginError', 'Your account has been linked successfully.');
201
+ UIUtils.setButtonLoading('firstRun', false);
202
+
203
+ // Force UI refresh after short delay
204
+ setTimeout(() => {
205
+ return true;
206
+ }, 100);
207
+
208
+ return true;
209
+ } else {
210
+ UIUtils.showWarning('loginError', result.message || 'Authentication incomplete. Please try again.');
211
+ UIUtils.setButtonLoading('firstRun', false);
212
+ return false;
311
213
  }
214
+ } catch (error) {
215
+ console.error('First run setup error:', error);
216
+ UIUtils.handleError(error, 'loginError');
217
+ UIUtils.setButtonLoading('firstRun', false);
312
218
  return false;
313
219
  }
314
220
  }
@@ -316,64 +222,29 @@
316
222
  name: 'homebridge-resideo'
317
223
  });
318
224
 
319
- // Legacy functionality for existing UI elements - simplified for webUi compatibility
320
- const setupLegacyHandlers = () => {
321
- const hostname = window.location.hostname;
322
-
323
- homebridge.addEventListener('creds-received', async event => {
324
- try {
325
- if (event.data.access && event.data.refresh) {
326
- // Create clean config structure for first version
327
- const currentConfig = [{
328
- platform: 'Resideo',
329
- name: 'Resideo',
330
- credentials: {
331
- consumerKey: event.data.key,
332
- consumerSecret: event.data.secret,
333
- accessToken: event.data.access,
334
- refreshToken: event.data.refresh
335
- }
336
- }];
337
-
338
- await homebridge.updatePluginConfig(currentConfig);
339
- await homebridge.savePluginConfig();
340
-
341
- // Notify success and give the UI time to process the credential update
342
- setTimeout(() => {
343
- homebridge.toast.success("Successfully Linked Resideo Account", "homebridge-resideo");
344
- }, 200);
345
- }
346
- } catch (err) {
347
- console.log(err);
348
- homebridge.toast.error('Setup failed. Please try again.', 'Error');
349
- }
350
- });
351
-
352
- window.copyMyText = () => {
353
- const textToCopy = document.getElementById("copyMe");
354
- if (textToCopy) {
355
- textToCopy.select();
356
- document.execCommand("copy");
357
- }
358
- };
225
+ // Setup OAuth credential handler and other legacy functionality
226
+ const setupLegacyIntegration = () => {
227
+ // Setup OAuth credential reception
228
+ oauthModule.setupCredentialHandler();
359
229
 
360
- // Initialize legacy components for backwards compatibility
361
- setupLegacyMenus();
362
- };
363
-
364
- const setupLegacyMenus = () => {
365
- // Legacy menu functionality removed for first version approach
366
- // The webUi handles all navigation automatically
230
+ // Setup copy to clipboard functionality
231
+ UIUtils.setupCopyToClipboard();
232
+
233
+ // Initialize callback URL display
234
+ const hostname = window.location.hostname;
235
+ const callbackUrl = `http://${hostname}:8585/auth`;
236
+ UIUtils.setFieldValue('copyMe', callbackUrl);
367
237
  };
368
238
 
369
- // Show the webUI
239
+ // Initialize the application
370
240
  (async () => {
371
241
  try {
372
242
  await homebridge.request('Start Resideo Login Server');
373
- setupLegacyHandlers();
243
+ setupLegacyIntegration();
374
244
  ui.show();
375
245
  } catch (err) {
376
- homebridge.toast.error(err.message, 'Error');
246
+ console.error('Initialization error:', err);
247
+ homebridge.toast.error(err.message || 'Failed to initialize plugin UI', 'Error');
377
248
  }
378
249
  })();
379
250
  </script>
@@ -0,0 +1,94 @@
1
+ /* Copyright(C) 2017-2025, homebridge-plugins/homebridge-resideo developers. All rights reserved.
2
+ *
3
+ * config-module.mjs: Configuration management module for Resideo plugin.
4
+ */
5
+
6
+ /**
7
+ * Configuration management module for Resideo plugin.
8
+ */
9
+ export class ConfigModule {
10
+ constructor() {
11
+ // Initialize configuration management
12
+ }
13
+
14
+ /**
15
+ * Checks if first-run setup is required.
16
+ */
17
+ isFirstRunRequired() {
18
+ const currentConfig = homebridge.getPluginConfig();
19
+ if (!Array.isArray(currentConfig) || !currentConfig[0]) {
20
+ return true;
21
+ }
22
+
23
+ const config = currentConfig[0];
24
+ const credentials = config?.credentials;
25
+
26
+ if (!credentials) {
27
+ return true;
28
+ }
29
+
30
+ // Check for minimum credentials needed to skip first-run
31
+ const hasMinimumCredentials = credentials.consumerKey &&
32
+ credentials.consumerKey.trim() !== '' &&
33
+ credentials.consumerSecret &&
34
+ credentials.consumerSecret.trim() !== '' &&
35
+ credentials.refreshToken &&
36
+ credentials.refreshToken.trim() !== '';
37
+
38
+ return !hasMinimumCredentials;
39
+ }
40
+
41
+ /**
42
+ * Creates a clean initial configuration.
43
+ */
44
+ createInitialConfig(key, secret) {
45
+ return [{
46
+ platform: 'Resideo',
47
+ name: 'Resideo',
48
+ credentials: {
49
+ consumerKey: key,
50
+ consumerSecret: secret
51
+ }
52
+ }];
53
+ }
54
+
55
+ /**
56
+ * Updates plugin configuration.
57
+ */
58
+ async updateConfig(config) {
59
+ try {
60
+ await homebridge.updatePluginConfig(config);
61
+ return { success: true };
62
+ } catch (error) {
63
+ console.error('Config update error:', error);
64
+ return { success: false, error: error.message };
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Gets current plugin configuration.
70
+ */
71
+ getCurrentConfig() {
72
+ const config = homebridge.getPluginConfig();
73
+ return Array.isArray(config) ? config[0] : null;
74
+ }
75
+
76
+ /**
77
+ * Validates configuration structure.
78
+ */
79
+ validateConfig(config) {
80
+ if (!config) {
81
+ return { valid: false, message: 'Configuration is missing' };
82
+ }
83
+
84
+ if (config.platform !== 'Resideo') {
85
+ return { valid: false, message: 'Invalid platform configuration' };
86
+ }
87
+
88
+ if (!config.credentials) {
89
+ return { valid: false, message: 'Credentials are missing' };
90
+ }
91
+
92
+ return { valid: true };
93
+ }
94
+ }
@@ -0,0 +1,139 @@
1
+ /* Copyright(C) 2017-2025, homebridge-plugins/homebridge-resideo developers. All rights reserved.
2
+ *
3
+ * oauth-module.mjs: OAuth authentication module for Resideo plugin.
4
+ */
5
+
6
+ /**
7
+ * OAuth authentication module for Resideo plugin setup.
8
+ */
9
+ export class OAuthModule {
10
+ constructor() {
11
+ this.hostname = window.location.hostname;
12
+ }
13
+
14
+ /**
15
+ * Validates OAuth credentials format.
16
+ */
17
+ validateCredentials(key, secret) {
18
+ if (!key || !secret) {
19
+ return { valid: false, message: 'Please enter both Consumer Key and Consumer Secret.' };
20
+ }
21
+
22
+ if (key.length < 10 || secret.length < 10) {
23
+ return {
24
+ valid: false,
25
+ message: 'Consumer Key and Secret appear to be too short. Please verify your credentials.'
26
+ };
27
+ }
28
+
29
+ return { valid: true };
30
+ }
31
+
32
+ /**
33
+ * Sets up OAuth flow with popup window.
34
+ */
35
+ async startOAuthFlow(key, secret) {
36
+ try {
37
+ await homebridge.request('Start Resideo Login Server');
38
+
39
+ const w = 450;
40
+ const h = 700;
41
+ const y = window.top.outerHeight / 2 + window.top.screenY - (h / 2);
42
+ const x = window.top.outerWidth / 2 + window.top.screenX - (w / 2);
43
+
44
+ const urlToOpen = `http://${this.hostname}:8585/start?` +
45
+ `key=${encodeURIComponent(key)}&` +
46
+ `secret=${encodeURIComponent(secret)}&` +
47
+ `host=${encodeURIComponent(this.hostname)}`;
48
+
49
+ const popup = window.open(
50
+ urlToOpen,
51
+ 'resideo-auth',
52
+ `toolbar=no, location=no, directories=no, status=no, menubar=no scrollbars=no, resizable=no, copyhistory=no, ` +
53
+ `width=${w}, height=${h}, top=${y}, left=${x}`
54
+ );
55
+
56
+ return popup;
57
+ } catch (error) {
58
+ console.error('OAuth flow error:', error);
59
+ throw error;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Verifies OAuth completion with multiple attempts.
65
+ */
66
+ async verifyOAuthCompletion(popup, maxAttempts = 10) {
67
+ return new Promise((resolve) => {
68
+ const checkClosed = setInterval(() => {
69
+ if (popup.closed) {
70
+ clearInterval(checkClosed);
71
+
72
+ let verificationAttempts = 0;
73
+
74
+ const verifyCredentials = () => {
75
+ verificationAttempts++;
76
+
77
+ const updatedConfig = homebridge.getPluginConfig();
78
+ const credentials = Array.isArray(updatedConfig) && updatedConfig[0]?.credentials;
79
+
80
+ // Check for minimum required credentials
81
+ const hasMinimumAuth = credentials?.consumerKey &&
82
+ credentials?.consumerSecret &&
83
+ credentials?.refreshToken;
84
+
85
+ const hasCompleteAuth = hasMinimumAuth && credentials?.accessToken;
86
+
87
+ if (hasCompleteAuth) {
88
+ resolve({ success: true, complete: true });
89
+ } else if (hasMinimumAuth && verificationAttempts >= maxAttempts) {
90
+ // Minimum credentials are acceptable
91
+ resolve({ success: true, complete: false, message: 'Minimum credentials received' });
92
+ } else if (verificationAttempts >= maxAttempts) {
93
+ resolve({
94
+ success: false,
95
+ message: 'Authentication incomplete. Please try the setup process again.'
96
+ });
97
+ } else {
98
+ setTimeout(verifyCredentials, 1000);
99
+ }
100
+ };
101
+
102
+ setTimeout(verifyCredentials, 500);
103
+ }
104
+ }, 1000);
105
+ });
106
+ }
107
+
108
+ /**
109
+ * Sets up OAuth credential reception handler.
110
+ */
111
+ setupCredentialHandler() {
112
+ homebridge.addEventListener('creds-received', async (event) => {
113
+ try {
114
+ if (event.data.access && event.data.refresh) {
115
+ const currentConfig = [{
116
+ platform: 'Resideo',
117
+ name: 'Resideo',
118
+ credentials: {
119
+ consumerKey: event.data.key,
120
+ consumerSecret: event.data.secret,
121
+ accessToken: event.data.access,
122
+ refreshToken: event.data.refresh
123
+ }
124
+ }];
125
+
126
+ await homebridge.updatePluginConfig(currentConfig);
127
+ await homebridge.savePluginConfig();
128
+
129
+ setTimeout(() => {
130
+ homebridge.toast.success("Successfully Linked Resideo Account", "homebridge-resideo");
131
+ }, 200);
132
+ }
133
+ } catch (err) {
134
+ console.error('Credential handling error:', err);
135
+ homebridge.toast.error('Setup failed. Please try again.', 'Error');
136
+ }
137
+ });
138
+ }
139
+ }
@@ -0,0 +1,127 @@
1
+ /* Copyright(C) 2017-2025, homebridge-plugins/homebridge-resideo developers. All rights reserved.
2
+ *
3
+ * ui-utils.mjs: UI utility functions for Resideo plugin.
4
+ */
5
+
6
+ /**
7
+ * UI utility functions for Resideo plugin.
8
+ */
9
+ export class UIUtils {
10
+ /**
11
+ * Shows error message in specified element.
12
+ */
13
+ static showError(elementId, message) {
14
+ const errorDiv = document.getElementById(elementId);
15
+ if (errorDiv) {
16
+ errorDiv.innerHTML = `<div class="alert alert-danger"><strong>Error:</strong> ${message}</div>`;
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Shows success message in specified element.
22
+ */
23
+ static showSuccess(elementId, message) {
24
+ const errorDiv = document.getElementById(elementId);
25
+ if (errorDiv) {
26
+ errorDiv.innerHTML = `<div class="alert alert-success"><strong>Success!</strong> ${message}</div>`;
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Shows warning message in specified element.
32
+ */
33
+ static showWarning(elementId, message) {
34
+ const errorDiv = document.getElementById(elementId);
35
+ if (errorDiv) {
36
+ errorDiv.innerHTML = `<div class="alert alert-warning"><strong>Warning:</strong> ${message}</div>`;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Clears message in specified element.
42
+ */
43
+ static clearMessage(elementId) {
44
+ const errorDiv = document.getElementById(elementId);
45
+ if (errorDiv) {
46
+ errorDiv.innerHTML = '&nbsp;';
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Gets form field value by ID.
52
+ */
53
+ static getFieldValue(fieldId) {
54
+ const field = document.getElementById(fieldId);
55
+ return field ? field.value.trim() : '';
56
+ }
57
+
58
+ /**
59
+ * Sets form field value by ID.
60
+ */
61
+ static setFieldValue(fieldId, value) {
62
+ const field = document.getElementById(fieldId);
63
+ if (field) {
64
+ field.value = value;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Copy text to clipboard functionality.
70
+ */
71
+ static setupCopyToClipboard() {
72
+ window.copyMyText = () => {
73
+ const textToCopy = document.getElementById("copyMe");
74
+ if (textToCopy) {
75
+ textToCopy.select();
76
+ document.execCommand("copy");
77
+ }
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Handle different types of errors with appropriate user feedback.
83
+ */
84
+ static handleError(error, elementId) {
85
+ console.error('UI Error:', error);
86
+
87
+ let message = 'An unexpected error occurred. Please try again.';
88
+
89
+ if (error.message) {
90
+ if (error.message.includes('port')) {
91
+ message = 'OAuth server port conflict. Please wait a moment and try again.';
92
+ } else if (error.message.includes('network') || error.message.includes('fetch')) {
93
+ message = 'Network error. Please check your connection and try again.';
94
+ } else {
95
+ message = error.message;
96
+ }
97
+ }
98
+
99
+ UIUtils.showError(elementId, message);
100
+ }
101
+
102
+ /**
103
+ * Show loading state on button.
104
+ */
105
+ static setButtonLoading(buttonId, loading = true) {
106
+ const button = document.getElementById(buttonId);
107
+ if (button) {
108
+ if (loading) {
109
+ button.disabled = true;
110
+ button.innerHTML = '<span class="spinner-border spinner-border-sm me-2" role="status"></span>Processing...';
111
+ } else {
112
+ button.disabled = false;
113
+ button.innerHTML = button.dataset.originalText || 'Submit';
114
+ }
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Store original button text for loading states.
120
+ */
121
+ static storeButtonText(buttonId) {
122
+ const button = document.getElementById(buttonId);
123
+ if (button && !button.dataset.originalText) {
124
+ button.dataset.originalText = button.innerHTML;
125
+ }
126
+ }
127
+ }