@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.
- package/dist/homebridge-ui/public/index.html +65 -194
- package/dist/homebridge-ui/public/lib/config-module.mjs +94 -0
- package/dist/homebridge-ui/public/lib/oauth-module.mjs +139 -0
- package/dist/homebridge-ui/public/lib/ui-utils.mjs +127 -0
- package/dist/homebridge-ui/public/lib/webUi.mjs +4 -186
- package/dist/homebridge-ui/server.d.ts +12 -0
- package/dist/homebridge-ui/server.d.ts.map +1 -1
- package/dist/homebridge-ui/server.js +99 -150
- package/dist/homebridge-ui/server.js.map +1 -1
- package/dist/platform.d.ts +33 -0
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +290 -35
- package/dist/platform.js.map +1 -1
- package/dist/settings.d.ts +4 -0
- package/dist/settings.d.ts.map +1 -1
- package/docs/variables/default.html +1 -1
- package/package.json +2 -2
|
@@ -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
|
-
//
|
|
164
|
+
// Clean first run initialization
|
|
183
165
|
return true;
|
|
184
166
|
},
|
|
185
167
|
onSubmit: async () => {
|
|
186
|
-
// Handle first run setup with
|
|
187
|
-
const key =
|
|
188
|
-
const secret =
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
errorDiv.innerHTML = ' ';
|
|
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
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
|
214
|
-
const
|
|
215
|
-
|
|
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
|
-
|
|
226
|
-
|
|
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
|
-
//
|
|
229
|
-
const
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
'
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
//
|
|
320
|
-
const
|
|
321
|
-
|
|
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
|
-
//
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
//
|
|
239
|
+
// Initialize the application
|
|
370
240
|
(async () => {
|
|
371
241
|
try {
|
|
372
242
|
await homebridge.request('Start Resideo Login Server');
|
|
373
|
-
|
|
243
|
+
setupLegacyIntegration();
|
|
374
244
|
ui.show();
|
|
375
245
|
} catch (err) {
|
|
376
|
-
|
|
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 = ' ';
|
|
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
|
+
}
|