@homebridge-plugins/homebridge-resideo 3.0.3-beta.17 → 3.0.3-beta.19

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.
@@ -18,7 +18,6 @@ jobs:
18
18
 
19
19
  publish:
20
20
  needs: lint
21
- if: ${{ github.repository == 'homebridge-plugins/homebridge-resideo' }}
22
21
  permissions:
23
22
  id-token: write
24
23
  uses: homebridge/.github/.github/workflows/npm-publish-esm.yml@latest
@@ -32,7 +31,6 @@ jobs:
32
31
 
33
32
  pre-release:
34
33
  needs: publish
35
- if: ${{ github.repository == 'homebridge-plugins/homebridge-resideo' }}
36
34
  uses: homebridge/.github/.github/workflows/pre-release.yml@latest
37
35
  with:
38
36
  npm_version: ${{ needs.publish.outputs.NPM_VERSION }}
@@ -44,7 +42,6 @@ jobs:
44
42
  github-releases-to-discord:
45
43
  name: Discord Webhooks
46
44
  needs: [build_and_test,publish]
47
- if: ${{ github.repository == 'homebridge-plugins/homebridge-resideo' }}
48
45
  uses: homebridge/.github/.github/workflows/discord-webhooks.yml@latest
49
46
  with:
50
47
  title: "Resideo Beta Release"
@@ -52,4 +49,4 @@ jobs:
52
49
  Version `v${{ needs.publish.outputs.NPM_VERSION }}`
53
50
  url: "https://github.com/homebridge-plugins/homebridge-resideo/releases/tag/v${{ needs.publish.outputs.NPM_VERSION }}"
54
51
  secrets:
55
- DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_URL_BETA || secrets.DISCORD_WEBHOOK_URL_LATEST }}
52
+ DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_URL_LATEST }}
@@ -1,35 +1,75 @@
1
- name: Release
1
+ name: Unified Release
2
2
 
3
3
  on:
4
- release:
5
- types: [published]
4
+ push:
5
+ branches:
6
+ #- "alpha-*"
7
+ #- "beta-*"
8
+ - latest
9
+ workflow_dispatch:
6
10
 
7
11
  jobs:
8
- build_and_test:
9
- uses: homebridge/.github/.github/workflows/nodejs-build-and-test.yml@latest
12
+ # 1️⃣ Determine release type, ESM status, and branch name
13
+ determine-release-type:
14
+ uses: homebridge/.github/.github/workflows/determine-release-type.yml@latest
10
15
  with:
11
- enable_coverage: false
16
+ ref_name: ${{ github.ref_name }}
17
+
18
+ # 2️⃣ Update version and changelog using the scripts
19
+ update-version:
20
+ needs: determine-release-type
21
+ uses: homebridge/.github/.github/workflows/update-version.yml@latest
22
+ with:
23
+ release_type: ${{ needs.determine-release-type.outputs.release_type }}
24
+ is_esm: ${{ needs.determine-release-type.outputs.is_esm == 'true' }}
12
25
  secrets:
13
- token: ${{ secrets.GITHUB_TOKEN }}
26
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
14
27
 
15
- publish:
16
- needs: build_and_test
17
- if: ${{ github.repository == 'homebridge-plugins/homebridge-resideo' }}
28
+ # 3️⃣ Publish to NPM and create GitHub release
29
+ publish-release:
30
+ needs: [determine-release-type, update-version]
18
31
  permissions:
19
32
  id-token: write
20
- uses: homebridge/.github/.github/workflows/npm-publish-esm.yml@latest
33
+ contents: write
34
+ uses: homebridge/.github/.github/workflows/publish-release.yml@latest
35
+ with:
36
+ release_type: ${{ needs.determine-release-type.outputs.release_type }}
37
+ version: ${{ needs.update-version.outputs.version }}
38
+ is_esm: ${{ needs.determine-release-type.outputs.is_esm == 'true' }}
39
+ secrets:
40
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
41
+
42
+ # 4️⃣ Promote branch if this is a prerelease (alpha/beta)
43
+ promote-branch:
44
+ needs: [determine-release-type, publish-release]
45
+ if: ${{ needs.determine-release-type.outputs.release_type != 'latest' && needs.determine-release-type.outputs.release_type != 'skip' }}
46
+ uses: homebridge/.github/.github/workflows/promote-branch.yml@latest
47
+ with:
48
+ branch_name: ${{ needs.determine-release-type.outputs.branch_name }}
49
+ release_type: ${{ needs.determine-release-type.outputs.release_type }}
50
+ is_esm: ${{ needs.determine-release-type.outputs.is_esm == 'true' }}
21
51
  secrets:
