homebridge-omlet 0.9.2

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,918 @@
1
+ <style>
2
+ .card {
3
+ padding: 20px;
4
+ background: #fff;
5
+ border-radius: 8px;
6
+ margin-bottom: 20px;
7
+ }
8
+ .form-group {
9
+ margin-bottom: 15px;
10
+ position: relative;
11
+ }
12
+ .form-group label {
13
+ display: block;
14
+ margin-bottom: 5px;
15
+ font-weight: 500;
16
+ }
17
+ .password-wrapper {
18
+ position: relative;
19
+ }
20
+ .password-toggle {
21
+ position: absolute;
22
+ right: 10px;
23
+ top: 50%;
24
+ transform: translateY(-50%);
25
+ background: none;
26
+ border: none;
27
+ cursor: pointer;
28
+ padding: 5px;
29
+ color: #6c757d;
30
+ }
31
+ .password-toggle:hover {
32
+ color: #495057;
33
+ }
34
+ .form-control {
35
+ width: 100%;
36
+ padding: 8px 12px;
37
+ border: 1px solid #ddd;
38
+ border-radius: 4px;
39
+ font-size: 14px;
40
+ }
41
+ .btn {
42
+ padding: 10px 20px;
43
+ border: none;
44
+ border-radius: 4px;
45
+ cursor: pointer;
46
+ font-size: 14px;
47
+ font-weight: 500;
48
+ }
49
+ .btn-primary {
50
+ background: #007bff;
51
+ color: white;
52
+ }
53
+ .btn-primary:hover {
54
+ background: #0056b3;
55
+ }
56
+ .btn-primary:disabled {
57
+ background: #6c757d;
58
+ cursor: not-allowed;
59
+ }
60
+ .btn-link {
61
+ background: none;
62
+ border: none;
63
+ color: #007bff;
64
+ cursor: pointer;
65
+ font-size: 14px;
66
+ padding: 0;
67
+ text-decoration: none;
68
+ font-weight: 500;
69
+ }
70
+ .btn-link:hover {
71
+ text-decoration: underline;
72
+ }
73
+ .device-list {
74
+ margin-top: 20px;
75
+ }
76
+ .device-item {
77
+ padding: 15px;
78
+ background: #f8f9fa;
79
+ border-radius: 6px;
80
+ margin-bottom: 10px;
81
+ }
82
+ .device-item h4 {
83
+ margin: 0 0 5px 0;
84
+ font-size: 16px;
85
+ }
86
+ .device-item p {
87
+ margin: 0;
88
+ font-size: 13px;
89
+ color: #666;
90
+ }
91
+ .status-message {
92
+ padding: 12px;
93
+ border-radius: 4px;
94
+ margin-bottom: 15px;
95
+ }
96
+ .status-success {
97
+ background: #d4edda;
98
+ color: #155724;
99
+ border: 1px solid #c3e6cb;
100
+ }
101
+ .status-error {
102
+ background: #f8d7da;
103
+ color: #721c24;
104
+ border: 1px solid #f5c6cb;
105
+ }
106
+ .status-info {
107
+ background: #d1ecf1;
108
+ color: #0c5460;
109
+ border: 1px solid #bee5eb;
110
+ }
111
+ .hidden {
112
+ display: none;
113
+ }
114
+ small {
115
+ display: block;
116
+ margin-top: 2px;
117
+ line-height: 1.3;
118
+ }
119
+ .field-error {
120
+ color: #dc3545;
121
+ font-size: 12px;
122
+ margin-top: 4px;
123
+ display: none;
124
+ }
125
+ .field-error.show {
126
+ display: block;
127
+ }
128
+ </style>
129
+
130
+ <div class="card">
131
+ <h3>Omlet Coop Setup</h3>
132
+ <p>Enter your Omlet account credentials to automatically configure your plugin.</p>
133
+
134
+ <div id="statusMessage" class="hidden"></div>
135
+
136
+ <div id="loginForm">
137
+ <div class="form-group">
138
+ <label for="email">Email Address</label>
139
+ <input type="email" class="form-control" id="email" placeholder="your@email.com" pattern="[^\s@]+@[^\s@]+\.[^\s@]+">
140
+ <div class="field-error" id="emailError"></div>
141
+ </div>
142
+
143
+ <div class="form-group">
144
+ <label for="password">Password</label>
145
+ <div class="password-wrapper">
146
+ <input type="password" class="form-control" id="password" placeholder="">
147
+ <button type="button" class="password-toggle" onclick="togglePasswordVisibility()">
148
+ <svg id="eyeIcon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
149
+ <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
150
+ <circle cx="12" cy="12" r="3"></circle>
151
+ </svg>
152
+ </button>
153
+ </div>
154
+ </div>
155
+
156
+ <div class="form-group">
157
+ <label for="countryCode">Country Code</label>
158
+ <select class="form-control" id="countryCode" required>
159
+ <option value="AU">AU - Australia</option>
160
+ <option value="DK">DK - Denmark</option>
161
+ <option value="FR">FR - France</option>
162
+ <option value="DE">DE - Germany</option>
163
+ <option value="IT">IT - Italy</option>
164
+ <option value="IE">IE - Ireland</option>
165
+ <option value="NL">NL - Netherlands</option>
166
+ <option value="SE">SE - Sweden</option>
167
+ <option value="UK">UK - United Kingdom</option>
168
+ <option value="US" selected>US - United States</option>
169
+ </select>
170
+ </div>
171
+
172
+ <div class="form-group">
173
+ <label>
174
+ <input type="checkbox" id="enableLight" checked> Enable Coop Light Accessory
175
+ </label>
176
+ <small style="color: #6c757d;">Uncheck if you do not have the Omlet coop light module</small>
177
+ </div>
178
+
179
+ <!-- Advanced Settings Section -->
180
+ <div style="margin-top: 25px; margin-bottom: 15px;">
181
+ <button type="button" class="btn-link" onclick="toggleAdvanced()" style="background: none; border: none; color: #007bff; cursor: pointer; font-size: 14px; padding: 0; text-decoration: none;">
182
+ <span id="advancedToggle">▶</span> Advanced Settings
183
+ </button>
184
+ </div>
185
+
186
+ <div id="advancedSection" class="hidden" style="padding: 15px; background: #f8f9fa; border-radius: 6px; margin-bottom: 15px;">
187
+ <div class="form-group">
188
+ <label for="apiServer">API Server</label>
189
+ <input type="text" class="form-control" id="apiServer" placeholder="x107.omlet.co.uk" pattern="[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*">
190
+ <small style="color: #6c757d;">Default: x107.omlet.co.uk</small>
191
+ <div class="field-error" id="apiServerError"></div>
192
+ </div>
193
+
194
+ <div class="form-group">
195
+ <label for="manualToken">API Token</label>
196
+ <input type="text" class="form-control" id="manualToken" placeholder="optional, will be auto-discovered" pattern="[a-zA-Z0-9]{1,64}">
197
+ <small style="color: #6c757d;">Provide API token instead of email address and password, if desired.</small>
198
+ <div class="field-error" id="manualTokenError"></div>
199
+ </div>
200
+
201
+ <div class="form-group">
202
+ <label for="manualDeviceId">Device ID</label>
203
+ <input type="text" class="form-control" id="manualDeviceId" placeholder="optional, will be auto-discovered" pattern="[a-zA-Z0-9]{1,32}">
204
+ <small style="color: #6c757d;">Provide Device ID to skip coop door auto-discovery, or to specify a coop door if you have multiple doors.</small>
205
+ <div class="field-error" id="manualDeviceIdError"></div>
206
+ </div>
207
+
208
+ <div class="form-group">
209
+ <label for="pollInterval">Poll Interval (seconds)</label>
210
+ <input type="number" class="form-control" id="pollInterval" placeholder="30" min="30" max="300" step="1">
211
+ <small style="color: #6c757d;">How often to check device status (30-300 seconds)</small>
212
+ <div class="field-error" id="pollIntervalError"></div>
213
+ </div>
214
+
215
+ <div class="form-group">
216
+ <label>
217
+ <input type="checkbox" id="enableBattery"> Enable Battery Status
218
+ </label>
219
+ <small style="color: #6c757d;">Show battery level in Homebridge and third-party HomeKit apps (not visible in Apple Home app)</small>
220
+ </div>
221
+
222
+ <div class="form-group">
223
+ <label>
224
+ <input type="checkbox" id="debugMode"> Enable Debug Mode
225
+ </label>
226
+ <small style="color: #6c757d;">Enable detailed logging for troubleshooting</small>
227
+ </div>
228
+ </div>
229
+
230
+ <button id="loginButton" class="btn btn-primary">Login</button>
231
+ </div>
232
+
233
+ <div id="devicesSection" class="hidden">
234
+ <h4>Discovered Devices</h4>
235
+ <div id="deviceList" class="device-list"></div>
236
+ </div>
237
+ </div>
238
+
239
+ <script>
240
+ // Toggle advanced settings
241
+ function toggleAdvanced() {
242
+ const section = document.getElementById('advancedSection');
243
+ const toggle = document.getElementById('advancedToggle');
244
+
245
+ if (section.classList.contains('hidden')) {
246
+ section.classList.remove('hidden');
247
+ toggle.textContent = '▼';
248
+ } else {
249
+ section.classList.add('hidden');
250
+ toggle.textContent = '▶';
251
+ }
252
+ }
253
+
254
+ // Toggle password visibility
255
+ function togglePasswordVisibility() {
256
+ const passwordInput = document.getElementById('password');
257
+ const eyeIcon = document.getElementById('eyeIcon');
258
+
259
+ if (passwordInput.type === 'password') {
260
+ passwordInput.type = 'text';
261
+ // Eye with slash (hidden)
262
+ eyeIcon.innerHTML = `
263
+ <path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path>
264
+ <line x1="1" y1="1" x2="23" y2="23"></line>
265
+ `;
266
+ } else {
267
+ passwordInput.type = 'password';
268
+ // Eye without slash (visible)
269
+ eyeIcon.innerHTML = `
270
+ <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
271
+ <circle cx="12" cy="12" r="3"></circle>
272
+ `;
273
+ }
274
+ }
275
+
276
+ (async () => {
277
+ let currentConfig = null;
278
+ let discoveredToken = null;
279
+ let discoveredDevices = [];
280
+
281
+ // Get current config
282
+ try {
283
+ const configs = await homebridge.getPluginConfig();
284
+ currentConfig = configs && configs.length > 0 ? configs[0] : {};
285
+ } catch (error) {
286
+ console.error('Error loading config:', error);
287
+ currentConfig = {};
288
+ }
289
+
290
+ // Pre-fill form if we have existing config
291
+ if (currentConfig.email) {
292
+ document.getElementById('email').value = currentConfig.email;
293
+ }
294
+ if (currentConfig.password) {
295
+ document.getElementById('password').value = currentConfig.password;
296
+ }
297
+ if (currentConfig.countryCode) {
298
+ document.getElementById('countryCode').value = currentConfig.countryCode;
299
+ }
300
+
301
+ // Pre-fill advanced settings
302
+ if (currentConfig.apiServer) {
303
+ document.getElementById('apiServer').value = currentConfig.apiServer;
304
+ }
305
+ if (currentConfig.bearerToken) {
306
+ document.getElementById('manualToken').value = currentConfig.bearerToken;
307
+ }
308
+ if (currentConfig.deviceId) {
309
+ document.getElementById('manualDeviceId').value = currentConfig.deviceId;
310
+ }
311
+ if (currentConfig.pollInterval) {
312
+ document.getElementById('pollInterval').value = currentConfig.pollInterval;
313
+ }
314
+ if (currentConfig.debug) {
315
+ document.getElementById('debugMode').checked = currentConfig.debug;
316
+ }
317
+ if (currentConfig.enableLight !== undefined) {
318
+ document.getElementById('enableLight').checked = currentConfig.enableLight;
319
+ }
320
+ if (currentConfig.enableBattery !== undefined) {
321
+ document.getElementById('enableBattery').checked = currentConfig.enableBattery;
322
+ }
323
+
324
+ // Auto-update config when form fields change (so built-in Save button works)
325
+ async function updateConfigFromForm() {
326
+ // Build config explicitly (don't spread currentConfig - causes clone errors)
327
+ const updatedConfig = {
328
+ name: currentConfig.name || 'Omlet Coop',
329
+ platform: 'OmletCoop',
330
+ email: document.getElementById('email').value.trim() || undefined,
331
+ password: document.getElementById('password').value.trim() || undefined,
332
+ countryCode: document.getElementById('countryCode').value,
333
+ bearerToken: document.getElementById('manualToken').value.trim() || undefined,
334
+ deviceId: document.getElementById('manualDeviceId').value.trim() || undefined,
335
+ enableLight: document.getElementById('enableLight').checked,
336
+ enableBattery: document.getElementById('enableBattery').checked,
337
+ debug: document.getElementById('debugMode').checked
338
+ };
339
+
340
+ // Only include apiServer if user explicitly set it (not empty and not default)
341
+ const apiServerValue = document.getElementById('apiServer').value.trim();
342
+ if (apiServerValue && apiServerValue !== 'x107.omlet.co.uk') {
343
+ updatedConfig.apiServer = apiServerValue;
344
+ }
345
+
346
+ // Only include pollInterval if user explicitly set it (not empty)
347
+ const pollIntervalValue = document.getElementById('pollInterval').value.trim();
348
+ if (pollIntervalValue) {
349
+ updatedConfig.pollInterval = parseInt(pollIntervalValue);
350
+ }
351
+
352
+ // Remove undefined values to keep config clean
353
+ Object.keys(updatedConfig).forEach(key => {
354
+ if (updatedConfig[key] === undefined) {
355
+ delete updatedConfig[key];
356
+ }
357
+ });
358
+
359
+ await homebridge.updatePluginConfig([updatedConfig]);
360
+ }
361
+
362
+ // Add event listeners to all form fields
363
+ document.getElementById('email').addEventListener('change', updateConfigFromForm);
364
+ document.getElementById('password').addEventListener('change', updateConfigFromForm);
365
+ document.getElementById('countryCode').addEventListener('change', updateConfigFromForm);
366
+ document.getElementById('apiServer').addEventListener('change', updateConfigFromForm);
367
+ document.getElementById('manualToken').addEventListener('change', updateConfigFromForm);
368
+ document.getElementById('manualDeviceId').addEventListener('change', updateConfigFromForm);
369
+ document.getElementById('pollInterval').addEventListener('change', updateConfigFromForm);
370
+ document.getElementById('enableLight').addEventListener('change', updateConfigFromForm);
371
+ document.getElementById('enableBattery').addEventListener('change', updateConfigFromForm);
372
+ document.getElementById('debugMode').addEventListener('change', updateConfigFromForm);
373
+
374
+ // Show status message
375
+ function showStatus(message, type) {
376
+ const statusDiv = document.getElementById('statusMessage');
377
+ statusDiv.className = `status-message status-${type}`;
378
+ statusDiv.textContent = message;
379
+ statusDiv.classList.remove('hidden');
380
+ }
381
+
382
+ // Hide status message
383
+ function hideStatus() {
384
+ document.getElementById('statusMessage').classList.add('hidden');
385
+ }
386
+
387
+ // Client-side validation helper
388
+ function validateInputs(email, apiServer, pollInterval) {
389
+ let isValid = true;
390
+
391
+ // Email validation (if provided)
392
+ if (email) {
393
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
394
+ if (!emailRegex.test(email)) {
395
+ showStatus('Please enter a valid email address', 'error');
396
+ isValid = false;
397
+ }
398
+ }
399
+
400
+ // API Server validation (if provided)
401
+ if (apiServer && apiServer !== 'x107.omlet.co.uk') {
402
+ const hostnameRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
403
+ if (!hostnameRegex.test(apiServer)) {
404
+ showStatus('Please enter a valid hostname for API Server', 'error');
405
+ isValid = false;
406
+ }
407
+ }
408
+
409
+ // Poll Interval validation (if provided)
410
+ if (pollInterval) {
411
+ const interval = parseInt(pollInterval);
412
+ if (isNaN(interval)) {
413
+ showStatus('Poll interval must be a number', 'error');
414
+ isValid = false;
415
+ } else if (interval < 30 || interval > 300) {
416
+ showStatus('Poll interval must be between 30 and 300 seconds', 'error');
417
+ isValid = false;
418
+ }
419
+ }
420
+
421
+ return isValid;
422
+ }
423
+
424
+ // Real-time field validation
425
+ function validateField(fieldId) {
426
+ const field = document.getElementById(fieldId);
427
+ const errorDiv = document.getElementById(fieldId + 'Error');
428
+ const value = field.value.trim();
429
+
430
+ // Clear any existing error styling
431
+ field.style.borderColor = '';
432
+ errorDiv.classList.remove('show');
433
+ errorDiv.textContent = '';
434
+
435
+ if (!value) {
436
+ // Empty is OK for optional fields
437
+ updateButtonStates();
438
+ return true;
439
+ }
440
+
441
+ let isValid = true;
442
+ let errorMsg = '';
443
+
444
+ switch(fieldId) {
445
+ case 'email':
446
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
447
+ isValid = emailRegex.test(value);
448
+ errorMsg = 'Invalid email address format';
449
+ break;
450
+
451
+ case 'apiServer':
452
+ if (value === 'x107.omlet.co.uk') {
453
+ // Default is always valid
454
+ isValid = true;
455
+ } else {
456
+ const hostnameRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
457
+ isValid = hostnameRegex.test(value);
458
+ errorMsg = 'Invalid hostname format';
459
+ }
460
+ break;
461
+
462
+ case 'manualToken':
463
+ const tokenRegex = /^[a-zA-Z0-9]{1,64}$/;
464
+ isValid = tokenRegex.test(value);
465
+ if (value.length > 64) {
466
+ errorMsg = 'Token must be 64 characters or less';
467
+ } else if (!/^[a-zA-Z0-9]+$/.test(value)) {
468
+ errorMsg = 'Token must be alphanumeric only';
469
+ } else {
470
+ errorMsg = 'Invalid token format';
471
+ }
472
+ break;
473
+
474
+ case 'manualDeviceId':
475
+ const deviceIdRegex = /^[a-zA-Z0-9]{1,32}$/;
476
+ isValid = deviceIdRegex.test(value);
477
+ if (value.length > 32) {
478
+ errorMsg = 'Device ID must be 32 characters or less';
479
+ } else if (!/^[a-zA-Z0-9]+$/.test(value)) {
480
+ errorMsg = 'Device ID must be alphanumeric only';
481
+ } else {
482
+ errorMsg = 'Invalid device ID format';
483
+ }
484
+ break;
485
+
486
+ case 'pollInterval':
487
+ const interval = parseInt(value);
488
+ if (isNaN(interval)) {
489
+ isValid = false;
490
+ errorMsg = 'Must be a number';
491
+ } else if (interval < 30 || interval > 300) {
492
+ isValid = false;
493
+ errorMsg = 'Must be between 30-300 seconds';
494
+ }
495
+ break;
496
+ }
497
+
498
+ if (!isValid) {
499
+ field.style.borderColor = '#dc3545';
500
+ errorDiv.textContent = errorMsg;
501
+ errorDiv.classList.add('show');
502
+ } else {
503
+ field.style.borderColor = '#28a745';
504
+ }
505
+
506
+ updateButtonStates();
507
+ return isValid;
508
+ }
509
+
510
+ // Update Login and Save button states based on validation
511
+ function updateButtonStates() {
512
+ // Don't disable the button - we'll check validation on click instead
513
+ // Just keep track of validation state for when Login is clicked
514
+ }
515
+
516
+ // Add blur event listeners for real-time validation
517
+ document.getElementById('email').addEventListener('blur', () => validateField('email'));
518
+ document.getElementById('apiServer').addEventListener('blur', () => validateField('apiServer'));
519
+ document.getElementById('manualToken').addEventListener('blur', () => validateField('manualToken'));
520
+ document.getElementById('manualDeviceId').addEventListener('blur', () => validateField('manualDeviceId'));
521
+ document.getElementById('pollInterval').addEventListener('blur', () => validateField('pollInterval'));
522
+
523
+ // Also validate on input (as user types) for immediate feedback
524
+ document.getElementById('email').addEventListener('input', () => {
525
+ const field = document.getElementById('email');
526
+ const errorDiv = document.getElementById('emailError');
527
+ if (field.value.trim()) {
528
+ setTimeout(() => validateField('email'), 500); // Debounce 500ms
529
+ } else {
530
+ field.style.borderColor = '';
531
+ errorDiv.classList.remove('show');
532
+ updateButtonStates();
533
+ }
534
+ });
535
+
536
+ document.getElementById('apiServer').addEventListener('input', () => {
537
+ const field = document.getElementById('apiServer');
538
+ const errorDiv = document.getElementById('apiServerError');
539
+ if (field.value.trim()) {
540
+ setTimeout(() => validateField('apiServer'), 500);
541
+ } else {
542
+ field.style.borderColor = '';
543
+ errorDiv.classList.remove('show');
544
+ updateButtonStates();
545
+ }
546
+ });
547
+
548
+ document.getElementById('manualToken').addEventListener('input', () => {
549
+ const field = document.getElementById('manualToken');
550
+ const errorDiv = document.getElementById('manualTokenError');
551
+ if (field.value.trim()) {
552
+ setTimeout(() => validateField('manualToken'), 500);
553
+ } else {
554
+ field.style.borderColor = '';
555
+ errorDiv.classList.remove('show');
556
+ updateButtonStates();
557
+ }
558
+ });
559
+
560
+ document.getElementById('manualDeviceId').addEventListener('input', () => {
561
+ const field = document.getElementById('manualDeviceId');
562
+ const errorDiv = document.getElementById('manualDeviceIdError');
563
+ if (field.value.trim()) {
564
+ setTimeout(() => validateField('manualDeviceId'), 500);
565
+ } else {
566
+ field.style.borderColor = '';
567
+ errorDiv.classList.remove('show');
568
+ updateButtonStates();
569
+ }
570
+ });
571
+
572
+ document.getElementById('pollInterval').addEventListener('input', () => {
573
+ const field = document.getElementById('pollInterval');
574
+ const errorDiv = document.getElementById('pollIntervalError');
575
+ if (field.value.trim()) {
576
+ setTimeout(() => validateField('pollInterval'), 500);
577
+ } else {
578
+ field.style.borderColor = '';
579
+ errorDiv.classList.remove('show');
580
+ updateButtonStates();
581
+ }
582
+ });
583
+
584
+ // Handle login and discovery with smart credential validation
585
+ document.getElementById('loginButton').addEventListener('click', async () => {
586
+ const email = document.getElementById('email').value.trim();
587
+ const password = document.getElementById('password').value;
588
+ const countryCode = document.getElementById('countryCode').value;
589
+ const manualToken = document.getElementById('manualToken').value.trim();
590
+ const manualDeviceId = document.getElementById('manualDeviceId').value.trim();
591
+ const apiServer = document.getElementById('apiServer').value.trim() || 'x107.omlet.co.uk';
592
+ const pollInterval = parseInt(document.getElementById('pollInterval').value) || 30;
593
+ const debugMode = document.getElementById('debugMode').checked;
594
+ const enableLight = document.getElementById('enableLight').checked;
595
+ const enableBattery = document.getElementById('enableBattery').checked;
596
+
597
+ const loginButton = document.getElementById('loginButton');
598
+
599
+ // Check if there are any validation errors showing
600
+ const hasErrors = document.querySelector('.field-error.show') !== null;
601
+ if (hasErrors) {
602
+ showStatus('Please correct errors below before login.', 'error');
603
+ return;
604
+ }
605
+
606
+ loginButton.disabled = true;
607
+ hideStatus();
608
+
609
+ try {
610
+ // Determine what we have
611
+ const hasToken = !!manualToken;
612
+ const hasEmail = !!email;
613
+ const hasPassword = !!password;
614
+ const hasFullCreds = hasEmail && hasPassword;
615
+ const hasPartialCreds = (hasEmail || hasPassword) && !hasFullCreds;
616
+ const hasDeviceId = !!manualDeviceId;
617
+
618
+ // === FLOW A: Invalid Inputs ===
619
+ if (!hasToken && !hasFullCreds) {
620
+ throw new Error('Email address and password is required');
621
+ }
622
+
623
+ let finalToken = null;
624
+ let finalDeviceId = null;
625
+ let shouldSaveCredentials = false;
626
+
627
+ // === FLOW D: Token + Full Credentials (Token priority with fallback) ===
628
+ if (hasToken && hasFullCreds) {
629
+ shouldSaveCredentials = true;
630
+
631
+ showStatus('Validating token...', 'info');
632
+ loginButton.textContent = 'Validating...';
633
+
634
+ const validateResult = await homebridge.request('/validate', {
635
+ token: manualToken,
636
+ deviceId: hasDeviceId ? manualDeviceId : null,
637
+ debug: debugMode
638
+ });
639
+
640
+ if (validateResult.tokenValid) {
641
+ // Token is valid, use it
642
+ finalToken = manualToken;
643
+
644
+ if (hasDeviceId) {
645
+ if (validateResult.deviceValid) {
646
+ finalDeviceId = manualDeviceId;
647
+ showStatus('Token and Device ID validated successfully!', 'success');
648
+ } else {
649
+ // Device invalid, auto-discover
650
+ if (validateResult.devices.length > 0) {
651
+ discoveredDevices = validateResult.devices;
652
+ displayDevices(discoveredDevices);
653
+ finalDeviceId = discoveredDevices[0].deviceId;
654
+ showStatus(`Invalid device ID. Successfully replaced with discovered device: ${discoveredDevices[0].name}`, 'success');
655
+ } else {
656
+ throw new Error('No coop doors discovered. Please ensure your coop door is connected to your account and try again.');
657
+ }
658
+ }
659
+ } else {
660
+ // No device provided, discover
661
+ if (validateResult.devices.length > 0) {
662
+ discoveredDevices = validateResult.devices;
663
+ displayDevices(discoveredDevices);
664
+ finalDeviceId = discoveredDevices[0].deviceId;
665
+ showStatus(`Success! Found ${discoveredDevices.length} device(s): ${discoveredDevices[0].name}`, 'success');
666
+ } else {
667
+ throw new Error('No coop doors discovered. Please ensure your coop door is connected to your account and try again.');
668
+ }
669
+ }
670
+ } else {
671
+ // Token invalid, fall back to login
672
+ showStatus('Token invalid, retrying with email/password...', 'info');
673
+ loginButton.textContent = 'Logging in...';
674
+
675
+ const loginResult = await homebridge.request('/login', {
676
+ email: email,
677
+ password: password,
678
+ countryCode: countryCode,
679
+ debug: debugMode
680
+ });
681
+
682
+ if (!loginResult.success || !loginResult.token) {
683
+ throw new Error('Login has failed, please check email address and password and try again.');
684
+ }
685
+
686
+ finalToken = loginResult.token;
687
+
688
+ // Now validate/discover device
689
+ const validateResult2 = await homebridge.request('/validate', {
690
+ token: finalToken,
691
+ deviceId: hasDeviceId ? manualDeviceId : null,
692
+ debug: debugMode
693
+ });
694
+
695
+ if (hasDeviceId) {
696
+ if (validateResult2.deviceValid) {
697
+ finalDeviceId = manualDeviceId;
698
+ showStatus('Login successful! Device ID validated.', 'success');
699
+ } else {
700
+ if (validateResult2.devices.length > 0) {
701
+ discoveredDevices = validateResult2.devices;
702
+ displayDevices(discoveredDevices);
703
+ finalDeviceId = discoveredDevices[0].deviceId;
704
+ showStatus(`Login successful! Invalid device ID replaced with: ${discoveredDevices[0].name}`, 'success');
705
+ } else {
706
+ throw new Error('No coop doors discovered. Please ensure your coop door is connected to your account and try again.');
707
+ }
708
+ }
709
+ } else {
710
+ if (validateResult2.devices.length > 0) {
711
+ discoveredDevices = validateResult2.devices;
712
+ displayDevices(discoveredDevices);
713
+ finalDeviceId = discoveredDevices[0].deviceId;
714
+ showStatus(`Login successful! Found device: ${discoveredDevices[0].name}`, 'success');
715
+ } else {
716
+ throw new Error('No coop doors discovered. Please ensure your coop door is connected to your account and try again.');
717
+ }
718
+ }
719
+ }
720
+ }
721
+
722
+ // === FLOW B: Token Only (may have partial creds, ignore them) ===
723
+ else if (hasToken) {
724
+ // Don't save partial credentials
725
+ shouldSaveCredentials = false;
726
+
727
+ showStatus('Validating token...', 'info');
728
+ loginButton.textContent = 'Validating...';
729
+
730
+ const validateResult = await homebridge.request('/validate', {
731
+ token: manualToken,
732
+ deviceId: hasDeviceId ? manualDeviceId : null,
733
+ debug: debugMode
734
+ });
735
+
736
+ if (!validateResult.tokenValid) {
737
+ throw new Error('Token is invalid, please try again or use email address and password.');
738
+ }
739
+
740
+ finalToken = manualToken;
741
+
742
+ if (hasDeviceId) {
743
+ if (validateResult.deviceValid) {
744
+ finalDeviceId = manualDeviceId;
745
+ showStatus('Token and Device ID validated successfully!', 'success');
746
+ } else {
747
+ // Device invalid, auto-discover
748
+ if (validateResult.devices.length > 0) {
749
+ discoveredDevices = validateResult.devices;
750
+ displayDevices(discoveredDevices);
751
+ finalDeviceId = discoveredDevices[0].deviceId;
752
+ showStatus(`Invalid device ID. Successfully replaced with discovered device: ${discoveredDevices[0].name}`, 'success');
753
+ } else {
754
+ throw new Error('No coop doors discovered. Please ensure your coop door is connected to your account and try again.');
755
+ }
756
+ }
757
+ } else {
758
+ // No device provided, discover
759
+ if (validateResult.devices.length > 0) {
760
+ discoveredDevices = validateResult.devices;
761
+ displayDevices(discoveredDevices);
762
+ finalDeviceId = discoveredDevices[0].deviceId;
763
+ showStatus(`Success! Found ${discoveredDevices.length} device(s): ${discoveredDevices[0].name}`, 'success');
764
+ } else {
765
+ throw new Error('No coop doors discovered. Please ensure your coop door is connected to your account and try again.');
766
+ }
767
+ }
768
+ }
769
+
770
+ // === FLOW C: Email+Password Only ===
771
+ else if (hasFullCreds) {
772
+ shouldSaveCredentials = true;
773
+
774
+ showStatus('Logging in to Omlet...', 'info');
775
+ loginButton.textContent = 'Logging in...';
776
+
777
+ const loginResult = await homebridge.request('/login', {
778
+ email: email,
779
+ password: password,
780
+ countryCode: countryCode
781
+ });
782
+
783
+ if (!loginResult.success || !loginResult.token) {
784
+ throw new Error('Login has failed, please check email address and password and try again.');
785
+ }
786
+
787
+ finalToken = loginResult.token;
788
+
789
+ // Now validate/discover device
790
+ const validateResult = await homebridge.request('/validate', {
791
+ token: finalToken,
792
+ deviceId: hasDeviceId ? manualDeviceId : null
793
+ });
794
+
795
+ if (hasDeviceId) {
796
+ if (validateResult.deviceValid) {
797
+ finalDeviceId = manualDeviceId;
798
+ showStatus('Login successful! Device ID validated.', 'success');
799
+ } else {
800
+ if (validateResult.devices.length > 0) {
801
+ discoveredDevices = validateResult.devices;
802
+ displayDevices(discoveredDevices);
803
+ finalDeviceId = discoveredDevices[0].deviceId;
804
+ showStatus(`Login successful! Invalid device ID replaced with: ${discoveredDevices[0].name}`, 'success');
805
+ } else {
806
+ throw new Error('No coop doors discovered. Please ensure your coop door is connected to your account and try again.');
807
+ }
808
+ }
809
+ } else {
810
+ if (validateResult.devices.length > 0) {
811
+ discoveredDevices = validateResult.devices;
812
+ displayDevices(discoveredDevices);
813
+ finalDeviceId = discoveredDevices[0].deviceId;
814
+ showStatus(`Login successful! Found device: ${discoveredDevices[0].name}`, 'success');
815
+ } else {
816
+ throw new Error('No coop doors discovered. Please ensure your coop door is connected to your account and try again.');
817
+ }
818
+ }
819
+ }
820
+
821
+ // Save configuration
822
+ // Build config explicitly (don't spread currentConfig - causes clone errors)
823
+ const newConfig = {
824
+ name: currentConfig.name || 'Omlet Coop',
825
+ platform: 'OmletCoop',
826
+ countryCode: countryCode,
827
+ enableLight: enableLight,
828
+ enableBattery: enableBattery,
829
+ debug: debugMode
830
+ };
831
+
832
+ // Only include apiServer if user explicitly set it (not empty and not default)
833
+ if (apiServer && apiServer !== 'x107.omlet.co.uk') {
834
+ newConfig.apiServer = apiServer;
835
+ }
836
+
837
+ // Only include pollInterval if user explicitly set it (not empty/blank)
838
+ if (pollInterval && pollInterval !== 30) {
839
+ newConfig.pollInterval = pollInterval;
840
+ }
841
+
842
+ // CRITICAL: Only save token/deviceId to config if user MANUALLY entered them
843
+ // Auto-discovered credentials are saved to storage by the plugin, not config
844
+
845
+ // Save token to config ONLY if user manually provided it (not auto-discovered)
846
+ if (hasToken && finalToken === manualToken) {
847
+ // User manually entered this token → save to config
848
+ newConfig.bearerToken = finalToken;
849
+ } else {
850
+ // Token was auto-discovered via email/password → don't save to config (goes to storage)
851
+ delete newConfig.bearerToken;
852
+ }
853
+
854
+ // Save deviceId to config ONLY if user manually provided it (not auto-discovered)
855
+ if (hasDeviceId && finalDeviceId === manualDeviceId) {
856
+ // User manually entered this deviceId → save to config
857
+ newConfig.deviceId = finalDeviceId;
858
+ } else {
859
+ // DeviceId was auto-discovered → don't save to config (goes to storage)
860
+ delete newConfig.deviceId;
861
+ }
862
+
863
+ // Only save credentials if appropriate
864
+ if (shouldSaveCredentials) {
865
+ newConfig.email = email;
866
+ newConfig.password = password;
867
+ } else {
868
+ // Clear credentials from config if not saving
869
+ delete newConfig.email;
870
+ delete newConfig.password;
871
+ }
872
+
873
+ // Debug: Log what we're about to save
874
+ console.log('Saving config:', JSON.stringify(newConfig, null, 2));
875
+
876
+ try {
877
+ // Ensure config is JSON-serializable (fixes clone errors)
878
+ const serializableConfig = JSON.parse(JSON.stringify(newConfig));
879
+ await homebridge.updatePluginConfig([serializableConfig]);
880
+ showStatus('Login successful! Click "Save" to apply changes.', 'success');
881
+ loginButton.textContent = 'Login Successful ✓';
882
+ loginButton.disabled = false;
883
+ } catch (configError) {
884
+ console.error('Config update error:', configError);
885
+ throw new Error('Failed to update config: ' + configError.message);
886
+ }
887
+
888
+ } catch (error) {
889
+ console.error('Error:', error);
890
+ showStatus(`Error: ${error.message}`, 'error');
891
+ loginButton.disabled = false;
892
+ loginButton.textContent = 'Login';
893
+ }
894
+ });
895
+
896
+ // Display discovered devices
897
+ function displayDevices(devices) {
898
+ const deviceList = document.getElementById('deviceList');
899
+ const devicesSection = document.getElementById('devicesSection');
900
+
901
+ deviceList.innerHTML = '';
902
+
903
+ devices.forEach((device, index) => {
904
+ const deviceDiv = document.createElement('div');
905
+ deviceDiv.className = 'device-item';
906
+ deviceDiv.innerHTML = `
907
+ <h4>${device.name}</h4>
908
+ <p><strong>Device ID:</strong> ${device.deviceId}</p>
909
+ <p><strong>Type:</strong> ${device.type}</p>
910
+ ${index === 0 ? '<p style="color: #28a745; font-weight: 500;">✓ This device will be used</p>' : ''}
911
+ `;
912
+ deviceList.appendChild(deviceDiv);
913
+ });
914
+
915
+ devicesSection.classList.remove('hidden');
916
+ }
917
+ })();
918
+ </script>