@switchbot/homebridge-switchbot 5.0.0-beta.112 → 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.
Files changed (82) hide show
  1. package/MIGRATION.md +10 -0
  2. package/README.md +43 -0
  3. package/TODO.md +263 -0
  4. package/config.schema.json +27 -10
  5. package/dist/device-types.d.ts.map +1 -1
  6. package/dist/device-types.js +93 -63
  7. package/dist/device-types.js.map +1 -1
  8. package/dist/deviceFactory.d.ts.map +1 -1
  9. package/dist/deviceFactory.js +55 -33
  10. package/dist/deviceFactory.js.map +1 -1
  11. package/dist/devices/genericDevice.d.ts +12 -1
  12. package/dist/devices/genericDevice.d.ts.map +1 -1
  13. package/dist/devices/genericDevice.js +121 -1
  14. package/dist/devices/genericDevice.js.map +1 -1
  15. package/dist/errors.d.ts +5 -0
  16. package/dist/errors.d.ts.map +1 -0
  17. package/dist/errors.js +51 -0
  18. package/dist/errors.js.map +1 -0
  19. package/dist/homebridge-ui/endpoints/devices.d.ts.map +1 -1
  20. package/dist/homebridge-ui/endpoints/devices.js +80 -1
  21. package/dist/homebridge-ui/endpoints/devices.js.map +1 -1
  22. package/dist/homebridge-ui/endpoints/discovery.d.ts.map +1 -1
  23. package/dist/homebridge-ui/endpoints/discovery.js +73 -41
  24. package/dist/homebridge-ui/endpoints/discovery.js.map +1 -1
  25. package/dist/homebridge-ui/public/css/styles.css +98 -0
  26. package/dist/homebridge-ui/public/index.html +57 -0
  27. package/dist/homebridge-ui/public/js/api.d.ts +24 -1
  28. package/dist/homebridge-ui/public/js/api.d.ts.map +1 -1
  29. package/dist/homebridge-ui/public/js/api.js +33 -2
  30. package/dist/homebridge-ui/public/js/api.js.map +1 -1
  31. package/dist/homebridge-ui/public/js/api.ts +52 -3
  32. package/dist/homebridge-ui/public/js/app.js +1503 -104
  33. package/dist/homebridge-ui/public/js/app.js.map +3 -3
  34. package/dist/homebridge-ui/public/js/app.ts +2 -1
  35. package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -1
  36. package/dist/homebridge-ui/public/js/devices.js +2 -0
  37. package/dist/homebridge-ui/public/js/devices.js.map +1 -1
  38. package/dist/homebridge-ui/public/js/devices.ts +13 -1
  39. package/dist/homebridge-ui/public/js/discovery.d.ts +1 -0
  40. package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -1
  41. package/dist/homebridge-ui/public/js/discovery.js +1223 -126
  42. package/dist/homebridge-ui/public/js/discovery.js.map +1 -1
  43. package/dist/homebridge-ui/public/js/discovery.ts +1323 -145
  44. package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -1
  45. package/dist/homebridge-ui/public/js/modals.js +42 -0
  46. package/dist/homebridge-ui/public/js/modals.js.map +1 -1
  47. package/dist/homebridge-ui/public/js/modals.ts +50 -1
  48. package/dist/homebridge-ui/public/js/render.d.ts +3 -0
  49. package/dist/homebridge-ui/public/js/render.d.ts.map +1 -1
  50. package/dist/homebridge-ui/public/js/render.js +311 -1
  51. package/dist/homebridge-ui/public/js/render.js.map +1 -1
  52. package/dist/homebridge-ui/public/js/render.ts +341 -2
  53. package/dist/platform.d.ts +1 -0
  54. package/dist/platform.d.ts.map +1 -1
  55. package/dist/platform.js +6 -2
  56. package/dist/platform.js.map +1 -1
  57. package/dist/switchbotClient.d.ts.map +1 -1
  58. package/dist/switchbotClient.js +51 -5
  59. package/dist/switchbotClient.js.map +1 -1
  60. package/docs/assets/highlight.css +9 -2
  61. package/docs/index.html +28 -2
  62. package/docs/variables/default.html +1 -1
  63. package/package.json +2 -2
  64. package/src/device-types.ts +95 -64
  65. package/src/deviceFactory.ts +55 -33
  66. package/src/devices/genericDevice.ts +113 -1
  67. package/src/errors.ts +43 -0
  68. package/src/homebridge-ui/endpoints/devices.ts +82 -1
  69. package/src/homebridge-ui/endpoints/discovery.ts +78 -44
  70. package/src/homebridge-ui/public/css/styles.css +98 -0
  71. package/src/homebridge-ui/public/index.html +57 -0
  72. package/src/homebridge-ui/public/js/api.ts +52 -3
  73. package/src/homebridge-ui/public/js/app.ts +2 -1
  74. package/src/homebridge-ui/public/js/devices.ts +13 -1
  75. package/src/homebridge-ui/public/js/discovery.ts +1323 -145
  76. package/src/homebridge-ui/public/js/modals.ts +50 -1
  77. package/src/homebridge-ui/public/js/render.ts +341 -2
  78. package/src/platform.ts +6 -2
  79. package/src/switchbotClient.ts +44 -9
  80. package/test/matter-integration.spec.ts +21 -0
  81. package/todo/COMPLETED.md +300 -0
  82. package/todo/TODO.md +80 -66
@@ -42,10 +42,12 @@ __export(api_exports, {
42
42
  deleteAllDevices: () => deleteAllDevices,
43
43
  deleteDevice: () => deleteDevice,
44
44
  discoverDevices: () => discoverDevices,
45
+ fetchBluetoothStatus: () => fetchBluetoothStatus,
45
46
  fetchCredentialStatus: () => fetchCredentialStatus,
46
47
  fetchDevices: () => fetchDevices,
47
48
  saveCredentials: () => saveCredentials,
48
49
  syncParentPluginConfigFromDisk: () => syncParentPluginConfigFromDisk,
50
+ testDeviceConnection: () => testDeviceConnection,
49
51
  updateDevice: () => updateDevice
50
52
  });
51
53
  function isSwitchBotPlatformConfig(block) {
@@ -125,9 +127,9 @@ async function fetchDevices() {
125
127
  return [];
126
128
  }
127
129
  }
