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.
- package/LICENSE +201 -0
- package/README.md +177 -0
- package/config.schema.json +109 -0
- package/homebridge-ui/public/index.html +918 -0
- package/homebridge-ui/server.js +309 -0
- package/index.js +1310 -0
- package/logo.png +0 -0
- package/package.json +36 -0
|
@@ -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>
|