22
- npm_auth_token: ${{ secrets.npm_token }}
52
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
53
+
54
+ # 5️⃣ Notify if any previous job fails
55
+ workflow-failure:
56
+ if: ${{ failure() }}
57
+ needs: [determine-release-type, update-version, publish-release, promote-branch]
58
+ uses: homebridge/.github/.github/workflows/report-failure.yml@latest
59
+ with:
60
+ workflow_name: ${{ github.workflow }}
61
+ job_name: ${{ github.job }}
62
+ run_url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
23
63
 
64
+ # 6️⃣ Post to Discord
24
65
  github-releases-to-discord:
25
66
  name: Discord Webhooks
26
- needs: [build_and_test,publish]
27
- if: ${{ github.repository == 'homebridge-plugins/homebridge-resideo' }}
67
+ needs: [determine-release-type, update-version, publish-release]
28
68
  uses: homebridge/.github/.github/workflows/discord-webhooks.yml@latest
29
69
  with:
30
70
  title: "Resideo Release"
31
71
  description: |
32
- Version `v${{ needs.publish.outputs.NPM_VERSION }}`
33
- url: "https://github.com/homebridge-plugins/homebridge-resideo/releases/tag/v${{ needs.publish.outputs.NPM_VERSION }}"
72
+ Version `v${{ needs.update-version.outputs.version }}`
73
+ url: "https://github.com/homebridge-plugins/homebridge-resideo/releases/tag/v${{ needs.update-version.outputs.version }}"
34
74
  secrets:
35
75
  DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_URL_LATEST }}
@@ -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,130 +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
- // All four credential fields are required to skip first-run setup
166
- const requiredCredentials = ['consumerKey', 'consumerSecret', 'accessToken', 'refreshToken'];
167
- const hasAllCredentials = requiredCredentials.every(field =>
168
- credentials?.[field] && credentials[field].trim() !== ''
169
- );
170
-
171
- // Return true if first-run is required (missing credentials), false if can skip
172
- return !hasAllCredentials;
173
- },
162
+ isRequired: () => configModule.isFirstRunRequired(),
174
163
  onStart: () => {
175
- // Simple first run initialization - no legacy config handling
164
+ // Clean first run initialization
176
165
  return true;
177
166
  },
178
167
  onSubmit: async () => {
179
- // Handle first run setup with clean config creation
180
- const key = document.getElementById('firstRunKey')?.value?.trim();
181
- const secret = document.getElementById('firstRunSecret')?.value?.trim();
182
- const errorDiv = document.getElementById('loginError');
183
-
184
- // Clear any previous errors
185
- if (errorDiv) {
186
- errorDiv.innerHTML = '&nbsp;';
187
- }
188
-
189
- // Validate input fields
190
- if (!key || !secret) {
191
- if (errorDiv) {
192
- errorDiv.innerHTML = '<div class="alert alert-danger"><strong>Error:</strong> Please enter both Consumer Key and Consumer Secret.</div>';
193
- }
194
- return false;
195
- }
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);
196
175
 
197
- // Basic validation for credential format
198
- if (key.length < 10 || secret.length < 10) {
199
- if (errorDiv) {
200
- errorDiv.innerHTML = '<div class="alert alert-danger"><strong>Error:</strong> Consumer Key and Secret appear to be too short. Please verify your credentials.</div>';
201
- }
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);
202
181
  return false;
203
182
  }
204
183
 
205
184
  try {
206
- // Create clean config structure for first version
207
- const currentConfig = [{
208
- platform: 'Resideo',
209
- name: 'Resideo',
210
- credentials: {
211
- consumerKey: key,
212
- consumerSecret: secret
213
- }
214
- }];
215
-
216
- await homebridge.updatePluginConfig(currentConfig);
185
+ // Create initial config
186
+ const initialConfig = configModule.createInitialConfig(key, secret);
187
+ const updateResult = await configModule.updateConfig(initialConfig);
217
188
 
218
- // Now trigger the OAuth flow
219
- 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);
220
195
 
221
- // Open OAuth popup
222
- const hostname = window.location.hostname;
223
- const w = 450;
224
- const h = 700;
225
- const y = window.top.outerHeight / 2 + window.top.screenY - (h / 2);
226
- const x = window.top.outerWidth / 2 + window.top.screenX - (w / 2);
227
- const urlToOpen = 'http://' + hostname + ':8585/start?' +
228
- 'key=' + encodeURIComponent(key) +
229
- '&secret=' + encodeURIComponent(secret) +
230
- '&host=' + encodeURIComponent(hostname);
196
+ // Verify OAuth completion
197
+ const result = await oauthModule.verifyOAuthCompletion(popup);
231
198
 
