@switchbot/homebridge-switchbot 5.0.0-beta.109 → 5.0.0-beta.110

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 (78) hide show
  1. package/config.schema.json +4 -0
  2. package/dist/device-types.d.ts +18 -2
  3. package/dist/device-types.d.ts.map +1 -1
  4. package/dist/device-types.js +71 -0
  5. package/dist/device-types.js.map +1 -1
  6. package/dist/deviceFactory.d.ts.map +1 -1
  7. package/dist/deviceFactory.js +1 -0
  8. package/dist/deviceFactory.js.map +1 -1
  9. package/dist/homebridge-ui/endpoints/config.d.ts +0 -3
  10. package/dist/homebridge-ui/endpoints/config.d.ts.map +1 -1
  11. package/dist/homebridge-ui/endpoints/config.js +40 -9
  12. package/dist/homebridge-ui/endpoints/config.js.map +1 -1
  13. package/dist/homebridge-ui/endpoints/devices.d.ts.map +1 -1
  14. package/dist/homebridge-ui/endpoints/devices.js +59 -2
  15. package/dist/homebridge-ui/endpoints/devices.js.map +1 -1
  16. package/dist/homebridge-ui/public/css/styles.css +254 -0
  17. package/dist/homebridge-ui/public/index.html +23 -168
  18. package/dist/homebridge-ui/public/js/api.d.ts +4 -0
  19. package/dist/homebridge-ui/public/js/api.d.ts.map +1 -1
  20. package/dist/homebridge-ui/public/js/api.js +14 -0
  21. package/dist/homebridge-ui/public/js/api.js.map +1 -1
  22. package/dist/homebridge-ui/public/js/api.ts +17 -1
  23. package/dist/homebridge-ui/public/js/app.js +556 -44
  24. package/dist/homebridge-ui/public/js/app.js.map +3 -3
  25. package/dist/homebridge-ui/public/js/app.ts +2 -1
  26. package/dist/homebridge-ui/public/js/devices-delete.d.ts +1 -0
  27. package/dist/homebridge-ui/public/js/devices-delete.d.ts.map +1 -1
  28. package/dist/homebridge-ui/public/js/devices-delete.js +108 -1
  29. package/dist/homebridge-ui/public/js/devices-delete.js.map +1 -1
  30. package/dist/homebridge-ui/public/js/devices-delete.ts +124 -1
  31. package/dist/homebridge-ui/public/js/devices.d.ts +1 -0
  32. package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -1
  33. package/dist/homebridge-ui/public/js/devices.js +10 -0
  34. package/dist/homebridge-ui/public/js/devices.js.map +1 -1
  35. package/dist/homebridge-ui/public/js/devices.ts +10 -0
  36. package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -1
  37. package/dist/homebridge-ui/public/js/discovery.js +130 -4
  38. package/dist/homebridge-ui/public/js/discovery.js.map +1 -1
  39. package/dist/homebridge-ui/public/js/discovery.ts +158 -4
  40. package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -1
  41. package/dist/homebridge-ui/public/js/modals.js +71 -26
  42. package/dist/homebridge-ui/public/js/modals.js.map +1 -1
  43. package/dist/homebridge-ui/public/js/modals.ts +74 -27
  44. package/dist/homebridge-ui/public/js/render.d.ts +57 -0
  45. package/dist/homebridge-ui/public/js/render.d.ts.map +1 -1
  46. package/dist/homebridge-ui/public/js/render.js +286 -13
  47. package/dist/homebridge-ui/public/js/render.js.map +1 -1
  48. package/dist/homebridge-ui/public/js/render.ts +331 -11
  49. package/dist/homebridge-ui/utils/device-migration.d.ts +35 -0
  50. package/dist/homebridge-ui/utils/device-migration.d.ts.map +1 -0
  51. package/dist/homebridge-ui/utils/device-migration.js +111 -0
  52. package/dist/homebridge-ui/utils/device-migration.js.map +1 -0
  53. package/dist/platform.js +2 -2
  54. package/dist/platform.js.map +1 -1
  55. package/docs/variables/default.html +1 -1
  56. package/package.json +1 -1
  57. package/src/device-types.ts +85 -0
  58. package/src/deviceFactory.ts +1 -0
  59. package/src/homebridge-ui/endpoints/config.ts +54 -10
  60. package/src/homebridge-ui/endpoints/devices.ts +62 -3
  61. package/src/homebridge-ui/public/css/styles.css +254 -0
  62. package/src/homebridge-ui/public/index.html +23 -168
  63. package/src/homebridge-ui/public/js/api.ts +17 -1
  64. package/src/homebridge-ui/public/js/app.ts +2 -1
  65. package/src/homebridge-ui/public/js/devices-delete.ts +124 -1
  66. package/src/homebridge-ui/public/js/devices.ts +10 -0
  67. package/src/homebridge-ui/public/js/discovery.ts +158 -4
  68. package/src/homebridge-ui/public/js/modals.ts +74 -27
  69. package/src/homebridge-ui/public/js/render.ts +331 -11
  70. package/src/homebridge-ui/utils/device-migration.ts +144 -0
  71. package/src/platform.ts +2 -2
  72. package/todo/COMPLETED.md +189 -0
  73. package/todo/README.md +59 -0
  74. package/todo/TODO.md +205 -0
  75. package/todo/mockups/filtering-sorting.svg +112 -0
  76. package/todo/mockups/modal-switchbot-theme.svg +66 -0
  77. package/todo/mockups/signal-strength-visualization.svg +94 -0
  78. package/TODO.md +0 -564
