@switchbot/homebridge-switchbot 5.0.0-beta.111 → 5.0.0-beta.113
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/MIGRATION.md +10 -0
- package/README.md +43 -0
- package/TODO.md +263 -0
- package/config.schema.json +27 -10
- package/dist/device-types.d.ts.map +1 -1
- package/dist/device-types.js +93 -63
- package/dist/device-types.js.map +1 -1
- package/dist/deviceFactory.d.ts.map +1 -1
- package/dist/deviceFactory.js +55 -33
- package/dist/deviceFactory.js.map +1 -1
- package/dist/devices/genericDevice.d.ts +12 -1
- package/dist/devices/genericDevice.d.ts.map +1 -1
- package/dist/devices/genericDevice.js +121 -1
- package/dist/devices/genericDevice.js.map +1 -1
- package/dist/errors.d.ts +5 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +51 -0
- package/dist/errors.js.map +1 -0
- package/dist/homebridge-ui/endpoints/devices.d.ts.map +1 -1
- package/dist/homebridge-ui/endpoints/devices.js +80 -1
- package/dist/homebridge-ui/endpoints/devices.js.map +1 -1
- package/dist/homebridge-ui/endpoints/discovery.d.ts.map +1 -1
- package/dist/homebridge-ui/endpoints/discovery.js +73 -41
- package/dist/homebridge-ui/endpoints/discovery.js.map +1 -1
- package/dist/homebridge-ui/public/css/styles.css +209 -16
- package/dist/homebridge-ui/public/index.html +87 -56
- package/dist/homebridge-ui/public/js/api.d.ts +24 -1
- package/dist/homebridge-ui/public/js/api.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/api.js +33 -2
- package/dist/homebridge-ui/public/js/api.js.map +1 -1
- package/dist/homebridge-ui/public/js/api.ts +52 -3
- package/dist/homebridge-ui/public/js/app.js +1604 -143
- package/dist/homebridge-ui/public/js/app.js.map +3 -3
- package/dist/homebridge-ui/public/js/app.ts +2 -1
- package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/devices.js +2 -0
- package/dist/homebridge-ui/public/js/devices.js.map +1 -1
- package/dist/homebridge-ui/public/js/devices.ts +13 -1
- package/dist/homebridge-ui/public/js/discovery.d.ts +1 -0
- package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/discovery.js +1244 -124
- package/dist/homebridge-ui/public/js/discovery.js.map +1 -1
- package/dist/homebridge-ui/public/js/discovery.ts +1344 -142
- package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/modals.js +42 -0
- package/dist/homebridge-ui/public/js/modals.js.map +1 -1
- package/dist/homebridge-ui/public/js/modals.ts +50 -1
- package/dist/homebridge-ui/public/js/render.d.ts +3 -0
- package/dist/homebridge-ui/public/js/render.d.ts.map +1 -1
- package/dist/homebridge-ui/public/js/render.js +389 -36
- package/dist/homebridge-ui/public/js/render.js.map +1 -1
- package/dist/homebridge-ui/public/js/render.ts +421 -38
- package/dist/platform.d.ts +1 -0
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +3 -4
- package/dist/platform.js.map +1 -1
- package/dist/switchbotClient.d.ts.map +1 -1
- package/dist/switchbotClient.js +51 -5
- package/dist/switchbotClient.js.map +1 -1
- package/docs/assets/highlight.css +9 -2
- package/docs/index.html +28 -2
- package/docs/variables/default.html +1 -1
- package/package.json +9 -8
- package/src/device-types.ts +95 -64
- package/src/deviceFactory.ts +55 -33
- package/src/devices/genericDevice.ts +113 -1
- package/src/errors.ts +43 -0
- package/src/homebridge-ui/endpoints/devices.ts +82 -1
- package/src/homebridge-ui/endpoints/discovery.ts +78 -44
- package/src/homebridge-ui/public/css/styles.css +209 -16
- package/src/homebridge-ui/public/index.html +87 -56
- package/src/homebridge-ui/public/js/api.ts +52 -3
- package/src/homebridge-ui/public/js/app.ts +2 -1
- package/src/homebridge-ui/public/js/devices.ts +13 -1
- package/src/homebridge-ui/public/js/discovery.ts +1344 -142
- package/src/homebridge-ui/public/js/modals.ts +50 -1
- package/src/homebridge-ui/public/js/render.ts +421 -38
- package/src/platform.ts +3 -4
- package/src/switchbotClient.ts +44 -9
- package/test/e2e/run-e2e.spec.ts +12 -10
- package/test/helpers/matter-harness.ts +2 -2
- package/test/matter-integration.spec.ts +21 -0
- package/todo/COMPLETED.md +313 -0
- package/todo/TODO.md +80 -66
- package/switchbot-homebridge-switchbot-5.0.0.tgz +0 -0
|
@@ -151,8 +151,215 @@ function scrollToConfiguredDevice(deviceId) {
|
|
|
151
151
|
target.style.background = originalBackground;
|
|
152
152
|
}, 1800);
|
|
153
153
|
}
|
|
154
|
+
function createConnectionTestControls(device) {
|
|
155
|
+
const controls = document.createElement('div');
|
|
156
|
+
controls.style.display = 'inline-flex';
|
|
157
|
+
controls.style.alignItems = 'center';
|
|
158
|
+
controls.style.gap = '6px';
|
|
159
|
+
const button = document.createElement('button');
|
|
160
|
+
button.textContent = 'Test Connection';
|
|
161
|
+
button.className = 'secondary';
|
|
162
|
+
button.style.padding = '4px 9px';
|
|
163
|
+
button.style.fontSize = '11px';
|
|
164
|
+
const status = document.createElement('span');
|
|
165
|
+
status.style.fontSize = '10px';
|
|
166
|
+
status.style.opacity = '0.85';
|
|
167
|
+
status.style.whiteSpace = 'normal';
|
|
168
|
+
status.style.overflowWrap = 'anywhere';
|
|
169
|
+
button.onclick = async () => {
|
|
170
|
+
const startedAt = Date.now();
|
|
171
|
+
button.disabled = true;
|
|
172
|
+
button.textContent = 'Testing...';
|
|
173
|
+
status.textContent = 'Checking...';
|
|
174
|
+
status.style.color = '#6b7280';
|
|
175
|
+
try {
|
|
176
|
+
const { testDeviceConnection } = await import('./api.js');
|
|
177
|
+
const result = await testDeviceConnection({
|
|
178
|
+
deviceId: String(device?.id || device?.deviceId || ''),
|
|
179
|
+
connectionType: device?.connectionType,
|
|
180
|
+
address: device?.address,
|
|
181
|
+
});
|
|
182
|
+
const measuredLatency = Number(result?.latencyMs) > 0
|
|
183
|
+
? Number(result.latencyMs)
|
|
184
|
+
: Date.now() - startedAt;
|
|
185
|
+
if (result?.success) {
|
|
186
|
+
const method = result?.method || 'Auto';
|
|
187
|
+
status.textContent = `✓ ${method} · ${measuredLatency}ms`;
|
|
188
|
+
status.style.color = '#16a34a';
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
const detail = result?.message ? ` · ${result.message}` : '';
|
|
192
|
+
status.textContent = `✗ Failed · ${measuredLatency}ms${detail}`;
|
|
193
|
+
status.style.color = '#dc2626';
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch (e) {
|
|
197
|
+
status.textContent = `✗ Failed · ${Date.now() - startedAt}ms`;
|
|
198
|
+
status.style.color = '#dc2626';
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
button.disabled = false;
|
|
202
|
+
button.textContent = 'Test Connection';
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
controls.appendChild(button);
|
|
206
|
+
controls.appendChild(status);
|
|
207
|
+
return controls;
|
|
208
|
+
}
|
|
209
|
+
function formatLastSeen(value) {
|
|
210
|
+
if (!value) {
|
|
211
|
+
return 'N/A';
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
const date = new Date(value);
|
|
215
|
+
return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString();
|
|
216
|
+
}
|
|
217
|
+
catch (_e) {
|
|
218
|
+
return String(value);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
export function renderDeviceDetailsPanel(device) {
|
|
222
|
+
const details = document.createElement('div');
|
|
223
|
+
details.style.cssText = `
|
|
224
|
+
border-top: 1px solid #ddd;
|
|
225
|
+
padding: 8px;
|
|
226
|
+
background: #f9fafb;
|
|
227
|
+
border-radius: 4px;
|
|
228
|
+
font-size: 12px;
|
|
229
|
+
margin-top: 4px;
|
|
230
|
+
`;
|
|
231
|
+
// --- Battery history trending ---
|
|
232
|
+
// Persist battery readings in localStorage per device
|
|
233
|
+
const batteryHistoryKey = `batteryHistory:${device?.id || device?.deviceId}`;
|
|
234
|
+
let batteryHistory = [];
|
|
235
|
+
try {
|
|
236
|
+
const raw = localStorage.getItem(batteryHistoryKey);
|
|
237
|
+
if (raw)
|
|
238
|
+
batteryHistory = JSON.parse(raw);
|
|
239
|
+
}
|
|
240
|
+
catch { }
|
|
241
|
+
const now = Date.now();
|
|
242
|
+
if (typeof device?.battery === 'number') {
|
|
243
|
+
// Only add if different from last or >1h since last
|
|
244
|
+
const last = batteryHistory[batteryHistory.length - 1];
|
|
245
|
+
if (!last || last.value !== device.battery || now - last.ts > 60 * 60 * 1000) {
|
|
246
|
+
batteryHistory.push({ value: device.battery, ts: now });
|
|
247
|
+
// Keep only last 30 entries (about a month if daily)
|
|
248
|
+
if (batteryHistory.length > 30)
|
|
249
|
+
batteryHistory = batteryHistory.slice(-30);
|
|
250
|
+
try {
|
|
251
|
+
localStorage.setItem(batteryHistoryKey, JSON.stringify(batteryHistory));
|
|
252
|
+
}
|
|
253
|
+
catch { }
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const rows = [
|
|
257
|
+
{ label: 'Device ID', value: String(device?.id || device?.deviceId || 'N/A'), copyable: !!(device?.id || device?.deviceId) },
|
|
258
|
+
{ label: 'MAC Address', value: String(device?.address || 'N/A'), copyable: !!device?.address },
|
|
259
|
+
{ label: 'Device Type', value: String(device?.type || device?.configDeviceType || 'N/A') },
|
|
260
|
+
{ label: 'Model', value: String(device?.model || 'N/A') },
|
|
261
|
+
{ label: 'Hub ID', value: String(device?.hubDeviceId || 'N/A') },
|
|
262
|
+
{ label: 'Battery', value: device?.battery !== undefined && device?.battery !== null ? `${device.battery}%` : 'N/A' },
|
|
263
|
+
{ label: 'Firmware', value: String(device?.version || device?.firmware || 'N/A') },
|
|
264
|
+
{ label: 'Cloud Service', value: device?.enabled === false ? 'Disabled' : 'Enabled' },
|
|
265
|
+
{ label: 'Last Seen', value: formatLastSeen(device?.lastSeen || device?.lastseen || device?.updatedAt) },
|
|
266
|
+
];
|
|
267
|
+
for (const row of rows) {
|
|
268
|
+
const line = document.createElement('div');
|
|
269
|
+
line.style.display = 'flex';
|
|
270
|
+
line.style.alignItems = 'center';
|
|
271
|
+
line.style.justifyContent = 'space-between';
|
|
272
|
+
line.style.gap = '8px';
|
|
273
|
+
line.style.padding = '2px 0';
|
|
274
|
+
const label = document.createElement('span');
|
|
275
|
+
label.style.fontWeight = '600';
|
|
276
|
+
label.style.minWidth = '110px';
|
|
277
|
+
label.textContent = `${row.label}:`;
|
|
278
|
+
const valueWrap = document.createElement('span');
|
|
279
|
+
valueWrap.style.display = 'inline-flex';
|
|
280
|
+
valueWrap.style.alignItems = 'center';
|
|
281
|
+
valueWrap.style.gap = '6px';
|
|
282
|
+
valueWrap.style.flex = '1';
|
|
283
|
+
valueWrap.style.justifyContent = 'flex-end';
|
|
284
|
+
valueWrap.style.minWidth = '0';
|
|
285
|
+
const value = document.createElement('span');
|
|
286
|
+
value.style.fontFamily = 'monospace';
|
|
287
|
+
value.style.fontSize = '11px';
|
|
288
|
+
value.style.opacity = '0.9';
|
|
289
|
+
value.style.whiteSpace = 'normal';
|
|
290
|
+
value.style.overflowWrap = 'anywhere';
|
|
291
|
+
value.style.wordBreak = 'break-word';
|
|
292
|
+
value.style.textAlign = 'right';
|
|
293
|
+
value.textContent = row.value;
|
|
294
|
+
valueWrap.appendChild(value);
|
|
295
|
+
if (row.copyable && row.value && row.value !== 'N/A') {
|
|
296
|
+
const copyBtn = document.createElement('button');
|
|
297
|
+
copyBtn.textContent = '📋';
|
|
298
|
+
copyBtn.title = `Copy ${row.label}`;
|
|
299
|
+
copyBtn.style.padding = '2px 6px';
|
|
300
|
+
copyBtn.style.fontSize = '10px';
|
|
301
|
+
copyBtn.style.lineHeight = '1';
|
|
302
|
+
copyBtn.style.background = '#e5e7eb';
|
|
303
|
+
copyBtn.style.color = '#111827';
|
|
304
|
+
copyBtn.onclick = async () => {
|
|
305
|
+
try {
|
|
306
|
+
await navigator.clipboard.writeText(row.value);
|
|
307
|
+
copyBtn.textContent = '✓';
|
|
308
|
+
setTimeout(() => {
|
|
309
|
+
copyBtn.textContent = '📋';
|
|
310
|
+
}, 1200);
|
|
311
|
+
}
|
|
312
|
+
catch (_e) {
|
|
313
|
+
copyBtn.textContent = '!';
|
|
314
|
+
setTimeout(() => {
|
|
315
|
+
copyBtn.textContent = '📋';
|
|
316
|
+
}, 1200);
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
valueWrap.appendChild(copyBtn);
|
|
320
|
+
}
|
|
321
|
+
line.appendChild(label);
|
|
322
|
+
line.appendChild(valueWrap);
|
|
323
|
+
details.appendChild(line);
|
|
324
|
+
}
|
|
325
|
+
// --- Expose advanced/extra features dynamically ---
|
|
326
|
+
const featureKeys = [
|
|
327
|
+
'airQuality', 'pm25', 'pm10', 'voc', 'co2', 'humidity', 'temperature',
|
|
328
|
+
'preset', 'mode', 'presetMode', 'direction', 'calibration', 'multiCommand',
|
|
329
|
+
'extendedInfo', 'segmentedControl', 'features', 'capabilities', 'state',
|
|
330
|
+
];
|
|
331
|
+
const shown = new Set(rows.map(r => r.label.toLowerCase().replace(/\s/g, '')));
|
|
332
|
+
for (const key of featureKeys) {
|
|
333
|
+
if (device && device[key] !== undefined && !shown.has(key.toLowerCase())) {
|
|
334
|
+
const line = document.createElement('div');
|
|
335
|
+
line.style.display = 'flex';
|
|
336
|
+
line.style.alignItems = 'center';
|
|
337
|
+
line.style.justifyContent = 'space-between';
|
|
338
|
+
line.style.gap = '8px';
|
|
339
|
+
line.style.padding = '2px 0';
|
|
340
|
+
const label = document.createElement('span');
|
|
341
|
+
label.style.fontWeight = '600';
|
|
342
|
+
label.style.minWidth = '110px';
|
|
343
|
+
label.textContent = key.replace(/([A-Z])/g, ' $1').replace(/^./, s => s.toUpperCase()) + ':';
|
|
344
|
+
const value = document.createElement('span');
|
|
345
|
+
value.style.fontFamily = 'monospace';
|
|
346
|
+
value.style.fontSize = '11px';
|
|
347
|
+
value.style.opacity = '0.9';
|
|
348
|
+
value.style.whiteSpace = 'normal';
|
|
349
|
+
value.style.overflowWrap = 'anywhere';
|
|
350
|
+
value.style.wordBreak = 'break-word';
|
|
351
|
+
value.style.textAlign = 'right';
|
|
352
|
+
value.textContent = typeof device[key] === 'object' ? JSON.stringify(device[key]) : String(device[key]);
|
|
353
|
+
line.appendChild(label);
|
|
354
|
+
line.appendChild(value);
|
|
355
|
+
details.appendChild(line);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return details;
|
|
359
|
+
}
|
|
154
360
|
export async function renderDiscoveredDevices(devices, options = {}) {
|
|
155
361
|
const ul = document.createElement('ul');
|
|
362
|
+
ul.className = 'device-grid';
|
|
156
363
|
ul.style.maxHeight = '400px';
|
|
157
364
|
ul.style.overflowY = 'auto';
|
|
158
365
|
ul.style.marginTop = '12px';
|
|
@@ -161,36 +368,104 @@ export async function renderDiscoveredDevices(devices, options = {}) {
|
|
|
161
368
|
const { addDeviceToConfig } = await import('./discovery.js');
|
|
162
369
|
const { loadConfiguredDevices } = await import('./devices.js');
|
|
163
370
|
const configuredIds = options.configuredIds ?? new Set();
|
|
371
|
+
const selectedIds = options.selectedIds ?? new Set();
|
|
372
|
+
const onToggleSelect = options.onToggleSelect;
|
|
164
373
|
for (const d of devices) {
|
|
165
374
|
const deviceId = normalizeId(d.id);
|
|
166
375
|
const alreadyAdded = configuredIds.has(deviceId);
|
|
167
376
|
const li = document.createElement('li');
|
|
168
377
|
li.className = 'device-item';
|
|
169
378
|
li.style.display = 'flex';
|
|
170
|
-
li.style.
|
|
171
|
-
li.style.
|
|
172
|
-
li.style.
|
|
173
|
-
li.style.
|
|
174
|
-
li.style.
|
|
379
|
+
li.style.flexDirection = 'column';
|
|
380
|
+
li.style.alignItems = 'stretch';
|
|
381
|
+
li.style.justifyContent = 'flex-start';
|
|
382
|
+
li.style.padding = '5px 8px';
|
|
383
|
+
li.style.marginBottom = '0';
|
|
384
|
+
li.style.borderRadius = '5px';
|
|
175
385
|
li.style.transition = 'all 0.2s ease';
|
|
176
386
|
const info = document.createElement('div');
|
|
177
|
-
info.style.flex = '1';
|
|
387
|
+
info.style.flex = '1 1 auto';
|
|
388
|
+
info.style.width = '100%';
|
|
389
|
+
info.style.minWidth = '0';
|
|
178
390
|
const nameContainer = document.createElement('div');
|
|
179
391
|
nameContainer.style.display = 'flex';
|
|
180
392
|
nameContainer.style.alignItems = 'center';
|
|
181
|
-
nameContainer.style.marginBottom = '
|
|
393
|
+
nameContainer.style.marginBottom = '0';
|
|
182
394
|
nameContainer.style.flexWrap = 'wrap';
|
|
183
|
-
nameContainer.style.gap = '
|
|
395
|
+
nameContainer.style.gap = '4px';
|
|
184
396
|
const name = document.createElement('div');
|
|
185
397
|
name.style.fontWeight = '500';
|
|
186
|
-
name.style.fontSize = '
|
|
398
|
+
name.style.fontSize = '13px';
|
|
187
399
|
name.textContent = d.name || d.id;
|
|
400
|
+
const selectCheckbox = document.createElement('input');
|
|
401
|
+
selectCheckbox.type = 'checkbox';
|
|
402
|
+
selectCheckbox.style.width = 'auto';
|
|
403
|
+
selectCheckbox.style.margin = '0 2px 0 0';
|
|
404
|
+
selectCheckbox.checked = selectedIds.has(deviceId);
|
|
405
|
+
if (alreadyAdded) {
|
|
406
|
+
selectCheckbox.disabled = true;
|
|
407
|
+
selectCheckbox.title = 'Already configured';
|
|
408
|
+
}
|
|
409
|
+
selectCheckbox.onchange = () => {
|
|
410
|
+
onToggleSelect?.(d, selectCheckbox.checked);
|
|
411
|
+
// Notify listeners (e.g., batch buttons) of selection change
|
|
412
|
+
window.dispatchEvent(new CustomEvent('discovery-selection-changed'));
|
|
413
|
+
};
|
|
414
|
+
nameContainer.appendChild(selectCheckbox);
|
|
188
415
|
nameContainer.appendChild(name);
|
|
416
|
+
// Show firmware update available indicator if present
|
|
417
|
+
if (d.firmwareUpdateAvailable) {
|
|
418
|
+
const fwBadge = document.createElement('span');
|
|
419
|
+
fwBadge.textContent = 'Update Available';
|
|
420
|
+
fwBadge.style.cssText = 'background: #fb923c; color: #111; padding: 1px 7px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;';
|
|
421
|
+
fwBadge.title = 'A firmware update is available for this device.';
|
|
422
|
+
nameContainer.appendChild(fwBadge);
|
|
423
|
+
}
|
|
424
|
+
// Show offline/unreachable indicator if device is offline
|
|
425
|
+
let offline = false;
|
|
426
|
+
let lastSeen = d.lastSeen || d.lastseen || d.updatedAt;
|
|
427
|
+
if (typeof d.offline === 'boolean') {
|
|
428
|
+
offline = d.offline;
|
|
429
|
+
}
|
|
430
|
+
else if (lastSeen) {
|
|
431
|
+
try {
|
|
432
|
+
const last = new Date(lastSeen).getTime();
|
|
433
|
+
if (!isNaN(last) && Date.now() - last > 1000 * 60 * 60) { // 1 hour
|
|
434
|
+
offline = true;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
catch { }
|
|
438
|
+
}
|
|
439
|
+
if (offline) {
|
|
440
|
+
const offlineBadge = document.createElement('span');
|
|
441
|
+
offlineBadge.textContent = 'Offline';
|
|
442
|
+
offlineBadge.style.cssText = 'background: #dc2626; color: white; padding: 1px 7px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;';
|
|
443
|
+
offlineBadge.title = 'Device is offline or unreachable.';
|
|
444
|
+
nameContainer.appendChild(offlineBadge);
|
|
445
|
+
}
|
|
446
|
+
const expandedDetails = document.createElement('div');
|
|
447
|
+
expandedDetails.style.display = 'none';
|
|
448
|
+
expandedDetails.appendChild(renderDeviceDetailsPanel(d));
|
|
449
|
+
const expandBtn = document.createElement('button');
|
|
450
|
+
expandBtn.textContent = '▾';
|
|
451
|
+
expandBtn.title = 'Show details';
|
|
452
|
+
expandBtn.style.padding = '2px 6px';
|
|
453
|
+
expandBtn.style.fontSize = '11px';
|
|
454
|
+
expandBtn.style.marginLeft = '4px';
|
|
455
|
+
expandBtn.style.background = '#e5e7eb';
|
|
456
|
+
expandBtn.style.color = '#111827';
|
|
457
|
+
expandBtn.style.transition = 'transform 0.2s ease';
|
|
458
|
+
expandBtn.onclick = () => {
|
|
459
|
+
const isHidden = expandedDetails.style.display === 'none';
|
|
460
|
+
expandedDetails.style.display = isHidden ? 'block' : 'none';
|
|
461
|
+
expandBtn.style.transform = isHidden ? 'rotate(180deg)' : 'rotate(0deg)';
|
|
462
|
+
};
|
|
463
|
+
nameContainer.appendChild(expandBtn);
|
|
189
464
|
const duplicateBadge = document.createElement('span');
|
|
190
465
|
duplicateBadge.textContent = alreadyAdded ? '✓ Already Added' : '➕ New Device';
|
|
191
466
|
duplicateBadge.style.cssText = alreadyAdded
|
|
192
|
-
? 'background: #16a34a; color: white; padding:
|
|
193
|
-
: 'background: #2563eb; color: white; padding:
|
|
467
|
+
? 'background: #16a34a; color: white; padding: 1px 5px; border-radius: 3px; font-size: 9px; font-weight: 600;'
|
|
468
|
+
: 'background: #2563eb; color: white; padding: 1px 5px; border-radius: 3px; font-size: 9px; font-weight: 600;';
|
|
194
469
|
nameContainer.appendChild(duplicateBadge);
|
|
195
470
|
// Add connection type badge
|
|
196
471
|
if (d.connectionType) {
|
|
@@ -208,11 +483,25 @@ export async function renderDiscoveredDevices(devices, options = {}) {
|
|
|
208
483
|
nameContainer.appendChild(renderSignalBars(d.rssi));
|
|
209
484
|
nameContainer.appendChild(renderSignalQualityBadge(d.rssi));
|
|
210
485
|
}
|
|
486
|
+
// Add battery warning indicator if battery < 20%
|
|
487
|
+
if (typeof d.battery === 'number' && d.battery < 20) {
|
|
488
|
+
const batteryWarn = document.createElement('span');
|
|
489
|
+
batteryWarn.textContent = `⚠️ ${d.battery}%`;
|
|
490
|
+
batteryWarn.style.cssText =
|
|
491
|
+
d.battery < 10
|
|
492
|
+
? 'background: #dc2626; color: white; padding: 1px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;'
|
|
493
|
+
: 'background: #fbbf24; color: #111; padding: 1px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;';
|
|
494
|
+
batteryWarn.title = d.battery < 10 ? 'Battery critically low' : 'Battery low';
|
|
495
|
+
nameContainer.appendChild(batteryWarn);
|
|
496
|
+
}
|
|
211
497
|
const details = document.createElement('div');
|
|
212
|
-
details.style.fontSize = '
|
|
498
|
+
details.style.fontSize = '10px';
|
|
213
499
|
details.style.opacity = '0.7';
|
|
214
|
-
details.style.marginTop = '
|
|
500
|
+
details.style.marginTop = '0';
|
|
215
501
|
details.style.fontFamily = 'monospace';
|
|
502
|
+
details.style.whiteSpace = 'normal';
|
|
503
|
+
details.style.overflowWrap = 'anywhere';
|
|
504
|
+
details.style.wordBreak = 'break-word';
|
|
216
505
|
let detailsText = `ID: ${d.id} | Type: ${d.type} | Model: ${d.model || 'N/A'}`;
|
|
217
506
|
if (d.hubDeviceId) {
|
|
218
507
|
detailsText += ` | Hub: ${d.hubDeviceId}`;
|
|
@@ -223,11 +512,13 @@ export async function renderDiscoveredDevices(devices, options = {}) {
|
|
|
223
512
|
details.textContent = detailsText;
|
|
224
513
|
info.appendChild(nameContainer);
|
|
225
514
|
info.appendChild(details);
|
|
515
|
+
info.appendChild(expandedDetails);
|
|
226
516
|
const addBtn = document.createElement('button');
|
|
227
517
|
addBtn.textContent = alreadyAdded ? 'Already Added' : 'Add to Config';
|
|
228
|
-
addBtn.style.marginLeft = '
|
|
229
|
-
addBtn.style.
|
|
230
|
-
addBtn.style.
|
|
518
|
+
addBtn.style.marginLeft = '0';
|
|
519
|
+
addBtn.style.marginTop = '2px';
|
|
520
|
+
addBtn.style.padding = '4px 9px';
|
|
521
|
+
addBtn.style.fontSize = '11px';
|
|
231
522
|
addBtn.style.whiteSpace = 'nowrap';
|
|
232
523
|
addBtn.style.flexShrink = '0';
|
|
233
524
|
addBtn.disabled = alreadyAdded;
|
|
@@ -246,25 +537,44 @@ export async function renderDiscoveredDevices(devices, options = {}) {
|
|
|
246
537
|
const viewBtn = document.createElement('button');
|
|
247
538
|
viewBtn.textContent = 'View in Config';
|
|
248
539
|
viewBtn.className = 'secondary';
|
|
249
|
-
viewBtn.style.marginLeft = '
|
|
250
|
-
viewBtn.style.padding = '
|
|
251
|
-
viewBtn.style.fontSize = '
|
|
540
|
+
viewBtn.style.marginLeft = '0';
|
|
541
|
+
viewBtn.style.padding = '4px 9px';
|
|
542
|
+
viewBtn.style.fontSize = '11px';
|
|
252
543
|
viewBtn.onclick = async () => {
|
|
253
544
|
await loadConfiguredDevices();
|
|
254
545
|
scrollToConfiguredDevice(d.id);
|
|
255
546
|
};
|
|
256
547
|
li.appendChild(info);
|
|
257
548
|
const actions = document.createElement('div');
|
|
549
|
+
actions.className = 'device-actions';
|
|
258
550
|
actions.style.display = 'flex';
|
|
259
551
|
actions.style.alignItems = 'center';
|
|
552
|
+
actions.style.flexWrap = 'wrap';
|
|
553
|
+
actions.style.justifyContent = 'flex-start';
|
|
554
|
+
actions.style.marginLeft = '0';
|
|
555
|
+
actions.style.width = '100%';
|
|
556
|
+
actions.style.marginTop = '2px';
|
|
557
|
+
actions.style.gap = '5px';
|
|
260
558
|
actions.appendChild(viewBtn);
|
|
261
559
|
actions.appendChild(addBtn);
|
|
560
|
+
actions.appendChild(createConnectionTestControls(d));
|
|
262
561
|
li.appendChild(actions);
|
|
263
562
|
ul.appendChild(li);
|
|
264
563
|
continue;
|
|
265
564
|
}
|
|
565
|
+
const actions = document.createElement('div');
|
|
566
|
+
actions.className = 'device-actions';
|
|
567
|
+
actions.style.display = 'flex';
|
|
568
|
+
actions.style.flexWrap = 'wrap';
|
|
569
|
+
actions.style.justifyContent = 'flex-start';
|
|
570
|
+
actions.style.marginLeft = '0';
|
|
571
|
+
actions.style.width = '100%';
|
|
572
|
+
actions.style.marginTop = '2px';
|
|
573
|
+
actions.style.gap = '5px';
|
|
574
|
+
actions.appendChild(addBtn);
|
|
575
|
+
actions.appendChild(createConnectionTestControls(d));
|
|
266
576
|
li.appendChild(info);
|
|
267
|
-
li.appendChild(
|
|
577
|
+
li.appendChild(actions);
|
|
268
578
|
ul.appendChild(li);
|
|
269
579
|
}
|
|
270
580
|
return ul;
|
|
@@ -409,6 +719,7 @@ export function renderDeviceList(list) {
|
|
|
409
719
|
return;
|
|
410
720
|
}
|
|
411
721
|
status.textContent = `Found ${list.length} device(s)`;
|
|
722
|
+
ul.classList.add('device-grid');
|
|
412
723
|
ul.innerHTML = '';
|
|
413
724
|
// Show remove all button when devices exist
|
|
414
725
|
if (removeAllContainer) {
|
|
@@ -418,22 +729,56 @@ export function renderDeviceList(list) {
|
|
|
418
729
|
const li = document.createElement('li');
|
|
419
730
|
li.className = 'device-item';
|
|
420
731
|
li.setAttribute('data-device-id', normalizeId(d.id));
|
|
732
|
+
li.style.display = 'flex';
|
|
733
|
+
li.style.flexDirection = 'column';
|
|
734
|
+
li.style.alignItems = 'stretch';
|
|
735
|
+
li.style.marginBottom = '0';
|
|
421
736
|
const info = document.createElement('div');
|
|
422
|
-
info.style.flex = '1';
|
|
737
|
+
info.style.flex = '1 1 auto';
|
|
738
|
+
info.style.width = '100%';
|
|
739
|
+
info.style.minWidth = '0';
|
|
423
740
|
const nameContainer = document.createElement('div');
|
|
424
741
|
nameContainer.style.display = 'flex';
|
|
425
|
-
nameContainer.style.
|
|
426
|
-
nameContainer.style.
|
|
427
|
-
nameContainer.style.gap = '
|
|
742
|
+
nameContainer.style.flexDirection = 'column';
|
|
743
|
+
nameContainer.style.alignItems = 'flex-start';
|
|
744
|
+
nameContainer.style.gap = '0';
|
|
428
745
|
const name = document.createElement('div');
|
|
429
746
|
name.style.fontWeight = '500';
|
|
747
|
+
name.style.fontSize = '13px';
|
|
430
748
|
name.textContent = d.name || d.id;
|
|
749
|
+
const expandedDetails = document.createElement('div');
|
|
750
|
+
expandedDetails.style.display = 'none';
|
|
751
|
+
expandedDetails.appendChild(renderDeviceDetailsPanel(d));
|
|
752
|
+
const expandBtn = document.createElement('button');
|
|
753
|
+
expandBtn.textContent = '▾';
|
|
754
|
+
expandBtn.title = 'Show details';
|
|
755
|
+
expandBtn.style.padding = '2px 6px';
|
|
756
|
+
expandBtn.style.fontSize = '11px';
|
|
757
|
+
expandBtn.style.marginLeft = '4px';
|
|
758
|
+
expandBtn.style.background = '#e5e7eb';
|
|
759
|
+
expandBtn.style.color = '#111827';
|
|
760
|
+
expandBtn.style.transition = 'transform 0.2s ease';
|
|
761
|
+
expandBtn.onclick = () => {
|
|
762
|
+
const isHidden = expandedDetails.style.display === 'none';
|
|
763
|
+
expandedDetails.style.display = isHidden ? 'block' : 'none';
|
|
764
|
+
expandBtn.style.transform = isHidden ? 'rotate(180deg)' : 'rotate(0deg)';
|
|
765
|
+
};
|
|
431
766
|
const code = document.createElement('code');
|
|
432
767
|
code.textContent = d.id;
|
|
433
|
-
code.style.fontSize = '
|
|
768
|
+
code.style.fontSize = '10px';
|
|
434
769
|
code.style.opacity = '0.75';
|
|
435
|
-
code.style.marginLeft = '
|
|
436
|
-
|
|
770
|
+
code.style.marginLeft = '0';
|
|
771
|
+
code.style.whiteSpace = 'normal';
|
|
772
|
+
code.style.overflowWrap = 'anywhere';
|
|
773
|
+
code.style.wordBreak = 'break-word';
|
|
774
|
+
code.style.maxWidth = '100%';
|
|
775
|
+
const headerRow = document.createElement('div');
|
|
776
|
+
headerRow.style.display = 'inline-flex';
|
|
777
|
+
headerRow.style.alignItems = 'center';
|
|
778
|
+
headerRow.style.gap = '4px';
|
|
779
|
+
headerRow.appendChild(name);
|
|
780
|
+
headerRow.appendChild(expandBtn);
|
|
781
|
+
nameContainer.appendChild(headerRow);
|
|
437
782
|
nameContainer.appendChild(code);
|
|
438
783
|
// Add signal strength visualization if RSSI is available
|
|
439
784
|
if (d.rssi !== undefined && d.rssi !== null && d.rssi !== 0) {
|
|
@@ -442,29 +787,36 @@ export function renderDeviceList(list) {
|
|
|
442
787
|
}
|
|
443
788
|
const meta = document.createElement('div');
|
|
444
789
|
meta.style.opacity = '0.75';
|
|
445
|
-
meta.style.marginTop = '
|
|
446
|
-
meta.style.fontSize = '
|
|
790
|
+
meta.style.marginTop = '0';
|
|
791
|
+
meta.style.fontSize = '11px';
|
|
447
792
|
const typeText = d.type ? `type: ${d.type}` : '';
|
|
448
793
|
const connText = d.connectionPreference ? `conn: ${d.connectionPreference}` : '';
|
|
449
794
|
const roomText = d.room ? `room: ${d.room}` : '';
|
|
450
795
|
meta.textContent = [typeText, connText, roomText].filter(Boolean).join(' | ');
|
|
451
796
|
info.appendChild(nameContainer);
|
|
452
797
|
info.appendChild(meta);
|
|
798
|
+
info.appendChild(expandedDetails);
|
|
453
799
|
const buttons = document.createElement('div');
|
|
800
|
+
buttons.className = 'device-actions';
|
|
454
801
|
buttons.style.display = 'flex';
|
|
455
|
-
buttons.style.
|
|
802
|
+
buttons.style.flexWrap = 'wrap';
|
|
803
|
+
buttons.style.justifyContent = 'flex-start';
|
|
804
|
+
buttons.style.marginLeft = '0';
|
|
805
|
+
buttons.style.width = '100%';
|
|
806
|
+
buttons.style.marginTop = '2px';
|
|
807
|
+
buttons.style.gap = '5px';
|
|
456
808
|
const editBtn = document.createElement('button');
|
|
457
809
|
editBtn.textContent = '✏️ Edit';
|
|
458
|
-
editBtn.style.padding = '
|
|
459
|
-
editBtn.style.fontSize = '
|
|
810
|
+
editBtn.style.padding = '4px 9px';
|
|
811
|
+
editBtn.style.fontSize = '11px';
|
|
460
812
|
editBtn.onclick = async () => {
|
|
461
813
|
const { editDevice } = await import('./modals.js');
|
|
462
814
|
await editDevice(d);
|
|
463
815
|
};
|
|
464
816
|
const copyBtn = document.createElement('button');
|
|
465
817
|
copyBtn.textContent = 'Copy ID';
|
|
466
|
-
copyBtn.style.padding = '
|
|
467
|
-
copyBtn.style.fontSize = '
|
|
818
|
+
copyBtn.style.padding = '4px 9px';
|
|
819
|
+
copyBtn.style.fontSize = '11px';
|
|
468
820
|
copyBtn.addEventListener('click', async () => {
|
|
469
821
|
try {
|
|
470
822
|
await navigator.clipboard.writeText(d.id);
|
|
@@ -481,8 +833,8 @@ export function renderDeviceList(list) {
|
|
|
481
833
|
});
|
|
482
834
|
const deleteBtn = document.createElement('button');
|
|
483
835
|
deleteBtn.textContent = '🗑️ Delete';
|
|
484
|
-
deleteBtn.style.padding = '
|
|
485
|
-
deleteBtn.style.fontSize = '
|
|
836
|
+
deleteBtn.style.padding = '4px 9px';
|
|
837
|
+
deleteBtn.style.fontSize = '11px';
|
|
486
838
|
deleteBtn.style.background = '#ef4444';
|
|
487
839
|
deleteBtn.onclick = async () => {
|
|
488
840
|
const { deleteDeviceFromConfig } = await import('./devices-delete.js');
|
|
@@ -490,6 +842,7 @@ export function renderDeviceList(list) {
|
|
|
490
842
|
};
|
|
491
843
|
buttons.appendChild(editBtn);
|
|
492
844
|
buttons.appendChild(copyBtn);
|
|
845
|
+
buttons.appendChild(createConnectionTestControls(d));
|
|
493
846
|
buttons.appendChild(deleteBtn);
|
|
494
847
|
li.appendChild(info);
|
|
495
848
|
li.appendChild(buttons);
|