232
- const popup = window.open(
233
- urlToOpen, 'resideo-auth',
234
- 'toolbar=no, location=no, directories=no, status=no, menubar=no scrollbars=no, resizable=no, copyhistory=no, ' +
235
- 'width=' + w + ', height=' + h + ', top=' + y + ', left=' + x,
236
- );
237
-
238
- return new Promise((resolve) => {
239
- const checkClosed = setInterval(() => {
240
- if (popup.closed) {
241
- clearInterval(checkClosed);
242
-
243
- // Wait a moment for config to update
244
- setTimeout(() => {
245
- // Check if authentication completed successfully by verifying all required credentials
246
- const updatedConfig = homebridge.getPluginConfig();
247
- const credentials = Array.isArray(updatedConfig) && updatedConfig[0]?.credentials;
248
- const hasCompleteAuth = credentials?.accessToken &&
249
- credentials?.refreshToken &&
250
- credentials?.consumerKey &&
251
- credentials?.consumerSecret;
252
-
253
- if (hasCompleteAuth) {
254
- if (errorDiv) {
255
- errorDiv.innerHTML = '<div class="alert alert-success"><strong>Success!</strong> Your account has been linked successfully.</div>';
256
- }
257
- resolve(true);
258
- } else {
259
- if (errorDiv) {
260
- errorDiv.innerHTML = '<div class="alert alert-warning"><strong>Authentication incomplete.</strong> Please try the setup process again or check your Resideo developer credentials.</div>';
261
- }
262
- resolve(false);
263
- }
264
- }, 500);
265
- }
266
- }, 1000);
267
- });
268
- } catch (error) {
269
- console.error('Resideo setup error:', error);
270
- if (errorDiv) {
271
- if (error.message.includes('port')) {
272
- errorDiv.innerHTML = '<div class="alert alert-danger"><strong>Error:</strong> OAuth server port conflict. Please wait a moment and try again.</div>';
273
- } else if (error.message.includes('network') || error.message.includes('fetch')) {
274
- errorDiv.innerHTML = '<div class="alert alert-danger"><strong>Error:</strong> Network error. Please check your connection and try again.</div>';
275
- } else {
276
- errorDiv.innerHTML = `<div class="alert alert-danger"><strong>Error:</strong> Setup failed. ${error.message || 'Please try again.'}</div>`;
277
- }
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;
278
213
  }
214
+ } catch (error) {
215
+ console.error('First run setup error:', error);
216
+ UIUtils.handleError(error, 'loginError');
217
+ UIUtils.setButtonLoading('firstRun', false);
279
218
  return false;
280
219
  }
281
220
  }
@@ -283,60 +222,29 @@
283
222
  name: 'homebridge-resideo'
284
223
  });
285
224
 
286
- // Legacy functionality for existing UI elements - simplified for webUi compatibility
287
- const setupLegacyHandlers = () => {
288
- const hostname = window.location.hostname;
289
-
290
- homebridge.addEventListener('creds-received', async event => {
291
- try {
292
- if (event.data.access && event.data.refresh) {
293
- // Create clean config structure for first version
294
- const currentConfig = [{
295
- platform: 'Resideo',
296
- name: 'Resideo',
297
- credentials: {
298
- consumerKey: event.data.key,
299
- consumerSecret: event.data.secret,
300
- accessToken: event.data.access,
301
- refreshToken: event.data.refresh
302
- }
303
- }];
304
-
305
- await homebridge.updatePluginConfig(currentConfig);
306
- await homebridge.savePluginConfig();
307
- homebridge.toast.success("Successfully Linked Resideo Account", "homebridge-resideo");
308
- }
309
- } catch (err) {
310
- console.log(err);
311
- homebridge.toast.error('Setup failed. Please try again.', 'Error');
312
- }
313
- });
314
-
315
- window.copyMyText = () => {
316
- const textToCopy = document.getElementById("copyMe");
317
- if (textToCopy) {
318
- textToCopy.select();
319
- document.execCommand("copy");
320
- }
321
- };
225
+ // Setup OAuth credential handler and other legacy functionality
226
+ const setupLegacyIntegration = () => {
227
+ // Setup OAuth credential reception
228
+ oauthModule.setupCredentialHandler();
322
229
 
323
- // Initialize legacy components for backwards compatibility
324
- setupLegacyMenus();
325
- };
326
-
327
- const setupLegacyMenus = () => {
328
- // Legacy menu functionality removed for first version approach
329
- // 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);
330
237
  };
331
238
 
332
- // Show the webUI
239
+ // Initialize the application
333
240
  (async () => {
334
241
  try {
335
242
  await homebridge.request('Start Resideo Login Server');
336
- setupLegacyHandlers();
243
+ setupLegacyIntegration();
337
244
  ui.show();
338
245
  } catch (err) {
339
- homebridge.toast.error(err.message, 'Error');
246
+ console.error('Initialization error:', err);
247
+ homebridge.toast.error(err.message || 'Failed to initialize plugin UI', 'Error');
340
248
  }
341
249
  })();
342
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
+ }