@@ -39,6 +39,7 @@ var api_exports = {};
39
39
  __export(api_exports, {
40
40
  addDevice: () => addDevice,
41
41
  addDevicesInBulk: () => addDevicesInBulk,
42
+ deleteAllDevices: () => deleteAllDevices,
42
43
  deleteDevice: () => deleteDevice,
43
44
  discoverDevices: () => discoverDevices,
44
45
  fetchCredentialStatus: () => fetchCredentialStatus,
@@ -194,6 +195,19 @@ async function deleteDevice(deviceId) {
194
195
  throw e;
195
196
  }
196
197
  }
198
+ async function deleteAllDevices() {
199
+ try {
200
+ uiLog.info("Sending delete all devices request");
201
+ const resp = await homebridge.request("/delete-all-devices", {});
202
+ uiLog.info("Delete all response:", resp);
203
+ if (!resp || resp.success === false) {
204
+ throw new Error(resp?.data?.message || "Failed to delete all devices");
205
+ }
206
+ return resp.data || resp;
207
+ } catch (e) {
208
+ throw e;
209
+ }
210
+ }
197
211
  var init_api = __esm({
198
212
  "src/homebridge-ui/public/js/api.ts"() {
199
213
  "use strict";
@@ -300,7 +314,10 @@ async function discoverDevices2() {
300
314
  devices.map((d) => ({
301
315
  deviceId: d.id,
302
316
  name: d.name,
303
- type: d.type
317
+ type: d.type,
318
+ rssi: d.rssi,
319
+ address: d.address,
320
+ model: d.model
304
321
  }))
305
322
  );
306
323
  uiLog.info("Bulk add response:", bulkResult);
@@ -336,10 +353,102 @@ async function discoverDevices2() {
336
353
  return;
337
354
  }
338
355
  status.textContent = `Found ${devices.length} device(s)`;
339
- const ul = await renderDiscoveredDevices(devices);
340
- document.getElementById("discoveredDevices")?.replaceWith(ul);
341
- ul.id = "discoveredDevices";
356
+ const controlsDiv = document.createElement("div");
357
+ controlsDiv.style.cssText = "margin-bottom: 12px; display: flex; gap: 12px; flex-wrap: wrap; align-items: center;";
358
+ const preferences = getDiscoveryPreferences();
359
+ const filterLabel = document.createElement("label");
360
+ filterLabel.style.fontSize = "12px";
361
+ filterLabel.style.fontWeight = "500";
362
+ filterLabel.textContent = "Filter:";
363
+ const filterGroup = document.createElement("div");
364
+ filterGroup.style.display = "flex";
365
+ filterGroup.style.gap = "4px";
366
+ const filterOptions = [
367
+ { label: "All", value: "all" },
368
+ { label: "BLE", value: "ble" },
369
+ { label: "API", value: "api" },
370
+ { label: "Both", value: "both" },
371
+ { label: "IR", value: "ir" }
372
+ ];
373
+ for (const option of filterOptions) {
374
+ const btn2 = document.createElement("button");
375
+ btn2.textContent = option.label;
376
+ btn2.style.padding = "4px 8px";
377
+ btn2.style.fontSize = "11px";
378
+ btn2.style.borderRadius = "3px";
379
+ btn2.style.cursor = "pointer";
380
+ btn2.style.border = preferences.connectionType === option.value ? "2px solid #007AFF" : "1px solid #ccc";
381
+ btn2.style.backgroundColor = preferences.connectionType === option.value ? "#f0f7ff" : "#fff";
382
+ btn2.onclick = () => {
383
+ preferences.connectionType = option.value;
384
+ setDiscoveryPreferences(preferences);
385
+ updateDiscoveryView(devices, preferences);
386
+ for (const b of Array.from(filterGroup.querySelectorAll("button"))) {
387
+ ;
388
+ b.style.border = "1px solid #ccc";
389
+ b.style.backgroundColor = "#fff";
390
+ }
391
+ btn2.style.border = "2px solid #007AFF";
392
+ btn2.style.backgroundColor = "#f0f7ff";
393
+ };
394
+ filterGroup.appendChild(btn2);
395
+ }
396
+ const sortLabel = document.createElement("label");
397
+ sortLabel.style.fontSize = "12px";
398
+ sortLabel.style.fontWeight = "500";
399
+ sortLabel.style.marginLeft = "8px";
400
+ sortLabel.textContent = "Sort:";
401
+ const sortSelect = document.createElement("select");
402
+ sortSelect.style.fontSize = "11px";
403
+ sortSelect.style.padding = "4px 8px";
404
+ sortSelect.style.borderRadius = "3px";
405
+ sortSelect.value = preferences.sortBy;
406
+ const sortOptions = [
407
+ { label: "Name", value: "name" },
408
+ { label: "Signal Strength", value: "signal" },
409
+ { label: "Type", value: "type" },
410
+ { label: "Connection", value: "connection" }
411
+ ];
412
+ for (const opt of sortOptions) {
413
+ const option = document.createElement("option");
414
+ option.value = opt.value;
415
+ option.textContent = opt.label;
416
+ sortSelect.appendChild(option);
417
+ }
418
+ sortSelect.onchange = () => {
419
+ preferences.sortBy = sortSelect.value;
420
+ setDiscoveryPreferences(preferences);
421
+ updateDiscoveryView(devices, preferences);
422
+ };
423
+ const searchInput = document.createElement("input");
424
+ searchInput.type = "text";
425
+ searchInput.placeholder = "Search by name, ID, or type...";
426
+ searchInput.style.fontSize = "11px";
427
+ searchInput.style.padding = "4px 8px";
428
+ searchInput.style.borderRadius = "3px";
429
+ searchInput.style.border = "1px solid #ccc";
430
+ searchInput.style.flex = "1";
431
+ searchInput.style.minWidth = "200px";
432
+ searchInput.style.maxWidth = "300px";
433
+ searchInput.value = preferences.searchQuery;
434
+ let searchTimeout;
435
+ searchInput.oninput = () => {
436
+ clearTimeout(searchTimeout);
437
+ searchTimeout = setTimeout(() => {
438
+ preferences.searchQuery = searchInput.value;
439
+ setDiscoveryPreferences(preferences);
440
+ updateDiscoveryView(devices, preferences);
441
+ }, 300);
442
+ };
443
+ controlsDiv.appendChild(filterLabel);
444
+ controlsDiv.appendChild(filterGroup);
445
+ controlsDiv.appendChild(sortLabel);
446
+ controlsDiv.appendChild(sortSelect);
447
+ controlsDiv.appendChild(searchInput);
448
+ list.innerHTML = "";
449
+ list.appendChild(controlsDiv);
342
450
  list.style.display = "block";
451
+ await updateDiscoveryView(devices, preferences);
343
452
  } catch (e) {
344
453
  uiLog.error("Discovery error:", e);
345
454
  status.textContent = `Error: ${e instanceof Error ? e.message : "Discovery failed"}`;
@@ -352,6 +461,19 @@ async function discoverDevices2() {
352
461
  btn.textContent = "\u{1F50D} Discover Devices";
353
462
  }
354
463
  }
464
+ async function updateDiscoveryView(allDevices, preferences) {
465
+ const filtered = filterDevices(allDevices, preferences.connectionType, preferences.searchQuery);
466
+ const sorted = sortDevices(filtered, preferences.sortBy);
467
+ const ul = await renderDiscoveredDevices(sorted);
468
+ document.getElementById("discoveredDevices")?.replaceWith(ul);
469
+ ul.id = "discoveredDevices";
470
+ const status = document.getElementById("discoverStatus");
471
+ if (status) {
472
+ const totalCount = allDevices.length;
473
+ const filteredCount = sorted.length;
474
+ status.textContent = filteredCount === totalCount ? `Found ${totalCount} device(s)` : `Showing ${filteredCount} of ${totalCount} device(s)`;
475
+ }
476
+ }
355
477
  async function addDeviceToConfig(device) {
356
478
  const { addDeviceToConfig: addDevice2 } = await Promise.resolve().then(() => (init_devices(), devices_exports));
357
479
  await addDevice2(device);
@@ -380,6 +502,7 @@ var init_device_types = __esm({
380
502
  "Keypad Touch",
381
503
  "Keypad Vision",
382
504
  "Keypad Vision Pro",
505
+ "Lock Vision Pro",
383
506
  "Lock Lite",
384
507
  "Smart Lock",
385
508
  "Smart Lock Pro",
@@ -411,8 +534,11 @@ var init_device_types = __esm({
411
534
  "Humidifier2",
412
535
  "Meter",
413
536
  "MeterPlus",
537
+ "Meter Plus",
414
538
  "MeterPro",
539
+ "Meter Pro",
415
540
  "MeterPro(CO2)",
541
+ "Meter Pro (CO2)",
416
542
  "Smart Radiator Thermostat",
417
543
  "Standing Circulator Fan",
418
544
  "WoIOSensor"
@@ -495,49 +621,80 @@ async function editDevice(device) {
495
621
  const modal = document.createElement("div");
496
622
  modal.style.background = getComputedStyle(document.body).backgroundColor;
497
623
  modal.style.color = getComputedStyle(document.body).color;
498
- modal.style.padding = "24px";
499
- modal.style.borderRadius = "8px";
500
- modal.style.minWidth = "400px";
501
- modal.style.boxShadow = "0 10px 40px rgba(0,0,0,0.3)";
624
+ modal.style.padding = "0";
625
+ modal.style.borderRadius = "10px";
626
+ modal.style.minWidth = "440px";
627
+ modal.style.maxWidth = "90vw";
628
+ modal.style.boxShadow = "0 8px 32px rgba(0,0,0,0.35)";
629
+ modal.style.overflow = "hidden";
630
+ modal.style.borderTop = "3px solid var(--switchbot-red, #ef4444)";
502
631
  const title = document.createElement("h3");
503
632
  title.textContent = "Edit Device";
504
633
  title.style.marginTop = "0";
634
+ title.style.marginBottom = "16px";
635
+ title.style.padding = "20px 20px 0";
636
+ title.style.fontSize = "18px";
637
+ title.style.fontWeight = "600";
638
+ title.style.color = "var(--switchbot-red, #ef4444)";
639
+ title.style.letterSpacing = "-0.02em";
640
+ const contentDiv = document.createElement("div");
641
+ contentDiv.style.padding = "0 20px 20px";
505
642
  const nameLabel = document.createElement("label");
506
643
  nameLabel.textContent = "Device Name";
507
644
  nameLabel.style.display = "block";
508
- nameLabel.style.marginBottom = "8px";
645
+ nameLabel.style.marginBottom = "6px";
509
646
  nameLabel.style.fontWeight = "500";
647
+ nameLabel.style.fontSize = "12px";
648
+ nameLabel.style.color = "#6b7280";
510
649
  const nameInput = document.createElement("input");
511
650
  nameInput.type = "text";
512
651
  nameInput.value = device.name || device.id;
513
652
  nameInput.style.width = "100%";
514
- nameInput.style.marginBottom = "16px";
653
+ nameInput.style.marginBottom = "12px";
654
+ nameInput.style.padding = "8px 10px";
655
+ nameInput.style.borderRadius = "6px";
656
+ nameInput.style.fontSize = "14px";
657
+ nameInput.style.boxSizing = "border-box";
658
+ nameInput.style.transition = "border-color 0.2s ease";
515
659
  const apiTypeLabel = document.createElement("label");
516
660
  apiTypeLabel.textContent = "Device Type (API - Read Only)";
517
661
  apiTypeLabel.style.display = "block";
518
- apiTypeLabel.style.marginBottom = "8px";
662
+ apiTypeLabel.style.marginBottom = "6px";
519
663
  apiTypeLabel.style.fontWeight = "500";
664
+ apiTypeLabel.style.fontSize = "12px";
665
+ apiTypeLabel.style.color = "#6b7280";
520
666
  const apiTypeInput = document.createElement("input");
521
667
  apiTypeInput.type = "text";
522
668
  apiTypeInput.value = device.deviceType || device.type || "Unknown";
523
669
  apiTypeInput.readOnly = true;
524
670
  apiTypeInput.style.width = "100%";
525
- apiTypeInput.style.marginBottom = "16px";
671
+ apiTypeInput.style.marginBottom = "12px";
672
+ apiTypeInput.style.padding = "8px 10px";
673
+ apiTypeInput.style.borderRadius = "6px";
674
+ apiTypeInput.style.fontSize = "13px";
526
675
  apiTypeInput.style.opacity = "0.6";
527
676
  apiTypeInput.style.cursor = "not-allowed";
677
+ apiTypeInput.style.boxSizing = "border-box";
678
+ apiTypeInput.style.backgroundColor = "#f9fafb";
528
679
  const typeLabel = document.createElement("label");
529
680
  typeLabel.textContent = "Config Device Type (Override)";
530
681
  typeLabel.style.display = "block";
531
- typeLabel.style.marginBottom = "8px";
682
+ typeLabel.style.marginBottom = "6px";
532
683
  typeLabel.style.fontWeight = "500";
684
+ typeLabel.style.fontSize = "12px";
685
+ typeLabel.style.color = "#6b7280";
533
686
  const typeSelect = document.createElement("select");
534
687
  typeSelect.style.width = "100%";
535
- typeSelect.style.padding = "8px";
536
- typeSelect.style.marginBottom = "16px";
537
- typeSelect.style.borderRadius = "4px";
688
+ typeSelect.style.padding = "8px 10px";
689
+ typeSelect.style.marginBottom = "12px";
690
+ typeSelect.style.borderRadius = "6px";
691
+ typeSelect.style.fontSize = "14px";
538
692
  typeSelect.style.background = getComputedStyle(nameInput).background;
539
693
  typeSelect.style.color = getComputedStyle(nameInput).color;
540
694
  typeSelect.style.border = getComputedStyle(nameInput).border;
695
+ typeSelect.style.boxSizing = "border-box";
696
+ typeSelect.style.cursor = "pointer";
697
+ typeSelect.style.transition = "border-color 0.2s ease";
541
698
  Object.keys(DEVICE_TYPES).forEach((categoryName) => {
542
699
  const optgroup = document.createElement("optgroup");
543
700
  optgroup.label = categoryName;
@@ -551,21 +708,34 @@ async function editDevice(device) {
551
708
  });
552
709
  typeSelect.appendChild(optgroup);
553
710
  });
554
- const buttons = document.createElement("div");
555
- buttons.style.display = "flex";
556
- buttons.style.gap = "8px";
557
- buttons.style.justifyContent = "flex-end";
558
711
  const errorMessage = document.createElement("div");
559
- errorMessage.style.color = "#ef4444";
712
+ errorMessage.style.color = "var(--switchbot-red, #ef4444)";
560
713
  errorMessage.style.marginBottom = "12px";
561
714
  errorMessage.style.fontSize = "12px";
562
715
  errorMessage.style.display = "none";
716
+ errorMessage.style.padding = "8px 10px";
717
+ errorMessage.style.background = "var(--switchbot-red-light, #fee2e2)";
718
+ errorMessage.style.borderRadius = "6px";
719
+ errorMessage.style.fontWeight = "500";
720
+ const buttons = document.createElement("div");
721
+ buttons.style.display = "flex";
722
+ buttons.style.gap = "10px";
723
+ buttons.style.justifyContent = "flex-end";
724
+ buttons.style.marginTop = "18px";
725
+ buttons.style.paddingTop = "18px";
726
+ buttons.style.borderTop = "1px solid rgba(0, 0, 0, 0.08)";
563
727
  const cancelBtn = document.createElement("button");
564
728
  cancelBtn.textContent = "Cancel";
565
- cancelBtn.style.background = "#666";
729
+ cancelBtn.className = "secondary";
730
+ cancelBtn.style.background = "#6b7280";
731
+ cancelBtn.style.padding = "8px 16px";
732
+ cancelBtn.style.fontSize = "13px";
566
733
  cancelBtn.onclick = () => div.remove();
567
734
  const saveBtn = document.createElement("button");
568
735
  saveBtn.textContent = "Save";
736
+ saveBtn.style.background = "var(--switchbot-red, #ef4444)";
737
+ saveBtn.style.padding = "8px 20px";
738
+ saveBtn.style.fontSize = "13px";
569
739
  saveBtn.onclick = async () => {
570
740
  try {
571
741
  const { updateDevice: updateDevice2, syncParentPluginConfigFromDisk: syncParentPluginConfigFromDisk2, fetchDevices: fetchDevices3 } = await Promise.resolve().then(() => (init_api(), api_exports));
@@ -589,15 +759,16 @@ async function editDevice(device) {
589
759
  };
590
760
  buttons.appendChild(cancelBtn);
591
761
  buttons.appendChild(saveBtn);
762
+ contentDiv.appendChild(nameLabel);
763
+ contentDiv.appendChild(nameInput);
764
+ contentDiv.appendChild(apiTypeLabel);
765
+ contentDiv.appendChild(apiTypeInput);
766
+ contentDiv.appendChild(typeLabel);
767
+ contentDiv.appendChild(typeSelect);
768
+ contentDiv.appendChild(errorMessage);
769
+ contentDiv.appendChild(buttons);
592
770
  modal.appendChild(title);
593
- modal.appendChild(nameLabel);
594
- modal.appendChild(nameInput);
595
- modal.appendChild(apiTypeLabel);
596
- modal.appendChild(apiTypeInput);
597
- modal.appendChild(typeLabel);
598
- modal.appendChild(typeSelect);
599
- modal.appendChild(errorMessage);
600
- modal.appendChild(buttons);
771
+ modal.appendChild(contentDiv);
601
772
  div.appendChild(modal);
602
773
  document.body.appendChild(div);
603
774
  nameInput.focus();
@@ -613,6 +784,7 @@ var init_modals = __esm({
613
784
  // src/homebridge-ui/public/js/devices-delete.ts
614
785
  var devices_delete_exports = {};
615
786
  __export(devices_delete_exports, {
787
+ deleteAllDevicesFromConfig: () => deleteAllDevicesFromConfig,
616
788
  deleteDeviceFromConfig: () => deleteDeviceFromConfig
617
789
  });
618
790
  async function confirmDeleteDialog(deviceNameOrId) {
@@ -705,6 +877,110 @@ async function deleteDeviceFromConfig(deviceId, deviceName) {
705
877
  hideBusyUi();
706
878
  }
707
879
  }
880
+ async function confirmDeleteAllDialog(deviceCount) {
881
+ return new Promise((resolve) => {
882
+ const overlay = document.createElement("div");
883
+ overlay.style.position = "fixed";
884
+ overlay.style.top = "0";
885
+ overlay.style.left = "0";
886
+ overlay.style.width = "100%";
887
+ overlay.style.height = "100%";
888
+ overlay.style.background = "rgba(0,0,0,0.7)";
889
+ overlay.style.display = "flex";
890
+ overlay.style.alignItems = "center";
891
+ overlay.style.justifyContent = "center";
892
+ overlay.style.zIndex = "10000";
893
+ const modal = document.createElement("div");
894
+ modal.style.background = getComputedStyle(document.body).backgroundColor;
895
+ modal.style.color = getComputedStyle(document.body).color;
896
+ modal.style.padding = "20px";
897
+ modal.style.borderRadius = "10px";
898
+ modal.style.minWidth = "380px";
899
+ modal.style.maxWidth = "90vw";
900
+ modal.style.boxShadow = "0 12px 40px rgba(0,0,0,0.35)";
901
+ modal.style.borderTop = "3px solid #ef4444";
902
+ const title = document.createElement("h3");
903
+ title.textContent = "\u26A0\uFE0F Remove All Devices";
904
+ title.style.margin = "0 0 12px 0";
905
+ title.style.color = "#ef4444";
906
+ const message = document.createElement("p");
907
+ message.innerHTML = `Are you sure you want to remove <strong>all ${deviceCount} device(s)</strong> from your configuration?<br><br>This action cannot be undone.`;
908
+ message.style.margin = "0 0 18px 0";
909
+ message.style.lineHeight = "1.5";
910
+ const actions = document.createElement("div");
911
+ actions.style.display = "flex";
912
+ actions.style.justifyContent = "flex-end";
913
+ actions.style.gap = "10px";
914
+ const cancelBtn = document.createElement("button");
915
+ cancelBtn.textContent = "Cancel";
916
+ cancelBtn.style.background = "#6b7280";
917
+ cancelBtn.style.padding = "8px 16px";
918
+ cancelBtn.style.fontSize = "13px";
919
+ cancelBtn.className = "secondary";
920
+ const deleteBtn = document.createElement("button");
921
+ deleteBtn.textContent = "Remove All";
922
+ deleteBtn.style.background = "#ef4444";
923
+ deleteBtn.style.padding = "8px 20px";
924
+ deleteBtn.style.fontSize = "13px";
925
+ const cleanup = (result) => {
926
+ overlay.remove();
927
+ resolve(result);
928
+ };
929
+ cancelBtn.onclick = () => cleanup(false);
930
+ deleteBtn.onclick = () => cleanup(true);
931
+ overlay.addEventListener("click", (event) => {
932
+ if (event.target === overlay) {
933
+ cleanup(false);
934
+ }
935
+ });
936
+ actions.appendChild(cancelBtn);
937
+ actions.appendChild(deleteBtn);
938
+ modal.appendChild(title);
939
+ modal.appendChild(message);
940
+ modal.appendChild(actions);
941
+ overlay.appendChild(modal);
942
+ document.body.appendChild(overlay);
943
+ });
944
+ }
945
+ async function deleteAllDevicesFromConfig() {
946
+ uiLog.info("Remove All Devices button clicked");
947
+ try {
948
+ const list = await fetchDevices();
949
+ if (!list || list.length === 0) {
950
+ toastWarning("No devices to remove");
951
+ return;
952
+ }
953
+ const confirmed = await confirmDeleteAllDialog(list.length);
954
+ if (!confirmed) {
955
+ uiLog.info("Remove all cancelled by user");
956
+ return;
957
+ }
958
+ showBusyUi();
959
+ uiLog.info("Sending delete all devices request");
960
+ const resp = await deleteAllDevices();
961
+ uiLog.info("Delete all response:", resp);
962
+ if (!resp || resp.success === false) {
963
+ throw new Error(resp?.data?.message || "Failed to delete all devices");
964
+ }
965
+ uiLog.info("Syncing parent config from disk...");
966
+ const synced = await syncParentPluginConfigFromDisk(true);
967
+ if (!synced) {
968
+ toastWarning("Devices deleted, but configuration sync failed");
969
+ }
970
+ uiLog.info("Refreshing device list...");
971
+ const updatedList = await fetchDevices();
972
+ uiLog.info("Rendering devices:", updatedList.length);
973
+ renderDeviceList(updatedList);
974
+ const deletedCount = resp?.deletedCount || list.length;
975
+ uiLog.info(`\u2713 Removed ${deletedCount} device(s) successfully`);
976
+ toastSuccess(`Removed ${deletedCount} device(s) successfully`);
977
+ } catch (e) {
978
+ uiLog.error("Delete all error:", e);
979
+ toastError(e instanceof Error ? e.message : "Failed to delete all devices");
980
+ } finally {
981
+ hideBusyUi();
982
+ }
983
+ }
708
984
  var init_devices_delete = __esm({
709
985
  "src/homebridge-ui/public/js/devices-delete.ts"() {
710
986
  "use strict";
@@ -719,12 +995,104 @@ var init_devices_delete = __esm({
719
995
  // src/homebridge-ui/public/js/render.ts
720
996
  var render_exports = {};
721
997
  __export(render_exports, {
998
+ filterDevices: () => filterDevices,
999
+ getDiscoveryPreferences: () => getDiscoveryPreferences,
1000
+ getRssiSignalQuality: () => getRssiSignalQuality,
722
1001
  renderBadge: () => renderBadge,
723
1002
  renderConnectionBadge: () => renderConnectionBadge,
724
1003
  renderDeviceList: () => renderDeviceList,
725
1004
  renderDiscoveredDevices: () => renderDiscoveredDevices,
726
- renderIRBadge: () => renderIRBadge
1005
+ renderIRBadge: () => renderIRBadge,
1006
+ renderSignalBars: () => renderSignalBars,
1007
+ renderSignalQualityBadge: () => renderSignalQualityBadge,
1008
+ setDiscoveryPreferences: () => setDiscoveryPreferences,
1009
+ sortDevices: () => sortDevices
727
1010
  });
1011
+ function getRssiSignalQuality(rssi) {
1012
+ if (!rssi || rssi === 0) {
1013
+ return {
1014
+ level: "unknown",
1015
+ color: "#999",
1016
+ bgColor: "#f5f5f5",
1017
+ description: "Signal strength unknown",
1018
+ bars: 0
1019
+ };
1020
+ }
1021
+ const dbm = Math.floor(rssi);
1022
+ if (dbm > -60) {
1023
+ return {
1024
+ level: "excellent",
1025
+ color: "#34a853",
1026
+ bgColor: "#e8f5e9",
1027
+ description: `Excellent (${dbm} dBm)`,
1028
+ bars: 4
1029
+ };
1030
+ } else if (dbm > -75) {
1031
+ return {
1032
+ level: "good",
1033
+ color: "#fbbc04",
1034
+ bgColor: "#fffde7",
1035
+ description: `Good (${dbm} dBm)`,
1036
+ bars: 3
1037
+ };
1038
+ } else if (dbm > -85) {
1039
+ return {
1040
+ level: "fair",
1041
+ color: "#ff9800",
1042
+ bgColor: "#fff3e0",
1043
+ description: `Fair (${dbm} dBm)`,
1044
+ bars: 2
1045
+ };
1046
+ } else {
1047
+ return {
1048
+ level: "poor",
1049
+ color: "#ea4335",
1050
+ bgColor: "#ffebee",
1051
+ description: `Poor (${dbm} dBm) - unreliable`,
1052
+ bars: 1
1053
+ };
1054
+ }
1055
+ }
1056
+ function renderSignalBars(rssi) {
1057
+ const quality = getRssiSignalQuality(rssi);
1058
+ const container = document.createElement("span");
1059
+ container.style.display = "inline-flex";
1060
+ container.style.gap = "2px";
1061
+ container.style.alignItems = "center";
1062
+ container.style.marginLeft = "8px";
1063
+ container.style.fontSize = "12px";
1064
+ for (let i = 1; i <= 4; i++) {
1065
+ const bar = document.createElement("span");
1066
+ bar.style.height = `${i * 3}px`;
1067
+ bar.style.width = "3px";
1068
+ bar.style.borderRadius = "1px";
1069
+ bar.style.border = `1px solid ${quality.color}`;
1070
+ if (i <= quality.bars) {
1071
+ bar.style.backgroundColor = quality.color;
1072
+ } else {
1073
+ bar.style.backgroundColor = "transparent";
1074
+ }
1075
+ container.appendChild(bar);
1076
+ }
1077
+ container.title = quality.description;
1078
+ return container;
1079
+ }
1080
+ function renderSignalQualityBadge(rssi) {
1081
+ const quality = getRssiSignalQuality(rssi);
1082
+ const badge = document.createElement("span");
1083
+ badge.textContent = quality.level.charAt(0).toUpperCase() + quality.level.slice(1);
1084
+ badge.style.cssText = `
1085
+ background: ${quality.color};
1086
+ color: white;
1087
+ padding: 2px 6px;
1088
+ border-radius: 3px;
1089
+ font-size: 10px;
1090
+ font-weight: 600;
1091
+ margin-left: 8px;
1092
+ `;
1093
+ badge.title = quality.description;
1094
+ return badge;
1095
+ }
728
1096
  function renderBadge(text, style) {
729
1097
  const badge = document.createElement("span");
730
1098
  badge.textContent = text;
@@ -753,47 +1121,70 @@ function renderIRBadge() {
753
1121
  }
754
1122
  async function renderDiscoveredDevices(devices) {
755
1123
  const ul = document.createElement("ul");
756
- ul.style.maxHeight = "300px";
1124
+ ul.style.maxHeight = "400px";
757
1125
  ul.style.overflowY = "auto";
1126
+ ul.style.marginTop = "12px";
1127
+ ul.style.padding = "0";
1128
+ ul.style.listStyle = "none";
758
1129
  const { addDeviceToConfig: addDeviceToConfig3 } = await Promise.resolve().then(() => (init_discovery(), discovery_exports));
759
1130
  for (const d of devices) {
760
1131
  const li = document.createElement("li");
761
1132
  li.className = "device-item";
1133
+ li.style.display = "flex";
1134
+ li.style.alignItems = "center";
1135
+ li.style.justifyContent = "space-between";
1136
+ li.style.padding = "10px 12px";
1137
+ li.style.marginBottom = "6px";
1138
+ li.style.borderRadius = "6px";
1139
+ li.style.transition = "all 0.2s ease";
762
1140
  const info = document.createElement("div");
763
1141
  info.style.flex = "1";
1142
+ const nameContainer = document.createElement("div");
1143
+ nameContainer.style.display = "flex";
1144
+ nameContainer.style.alignItems = "center";
1145
+ nameContainer.style.marginBottom = "4px";
1146
+ nameContainer.style.flexWrap = "wrap";
1147
+ nameContainer.style.gap = "6px";
764
1148
  const name = document.createElement("div");
765
1149
  name.style.fontWeight = "500";
1150
+ name.style.fontSize = "14px";
766
1151
  name.textContent = d.name || d.id;
1152
+ nameContainer.appendChild(name);
767
1153
  if (d.connectionType) {
768
- name.appendChild(document.createTextNode(" "));
769
1154
  const badge = renderConnectionBadge(d.connectionType);
770
1155
  if (badge) {
771
- name.appendChild(badge);
1156
+ nameContainer.appendChild(badge);
772
1157
  }
773
1158
  }
774
1159
  if (d.isIR) {
775
- name.appendChild(document.createTextNode(" "));
776
- name.appendChild(renderIRBadge());
1160
+ nameContainer.appendChild(renderIRBadge());
1161
+ }
1162
+ if (d.rssi !== void 0 && d.rssi !== null && d.rssi !== 0) {
1163
+ nameContainer.appendChild(renderSignalBars(d.rssi));
1164
+ nameContainer.appendChild(renderSignalQualityBadge(d.rssi));
777
1165
  }
778
1166
  const details = document.createElement("div");
779
- details.style.fontSize = "12px";
780
- details.style.opacity = "0.75";
1167
+ details.style.fontSize = "11px";
1168
+ details.style.opacity = "0.7";
1169
+ details.style.marginTop = "4px";
1170
+ details.style.fontFamily = "monospace";
781
1171
  let detailsText = `ID: ${d.id} | Type: ${d.type} | Model: ${d.model || "N/A"}`;
782
1172
  if (d.hubDeviceId) {
783
1173
  detailsText += ` | Hub: ${d.hubDeviceId}`;
784
1174
  }
785
- if (d.rssi) {
786
- detailsText += ` | Signal: ${d.rssi} dBm`;
787
- }
788
1175
  if (d.address) {
789
1176
  detailsText += ` | MAC: ${d.address}`;
790
1177
  }
791
1178
  details.textContent = detailsText;
792
- info.appendChild(name);
1179
+ info.appendChild(nameContainer);
793
1180
  info.appendChild(details);
794
1181
  const addBtn = document.createElement("button");
795
1182
  addBtn.textContent = "+ Add";
796
1183
  addBtn.style.marginLeft = "8px";
1184
+ addBtn.style.padding = "6px 14px";
1185
+ addBtn.style.fontSize = "12px";
1186
+ addBtn.style.whiteSpace = "nowrap";
1187
+ addBtn.style.flexShrink = "0";
797
1188
  addBtn.onclick = async () => {
798
1189
  await addDeviceToConfig3(d);
799
1190
  };
@@ -803,24 +1194,128 @@ async function renderDiscoveredDevices(devices) {
803
1194
  }
804
1195
  return ul;
805
1196
  }
1197
+ function filterDevices(devices, connectionType = "all", searchQuery = "") {
1198
+ let filtered = [...devices];
1199
+ if (connectionType !== "all") {
1200
+ filtered = filtered.filter((d) => {
1201
+ if (connectionType === "ir") {
1202
+ return d.isIR === true;
1203
+ }
1204
+ if (connectionType === "ble") {
1205
+ return d.connectionType === "BLE" || d.connectionType?.includes("BLE");
1206
+ }
1207
+ if (connectionType === "api") {
1208
+ return d.connectionType === "OpenAPI" || d.connectionType === "API" || d.connectionType?.includes("API");
1209
+ }
1210
+ if (connectionType === "both") {
1211
+ return d.connectionType === "Both" || d.connectionType?.includes("Both");
1212
+ }
1213
+ return true;
1214
+ });
1215
+ }
1216
+ if (searchQuery.trim()) {
1217
+ const query = searchQuery.toLowerCase();
1218
+ filtered = filtered.filter((d) => {
1219
+ const name = (d.name || "").toLowerCase();
1220
+ const id = (d.id || "").toLowerCase();
1221
+ const type = (d.type || "").toLowerCase();
1222
+ const model = (d.model || "").toLowerCase();
1223
+ return name.includes(query) || id.includes(query) || type.includes(query) || model.includes(query);
1224
+ });
1225
+ }
1226
+ return filtered;
1227
+ }
1228
+ function sortDevices(devices, sortBy = "name") {
1229
+ const sorted = [...devices];
1230
+ switch (sortBy) {
1231
+ case "signal": {
1232
+ sorted.sort((a, b) => {
1233
+ const aRssi = a.rssi || 0;
1234
+ const bRssi = b.rssi || 0;
1235
+ return bRssi - aRssi;
1236
+ });
1237
+ break;
1238
+ }
1239
+ case "type": {
1240
+ sorted.sort((a, b) => {
1241
+ const aType = (a.type || "").localeCompare(b.type || "");
1242
+ return aType;
1243
+ });
1244
+ break;
1245
+ }
1246
+ case "connection": {
1247
+ const connectionOrder = {
1248
+ "Both": 0,
1249
+ "BLE": 1,
1250
+ "OpenAPI": 2,
1251
+ "API": 2,
1252
+ "Unknown": 3
1253
+ };
1254
+ sorted.sort((a, b) => {
1255
+ const aOrder = connectionOrder[a.connectionType || "Unknown"] ?? 3;
1256
+ const bOrder = connectionOrder[b.connectionType || "Unknown"] ?? 3;
1257
+ return aOrder - bOrder;
1258
+ });
1259
+ break;
1260
+ }
1261
+ case "name":
1262
+ default: {
1263
+ sorted.sort((a, b) => (a.name || a.id || "").localeCompare(b.name || b.id || ""));
1264
+ break;
1265
+ }
1266
+ }
1267
+ return sorted;
1268
+ }
1269
+ function getDiscoveryPreferences() {
1270
+ try {
1271
+ const stored = localStorage.getItem("discoveryPreferences");
1272
+ if (stored) {
1273
+ return JSON.parse(stored);
1274
+ }
1275
+ } catch (_e) {
1276
+ }
1277
+ return {
1278
+ connectionType: "all",
1279
+ sortBy: "name",
1280
+ searchQuery: ""
1281
+ };
1282
+ }
1283
+ function setDiscoveryPreferences(preferences) {
1284
+ try {
1285
+ localStorage.setItem("discoveryPreferences", JSON.stringify(preferences));
1286
+ } catch (_e) {
1287
+ }
1288
+ }
806
1289
  function renderDeviceList(list) {
807
1290
  const ul = document.getElementById("devices");
808
1291
  const status = document.getElementById("status");
1292
+ const removeAllContainer = document.getElementById("removeAllContainer");
809
1293
  if (!ul || !status) {
810
1294
  return;
811
1295
  }
812
1296
  if (!list.length) {
813
1297
  status.textContent = "No devices found in config.";
814
1298
  ul.innerHTML = "";
1299
+ if (removeAllContainer) {
1300
+ removeAllContainer.style.display = "none";
1301
+ }
815
1302
  return;
816
1303
  }
817
1304
  status.textContent = `Found ${list.length} device(s)`;
818
1305
  ul.innerHTML = "";
1306
+ if (removeAllContainer) {
1307
+ removeAllContainer.style.display = "block";
1308
+ }
819
1309
  for (const d of list) {
820
1310
  const li = document.createElement("li");
821
1311
  li.className = "device-item";
822
1312
  const info = document.createElement("div");
823
1313
  info.style.flex = "1";
1314
+ const nameContainer = document.createElement("div");
1315
+ nameContainer.style.display = "flex";
1316
+ nameContainer.style.alignItems = "center";
1317
+ nameContainer.style.flexWrap = "wrap";
1318
+ nameContainer.style.gap = "6px";
824
1319
  const name = document.createElement("div");
825
1320
  name.style.fontWeight = "500";
826
1321
  name.textContent = d.name || d.id;
@@ -829,6 +1324,12 @@ function renderDeviceList(list) {
829
1324
  code.style.fontSize = "12px";
830
1325
  code.style.opacity = "0.75";
831
1326
  code.style.marginLeft = "8px";
1327
+ nameContainer.appendChild(name);
1328
+ nameContainer.appendChild(code);
1329
+ if (d.rssi !== void 0 && d.rssi !== null && d.rssi !== 0) {
1330
+ nameContainer.appendChild(renderSignalBars(d.rssi));
1331
+ nameContainer.appendChild(renderSignalQualityBadge(d.rssi));
1332
+ }
832
1333
  const meta = document.createElement("div");
833
1334
  meta.style.opacity = "0.75";
834
1335
  meta.style.marginTop = "4px";
@@ -837,8 +1338,7 @@ function renderDeviceList(list) {
837
1338
  const connText = d.connectionPreference ? `conn: ${d.connectionPreference}` : "";
838
1339
  const roomText = d.room ? `room: ${d.room}` : "";
839
1340
  meta.textContent = [typeText, connText, roomText].filter(Boolean).join(" | ");
840
- info.appendChild(name);
841
- info.appendChild(code);
1341
+ info.appendChild(nameContainer);
842
1342
  info.appendChild(meta);
843
1343
  const buttons = document.createElement("div");
844
1344
  buttons.style.display = "flex";
@@ -895,6 +1395,7 @@ var init_render = __esm({
895
1395
  var devices_exports = {};
896
1396
  __export(devices_exports, {
897
1397
  addDeviceToConfig: () => addDeviceToConfig2,
1398
+ initRemoveAllButton: () => initRemoveAllButton,
898
1399
  loadConfiguredDevices: () => loadConfiguredDevices
899
1400
  });
900
1401
  async function addDeviceToConfig2(device, options = {}) {
@@ -948,6 +1449,16 @@ async function addDeviceToConfig2(device, options = {}) {
948
1449
  hideBusyUi();
949
1450
  }
950
1451
  }
1452
+ async function initRemoveAllButton() {
1453
+ const removeAllBtn = document.getElementById("removeAllBtn");
1454
+ if (!removeAllBtn) {
1455
+ return;
1456
+ }
1457
+ removeAllBtn.addEventListener("click", async () => {
1458
+ const { deleteAllDevicesFromConfig: deleteAllDevicesFromConfig2 } = await Promise.resolve().then(() => (init_devices_delete(), devices_delete_exports));
1459
+ await deleteAllDevicesFromConfig2();
1460
+ });
1461
+ }
951
1462
  async function loadConfiguredDevices() {
952
1463
  const list = await fetchDevices();
953
1464
  renderDeviceList(list);
@@ -1060,6 +1571,7 @@ window.discoverDevices = discoverDevices2;
1060
1571
  async function init() {
1061
1572
  await loadCredentialStatus();
1062
1573
  const list = await loadConfiguredDevices();
1574
+ await initRemoveAllButton();
1063
1575
  }
1064
1576
  if (document.readyState === "loading") {
1065
1577
  document.addEventListener("DOMContentLoaded", init);