128
- async function discoverDevices() {
130
+ async function discoverDevices(mode = "all", options) {
129
131
  try {
130
- const resp = await homebridge.request("/discover", {});
132
+ const resp = await homebridge.request("/discover", { mode, ...options });
131
133
  uiLog.info("Discover response:", resp);
132
134
  if (!resp || resp.success === false) {
133
135
  throw new Error(resp?.data?.message || "Discovery failed");
@@ -137,6 +139,30 @@ async function discoverDevices() {
137
139
  throw e;
138
140
  }
139
141
  }
142
+ async function fetchBluetoothStatus() {
143
+ try {
144
+ const resp = await homebridge.request("/ble-status", {});
145
+ if (!resp || resp.success === false) {
146
+ return { available: false, message: "Bluetooth status unavailable" };
147
+ }
148
+ return resp.data || { available: false, message: "Bluetooth status unavailable" };
149
+ } catch (_e) {
150
+ return { available: false, message: "Bluetooth status unavailable" };
151
+ }
152
+ }
153
+ async function testDeviceConnection(payload) {
154
+ const resp = await homebridge.request("/test-connection", payload);
155
+ if (!resp || resp.success === false) {
156
+ throw new Error(resp?.data?.message || "Connection test failed");
157
+ }
158
+ return resp.data || {
159
+ success: false,
160
+ deviceId: payload.deviceId,
161
+ method: "Auto",
162
+ latencyMs: 0,
163
+ message: "Connection test failed"
164
+ };
165
+ }
140
166
  async function addDevice(deviceId, name, type, options) {
141
167
  try {
142
168
  const payload = { deviceId, name, type };
@@ -149,6 +175,12 @@ async function addDevice(deviceId, name, type, options) {
149
175
  if (options?.rssi !== void 0 && options?.rssi !== null && options?.rssi !== 0) {
150
176
  payload.rssi = options.rssi;
151
177
  }
178
+ if (options?.encryptionKey) {
179
+ payload.encryptionKey = options.encryptionKey;
180
+ }
181
+ if (options?.keyId) {
182
+ payload.keyId = options.keyId;
183
+ }
152
184
  uiLog.info("Adding device to config:", payload);
153
185
  const resp = await homebridge.request("/add-device", payload);
154
186
  uiLog.info("Add device response:", resp);
@@ -292,83 +324,466 @@ var init_toast = __esm({
292
324
  var discovery_exports = {};
293
325
  __export(discovery_exports, {
294
326
  addDeviceToConfig: () => addDeviceToConfig,
295
- discoverDevices: () => discoverDevices2
327
+ discoverDevices: () => discoverDevices2,
328
+ initializeDiscoverySettings: () => initializeDiscoverySettings
296
329
  });
330
+ async function batchSetDeviceEnabled(selectedIds, enabled) {
331
+ const resp = await homebridge.request("/platform-config", {});
332
+ if (!resp || resp.success === false || !resp.data) {
333
+ throw new Error("Failed to load config");
334
+ }
335
+ const config = resp.data;
336
+ const configArr = Array.isArray(config) ? config : [config];
337
+ const platformIdx = configArr.findIndex((c) => (c.platform || c.name || "").toLowerCase().includes("switchbot"));
338
+ if (platformIdx === -1) {
339
+ throw new Error("SwitchBot platform config not found");
340
+ }
341
+ const platformConfig = configArr[platformIdx];
342
+ if (!Array.isArray(platformConfig.devices)) {
343
+ throw new TypeError("No devices array in config");
344
+ }
345
+ let changed = false;
346
+ for (const dev of platformConfig.devices) {
347
+ const id = String(dev.deviceId || dev.id || "").trim().toLowerCase();
348
+ if (selectedIds.has(id)) {
349
+ if (dev.enabled !== enabled) {
350
+ dev.enabled = enabled;
351
+ changed = true;
352
+ }
353
+ }
354
+ }
355
+ if (changed) {
356
+ if (typeof homebridge.updatePluginConfig === "function") {
357
+ await homebridge.updatePluginConfig(configArr);
358
+ } else {
359
+ throw new TypeError("homebridge.updatePluginConfig is not available");
360
+ }
361
+ if (typeof homebridge.savePluginConfig === "function") {
362
+ await homebridge.savePluginConfig();
363
+ }
364
+ }
365
+ }
297
366
  function normalizeId(value) {
298
367
  return String(value ?? "").trim().toLowerCase();
299
368
  }
369
+ function dedupeById(devices) {
370
+ return devices.filter((d, index, arr) => !!d?.id && arr.findIndex((x) => x?.id === d.id) === index);
371
+ }
372
+ function mergeDiscoveredDevices(existingDevices, incomingDevices) {
373
+ const deviceMap = /* @__PURE__ */ new Map();
374
+ for (const d of dedupeById(existingDevices)) {
375
+ deviceMap.set(d.id, { ...d });
376
+ }
377
+ for (const d of dedupeById(incomingDevices)) {
378
+ const current = deviceMap.get(d.id);
379
+ if (current) {
380
+ const nextConnectionType = current.connectionType === d.connectionType ? current.connectionType : "Both";
381
+ deviceMap.set(d.id, {
382
+ ...current,
383
+ ...d,
384
+ connectionType: nextConnectionType
385
+ });
386
+ } else {
387
+ deviceMap.set(d.id, { ...d });
388
+ }
389
+ }
390
+ return [...deviceMap.values()];
391
+ }
392
+ function setDiscoveryCache(devices) {
393
+ try {
394
+ const payload = { timestamp: Date.now(), devices };
395
+ localStorage.setItem(DISCOVERY_CACHE_KEY, JSON.stringify(payload));
396
+ } catch (_e) {
397
+ }
398
+ }
399
+ function clearDiscoveryCache() {
400
+ try {
401
+ localStorage.removeItem(DISCOVERY_CACHE_KEY);
402
+ } catch (_e) {
403
+ }
404
+ }
405
+ function getDiscoveryCache(validOnly = true) {
406
+ try {
407
+ const stored = localStorage.getItem(DISCOVERY_CACHE_KEY);
408
+ if (!stored) {
409
+ return null;
410
+ }
411
+ const payload = JSON.parse(stored);
412
+ if (!payload || !Array.isArray(payload.devices) || typeof payload.timestamp !== "number") {
413
+ return null;
414
+ }
415
+ const age = Date.now() - payload.timestamp;
416
+ if (validOnly && age > DISCOVERY_CACHE_TTL_MS) {
417
+ return null;
418
+ }
419
+ return payload;
420
+ } catch (_e) {
421
+ return null;
422
+ }
423
+ }
424
+ function getDiscoveryAutoRefreshSeconds() {
425
+ try {
426
+ const stored = localStorage.getItem(DISCOVERY_AUTO_REFRESH_KEY);
427
+ const value = Number(stored || 0);
428
+ return Number.isFinite(value) && value >= 0 ? value : 0;
429
+ } catch (_e) {
430
+ return 0;
431
+ }
432
+ }
433
+ function setDiscoveryAutoRefreshSeconds(value) {
434
+ try {
435
+ localStorage.setItem(DISCOVERY_AUTO_REFRESH_KEY, String(Math.max(0, value)));
436
+ } catch (_e) {
437
+ }
438
+ }
439
+ function getDiscoveryHideAddedPreference() {
440
+ try {
441
+ return localStorage.getItem(DISCOVERY_HIDE_ADDED_KEY) === "true";
442
+ } catch (_e) {
443
+ return false;
444
+ }
445
+ }
446
+ function setDiscoveryHideAddedPreference(value) {
447
+ try {
448
+ localStorage.setItem(DISCOVERY_HIDE_ADDED_KEY, String(value));
449
+ } catch (_e) {
450
+ }
451
+ }
452
+ function formatElapsedShort(ms) {
453
+ const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
454
+ if (totalSeconds < 60) {
455
+ return `${totalSeconds}s ago`;
456
+ }
457
+ const minutes = Math.floor(totalSeconds / 60);
458
+ if (minutes < 60) {
459
+ return `${minutes}m ago`;
460
+ }
461
+ const hours = Math.floor(minutes / 60);
462
+ if (hours < 24) {
463
+ return `${hours}h ago`;
464
+ }
465
+ const days = Math.floor(hours / 24);
466
+ return `${days}d ago`;
467
+ }
468
+ function updateLastScannedStatus() {
469
+ const lastScannedStatus = document.getElementById("lastScannedStatus");
470
+ if (!lastScannedStatus) {
471
+ return;
472
+ }
473
+ const cache = getDiscoveryCache(false);
474
+ if (!cache) {
475
+ lastScannedStatus.textContent = "Last scanned: never";
476
+ return;
477
+ }
478
+ const ageMs = Date.now() - cache.timestamp;
479
+ const timestampText = new Date(cache.timestamp).toLocaleString();
480
+ const stale = ageMs > DISCOVERY_CACHE_TTL_MS;
481
+ lastScannedStatus.textContent = stale ? `Last scanned: ${formatElapsedShort(ageMs)} (${timestampText}, cache expired)` : `Last scanned: ${formatElapsedShort(ageMs)} (${timestampText})`;
482
+ }
483
+ async function renderCachedDiscoveryResults() {
484
+ const cache = getDiscoveryCache(true);
485
+ const list = document.getElementById("discoveredList");
486
+ if (!cache || !list || !cache.devices.length) {
487
+ return;
488
+ }
489
+ list.style.display = "block";
490
+ if (!window._discoverySelectedIds) {
491
+ window._discoverySelectedIds = /* @__PURE__ */ new Set();
492
+ }
493
+ await updateDiscoveryView(
494
+ cache.devices,
495
+ getDiscoveryPreferences(),
496
+ getDiscoveryGroupByPreference(),
497
+ getDiscoveryHideAddedPreference(),
498
+ window._discoverySelectedIds
499
+ );
500
+ }
501
+ function getDiscoveryBleSettings() {
502
+ try {
503
+ const stored = localStorage.getItem(DISCOVERY_BLE_SETTINGS_KEY);
504
+ if (!stored) {
505
+ return { bleEnabled: true, bleScanDurationSeconds: 5, bleTimeoutSeconds: 8 };
506
+ }
507
+ const parsed = JSON.parse(stored);
508
+ return {
509
+ bleEnabled: parsed?.bleEnabled !== false,
510
+ bleScanDurationSeconds: Math.max(3, Math.min(15, Number(parsed?.bleScanDurationSeconds || 5))),
511
+ bleTimeoutSeconds: Math.max(3, Math.min(30, Number(parsed?.bleTimeoutSeconds || 8)))
512
+ };
513
+ } catch (_e) {
514
+ return { bleEnabled: true, bleScanDurationSeconds: 5, bleTimeoutSeconds: 8 };
515
+ }
516
+ }
517
+ function setDiscoveryBleSettings(settings) {
518
+ try {
519
+ localStorage.setItem(DISCOVERY_BLE_SETTINGS_KEY, JSON.stringify(settings));
520
+ } catch (_e) {
521
+ }
522
+ }
523
+ async function initializeDiscoverySettings() {
524
+ const scanSelect = document.getElementById("bleScanDurationSelect");
525
+ const timeoutInput = document.getElementById("bleTimeoutInput");
526
+ const disableBleCheckbox = document.getElementById("disableBleScanCheckbox");
527
+ const scanSetting = document.getElementById("bleScanSetting");
528
+ const timeoutSetting = document.getElementById("bleTimeoutSetting");
529
+ const bluetoothStatus = document.getElementById("bluetoothStatus");
530
+ const autoRefreshSelect = document.getElementById("autoRefreshIntervalSelect");
531
+ const refreshBtn = document.getElementById("refreshDiscoverBtn");
532
+ const current = getDiscoveryBleSettings();
533
+ if (scanSelect) {
534
+ scanSelect.value = String(current.bleScanDurationSeconds);
535
+ }
536
+ if (timeoutInput) {
537
+ timeoutInput.value = String(current.bleTimeoutSeconds);
538
+ }
539
+ if (disableBleCheckbox) {
540
+ disableBleCheckbox.checked = !current.bleEnabled;
541
+ }
542
+ if (autoRefreshSelect) {
543
+ autoRefreshSelect.value = String(getDiscoveryAutoRefreshSeconds());
544
+ }
545
+ const updateBleSettingVisibility = () => {
546
+ const disabled = !!disableBleCheckbox?.checked;
547
+ if (scanSetting) {
548
+ scanSetting.style.display = disabled ? "none" : "inline-flex";
549
+ }
550
+ if (timeoutSetting) {
551
+ timeoutSetting.style.display = disabled ? "none" : "inline-flex";
552
+ }
553
+ };
554
+ const persistFromControls = () => {
555
+ const next = {
556
+ bleEnabled: !(disableBleCheckbox?.checked ?? false),
557
+ bleScanDurationSeconds: Math.max(3, Math.min(15, Number(scanSelect?.value || 5))),
558
+ bleTimeoutSeconds: Math.max(3, Math.min(30, Number(timeoutInput?.value || 8)))
559
+ };
560
+ setDiscoveryBleSettings(next);
561
+ };
562
+ scanSelect?.addEventListener("change", persistFromControls);
563
+ timeoutInput?.addEventListener("change", persistFromControls);
564
+ disableBleCheckbox?.addEventListener("change", () => {
565
+ persistFromControls();
566
+ updateBleSettingVisibility();
567
+ });
568
+ updateBleSettingVisibility();
569
+ if (bluetoothStatus) {
570
+ const status = await fetchBluetoothStatus();
571
+ bluetoothStatus.textContent = status.available ? `Bluetooth: available (${status.message})` : `Bluetooth: unavailable (${status.message})`;
572
+ }
573
+ updateLastScannedStatus();
574
+ if (discoveryLastScannedTimer) {
575
+ clearInterval(discoveryLastScannedTimer);
576
+ }
577
+ discoveryLastScannedTimer = setInterval(updateLastScannedStatus, 15e3);
578
+ refreshBtn?.addEventListener("click", () => {
579
+ void discoverDevices2();
580
+ });
581
+ const applyAutoRefresh = () => {
582
+ const seconds = Math.max(0, Number(autoRefreshSelect?.value || 0));
583
+ setDiscoveryAutoRefreshSeconds(seconds);
584
+ if (discoveryAutoRefreshTimer) {
585
+ clearInterval(discoveryAutoRefreshTimer);
586
+ discoveryAutoRefreshTimer = null;
587
+ }
588
+ if (seconds > 0) {
589
+ discoveryAutoRefreshTimer = setInterval(() => {
590
+ const discoverBtn = document.getElementById("discoverBtn");
591
+ if (!discoverBtn || discoverBtn.disabled || document.hidden) {
592
+ return;
593
+ }
594
+ void discoverDevices2();
595
+ }, seconds * 1e3);
596
+ }
597
+ };
598
+ autoRefreshSelect?.addEventListener("change", applyAutoRefresh);
599
+ applyAutoRefresh();
600
+ await renderCachedDiscoveryResults();
601
+ }
602
+ function getDiscoveryGroupByPreference() {
603
+ try {
604
+ const stored = localStorage.getItem(DISCOVERY_GROUP_BY_KEY);
605
+ if (stored === "hub" || stored === "type") {
606
+ return stored;
607
+ }
608
+ return "connection";
609
+ } catch (_e) {
610
+ return "connection";
611
+ }
612
+ }
613
+ function setDiscoveryGroupByPreference(groupBy) {
614
+ try {
615
+ localStorage.setItem(DISCOVERY_GROUP_BY_KEY, groupBy);
616
+ } catch (_e) {
617
+ }
618
+ }
619
+ function getDiscoveryGroupExpandedState() {
620
+ try {
621
+ const stored = localStorage.getItem(DISCOVERY_GROUP_EXPANDED_KEY);
622
+ if (!stored) {
623
+ return {};
624
+ }
625
+ const parsed = JSON.parse(stored);
626
+ return typeof parsed === "object" && parsed ? parsed : {};
627
+ } catch (_e) {
628
+ return {};
629
+ }
630
+ }
631
+ function setDiscoveryGroupExpandedState(state) {
632
+ try {
633
+ localStorage.setItem(DISCOVERY_GROUP_EXPANDED_KEY, JSON.stringify(state));
634
+ } catch (_e) {
635
+ }
636
+ }
637
+ function isDiscoveryGroupExpanded(groupKey) {
638
+ const state = getDiscoveryGroupExpandedState();
639
+ return state[groupKey] !== false;
640
+ }
641
+ function setDiscoveryGroupExpanded(groupKey, expanded) {
642
+ const state = getDiscoveryGroupExpandedState();
643
+ state[groupKey] = expanded;
644
+ setDiscoveryGroupExpandedState(state);
645
+ }
300
646
  async function discoverDevices2() {
301
647
  const btn = document.getElementById("discoverBtn");
648
+ const cancelBtn = document.getElementById("cancelDiscoverBtn");
302
649
  const status = document.getElementById("discoverStatus");
650
+ const phaseProgress = document.getElementById("discoverPhaseProgress");
651
+ const phaseFill = document.getElementById("discoverPhaseFill");
652
+ const phaseLabel = document.getElementById("discoverPhaseLabel");
303
653
  const list = document.getElementById("discoveredList");
304
654
  const autoAddAll = document.getElementById("autoAddAllCheckbox")?.checked;
655
+ const scanSelect = document.getElementById("bleScanDurationSelect");
656
+ const timeoutInput = document.getElementById("bleTimeoutInput");
657
+ const disableBleCheckbox = document.getElementById("disableBleScanCheckbox");
305
658
  if (!btn || !status || !list) {
306
659
  return;
307
660
  }
308
- try {
309
- showBusyUi();
310
- btn.disabled = true;
311
- btn.textContent = "\u{1F50D} Discovering...";
312
- status.textContent = "Searching SwitchBot account...";
313
- status.classList.remove("error");
314
- const devicesRaw = await discoverDevices();
315
- uiLog.info("Discover response:", devicesRaw);
316
- const devices = devicesRaw.filter((d, index, arr) => !!d?.id && arr.findIndex((x) => x?.id === d.id) === index);
317
- if (!devices.length) {
318
- status.textContent = "No devices found in your SwitchBot account";
319
- toastInfo("No devices found in your SwitchBot account");
320
- list.style.display = "none";
321
- return;
661
+ const spinnerFrames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
662
+ let spinnerIndex = 0;
663
+ const startedAt = Date.now();
664
+ let phaseStartedAt = startedAt;
665
+ let phase = "Preparing discovery...";
666
+ let cancelled = false;
667
+ const bleScanDurationSeconds = scanSelect ? Number(scanSelect.value) : 5;
668
+ const pollIntervalMs = 1e3;
669
+ const scanEndTime = Date.now() + bleScanDurationSeconds * 1e3;
670
+ const setPhase = (nextPhase) => {
671
+ phase = nextPhase;
672
+ phaseStartedAt = Date.now();
673
+ };
674
+ const getPhasePercent = (phaseName) => {
675
+ if (phaseName.includes("Scanning BLE")) {
676
+ return 35;
322
677
  }
323
- if (autoAddAll) {
324
- status.textContent = `Auto-adding ${devices.length} device(s)...`;
678
+ if (phaseName.includes("Fetching OpenAPI")) {
679
+ return 75;
680
+ }
681
+ if (phaseName.includes("Complete")) {
682
+ return 100;
683
+ }
684
+ return 10;
685
+ };
686
+ const renderProgress = () => {
687
+ const totalSeconds = Math.max(0, Math.floor((Date.now() - startedAt) / 1e3));
688
+ const phaseSeconds = Math.max(0, Math.floor((Date.now() - phaseStartedAt) / 1e3));
689
+ const frame = spinnerFrames[spinnerIndex % spinnerFrames.length];
690
+ spinnerIndex += 1;
691
+ status.textContent = `${frame} ${phase} (${phaseSeconds}s, ${totalSeconds}s total)`;
692
+ if (phaseProgress) {
693
+ phaseProgress.style.display = "block";
694
+ }
695
+ if (phaseFill) {
696
+ phaseFill.style.width = `${getPhasePercent(phase)}%`;
697
+ }
698
+ if (phaseLabel) {
699
+ phaseLabel.textContent = phase;
700
+ }
701
+ };
702
+ const progressTimer = setInterval(renderProgress, 250);
703
+ let discoveredDevices = [];
704
+ const preferences = getDiscoveryPreferences();
705
+ let groupBy = getDiscoveryGroupByPreference();
706
+ let hideAdded = getDiscoveryHideAddedPreference();
707
+ let controlsInitialized = false;
708
+ if (!window._discoverySelectedIds) {
709
+ window._discoverySelectedIds = /* @__PURE__ */ new Set();
710
+ }
711
+ const selectedIds = window._discoverySelectedIds;
712
+ setPhase("Scanning BLE (real-time RSSI)");
713
+ let pollTimer = null;
714
+ const pollDiscovery = async () => {
715
+ if (cancelled || Date.now() > scanEndTime) {
716
+ if (pollTimer) {
717
+ clearInterval(pollTimer);
718
+ }
719
+ setPhase("Scan Complete");
325
720
  try {
326
- const bulkResult = await addDevicesInBulk(
327
- devices.map((d) => ({
328
- deviceId: d.id,
329
- name: d.name,
330
- type: d.type,
331
- rssi: d.rssi,
332
- address: d.address,
333
- model: d.model
334
- }))
335
- );
336
- uiLog.info("Bulk add response:", bulkResult);
337
- if (!bulkResult || bulkResult.success === false) {
338
- throw new Error(bulkResult?.data?.message || "Bulk add failed");
339
- }
340
- const addedCount = bulkResult?.addedCount ?? bulkResult?.data?.addedCount ?? 0;
341
- const skippedCount = bulkResult?.skippedCount ?? bulkResult?.data?.skippedCount ?? 0;
342
- status.textContent = `\u2713 Added ${addedCount} device(s)${skippedCount > 0 ? ` (${skippedCount} skipped)` : ""}`;
343
- if (addedCount > 0) {
344
- toastSuccess(`Added ${addedCount} device(s)${skippedCount > 0 ? ` (${skippedCount} skipped)` : ""}`);
345
- } else if (skippedCount > 0) {
346
- toastWarning(`No new devices were added (${skippedCount} skipped)`);
347
- }
348
- status.classList.remove("error");
349
- list.style.display = "none";
350
- if (devices.length > 0) {
351
- const synced = await syncParentPluginConfigFromDisk(true);
352
- status.textContent += synced ? " - Config saved automatically." : " - Warning: config may not persist until you close/reopen settings.";
353
- if (synced) {
354
- toastSuccess("Configuration synced and saved automatically");
355
- } else {
356
- toastWarning("Configuration sync failed; close and reopen settings before Save");
357
- }
358
- }
721
+ const devices = await discoverDevices("ble");
722
+ discoveredDevices = mergeDiscoveredDevices(discoveredDevices, devices);
723
+ setDiscoveryCache(discoveredDevices);
724
+ await updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds);
359
725
  } catch (e) {
360
- uiLog.error("Bulk add error:", e);
361
- status.textContent = `\u2717 Error: ${e instanceof Error ? e.message : "Failed to add devices"}`;
362
- status.classList.add("error");
363
- toastError(e instanceof Error ? e.message : "Failed to add devices");
726
+ uiLog.error("Final BLE poll failed:", e);
364
727
  }
365
- await loadConfiguredDevices();
366
728
  return;
367
729
  }
368
- status.textContent = `Found ${devices.length} device(s)`;
730
+ try {
731
+ const devices = await discoverDevices("ble");
732
+ discoveredDevices = mergeDiscoveredDevices(discoveredDevices, devices);
733
+ setDiscoveryCache(discoveredDevices);
734
+ await updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds);
735
+ } catch (e) {
736
+ uiLog.error("BLE poll failed:", e);
737
+ }
738
+ };
739
+ pollTimer = setInterval(pollDiscovery, pollIntervalMs);
740
+ pollDiscovery();
741
+ async function batchSetDeviceEnabled2(selectedIds2, enabled) {
742
+ const resp = await homebridge.request("/platform-config", {});
743
+ if (!resp || resp.success === false || !resp.data) {
744
+ throw new Error("Failed to load config");
745
+ }
746
+ const config = resp.data;
747
+ const configArr = Array.isArray(config) ? config : [config];
748
+ const platformIdx = configArr.findIndex((c) => (c.platform || c.name || "").toLowerCase().includes("switchbot"));
749
+ if (platformIdx === -1) {
750
+ throw new Error("SwitchBot platform config not found");
751
+ }
752
+ const platformConfig = configArr[platformIdx];
753
+ if (!Array.isArray(platformConfig.devices)) {
754
+ throw new TypeError("No devices array in config");
755
+ }
756
+ let changed = false;
757
+ for (const dev of platformConfig.devices) {
758
+ const id = String(dev.deviceId || dev.id || "").trim().toLowerCase();
759
+ if (selectedIds2.has(id)) {
760
+ if (dev.enabled !== enabled) {
761
+ dev.enabled = enabled;
762
+ changed = true;
763
+ }
764
+ }
765
+ }
766
+ if (changed) {
767
+ if (typeof homebridge.updatePluginConfig === "function") {
768
+ await homebridge.updatePluginConfig(configArr);
769
+ } else {
770
+ throw new TypeError("homebridge.updatePluginConfig is not available");
771
+ }
772
+ if (typeof homebridge.savePluginConfig === "function") {
773
+ await homebridge.savePluginConfig();
774
+ }
775
+ }
776
+ }
777
+ const ensureDiscoveryControls = async () => {
778
+ if (controlsInitialized) {
779
+ return;
780
+ }
781
+ if (!window._discoverySelectedIds) {
782
+ window._discoverySelectedIds = /* @__PURE__ */ new Set();
783
+ }
784
+ const selectedIds2 = window._discoverySelectedIds;
369
785
  const controlsDiv = document.createElement("div");
370
786
  controlsDiv.style.cssText = "margin-bottom: 12px; display: flex; gap: 12px; flex-wrap: wrap; align-items: center;";
371
- const preferences = getDiscoveryPreferences();
372
787
  const filterLabel = document.createElement("label");
373
788
  filterLabel.style.fontSize = "12px";
374
789
  filterLabel.style.fontWeight = "500";
@@ -384,30 +799,29 @@ async function discoverDevices2() {
384
799
  { label: "IR", value: "ir" }
385
800
  ];
386
801
  for (const option of filterOptions) {
387
- const btn2 = document.createElement("button");
388
- btn2.textContent = option.label;
389
- btn2.style.padding = "4px 8px";
390
- btn2.style.fontSize = "11px";
391
- btn2.style.borderRadius = "3px";
392
- btn2.style.cursor = "pointer";
393
- btn2.style.border = preferences.connectionType === option.value ? "2px solid #007AFF" : "1px solid #ccc";
394
- btn2.style.backgroundColor = preferences.connectionType === option.value ? "#f0f7ff" : "#fff";
395
- btn2.style.color = preferences.connectionType === option.value ? "#1d4ed8" : "#374151";
396
- btn2.onclick = () => {
802
+ const filterBtn = document.createElement("button");
803
+ filterBtn.textContent = option.label;
804
+ filterBtn.style.padding = "4px 8px";
805
+ filterBtn.style.fontSize = "11px";
806
+ filterBtn.style.borderRadius = "3px";
807
+ filterBtn.style.cursor = "pointer";
808
+ filterBtn.style.border = preferences.connectionType === option.value ? "2px solid #007AFF" : "1px solid #ccc";
809
+ filterBtn.style.backgroundColor = preferences.connectionType === option.value ? "#f0f7ff" : "#fff";
810
+ filterBtn.style.color = preferences.connectionType === option.value ? "#1d4ed8" : "#374151";
811
+ filterBtn.onclick = () => {
397
812
  preferences.connectionType = option.value;
398
813
  setDiscoveryPreferences(preferences);
399
- updateDiscoveryView(devices, preferences);
400
- for (const b of Array.from(filterGroup.querySelectorAll("button"))) {
401
- ;
814
+ void updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds2);
815
+ Array.prototype.forEach.call(filterGroup.querySelectorAll("button"), (b) => {
402
816
  b.style.border = "1px solid #ccc";
403
817
  b.style.backgroundColor = "#fff";
404
818
  b.style.color = "#374151";
405
- }
406
- btn2.style.border = "2px solid #007AFF";
407
- btn2.style.backgroundColor = "#f0f7ff";
408
- btn2.style.color = "#1d4ed8";
819
+ });
820
+ filterBtn.style.border = "2px solid #007AFF";
821
+ filterBtn.style.backgroundColor = "#f0f7ff";
822
+ filterBtn.style.color = "#1d4ed8";
409
823
  };
410
- filterGroup.appendChild(btn2);
824
+ filterGroup.appendChild(filterBtn);
411
825
  }
412
826
  const sortLabel = document.createElement("label");
413
827
  sortLabel.style.fontSize = "12px";
@@ -426,26 +840,73 @@ async function discoverDevices2() {
426
840
  { label: "Connection", value: "connection" }
427
841
  ];
428
842
  for (const opt of sortOptions) {
429
- const option = document.createElement("option");
430
- option.value = opt.value;
431
- option.textContent = opt.label;
432
- sortSelect.appendChild(option);
843
+ const sortOption = document.createElement("option");
844
+ sortOption.value = opt.value;
845
+ sortOption.textContent = opt.label;
846
+ sortSelect.appendChild(sortOption);
433
847
  }
434
848
  sortSelect.onchange = () => {
435
849
  preferences.sortBy = sortSelect.value;
436
850
  setDiscoveryPreferences(preferences);
437
- updateDiscoveryView(devices, preferences);
851
+ void updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds2);
852
+ };
853
+ const groupLabel = document.createElement("label");
854
+ groupLabel.style.fontSize = "12px";
855
+ groupLabel.style.fontWeight = "500";
856
+ groupLabel.style.marginLeft = "8px";
857
+ groupLabel.textContent = "Group:";
858
+ const groupSelect = document.createElement("select");
859
+ groupSelect.style.fontSize = "11px";
860
+ groupSelect.style.padding = "4px 8px";
861
+ groupSelect.style.borderRadius = "3px";
862
+ groupSelect.value = groupBy;
863
+ const groupOptions = [
864
+ { label: "Connection", value: "connection" },
865
+ { label: "Hub", value: "hub" },
866
+ { label: "Device Type", value: "type" }
867
+ ];
868
+ for (const opt of groupOptions) {
869
+ const groupOption = document.createElement("option");
870
+ groupOption.value = opt.value;
871
+ groupOption.textContent = opt.label;
872
+ groupSelect.appendChild(groupOption);
873
+ }
874
+ groupSelect.onchange = () => {
875
+ groupBy = groupSelect.value;
876
+ setDiscoveryGroupByPreference(groupBy);
877
+ void updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds2);
878
+ };
879
+ const hideAddedLabel = document.createElement("label");
880
+ hideAddedLabel.style.display = "inline-flex";
881
+ hideAddedLabel.style.alignItems = "center";
882
+ hideAddedLabel.style.gap = "4px";
883
+ hideAddedLabel.style.fontSize = "11px";
884
+ hideAddedLabel.style.marginLeft = "8px";
885
+ const hideAddedCheckbox = document.createElement("input");
886
+ hideAddedCheckbox.type = "checkbox";
887
+ hideAddedCheckbox.checked = hideAdded;
888
+ hideAddedCheckbox.style.margin = "0";
889
+ hideAddedCheckbox.style.width = "auto";
890
+ const hideAddedText = document.createElement("span");
891
+ hideAddedText.textContent = "Hide Added";
892
+ hideAddedLabel.appendChild(hideAddedCheckbox);
893
+ hideAddedLabel.appendChild(hideAddedText);
894
+ hideAddedCheckbox.onchange = () => {
895
+ hideAdded = hideAddedCheckbox.checked;
896
+ setDiscoveryHideAddedPreference(hideAdded);
897
+ void updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds2);
438
898
  };
439
899
  const searchInput = document.createElement("input");
440
900
  searchInput.type = "text";
441
901
  searchInput.placeholder = "Search by name, ID, or type...";
442
- searchInput.style.fontSize = "11px";
443
- searchInput.style.padding = "4px 8px";
444
- searchInput.style.borderRadius = "3px";
902
+ searchInput.style.fontSize = "13px";
903
+ searchInput.style.padding = "8px 16px";
904
+ searchInput.style.borderRadius = "6px";
445
905
  searchInput.style.border = "1px solid #ccc";
446
- searchInput.style.flex = "1";
447
- searchInput.style.minWidth = "200px";
448
- searchInput.style.maxWidth = "300px";
906
+ searchInput.style.flex = "1 1 0%";
907
+ searchInput.style.minWidth = "120px";
908
+ searchInput.style.maxWidth = "100%";
909
+ searchInput.style.width = "100%";
449
910
  searchInput.value = preferences.searchQuery;
450
911
  let searchTimeout;
451
912
  searchInput.oninput = () => {
@@ -453,14 +914,139 @@ async function discoverDevices2() {
453
914
  searchTimeout = setTimeout(() => {
454
915
  preferences.searchQuery = searchInput.value;
455
916
  setDiscoveryPreferences(preferences);
456
- updateDiscoveryView(devices, preferences);
917
+ void updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds2);
457
918
  }, 300);
458
919
  };
920
+ const actionBtnStyle = {
921
+ fontSize: "16px",
922
+ padding: "10px 0",
923
+ borderRadius: "10px",
924
+ margin: "0 12px 0 0",
925
+ width: "100%",
926
+ maxWidth: "220px",
927
+ fontWeight: "bold",
928
+ background: "#ef4444",
929
+ color: "#fff",
930
+ border: "none",
931
+ cursor: "pointer",
932
+ boxShadow: "0 2px 8px #0001",
933
+ transition: "background 0.2s",
934
+ outline: "none",
935
+ display: "block"
936
+ };
937
+ const addSelectedBtn = document.createElement("button");
938
+ addSelectedBtn.textContent = "Add Selected to Config";
939
+ Object.assign(addSelectedBtn.style, actionBtnStyle);
940
+ addSelectedBtn.disabled = true;
941
+ addSelectedBtn.onclick = async () => {
942
+ if (!selectedIds2.size) {
943
+ return;
944
+ }
945
+ addSelectedBtn.disabled = true;
946
+ addSelectedBtn.textContent = "Adding...";
947
+ try {
948
+ showBusyUi();
949
+ const selectedDevices = discoveredDevices.filter((d) => selectedIds2.has(normalizeId(d.id)));
950
+ const bulkResult = await addDevicesInBulk(selectedDevices.map((d) => ({
951
+ deviceId: d.id,
952
+ name: d.name,
953
+ type: d.type,
954
+ rssi: d.rssi,
955
+ address: d.address,
956
+ model: d.model
957
+ })));
958
+ uiLog.info("Batch add response:", bulkResult);
959
+ if (!bulkResult || bulkResult.success === false) {
960
+ throw new Error(bulkResult?.data?.message || "Batch add failed");
961
+ }
962
+ const addedCount = bulkResult?.addedCount ?? bulkResult?.data?.addedCount ?? 0;
963
+ const skippedCount = bulkResult?.skippedCount ?? bulkResult?.data?.skippedCount ?? 0;
964
+ toastSuccess(`Added ${addedCount} device(s)${skippedCount > 0 ? ` (${skippedCount} skipped)` : ""}`);
965
+ await loadConfiguredDevices();
966
+ selectedIds2.clear();
967
+ addSelectedBtn.disabled = true;
968
+ addSelectedBtn.textContent = "Add Selected";
969
+ await updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds2);
970
+ } catch (e) {
971
+ uiLog.error("Batch add error:", e);
972
+ toastError(e instanceof Error ? e.message : "Failed to add devices");
973
+ addSelectedBtn.disabled = false;
974
+ addSelectedBtn.textContent = "Add Selected";
975
+ } finally {
976
+ hideBusyUi();
977
+ }
978
+ };
979
+ const enableSelectedBtn = document.createElement("button");
980
+ enableSelectedBtn.textContent = "Enable Selected";
981
+ Object.assign(enableSelectedBtn.style, actionBtnStyle);
982
+ enableSelectedBtn.disabled = true;
983
+ enableSelectedBtn.onclick = async () => {
984
+ if (!selectedIds2.size) {
985
+ return;
986
+ }
987
+ enableSelectedBtn.disabled = true;
988
+ enableSelectedBtn.textContent = "Enabling...";
989
+ try {
990
+ showBusyUi();
991
+ await batchSetDeviceEnabled2(selectedIds2, true);
992
+ toastSuccess("Selected devices enabled");
993
+ await loadConfiguredDevices();
994
+ enableSelectedBtn.disabled = true;
995
+ enableSelectedBtn.textContent = "Enable Selected";
996
+ await updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds2);
997
+ } catch (e) {
998
+ uiLog.error("Batch enable error:", e);
999
+ toastError(e instanceof Error ? e.message : "Failed to enable devices");
1000
+ enableSelectedBtn.disabled = false;
1001
+ enableSelectedBtn.textContent = "Enable Selected";
1002
+ } finally {
1003
+ hideBusyUi();
1004
+ }
1005
+ };
1006
+ const disableSelectedBtn = document.createElement("button");
1007
+ disableSelectedBtn.textContent = "Disable Selected";
1008
+ Object.assign(disableSelectedBtn.style, actionBtnStyle);
1009
+ disableSelectedBtn.disabled = true;
1010
+ disableSelectedBtn.onclick = async () => {
1011
+ if (!selectedIds2.size) {
1012
+ return;
1013
+ }
1014
+ disableSelectedBtn.disabled = true;
1015
+ disableSelectedBtn.textContent = "Disabling...";
1016
+ try {
1017
+ showBusyUi();
1018
+ await batchSetDeviceEnabled2(selectedIds2, false);
1019
+ toastSuccess("Selected devices disabled");
1020
+ await loadConfiguredDevices();
1021
+ disableSelectedBtn.disabled = true;
1022
+ disableSelectedBtn.textContent = "Disable Selected";
1023
+ await updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds2);
1024
+ } catch (e) {
1025
+ uiLog.error("Batch disable error:", e);
1026
+ toastError(e instanceof Error ? e.message : "Failed to disable devices");
1027
+ disableSelectedBtn.disabled = false;
1028
+ disableSelectedBtn.textContent = "Disable Selected";
1029
+ } finally {
1030
+ hideBusyUi();
1031
+ }
1032
+ };
459
1033
  controlsDiv.appendChild(filterLabel);
460
1034
  controlsDiv.appendChild(filterGroup);
461
1035
  controlsDiv.appendChild(sortLabel);
462
1036
  controlsDiv.appendChild(sortSelect);
1037
+ controlsDiv.appendChild(groupLabel);
1038
+ controlsDiv.appendChild(groupSelect);
1039
+ controlsDiv.appendChild(hideAddedLabel);
463
1040
  controlsDiv.appendChild(searchInput);
1041
+ const topActionRow = document.createElement("div");
1042
+ topActionRow.style.display = "flex";
1043
+ topActionRow.style.gap = "20px";
1044
+ topActionRow.style.margin = "18px 0 10px 0";
1045
+ topActionRow.style.justifyContent = "flex-start";
1046
+ topActionRow.appendChild(addSelectedBtn);
1047
+ topActionRow.appendChild(enableSelectedBtn);
1048
+ topActionRow.appendChild(disableSelectedBtn);
1049
+ list.insertBefore(topActionRow, controlsDiv);
464
1050
  list.innerHTML = "";
465
1051
  list.appendChild(controlsDiv);
466
1052
  let deviceListContainer = document.getElementById("discoveredDevices");
@@ -475,41 +1061,497 @@ async function discoverDevices2() {
475
1061
  list.appendChild(deviceListContainer);
476
1062
  }
477
1063
  list.style.display = "block";
478
- await updateDiscoveryView(devices, preferences);
1064
+ controlsInitialized = true;
1065
+ const updateActionButtons = () => {
1066
+ const hasSelection = selectedIds2.size > 0;
1067
+ addSelectedBtn.disabled = !hasSelection;
1068
+ enableSelectedBtn.disabled = !hasSelection;
1069
+ disableSelectedBtn.disabled = !hasSelection;
1070
+ };
1071
+ setInterval(updateActionButtons, 300);
1072
+ };
1073
+ try {
1074
+ const bleSettings = {
1075
+ bleEnabled: !(disableBleCheckbox?.checked ?? false),
1076
+ bleScanDurationSeconds: Math.max(3, Math.min(15, Number(scanSelect?.value || 5))),
1077
+ bleTimeoutSeconds: Math.max(3, Math.min(30, Number(timeoutInput?.value || 8)))
1078
+ };
1079
+ setDiscoveryBleSettings(bleSettings);
1080
+ showBusyUi();
1081
+ btn.disabled = true;
1082
+ btn.textContent = "\u{1F50D} Discovering...";
1083
+ setPhase(bleSettings.bleEnabled ? "Scanning BLE..." : "Skipping BLE scan...");
1084
+ renderProgress();
1085
+ status.classList.remove("error");
1086
+ const devicesFoundDisplay = document.getElementById("discoverDevicesFound");
1087
+ if (devicesFoundDisplay) {
1088
+ devicesFoundDisplay.style.display = "none";
1089
+ devicesFoundDisplay.classList.remove("discovery-scanning-pulse");
1090
+ }
1091
+ if (cancelBtn) {
1092
+ cancelBtn.style.display = "inline-block";
1093
+ cancelBtn.disabled = false;
1094
+ cancelBtn.onclick = () => {
1095
+ cancelled = true;
1096
+ const totalSeconds = Math.max(0, Math.floor((Date.now() - startedAt) / 1e3));
1097
+ status.textContent = `Discovery cancelled (${totalSeconds}s total)`;
1098
+ if (phaseProgress) {
1099
+ phaseProgress.style.display = "none";
1100
+ }
1101
+ if (devicesFoundDisplay) {
1102
+ devicesFoundDisplay.style.display = "none";
1103
+ devicesFoundDisplay.classList.remove("discovery-scanning-pulse");
1104
+ }
1105
+ cancelBtn.style.display = "none";
1106
+ btn.disabled = false;
1107
+ btn.textContent = "\u{1F50D} Discover Devices";
1108
+ hideBusyUi();
1109
+ };
1110
+ }
1111
+ if (bleSettings.bleEnabled) {
1112
+ const bleDevicesRaw = await discoverDevices("ble", bleSettings);
1113
+ if (cancelled) {
1114
+ return;
1115
+ }
1116
+ discoveredDevices = dedupeById(bleDevicesRaw);
1117
+ uiLog.info("BLE discover response:", bleDevicesRaw);
1118
+ if (devicesFoundDisplay && bleDevicesRaw.length > 0) {
1119
+ devicesFoundDisplay.style.display = "inline";
1120
+ devicesFoundDisplay.classList.add("discovery-scanning-pulse");
1121
+ devicesFoundDisplay.textContent = `\u{1F4CA} ${bleDevicesRaw.length} device(s) found (scanning...)`;
1122
+ }
1123
+ } else {
1124
+ discoveredDevices = [];
1125
+ uiLog.info("BLE discovery skipped by user setting");
1126
+ }
1127
+ if (!autoAddAll && discoveredDevices.length > 0) {
1128
+ await ensureDiscoveryControls();
1129
+ await updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds);
1130
+ status.textContent = `Showing ${discoveredDevices.length} device(s) from BLE, fetching OpenAPI...`;
1131
+ }
1132
+ setPhase("Fetching OpenAPI...");
1133
+ renderProgress();
1134
+ try {
1135
+ const openApiDevicesRaw = await discoverDevices("openapi");
1136
+ if (cancelled) {
1137
+ return;
1138
+ }
1139
+ uiLog.info("OpenAPI discover response:", openApiDevicesRaw);
1140
+ discoveredDevices = mergeDiscoveredDevices(discoveredDevices, openApiDevicesRaw);
1141
+ if (devicesFoundDisplay && discoveredDevices.length > 0) {
1142
+ devicesFoundDisplay.textContent = `\u{1F4CA} ${discoveredDevices.length} device(s) found (complete)`;
1143
+ }
1144
+ } catch (openApiError) {
1145
+ uiLog.warn("OpenAPI phase failed during discovery:", openApiError);
1146
+ if (!discoveredDevices.length) {
1147
+ throw openApiError;
1148
+ }
1149
+ if (devicesFoundDisplay) {
1150
+ devicesFoundDisplay.classList.remove("discovery-scanning-pulse");
1151
+ }
1152
+ }
1153
+ setPhase("Complete");
1154
+ renderProgress();
1155
+ uiLog.info("Final merged discover response:", discoveredDevices);
1156
+ if (!discoveredDevices.length) {
1157
+ status.textContent = "No devices found in your SwitchBot account";
1158
+ toastInfo("No devices found in your SwitchBot account");
1159
+ list.style.display = "none";
1160
+ if (devicesFoundDisplay) {
1161
+ devicesFoundDisplay.style.display = "none";
1162
+ devicesFoundDisplay.classList.remove("discovery-scanning-pulse");
1163
+ }
1164
+ clearDiscoveryCache();
1165
+ updateLastScannedStatus();
1166
+ return;
1167
+ }
1168
+ if (autoAddAll) {
1169
+ status.textContent = `Auto-adding ${discoveredDevices.length} device(s)...`;
1170
+ try {
1171
+ const bulkResult = await addDevicesInBulk(
1172
+ discoveredDevices.map((d) => ({
1173
+ deviceId: d.id,
1174
+ name: d.name,
1175
+ type: d.type,
1176
+ rssi: d.rssi,
1177
+ address: d.address,
1178
+ model: d.model
1179
+ }))
1180
+ );
1181
+ uiLog.info("Bulk add response:", bulkResult);
1182
+ if (!bulkResult || bulkResult.success === false) {
1183
+ throw new Error(bulkResult?.data?.message || "Bulk add failed");
1184
+ }
1185
+ const addedCount = bulkResult?.addedCount ?? bulkResult?.data?.addedCount ?? 0;
1186
+ const skippedCount = bulkResult?.skippedCount ?? bulkResult?.data?.skippedCount ?? 0;
1187
+ status.textContent = `\u2713 Added ${addedCount} device(s)${skippedCount > 0 ? ` (${skippedCount} skipped)` : ""}`;
1188
+ if (addedCount > 0) {
1189
+ toastSuccess(`Added ${addedCount} device(s)${skippedCount > 0 ? ` (${skippedCount} skipped)` : ""}`);
1190
+ } else if (skippedCount > 0) {
1191
+ toastWarning(`No new devices were added (${skippedCount} skipped)`);
1192
+ }
1193
+ status.classList.remove("error");
1194
+ list.style.display = "none";
1195
+ if (discoveredDevices.length > 0) {
1196
+ const synced = await syncParentPluginConfigFromDisk(true);
1197
+ status.textContent += synced ? " - Config saved automatically." : " - Warning: config may not persist until you close/reopen settings.";
1198
+ if (synced) {
1199
+ toastSuccess("Configuration synced and saved automatically");
1200
+ } else {
1201
+ toastWarning("Configuration sync failed; close and reopen settings before Save");
1202
+ }
1203
+ }
1204
+ } catch (e) {
1205
+ uiLog.error("Bulk add error:", e);
1206
+ status.textContent = `\u2717 Error: ${e instanceof Error ? e.message : "Failed to add devices"}`;
1207
+ status.classList.add("error");
1208
+ toastError(e instanceof Error ? e.message : "Failed to add devices");
1209
+ }
1210
+ await loadConfiguredDevices();
1211
+ return;
1212
+ }
1213
+ await ensureDiscoveryControls();
1214
+ await updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds);
1215
+ setDiscoveryCache(discoveredDevices);
1216
+ updateLastScannedStatus();
479
1217
  } catch (e) {
1218
+ if (cancelled) {
1219
+ return;
1220
+ }
480
1221
  uiLog.error("Discovery error:", e);
481
1222
  status.textContent = `Error: ${e instanceof Error ? e.message : "Discovery failed"}`;
482
1223
  status.classList.add("error");
483
1224
  toastError(e instanceof Error ? e.message : "Discovery failed");
484
1225
  list.style.display = "none";
485
1226
  } finally {
1227
+ clearInterval(progressTimer);
486
1228
  hideBusyUi();
1229
+ if (phaseProgress) {
1230
+ phaseProgress.style.display = "none";
1231
+ }
1232
+ if (phaseFill) {
1233
+ phaseFill.style.width = "0%";
1234
+ }
1235
+ if (phaseLabel) {
1236
+ phaseLabel.textContent = "";
1237
+ }
1238
+ const devicesFoundDisplay = document.getElementById("discoverDevicesFound");
1239
+ if (devicesFoundDisplay) {
1240
+ devicesFoundDisplay.style.display = "none";
1241
+ devicesFoundDisplay.classList.remove("discovery-scanning-pulse");
1242
+ }
487
1243
  btn.disabled = false;
488
1244
  btn.textContent = "\u{1F50D} Discover Devices";
1245
+ if (cancelBtn) {
1246
+ cancelBtn.disabled = false;
1247
+ cancelBtn.style.display = "none";
1248
+ cancelBtn.onclick = null;
1249
+ }
489
1250
  }
490
1251
  }
491
- async function updateDiscoveryView(allDevices, preferences) {
492
- const filtered = filterDevices(allDevices, preferences.connectionType, preferences.searchQuery);
493
- const sorted = sortDevices(filtered, preferences.sortBy);
494
- const configured = await fetchDevices();
495
- const configuredIds = new Set((configured || []).map((d) => normalizeId(d?.id || d?.deviceId)));
496
- const ul = await renderDiscoveredDevices(sorted, {
497
- configuredIds
1252
+ async function updateDiscoveryView(allDevices, preferences, groupBy, hideAdded, selectedIds) {
1253
+ const visibleDevices = allDevices.filter((d) => {
1254
+ if (hideAdded && d.added) {
1255
+ return false;
1256
+ }
1257
+ return true;
498
1258
  });
1259
+ const configuredIds = new Set(
1260
+ allDevices.filter((d) => d.added).map((d) => normalizeId(d.id))
1261
+ );
1262
+ const getConnectionGroup = (device) => {
1263
+ if (device?.isIR) {
1264
+ return "IR";
1265
+ }
1266
+ const connectionType = String(device?.connectionType || "").toLowerCase();
1267
+ if (connectionType.includes("both")) {
1268
+ return "Both";
1269
+ }
1270
+ if (connectionType.includes("ble")) {
1271
+ return "BLE";
1272
+ }
1273
+ if (connectionType.includes("api")) {
1274
+ return "OpenAPI";
1275
+ }
1276
+ return "Unknown";
1277
+ };
1278
+ const getHubGroup = (device) => {
1279
+ const hub = String(device?.hubDeviceId || "").trim();
1280
+ return hub ? `Hub ${hub}` : "No Hub";
1281
+ };
1282
+ const getTypeGroup = (device) => {
1283
+ const type = String(device?.type || "").trim();
1284
+ return type || "Unknown Type";
1285
+ };
1286
+ const groupedDevices = /* @__PURE__ */ new Map();
1287
+ for (const d of visibleDevices) {
1288
+ let group = getConnectionGroup(d);
1289
+ if (groupBy === "hub") {
1290
+ group = getHubGroup(d);
1291
+ } else if (groupBy === "type") {
1292
+ group = getTypeGroup(d);
1293
+ }
1294
+ const groupDevices = groupedDevices.get(group) || [];
1295
+ groupDevices.push(d);
1296
+ groupedDevices.set(group, groupDevices);
1297
+ }
1298
+ let orderedGroups = [];
1299
+ if (groupBy === "hub") {
1300
+ const hubGroups = [...groupedDevices.keys()].filter((group) => group !== "No Hub").sort((a, b) => a.localeCompare(b));
1301
+ orderedGroups = groupedDevices.has("No Hub") ? [...hubGroups, "No Hub"] : hubGroups;
1302
+ } else if (groupBy === "type") {
1303
+ const typeGroups = [...groupedDevices.keys()].filter((group) => group !== "Unknown Type").sort((a, b) => a.localeCompare(b));
1304
+ orderedGroups = groupedDevices.has("Unknown Type") ? [...typeGroups, "Unknown Type"] : typeGroups;
1305
+ } else {
1306
+ const groupOrder = ["Both", "BLE", "OpenAPI", "IR", "Unknown"];
1307
+ orderedGroups = groupOrder.filter((group) => groupedDevices.has(group));
1308
+ }
1309
+ const container = document.createElement("div");
1310
+ container.id = "discoveredDevices";
1311
+ container.className = "discovery-groups";
1312
+ if (!visibleDevices.length) {
1313
+ const empty = document.createElement("div");
1314
+ empty.className = "discovery-group-empty";
1315
+ empty.textContent = hideAdded ? "No devices match current filters (or all are already added)." : "No devices match current filters.";
1316
+ container.appendChild(empty);
1317
+ } else {
1318
+ for (const groupName of orderedGroups) {
1319
+ const groupItems = groupedDevices.get(groupName);
1320
+ if (!groupItems?.length) {
1321
+ continue;
1322
+ }
1323
+ const groupSection = document.createElement("section");
1324
+ groupSection.className = "discovery-group";
1325
+ const groupStorageKey = `${groupBy}:${groupName}`;
1326
+ let expanded = isDiscoveryGroupExpanded(groupStorageKey);
1327
+ const groupHeader = document.createElement("button");
1328
+ groupHeader.className = "discovery-group-header-btn";
1329
+ groupHeader.type = "button";
1330
+ const setGroupHeaderText = () => {
1331
+ const marker = expanded ? "\u25BE" : "\u25B8";
1332
+ groupHeader.textContent = `${marker} ${groupName} (${groupItems.length})`;
1333
+ };
1334
+ setGroupHeaderText();
1335
+ groupSection.appendChild(groupHeader);
1336
+ const groupList = await renderDiscoveredDevices(groupItems, {
1337
+ configuredIds,
1338
+ selectedIds,
1339
+ onToggleSelect: (device, selected) => {
1340
+ const id = normalizeId(device.id);
1341
+ if (selected) {
1342
+ selectedIds.add(id);
1343
+ } else {
1344
+ selectedIds.delete(id);
1345
+ }
1346
+ const btn = document.querySelector("button")?.parentElement?.querySelector("button");
1347
+ if (btn && btn.textContent?.includes("Add Selected")) {
1348
+ btn.disabled = selectedIds.size === 0;
1349
+ }
1350
+ }
1351
+ });
1352
+ if (!expanded) {
1353
+ groupList.style.display = "none";
1354
+ }
1355
+ groupHeader.onclick = () => {
1356
+ expanded = !expanded;
1357
+ setDiscoveryGroupExpanded(groupStorageKey, expanded);
1358
+ setGroupHeaderText();
1359
+ groupList.style.display = expanded ? "grid" : "none";
1360
+ };
1361
+ groupSection.appendChild(groupList);
1362
+ container.appendChild(groupSection);
1363
+ }
1364
+ }
499
1365
  const existingList = document.getElementById("discoveredDevices");
500
1366
  if (existingList && existingList.parentNode) {
501
- existingList.replaceWith(ul);
1367
+ existingList.replaceWith(container);
502
1368
  } else {
503
1369
  const listContainer = document.getElementById("discoveredList");
504
1370
  if (listContainer) {
505
- listContainer.appendChild(ul);
1371
+ listContainer.appendChild(container);
1372
+ }
1373
+ }
1374
+ const legacyAddSelectedBtns = [];
1375
+ const allBtns = document.querySelectorAll("button");
1376
+ for (let i = 0; i < allBtns.length; i++) {
1377
+ const btn = allBtns[i];
1378
+ if (btn.textContent && btn.textContent.trim() === "Add Selected") {
1379
+ legacyAddSelectedBtns.push(btn);
1380
+ }
1381
+ }
1382
+ for (let i = 0; i < legacyAddSelectedBtns.length; i++) {
1383
+ legacyAddSelectedBtns[i].remove();
1384
+ }
1385
+ function updateBatchButtonStates() {
1386
+ const addSelectedBtn = document.getElementById("addSelectedBtn");
1387
+ const enableSelectedBtn = document.querySelector("button")?.parentElement?.querySelector('button[aria-label="Enable Selected"]');
1388
+ const disableSelectedBtn = document.querySelector("button")?.parentElement?.querySelector('button[aria-label="Disable Selected"]');
1389
+ const hasSelection = selectedIds.size > 0;
1390
+ if (addSelectedBtn) addSelectedBtn.disabled = !hasSelection;
1391
+ if (enableSelectedBtn) enableSelectedBtn.disabled = !hasSelection;
1392
+ if (disableSelectedBtn) disableSelectedBtn.disabled = !hasSelection;
1393
+ }
1394
+ window.removeEventListener("discovery-selection-changed", updateBatchButtonStates);
1395
+ window.addEventListener("discovery-selection-changed", updateBatchButtonStates);
1396
+ let batchControls = document.getElementById("batchImportControls");
1397
+ if (!batchControls) {
1398
+ batchControls = document.createElement("div");
1399
+ batchControls.id = "batchImportControls";
1400
+ batchControls.style.display = "flex";
1401
+ batchControls.style.flexWrap = "wrap";
1402
+ batchControls.style.alignItems = "center";
1403
+ batchControls.style.gap = "18px";
1404
+ batchControls.style.margin = "8px 0 18px 0";
1405
+ batchControls.style.width = "100%";
1406
+ const buttonRow = document.createElement("div");
1407
+ buttonRow.style.display = "flex";
1408
+ buttonRow.style.flexWrap = "nowrap";
1409
+ buttonRow.style.alignItems = "center";
1410
+ buttonRow.style.justifyContent = "center";
1411
+ buttonRow.style.gap = "16px";
1412
+ buttonRow.style.width = "100%";
1413
+ const addSelectedBtn = document.createElement("button");
1414
+ addSelectedBtn.id = "addSelectedBtn";
1415
+ addSelectedBtn.textContent = "Add Selected to Config";
1416
+ addSelectedBtn.disabled = selectedIds.size === 0;
1417
+ addSelectedBtn.style.fontWeight = "600";
1418
+ const redButtonStyle = {
1419
+ width: "220px",
1420
+ padding: "12px 0",
1421
+ fontSize: "17px",
1422
+ marginBottom: "0",
1423
+ boxShadow: "0 2px 8px 0 rgba(220,38,38,0.10)",
1424
+ borderRadius: "8px"
1425
+ };
1426
+ Object.assign(addSelectedBtn.style, redButtonStyle);
1427
+ addSelectedBtn.style.background = "#ef4444";
1428
+ addSelectedBtn.style.color = "white";
1429
+ addSelectedBtn.style.transition = "background 0.2s, box-shadow 0.2s";
1430
+ addSelectedBtn.onmouseenter = function() {
1431
+ addSelectedBtn.style.background = "#dc2626";
1432
+ };
1433
+ addSelectedBtn.onmouseleave = function() {
1434
+ addSelectedBtn.style.background = "#ef4444";
1435
+ };
1436
+ addSelectedBtn.onclick = async () => {
1437
+ if (!selectedIds.size) {
1438
+ return;
1439
+ }
1440
+ addSelectedBtn.disabled = true;
1441
+ addSelectedBtn.textContent = "Adding...";
1442
+ try {
1443
+ const selectedDevices = visibleDevices.filter((d) => selectedIds.has(normalizeId(d.id)));
1444
+ const bulkResult = await addDevicesInBulk(selectedDevices.map((d) => ({
1445
+ deviceId: d.id,
1446
+ name: d.name,
1447
+ type: d.type,
1448
+ rssi: d.rssi,
1449
+ address: d.address,
1450
+ model: d.model
1451
+ })));
1452
+ uiLog.info("Batch add response:", bulkResult);
1453
+ if (!bulkResult || bulkResult.success === false) {
1454
+ throw new Error(bulkResult?.data?.message || "Batch add failed");
1455
+ }
1456
+ const addedCount = bulkResult?.addedCount ?? bulkResult?.data?.addedCount ?? 0;
1457
+ const skippedCount = bulkResult?.skippedCount ?? bulkResult?.data?.skippedCount ?? 0;
1458
+ toastSuccess(`Added ${addedCount} device(s)${skippedCount > 0 ? ` (${skippedCount} skipped)` : ""}`);
1459
+ await loadConfiguredDevices();
1460
+ selectedIds.clear();
1461
+ addSelectedBtn.disabled = true;
1462
+ addSelectedBtn.textContent = "Add Selected to Config";
1463
+ await updateDiscoveryView(allDevices, preferences, groupBy, hideAdded, selectedIds);
1464
+ } catch (e) {
1465
+ uiLog.error("Batch add error:", e);
1466
+ toastError(e instanceof Error ? e.message : "Failed to add devices");
1467
+ addSelectedBtn.disabled = false;
1468
+ addSelectedBtn.textContent = "Add Selected to Config";
1469
+ }
1470
+ };
1471
+ const enableSelectedBtn = document.createElement("button");
1472
+ enableSelectedBtn.textContent = "Enable Selected";
1473
+ enableSelectedBtn.style.fontWeight = "600";
1474
+ Object.assign(enableSelectedBtn.style, redButtonStyle);
1475
+ enableSelectedBtn.style.background = "#ef4444";
1476
+ enableSelectedBtn.style.color = "white";
1477
+ enableSelectedBtn.style.transition = "background 0.2s, box-shadow 0.2s";
1478
+ enableSelectedBtn.onmouseenter = function() {
1479
+ enableSelectedBtn.style.background = "#dc2626";
1480
+ };
1481
+ enableSelectedBtn.onmouseleave = function() {
1482
+ enableSelectedBtn.style.background = "#ef4444";
1483
+ };
1484
+ enableSelectedBtn.disabled = selectedIds.size === 0;
1485
+ enableSelectedBtn.onclick = async () => {
1486
+ if (!selectedIds.size) {
1487
+ return;
1488
+ }
1489
+ enableSelectedBtn.disabled = true;
1490
+ try {
1491
+ showBusyUi();
1492
+ await batchSetDeviceEnabled(selectedIds, true);
1493
+ toastSuccess("Selected devices enabled");
1494
+ await loadConfiguredDevices();
1495
+ await updateDiscoveryView(allDevices, preferences, groupBy, hideAdded, selectedIds);
1496
+ } catch (e) {
1497
+ uiLog.error("Batch enable error:", e);
1498
+ toastError(e instanceof Error ? e.message : "Failed to enable devices");
1499
+ } finally {
1500
+ hideBusyUi();
1501
+ enableSelectedBtn.disabled = false;
1502
+ }
1503
+ };
1504
+ const disableSelectedBtn = document.createElement("button");
1505
+ disableSelectedBtn.textContent = "Disable Selected";
1506
+ disableSelectedBtn.style.fontWeight = "600";
1507
+ Object.assign(disableSelectedBtn.style, redButtonStyle);
1508
+ disableSelectedBtn.style.background = "#ef4444";
1509
+ disableSelectedBtn.style.color = "white";
1510
+ disableSelectedBtn.style.transition = "background 0.2s, box-shadow 0.2s";
1511
+ disableSelectedBtn.onmouseenter = function() {
1512
+ disableSelectedBtn.style.background = "#dc2626";
1513
+ };
1514
+ disableSelectedBtn.onmouseleave = function() {
1515
+ disableSelectedBtn.style.background = "#ef4444";
1516
+ };
1517
+ disableSelectedBtn.disabled = selectedIds.size === 0;
1518
+ disableSelectedBtn.onclick = async () => {
1519
+ if (!selectedIds.size) {
1520
+ return;
1521
+ }
1522
+ disableSelectedBtn.disabled = true;
1523
+ try {
1524
+ showBusyUi();
1525
+ await batchSetDeviceEnabled(selectedIds, false);
1526
+ toastSuccess("Selected devices disabled");
1527
+ await loadConfiguredDevices();
1528
+ await updateDiscoveryView(allDevices, preferences, groupBy, hideAdded, selectedIds);
1529
+ } catch (e) {
1530
+ uiLog.error("Batch disable error:", e);
1531
+ toastError(e instanceof Error ? e.message : "Failed to disable devices");
1532
+ } finally {
1533
+ hideBusyUi();
1534
+ disableSelectedBtn.disabled = false;
1535
+ }
1536
+ };
1537
+ buttonRow.appendChild(addSelectedBtn);
1538
+ buttonRow.appendChild(enableSelectedBtn);
1539
+ buttonRow.appendChild(disableSelectedBtn);
1540
+ batchControls.appendChild(buttonRow);
1541
+ const listContainer = document.getElementById("discoveredList");
1542
+ if (listContainer) {
1543
+ listContainer.insertBefore(batchControls, listContainer.firstChild);
1544
+ }
1545
+ } else {
1546
+ const addSelectedBtn = document.getElementById("addSelectedBtn");
1547
+ if (addSelectedBtn) {
1548
+ addSelectedBtn.disabled = selectedIds.size === 0;
506
1549
  }
507
1550
  }
508
- ul.id = "discoveredDevices";
509
1551
  const status = document.getElementById("discoverStatus");
510
1552
  if (status) {
511
1553
  const totalCount = allDevices.length;
512
- const filteredCount = sorted.length;
1554
+ const filteredCount = visibleDevices.length;
513
1555
  status.textContent = filteredCount === totalCount ? `Found ${totalCount} device(s)` : `Showing ${filteredCount} of ${totalCount} device(s)`;
514
1556
  }
515
1557
  }
@@ -517,6 +1559,7 @@ async function addDeviceToConfig(device) {
517
1559
  const { addDeviceToConfig: addDevice2 } = await Promise.resolve().then(() => (init_devices(), devices_exports));
518
1560
  await addDevice2(device);
519
1561
  }
1562
+ var DISCOVERY_GROUP_BY_KEY, DISCOVERY_GROUP_EXPANDED_KEY, DISCOVERY_BLE_SETTINGS_KEY, DISCOVERY_HIDE_ADDED_KEY, DISCOVERY_CACHE_KEY, DISCOVERY_AUTO_REFRESH_KEY, DISCOVERY_CACHE_TTL_MS, discoveryAutoRefreshTimer, discoveryLastScannedTimer;
520
1563
  var init_discovery = __esm({
521
1564
  "src/homebridge-ui/public/js/discovery.ts"() {
522
1565
  "use strict";
@@ -526,6 +1569,15 @@ var init_discovery = __esm({
526
1569
  init_modal();
527
1570
  init_render();
528
1571
  init_toast();
1572
+ DISCOVERY_GROUP_BY_KEY = "discoveryGroupBy";
1573
+ DISCOVERY_GROUP_EXPANDED_KEY = "discoveryGroupExpanded";
1574
+ DISCOVERY_BLE_SETTINGS_KEY = "discoveryBleSettings";
1575
+ DISCOVERY_HIDE_ADDED_KEY = "discoveryHideAdded";
1576
+ DISCOVERY_CACHE_KEY = "discoveryCache";
1577
+ DISCOVERY_AUTO_REFRESH_KEY = "discoveryAutoRefreshSeconds";
1578
+ DISCOVERY_CACHE_TTL_MS = 5 * 60 * 1e3;
1579
+ discoveryAutoRefreshTimer = null;
1580
+ discoveryLastScannedTimer = null;
529
1581
  }
530
1582
  });
531
1583
 
@@ -743,6 +1795,40 @@ async function importDiscoveredDevice(device) {
743
1795
  macInput.style.borderRadius = "6px";
744
1796
  macInput.style.fontSize = "14px";
745
1797
  macInput.style.boxSizing = "border-box";
1798
+ const encryptionKeyLabel = document.createElement("label");
1799
+ encryptionKeyLabel.textContent = "BLE Encryption Key (optional)";
1800
+ encryptionKeyLabel.style.display = "block";
1801
+ encryptionKeyLabel.style.marginBottom = "6px";
1802
+ encryptionKeyLabel.style.fontWeight = "500";
1803
+ encryptionKeyLabel.style.fontSize = "12px";
1804
+ encryptionKeyLabel.style.color = "#6b7280";
1805
+ const encryptionKeyInput = document.createElement("input");
1806
+ encryptionKeyInput.type = "password";
1807
+ encryptionKeyInput.value = device.encryptionKey || "";
1808
+ encryptionKeyInput.placeholder = "Paste device BLE encryption key";
1809
+ encryptionKeyInput.style.width = "100%";
1810
+ encryptionKeyInput.style.marginBottom = "12px";
1811
+ encryptionKeyInput.style.padding = "8px 10px";
1812
+ encryptionKeyInput.style.borderRadius = "6px";
1813
+ encryptionKeyInput.style.fontSize = "14px";
1814
+ encryptionKeyInput.style.boxSizing = "border-box";
1815
+ const keyIdLabel = document.createElement("label");
1816
+ keyIdLabel.textContent = "BLE Key ID (optional)";
1817
+ keyIdLabel.style.display = "block";
1818
+ keyIdLabel.style.marginBottom = "6px";
1819
+ keyIdLabel.style.fontWeight = "500";
1820
+ keyIdLabel.style.fontSize = "12px";
1821
+ keyIdLabel.style.color = "#6b7280";
1822
+ const keyIdInput = document.createElement("input");
1823
+ keyIdInput.type = "text";
1824
+ keyIdInput.value = device.keyId || "";
1825
+ keyIdInput.placeholder = "e.g. ff";
1826
+ keyIdInput.style.width = "100%";
1827
+ keyIdInput.style.marginBottom = "12px";
1828
+ keyIdInput.style.padding = "8px 10px";
1829
+ keyIdInput.style.borderRadius = "6px";
1830
+ keyIdInput.style.fontSize = "14px";
1831
+ keyIdInput.style.boxSizing = "border-box";
746
1832
  const buttons = document.createElement("div");
747
1833
  buttons.style.display = "flex";
748
1834
  buttons.style.gap = "10px";
@@ -769,7 +1855,9 @@ async function importDiscoveredDevice(device) {
769
1855
  importBtn.onclick = () => cleanup({
770
1856
  configDeviceName: nameInput.value || device.name || device.id,
771
1857
  configDeviceType: typeSelect.value || device.type,
772
- address: macInput.value || void 0
1858
+ address: macInput.value || void 0,
1859
+ encryptionKey: encryptionKeyInput.value || void 0,
1860
+ keyId: keyIdInput.value || void 0
773
1861
  });
774
1862
  div.addEventListener("click", (event) => {
775
1863
  if (event.target === div) {
@@ -784,6 +1872,10 @@ async function importDiscoveredDevice(device) {
784
1872
  contentDiv.appendChild(typeSelect);
785
1873
  contentDiv.appendChild(macLabel);
786
1874
  contentDiv.appendChild(macInput);
1875
+ contentDiv.appendChild(encryptionKeyLabel);
1876
+ contentDiv.appendChild(encryptionKeyInput);
1877
+ contentDiv.appendChild(keyIdLabel);
1878
+ contentDiv.appendChild(keyIdInput);
787
1879
  contentDiv.appendChild(buttons);
788
1880
  modal.appendChild(title);
789
1881
  modal.appendChild(contentDiv);
@@ -1186,6 +2278,7 @@ __export(render_exports, {
1186
2278
  getRssiSignalQuality: () => getRssiSignalQuality,
1187
2279
  renderBadge: () => renderBadge,
1188
2280
  renderConnectionBadge: () => renderConnectionBadge,
2281
+ renderDeviceDetailsPanel: () => renderDeviceDetailsPanel,
1189
2282
  renderDeviceList: () => renderDeviceList,
1190
2283
  renderDiscoveredDevices: () => renderDiscoveredDevices,
1191
2284
  renderIRBadge: () => renderIRBadge,
@@ -1324,6 +2417,213 @@ function scrollToConfiguredDevice(deviceId) {
1324
2417
  target.style.background = originalBackground;
1325
2418
  }, 1800);
1326
2419
  }
2420
+ function createConnectionTestControls(device) {
2421
+ const controls = document.createElement("div");
2422
+ controls.style.display = "inline-flex";
2423
+ controls.style.alignItems = "center";
2424
+ controls.style.gap = "6px";
2425
+ const button = document.createElement("button");
2426
+ button.textContent = "Test Connection";
2427
+ button.className = "secondary";
2428
+ button.style.padding = "4px 9px";
2429
+ button.style.fontSize = "11px";
2430
+ const status = document.createElement("span");
2431
+ status.style.fontSize = "10px";
2432
+ status.style.opacity = "0.85";
2433
+ status.style.whiteSpace = "normal";
2434
+ status.style.overflowWrap = "anywhere";
2435
+ button.onclick = async () => {
2436
+ const startedAt = Date.now();
2437
+ button.disabled = true;
2438
+ button.textContent = "Testing...";
2439
+ status.textContent = "Checking...";
2440
+ status.style.color = "#6b7280";
2441
+ try {
2442
+ const { testDeviceConnection: testDeviceConnection2 } = await Promise.resolve().then(() => (init_api(), api_exports));
2443
+ const result = await testDeviceConnection2({
2444
+ deviceId: String(device?.id || device?.deviceId || ""),
2445
+ connectionType: device?.connectionType,
2446
+ address: device?.address
2447
+ });
2448
+ const measuredLatency = Number(result?.latencyMs) > 0 ? Number(result.latencyMs) : Date.now() - startedAt;
2449
+ if (result?.success) {
2450
+ const method = result?.method || "Auto";
2451
+ status.textContent = `\u2713 ${method} \xB7 ${measuredLatency}ms`;
2452
+ status.style.color = "#16a34a";
2453
+ } else {
2454
+ const detail = result?.message ? ` \xB7 ${result.message}` : "";
2455
+ status.textContent = `\u2717 Failed \xB7 ${measuredLatency}ms${detail}`;
2456
+ status.style.color = "#dc2626";
2457
+ }
2458
+ } catch (e) {
2459
+ status.textContent = `\u2717 Failed \xB7 ${Date.now() - startedAt}ms`;
2460
+ status.style.color = "#dc2626";
2461
+ } finally {
2462
+ button.disabled = false;
2463
+ button.textContent = "Test Connection";
2464
+ }
2465
+ };
2466
+ controls.appendChild(button);
2467
+ controls.appendChild(status);
2468
+ return controls;
2469
+ }
2470
+ function formatLastSeen(value) {
2471
+ if (!value) {
2472
+ return "N/A";
2473
+ }
2474
+ try {
2475
+ const date = new Date(value);
2476
+ return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString();
2477
+ } catch (_e) {
2478
+ return String(value);
2479
+ }
2480
+ }
2481
+ function renderDeviceDetailsPanel(device) {
2482
+ const details = document.createElement("div");
2483
+ details.style.cssText = `
2484
+ border-top: 1px solid #ddd;
2485
+ padding: 8px;
2486
+ background: #f9fafb;
2487
+ border-radius: 4px;
2488
+ font-size: 12px;
2489
+ margin-top: 4px;
2490
+ `;
2491
+ const batteryHistoryKey = `batteryHistory:${device?.id || device?.deviceId}`;
2492
+ let batteryHistory = [];
2493
+ try {
2494
+ const raw = localStorage.getItem(batteryHistoryKey);
2495
+ if (raw) batteryHistory = JSON.parse(raw);
2496
+ } catch {
2497
+ }
2498
+ const now = Date.now();
2499
+ if (typeof device?.battery === "number") {
2500
+ const last = batteryHistory[batteryHistory.length - 1];
2501
+ if (!last || last.value !== device.battery || now - last.ts > 60 * 60 * 1e3) {
2502
+ batteryHistory.push({ value: device.battery, ts: now });
2503
+ if (batteryHistory.length > 30) batteryHistory = batteryHistory.slice(-30);
2504
+ try {
2505
+ localStorage.setItem(batteryHistoryKey, JSON.stringify(batteryHistory));
2506
+ } catch {
2507
+ }
2508
+ }
2509
+ }
2510
+ const rows = [
2511
+ { label: "Device ID", value: String(device?.id || device?.deviceId || "N/A"), copyable: !!(device?.id || device?.deviceId) },
2512
+ { label: "MAC Address", value: String(device?.address || "N/A"), copyable: !!device?.address },
2513
+ { label: "Device Type", value: String(device?.type || device?.configDeviceType || "N/A") },
2514
+ { label: "Model", value: String(device?.model || "N/A") },
2515
+ { label: "Hub ID", value: String(device?.hubDeviceId || "N/A") },
2516
+ { label: "Battery", value: device?.battery !== void 0 && device?.battery !== null ? `${device.battery}%` : "N/A" },
2517
+ { label: "Firmware", value: String(device?.version || device?.firmware || "N/A") },
2518
+ { label: "Cloud Service", value: device?.enabled === false ? "Disabled" : "Enabled" },
2519
+ { label: "Last Seen", value: formatLastSeen(device?.lastSeen || device?.lastseen || device?.updatedAt) }
2520
+ ];
2521
+ for (const row of rows) {
2522
+ const line = document.createElement("div");
2523
+ line.style.display = "flex";
2524
+ line.style.alignItems = "center";
2525
+ line.style.justifyContent = "space-between";
2526
+ line.style.gap = "8px";
2527
+ line.style.padding = "2px 0";
2528
+ const label = document.createElement("span");
2529
+ label.style.fontWeight = "600";
2530
+ label.style.minWidth = "110px";
2531
+ label.textContent = `${row.label}:`;
2532
+ const valueWrap = document.createElement("span");
2533
+ valueWrap.style.display = "inline-flex";
2534
+ valueWrap.style.alignItems = "center";
2535
+ valueWrap.style.gap = "6px";
2536
+ valueWrap.style.flex = "1";
2537
+ valueWrap.style.justifyContent = "flex-end";
2538
+ valueWrap.style.minWidth = "0";
2539
+ const value = document.createElement("span");
2540
+ value.style.fontFamily = "monospace";
2541
+ value.style.fontSize = "11px";
2542
+ value.style.opacity = "0.9";
2543
+ value.style.whiteSpace = "normal";
2544
+ value.style.overflowWrap = "anywhere";
2545
+ value.style.wordBreak = "break-word";
2546
+ value.style.textAlign = "right";
2547
+ value.textContent = row.value;
2548
+ valueWrap.appendChild(value);
2549
+ if (row.copyable && row.value && row.value !== "N/A") {
2550
+ const copyBtn = document.createElement("button");
2551
+ copyBtn.textContent = "\u{1F4CB}";
2552
+ copyBtn.title = `Copy ${row.label}`;
2553
+ copyBtn.style.padding = "2px 6px";
2554
+ copyBtn.style.fontSize = "10px";
2555
+ copyBtn.style.lineHeight = "1";
2556
+ copyBtn.style.background = "#e5e7eb";
2557
+ copyBtn.style.color = "#111827";
2558
+ copyBtn.onclick = async () => {
2559
+ try {
2560
+ await navigator.clipboard.writeText(row.value);
2561
+ copyBtn.textContent = "\u2713";
2562
+ setTimeout(() => {
2563
+ copyBtn.textContent = "\u{1F4CB}";
2564
+ }, 1200);
2565
+ } catch (_e) {
2566
+ copyBtn.textContent = "!";
2567
+ setTimeout(() => {
2568
+ copyBtn.textContent = "\u{1F4CB}";
2569
+ }, 1200);
2570
+ }
2571
+ };
2572
+ valueWrap.appendChild(copyBtn);
2573
+ }
2574
+ line.appendChild(label);
2575
+ line.appendChild(valueWrap);
2576
+ details.appendChild(line);
2577
+ }
2578
+ const featureKeys = [
2579
+ "airQuality",
2580
+ "pm25",
2581
+ "pm10",
2582
+ "voc",
2583
+ "co2",
2584
+ "humidity",
2585
+ "temperature",
2586
+ "preset",
2587
+ "mode",
2588
+ "presetMode",
2589
+ "direction",
2590
+ "calibration",
2591
+ "multiCommand",
2592
+ "extendedInfo",
2593
+ "segmentedControl",
2594
+ "features",
2595
+ "capabilities",
2596
+ "state"
2597
+ ];
2598
+ const shown = new Set(rows.map((r) => r.label.toLowerCase().replace(/\s/g, "")));
2599
+ for (const key of featureKeys) {
2600
+ if (device && device[key] !== void 0 && !shown.has(key.toLowerCase())) {
2601
+ const line = document.createElement("div");
2602
+ line.style.display = "flex";
2603
+ line.style.alignItems = "center";
2604
+ line.style.justifyContent = "space-between";
2605
+ line.style.gap = "8px";
2606
+ line.style.padding = "2px 0";
2607
+ const label = document.createElement("span");
2608
+ label.style.fontWeight = "600";
2609
+ label.style.minWidth = "110px";
2610
+ label.textContent = key.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()) + ":";
2611
+ const value = document.createElement("span");
2612
+ value.style.fontFamily = "monospace";
2613
+ value.style.fontSize = "11px";
2614
+ value.style.opacity = "0.9";
2615
+ value.style.whiteSpace = "normal";
2616
+ value.style.overflowWrap = "anywhere";
2617
+ value.style.wordBreak = "break-word";
2618
+ value.style.textAlign = "right";
2619
+ value.textContent = typeof device[key] === "object" ? JSON.stringify(device[key]) : String(device[key]);
2620
+ line.appendChild(label);
2621
+ line.appendChild(value);
2622
+ details.appendChild(line);
2623
+ }
2624
+ }
2625
+ return details;
2626
+ }
1327
2627
  async function renderDiscoveredDevices(devices, options = {}) {
1328
2628
  const ul = document.createElement("ul");
1329
2629
  ul.className = "device-grid";
@@ -1335,6 +2635,8 @@ async function renderDiscoveredDevices(devices, options = {}) {
1335
2635
  const { addDeviceToConfig: addDeviceToConfig3 } = await Promise.resolve().then(() => (init_discovery(), discovery_exports));
1336
2636
  const { loadConfiguredDevices: loadConfiguredDevices2 } = await Promise.resolve().then(() => (init_devices(), devices_exports));
1337
2637
  const configuredIds = options.configuredIds ?? /* @__PURE__ */ new Set();
2638
+ const selectedIds = options.selectedIds ?? /* @__PURE__ */ new Set();
2639
+ const onToggleSelect = options.onToggleSelect;
1338
2640
  for (const d of devices) {
1339
2641
  const deviceId = normalizeId2(d.id);
1340
2642
  const alreadyAdded = configuredIds.has(deviceId);
@@ -1362,7 +2664,66 @@ async function renderDiscoveredDevices(devices, options = {}) {
1362
2664
  name.style.fontWeight = "500";
1363
2665
  name.style.fontSize = "13px";
1364
2666
  name.textContent = d.name || d.id;
2667
+ const selectCheckbox = document.createElement("input");
2668
+ selectCheckbox.type = "checkbox";
2669
+ selectCheckbox.style.width = "auto";
2670
+ selectCheckbox.style.margin = "0 2px 0 0";
2671
+ selectCheckbox.checked = selectedIds.has(deviceId);
2672
+ if (alreadyAdded) {
2673
+ selectCheckbox.disabled = true;
2674
+ selectCheckbox.title = "Already configured";
2675
+ }
2676
+ selectCheckbox.onchange = () => {
2677
+ onToggleSelect?.(d, selectCheckbox.checked);
2678
+ window.dispatchEvent(new CustomEvent("discovery-selection-changed"));
2679
+ };
2680
+ nameContainer.appendChild(selectCheckbox);
1365
2681
  nameContainer.appendChild(name);
2682
+ if (d.firmwareUpdateAvailable) {
2683
+ const fwBadge = document.createElement("span");
2684
+ fwBadge.textContent = "Update Available";
2685
+ fwBadge.style.cssText = "background: #fb923c; color: #111; padding: 1px 7px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;";
2686
+ fwBadge.title = "A firmware update is available for this device.";
2687
+ nameContainer.appendChild(fwBadge);
2688
+ }
2689
+ let offline = false;
2690
+ let lastSeen = d.lastSeen || d.lastseen || d.updatedAt;
2691
+ if (typeof d.offline === "boolean") {
2692
+ offline = d.offline;
2693
+ } else if (lastSeen) {
2694
+ try {
2695
+ const last = new Date(lastSeen).getTime();
2696
+ if (!isNaN(last) && Date.now() - last > 1e3 * 60 * 60) {
2697
+ offline = true;
2698
+ }
2699
+ } catch {
2700
+ }
2701
+ }
2702
+ if (offline) {
2703
+ const offlineBadge = document.createElement("span");
2704
+ offlineBadge.textContent = "Offline";
2705
+ offlineBadge.style.cssText = "background: #dc2626; color: white; padding: 1px 7px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;";
2706
+ offlineBadge.title = "Device is offline or unreachable.";
2707
+ nameContainer.appendChild(offlineBadge);
2708
+ }
2709
+ const expandedDetails = document.createElement("div");
2710
+ expandedDetails.style.display = "none";
2711
+ expandedDetails.appendChild(renderDeviceDetailsPanel(d));
2712
+ const expandBtn = document.createElement("button");
2713
+ expandBtn.textContent = "\u25BE";
2714
+ expandBtn.title = "Show details";
2715
+ expandBtn.style.padding = "2px 6px";
2716
+ expandBtn.style.fontSize = "11px";
2717
+ expandBtn.style.marginLeft = "4px";
2718
+ expandBtn.style.background = "#e5e7eb";
2719
+ expandBtn.style.color = "#111827";
2720
+ expandBtn.style.transition = "transform 0.2s ease";
2721
+ expandBtn.onclick = () => {
2722
+ const isHidden = expandedDetails.style.display === "none";
2723
+ expandedDetails.style.display = isHidden ? "block" : "none";
2724
+ expandBtn.style.transform = isHidden ? "rotate(180deg)" : "rotate(0deg)";
2725
+ };
2726
+ nameContainer.appendChild(expandBtn);
1366
2727
  const duplicateBadge = document.createElement("span");
1367
2728
  duplicateBadge.textContent = alreadyAdded ? "\u2713 Already Added" : "\u2795 New Device";
1368
2729
  duplicateBadge.style.cssText = alreadyAdded ? "background: #16a34a; color: white; padding: 1px 5px; border-radius: 3px; font-size: 9px; font-weight: 600;" : "background: #2563eb; color: white; padding: 1px 5px; border-radius: 3px; font-size: 9px; font-weight: 600;";
@@ -1380,6 +2741,13 @@ async function renderDiscoveredDevices(devices, options = {}) {
1380
2741
  nameContainer.appendChild(renderSignalBars(d.rssi));
1381
2742
  nameContainer.appendChild(renderSignalQualityBadge(d.rssi));
1382
2743
  }
2744
+ if (typeof d.battery === "number" && d.battery < 20) {
2745
+ const batteryWarn = document.createElement("span");
2746
+ batteryWarn.textContent = `\u26A0\uFE0F ${d.battery}%`;
2747
+ batteryWarn.style.cssText = d.battery < 10 ? "background: #dc2626; color: white; padding: 1px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;" : "background: #fbbf24; color: #111; padding: 1px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;";
2748
+ batteryWarn.title = d.battery < 10 ? "Battery critically low" : "Battery low";
2749
+ nameContainer.appendChild(batteryWarn);
2750
+ }
1383
2751
  const details = document.createElement("div");
1384
2752
  details.style.fontSize = "10px";
1385
2753
  details.style.opacity = "0.7";
@@ -1398,6 +2766,7 @@ async function renderDiscoveredDevices(devices, options = {}) {
1398
2766
  details.textContent = detailsText;
1399
2767
  info.appendChild(nameContainer);
1400
2768
  info.appendChild(details);
2769
+ info.appendChild(expandedDetails);
1401
2770
  const addBtn = document.createElement("button");
1402
2771
  addBtn.textContent = alreadyAdded ? "Already Added" : "Add to Config";
1403
2772
  addBtn.style.marginLeft = "0";
@@ -1442,6 +2811,7 @@ async function renderDiscoveredDevices(devices, options = {}) {
1442
2811
  actions2.style.gap = "5px";
1443
2812
  actions2.appendChild(viewBtn);
1444
2813
  actions2.appendChild(addBtn);
2814
+ actions2.appendChild(createConnectionTestControls(d));
1445
2815
  li.appendChild(actions2);
1446
2816
  ul.appendChild(li);
1447
2817
  continue;
@@ -1456,6 +2826,7 @@ async function renderDiscoveredDevices(devices, options = {}) {
1456
2826
  actions.style.marginTop = "2px";
1457
2827
  actions.style.gap = "5px";
1458
2828
  actions.appendChild(addBtn);
2829
+ actions.appendChild(createConnectionTestControls(d));
1459
2830
  li.appendChild(info);
1460
2831
  li.appendChild(actions);
1461
2832
  ul.appendChild(li);
@@ -1596,6 +2967,23 @@ function renderDeviceList(list) {
1596
2967
  name.style.fontWeight = "500";
1597
2968
  name.style.fontSize = "13px";
1598
2969
  name.textContent = d.name || d.id;
2970
+ const expandedDetails = document.createElement("div");
2971
+ expandedDetails.style.display = "none";
2972
+ expandedDetails.appendChild(renderDeviceDetailsPanel(d));
2973
+ const expandBtn = document.createElement("button");
2974
+ expandBtn.textContent = "\u25BE";
2975
+ expandBtn.title = "Show details";
2976
+ expandBtn.style.padding = "2px 6px";
2977
+ expandBtn.style.fontSize = "11px";
2978
+ expandBtn.style.marginLeft = "4px";
2979
+ expandBtn.style.background = "#e5e7eb";
2980
+ expandBtn.style.color = "#111827";
2981
+ expandBtn.style.transition = "transform 0.2s ease";
2982
+ expandBtn.onclick = () => {
2983
+ const isHidden = expandedDetails.style.display === "none";
2984
+ expandedDetails.style.display = isHidden ? "block" : "none";
2985
+ expandBtn.style.transform = isHidden ? "rotate(180deg)" : "rotate(0deg)";
2986
+ };
1599
2987
  const code = document.createElement("code");
1600
2988
  code.textContent = d.id;
1601
2989
  code.style.fontSize = "10px";
@@ -1605,7 +2993,13 @@ function renderDeviceList(list) {
1605
2993
  code.style.overflowWrap = "anywhere";
1606
2994
  code.style.wordBreak = "break-word";
1607
2995
  code.style.maxWidth = "100%";
1608
- nameContainer.appendChild(name);
2996
+ const headerRow = document.createElement("div");
2997
+ headerRow.style.display = "inline-flex";
2998
+ headerRow.style.alignItems = "center";
2999
+ headerRow.style.gap = "4px";
3000
+ headerRow.appendChild(name);
3001
+ headerRow.appendChild(expandBtn);
3002
+ nameContainer.appendChild(headerRow);
1609
3003
  nameContainer.appendChild(code);
1610
3004
  if (d.rssi !== void 0 && d.rssi !== null && d.rssi !== 0) {
1611
3005
  nameContainer.appendChild(renderSignalBars(d.rssi));
@@ -1621,6 +3015,7 @@ function renderDeviceList(list) {
1621
3015
  meta.textContent = [typeText, connText, roomText].filter(Boolean).join(" | ");
1622
3016
  info.appendChild(nameContainer);
1623
3017
  info.appendChild(meta);
3018
+ info.appendChild(expandedDetails);
1624
3019
  const buttons = document.createElement("div");
1625
3020
  buttons.className = "device-actions";
1626
3021
  buttons.style.display = "flex";
@@ -1666,6 +3061,7 @@ function renderDeviceList(list) {
1666
3061
  };
1667
3062
  buttons.appendChild(editBtn);
1668
3063
  buttons.appendChild(copyBtn);
3064
+ buttons.appendChild(createConnectionTestControls(d));
1669
3065
  buttons.appendChild(deleteBtn);
1670
3066
  li.appendChild(info);
1671
3067
  li.appendChild(buttons);
@@ -1698,7 +3094,9 @@ async function addDeviceToConfig2(device, options = {}) {
1698
3094
  const resp = await addDevice(device.id, importValues.configDeviceName, importValues.configDeviceType, {
1699
3095
  address: importValues.address,
1700
3096
  model: device.model,
1701
- rssi: device.rssi
3097
+ rssi: device.rssi,
3098
+ encryptionKey: importValues.encryptionKey,
3099
+ keyId: importValues.keyId
1702
3100
  });
1703
3101
  uiLog.info("Add device response:", resp);
1704
3102
  if (!resp || resp.success === false) {
@@ -1866,6 +3264,7 @@ window.saveCredentials = saveCredentials2;
1866
3264
  window.discoverDevices = discoverDevices2;
1867
3265
  async function init() {
1868
3266
  await loadCredentialStatus();
3267
+ await initializeDiscoverySettings();
1869
3268
  const list = await loadConfiguredDevices();
1870
3269
  await initRemoveAllButton();
1871
3270
  }