@switchbot/homebridge-switchbot 5.0.0-beta.153 → 5.0.0-beta.155

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 (102) hide show
  1. package/.github/workflows/release.yml +63 -15
  2. package/.github/workflows/stale.yml +2 -4
  3. package/CHANGELOG.md +21 -29
  4. package/MIGRATION.md +6 -6
  5. package/README.md +5 -3
  6. package/dist/device-types.js +7 -7
  7. package/dist/device-types.js.map +1 -1
  8. package/dist/deviceFactory.d.ts +1 -1
  9. package/dist/deviceFactory.d.ts.map +1 -1
  10. package/dist/deviceFactory.js +20 -20
  11. package/dist/deviceFactory.js.map +1 -1
  12. package/dist/homebridge-ui/device-types.js +246 -0
  13. package/dist/homebridge-ui/device-types.js.map +1 -0
  14. package/dist/homebridge-ui/deviceCommandMapper.js +319 -0
  15. package/dist/homebridge-ui/deviceCommandMapper.js.map +1 -0
  16. package/dist/homebridge-ui/endpoints/devices.d.ts.map +1 -1
  17. package/dist/homebridge-ui/endpoints/devices.js +64 -1
  18. package/dist/homebridge-ui/endpoints/devices.js.map +1 -1
  19. package/dist/homebridge-ui/endpoints/discovery.d.ts.map +1 -1
  20. package/dist/homebridge-ui/endpoints/discovery.js +5 -1
  21. package/dist/homebridge-ui/endpoints/discovery.js.map +1 -1
  22. package/dist/homebridge-ui/errors.js +32 -0
  23. package/dist/homebridge-ui/errors.js.map +1 -0
  24. package/dist/homebridge-ui/homebridge-ui/endpoints/config.js +90 -0
  25. package/dist/homebridge-ui/homebridge-ui/endpoints/config.js.map +1 -0
  26. package/dist/homebridge-ui/homebridge-ui/endpoints/devices.js +144 -0
  27. package/dist/homebridge-ui/homebridge-ui/endpoints/devices.js.map +1 -0
  28. package/dist/homebridge-ui/homebridge-ui/endpoints/discovery.js +219 -0
  29. package/dist/homebridge-ui/homebridge-ui/endpoints/discovery.js.map +1 -0
  30. package/dist/homebridge-ui/homebridge-ui/server.js +11 -0
  31. package/dist/homebridge-ui/homebridge-ui/server.js.map +1 -0
  32. package/dist/homebridge-ui/homebridge-ui/utils/config-parser.js +108 -0
  33. package/dist/homebridge-ui/homebridge-ui/utils/config-parser.js.map +1 -0
  34. package/dist/homebridge-ui/homebridge-ui/utils/device-migration.js +111 -0
  35. package/dist/homebridge-ui/homebridge-ui/utils/device-migration.js.map +1 -0
  36. package/dist/homebridge-ui/homebridge-ui/utils/logger.js +17 -0
  37. package/dist/homebridge-ui/homebridge-ui/utils/logger.js.map +1 -0
  38. package/dist/homebridge-ui/public/js/api.d.ts.map +1 -1
  39. package/dist/homebridge-ui/public/js/api.js +24 -11
  40. package/dist/homebridge-ui/public/js/api.js.map +1 -1
  41. package/dist/homebridge-ui/public/js/api.ts +24 -12
  42. package/dist/homebridge-ui/public/js/app.js +117 -267
  43. package/dist/homebridge-ui/public/js/app.js.map +3 -3
  44. package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -1
  45. package/dist/homebridge-ui/public/js/devices.js +2 -0
  46. package/dist/homebridge-ui/public/js/devices.js.map +1 -1
  47. package/dist/homebridge-ui/public/js/devices.ts +2 -0
  48. package/dist/homebridge-ui/public/js/discovery.d.ts +5 -0
  49. package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -1
  50. package/dist/homebridge-ui/public/js/discovery.js +79 -245
  51. package/dist/homebridge-ui/public/js/discovery.js.map +1 -1
  52. package/dist/homebridge-ui/public/js/discovery.ts +88 -247
  53. package/dist/homebridge-ui/public/js/render.d.ts.map +1 -1
  54. package/dist/homebridge-ui/public/js/render.js +2 -1
  55. package/dist/homebridge-ui/public/js/render.js.map +1 -1
  56. package/dist/homebridge-ui/public/js/render.ts +2 -1
  57. package/dist/homebridge-ui/public/js/toast.d.ts.map +1 -1
  58. package/dist/homebridge-ui/public/js/toast.js +15 -6
  59. package/dist/homebridge-ui/public/js/toast.js.map +1 -1
  60. package/dist/homebridge-ui/public/js/toast.ts +14 -7
  61. package/dist/homebridge-ui/settings.js +8 -0
  62. package/dist/homebridge-ui/settings.js.map +1 -0
  63. package/dist/homebridge-ui/switchbotClient.js +247 -0
  64. package/dist/homebridge-ui/switchbotClient.js.map +1 -0
  65. package/dist/homebridge-ui/utils/config-parser.d.ts +4 -0
  66. package/dist/homebridge-ui/utils/config-parser.d.ts.map +1 -1
  67. package/dist/homebridge-ui/utils/config-parser.js +21 -0
  68. package/dist/homebridge-ui/utils/config-parser.js.map +1 -1
  69. package/dist/switchbotClient.d.ts +7 -1
  70. package/dist/switchbotClient.d.ts.map +1 -1
  71. package/dist/switchbotClient.js +82 -10
  72. package/dist/switchbotClient.js.map +1 -1
  73. package/docs/assets/main.js +1 -1
  74. package/docs/index.html +10 -4
  75. package/docs/variables/default.html +1 -1
  76. package/eslint.config.js +9 -10
  77. package/package.json +26 -24
  78. package/src/device-types.js +246 -0
  79. package/src/device-types.js.map +1 -0
  80. package/src/device-types.ts +7 -7
  81. package/src/deviceCommandMapper.js +319 -0
  82. package/src/deviceCommandMapper.js.map +1 -0
  83. package/src/deviceFactory.ts +22 -21
  84. package/src/errors.js +32 -0
  85. package/src/errors.js.map +1 -0
  86. package/src/homebridge-ui/endpoints/devices.ts +66 -1
  87. package/src/homebridge-ui/endpoints/discovery.ts +5 -1
  88. package/src/homebridge-ui/public/js/api.ts +24 -12
  89. package/src/homebridge-ui/public/js/devices.ts +2 -0
  90. package/src/homebridge-ui/public/js/discovery.ts +88 -247
  91. package/src/homebridge-ui/public/js/render.ts +2 -1
  92. package/src/homebridge-ui/public/js/toast.ts +14 -7
  93. package/src/homebridge-ui/utils/config-parser.ts +17 -0
  94. package/src/settings.js +8 -0
  95. package/src/settings.js.map +1 -0
  96. package/src/switchbotClient.js +247 -0
  97. package/src/switchbotClient.js.map +1 -0
  98. package/src/switchbotClient.ts +95 -10
  99. package/test/client/switchbotClient.spec.ts +42 -1
  100. package/test/e2e/run-e2e.spec.ts +1 -0
  101. package/tsconfig.ui.json +11 -0
  102. package/.github/workflows/beta-release.yml +0 -52
@@ -76,15 +76,21 @@ var init_modal = __esm({
76
76
  // src/homebridge-ui/public/js/toast.ts
77
77
  function showToast(method, message, title = "SwitchBot") {
78
78
  try {
79
- const toast = homebridge?.toast;
80
- const fn = toast?.[method];
81
- if (typeof fn === "function") {
82
- fn(message, title);
83
- return;
79
+ const hb = typeof window !== "undefined" ? window.homebridge : void 0;
80
+ const toast = hb && typeof hb.toast === "object" ? hb.toast : void 0;
81
+ const fn = toast && typeof toast[method] === "function" ? toast[method] : void 0;
82
+ if (fn) {
83
+ try {
84
+ fn(message, title);
85
+ return;
86
+ } catch (err) {
87
+ uiLog.warn(`Toast ${method} threw:`, err);
88
+ }
84
89
  }
85
90
  uiLog.info(`[Toast:${method}] ${title} - ${message}`);
86
91
  } catch (e) {
87
- uiLog.warn(`Toast ${method} failed:`, e);
92
+ uiLog.warn(`Toast ${method} outer error:`, e);
93
+ uiLog.info(`[Toast:${method}] ${title} - ${message}`);
88
94
  }
89
95
  }
90
96
  function toastSuccess(message, title) {
@@ -107,7 +113,7 @@ var init_toast = __esm({
107
113
  }
108
114
  });
109
115
 
110
- // src/device-types.ts
116
+ // src/device-types.js
111
117
  function getValidDeviceTypes() {
112
118
  const validTypes = /* @__PURE__ */ new Set();
113
119
  for (const category of Object.values(DEVICE_TYPES)) {
@@ -142,7 +148,7 @@ function isValidDeviceType(deviceType) {
142
148
  }
143
149
  var DEVICE_TYPES, DEVICE_TYPE_NORMALIZATION_MAP;
144
150
  var init_device_types = __esm({
145
- "src/device-types.ts"() {
151
+ "src/device-types.js"() {
146
152
  "use strict";
147
153
  DEVICE_TYPES = {
148
154
  "Window Coverings": ["Blind Tilt", "Curtain", "Curtain3", "Roller Shade"],
@@ -240,11 +246,10 @@ var init_device_types = __esm({
240
246
  "Other Devices": ["AI Art Frame", "Bot", "Home Climate Panel", "Remote", "remote with screen"]
241
247
  };
242
248
  DEVICE_TYPE_NORMALIZATION_MAP = {
243
- // --- node-switchbot beta new types normalization additions ---
249
+ // --- node-switchbot v4 normalization additions ---
244
250
  "hub mini": "Hub Mini",
245
251
  "hub 3": "Hub 3",
246
252
  "keypad": "Keypad",
247
- // node-switchbot beta
248
253
  "plug mini": "Plug Mini (US)",
249
254
  // fallback to US if region not specified
250
255
  "art frame": "AI Art Frame",
@@ -253,13 +258,11 @@ var init_device_types = __esm({
253
258
  // alias for new lock vision
254
259
  "lock pro": "Smart Lock Pro",
255
260
  "lock lite": "Lock Lite",
256
- // node-switchbot beta
257
261
  "circulator fan": "Circulator Fan",
258
- // node-switchbot beta
259
262
  "smart thermostat radiator": "Smart Radiator Thermostat",
260
263
  "climate panel": "Home Climate Panel",
261
264
  "evaporative humidifier": "Humidifier",
262
- // --- end node-switchbot beta additions ---
265
+ // --- end node-switchbot v4 additions ---
263
266
  // Only keep the last occurrence for each key, all values canonical
264
267
  "air purifier pm2.5": "Air Purifier PM2.5",
265
268
  "pan/tilt cam plus 3k": "Pan/Tilt Cam Plus 3K",
@@ -298,7 +301,6 @@ var init_device_types = __esm({
298
301
  "rgbicww strip light": "RGBICWW Strip Light",
299
302
  "strip light": "Strip Light",
300
303
  "strip light 3": "Strip Light 3",
301
- // node-switchbot beta
302
304
  // Vacuum conversions
303
305
  "robot vacuum cleaner s1": "Robot Vacuum Cleaner S1",
304
306
  "robot vacuum cleaner s1 plus": "Robot Vacuum Cleaner S1 Plus",
@@ -331,7 +333,7 @@ var init_device_types = __esm({
331
333
  // Migration mappings for invalid/legacy device types
332
334
  "lock vision pro": "Lock Vision Pro",
333
335
  // Valid alias; map to canonical
334
- // 'lock vision': 'Keypad Vision', // Invalid type (removed, now node-switchbot beta alias above)
336
+ // 'lock vision': 'Keypad Vision', // Invalid type (removed, now alias above)
335
337
  "lock touch": "Keypad Touch",
336
338
  // Invalid type
337
339
  // Additional normalization for new/unknown types from logs
@@ -414,11 +416,6 @@ async function syncParentPluginConfigFromDisk(autoSave = false) {
414
416
  uiLog.warn("Parent config sync API not available");
415
417
  return false;
416
418
  }
417
- const diskResp = await homebridge.request("/platform-config", {});
418
- if (!diskResp || diskResp.success === false || !diskResp.data) {
419
- uiLog.warn("Failed to fetch fresh platform config from disk");
420
- return false;
421
- }
422
419
  const pluginConfigBlocks = await homebridge.getPluginConfig();
423
420
  if (!Array.isArray(pluginConfigBlocks) || !pluginConfigBlocks.length) {
424
421
  uiLog.warn("No plugin config blocks returned from Homebridge");
@@ -429,12 +426,11 @@ async function syncParentPluginConfigFromDisk(autoSave = false) {
429
426
  uiLog.warn("SwitchBot platform block not found in Homebridge plugin config");
430
427
  return false;
431
428
  }
432
- const errors = validateAndFixDeviceTypes(diskResp.data.devices || []);
429
+ const errors = validateAndFixDeviceTypes(pluginConfigBlocks[index].devices || []);
433
430
  if (errors.length > 0) {
434
431
  toastError(`Invalid device types found: ${errors.map((e) => `${e.name} (${e.type})`).join(", ")}`);
435
432
  return false;
436
433
  }
437
- pluginConfigBlocks[index] = diskResp.data;
438
434
  await homebridge.updatePluginConfig(pluginConfigBlocks);
439
435
  if (autoSave && typeof homebridge.savePluginConfig === "function") {
440
436
  uiLog.info("Auto-saving config to disk...");
@@ -600,6 +596,7 @@ async function deleteDevice(deviceId) {
600
596
  const normalizedDeviceId = String(deviceId).trim().toLowerCase();
601
597
  const before = config.devices.length;
602
598
  config.devices = config.devices.filter((d) => String(d.deviceId ?? d.id ?? "").trim().toLowerCase() !== normalizedDeviceId);
599
+ config.devices = config.devices.filter((d) => d && typeof d === "object" && d.deviceId && d.configDeviceType);
603
600
  if (config.devices.length === before) {
604
601
  throw new Error("Device not found in config");
605
602
  }
@@ -614,16 +611,24 @@ async function deleteAllDevices() {
614
611
  throw new TypeError("Homebridge UI API not available");
615
612
  }
616
613
  const configArr = await homebridge.getPluginConfig();
617
- const idx = Array.isArray(configArr) ? configArr.findIndex(isSwitchBotPlatformConfig) : -1;
614
+ let idx = Array.isArray(configArr) ? configArr.findIndex(isSwitchBotPlatformConfig) : -1;
618
615
  if (idx === -1) {
619
- throw new Error("SwitchBot config not found");
616
+ const newBlock = { platform: "SwitchBot", devices: [] };
617
+ configArr.push(newBlock);
618
+ idx = configArr.length - 1;
620
619
  }
621
620
  const config = configArr[idx];
622
621
  if (!Array.isArray(config.devices)) {
623
- throw new TypeError("No devices array in config");
622
+ config.devices = [];
624
623
  }
625
624
  const deletedCount = config.devices.length;
626
625
  config.devices = [];
626
+ if (!config.platform) {
627
+ config.platform = "SwitchBot";
628
+ }
629
+ if (!config.name) {
630
+ config.name = "SwitchBot";
631
+ }
627
632
  await homebridge.updatePluginConfig(configArr);
628
633
  if (typeof homebridge.savePluginConfig === "function") {
629
634
  await homebridge.savePluginConfig();
@@ -647,42 +652,6 @@ __export(discovery_exports, {
647
652
  discoverDevices: () => discoverDevices2,
648
653
  initializeDiscoverySettings: () => initializeDiscoverySettings
649
654
  });
650
- async function batchSetDeviceEnabled(selectedIds, enabled) {
651
- const resp = await homebridge.request("/platform-config", {});
652
- if (!resp || resp.success === false || !resp.data) {
653
- throw new Error("Failed to load config");
654
- }
655
- const config = resp.data;
656
- const configArr = Array.isArray(config) ? config : [config];
657
- const platformIdx = configArr.findIndex((c) => (c.platform || c.name || "").toLowerCase().includes("switchbot"));
658
- if (platformIdx === -1) {
659
- throw new Error("SwitchBot platform config not found");
660
- }
661
- const platformConfig = configArr[platformIdx];
662
- if (!Array.isArray(platformConfig.devices)) {
663
- throw new TypeError("No devices array in config");
664
- }
665
- let changed = false;
666
- for (const dev of platformConfig.devices) {
667
- const id = String(dev.deviceId || dev.id || "").trim().toLowerCase();
668
- if (selectedIds.has(id)) {
669
- if (dev.enabled !== enabled) {
670
- dev.enabled = enabled;
671
- changed = true;
672
- }
673
- }
674
- }
675
- if (changed) {
676
- if (typeof homebridge.updatePluginConfig === "function") {
677
- await homebridge.updatePluginConfig(configArr);
678
- } else {
679
- throw new TypeError("homebridge.updatePluginConfig is not available");
680
- }
681
- if (typeof homebridge.savePluginConfig === "function") {
682
- await homebridge.savePluginConfig();
683
- }
684
- }
685
- }
686
655
  function normalizeId(value) {
687
656
  return String(value ?? "").trim().toLowerCase();
688
657
  }
@@ -936,9 +905,9 @@ function getDiscoveryGroupByPreference() {
936
905
  if (stored === "hub" || stored === "type") {
937
906
  return stored;
938
907
  }
939
- return "connection";
908
+ return "type";
940
909
  } catch (_e) {
941
- return "connection";
910
+ return "type";
942
911
  }
943
912
  }
944
913
  function setDiscoveryGroupByPreference(groupBy) {
@@ -1042,19 +1011,17 @@ async function discoverDevices2() {
1042
1011
  const preferences = getDiscoveryPreferences();
1043
1012
  let groupBy = getDiscoveryGroupByPreference();
1044
1013
  let hideAdded = getDiscoveryHideAddedPreference();
1045
- let controlsInitialized = false;
1046
1014
  if (!window._discoverySelectedIds) {
1047
1015
  window._discoverySelectedIds = /* @__PURE__ */ new Set();
1048
1016
  }
1049
1017
  const selectedIds = window._discoverySelectedIds;
1050
- async function batchSetDeviceEnabled2(selectedIds2, enabled) {
1051
- const resp = await homebridge.request("/platform-config", {});
1052
- if (!resp || resp.success === false || !resp.data) {
1053
- throw new Error("Failed to load config");
1054
- }
1055
- const config = resp.data;
1056
- const configArr = Array.isArray(config) ? config : [config];
1057
- const platformIdx = configArr.findIndex((c) => (c.platform || c.name || "").toLowerCase().includes("switchbot"));
1018
+ let controlsInitialized = false;
1019
+ async function batchSetDeviceEnabled(selectedIds2, enabled) {
1020
+ if (typeof homebridge.getPluginConfig !== "function") {
1021
+ throw new TypeError("homebridge.getPluginConfig is not available");
1022
+ }
1023
+ const configArr = await homebridge.getPluginConfig();
1024
+ const platformIdx = Array.isArray(configArr) ? configArr.findIndex((c) => (c.platform || c.name || "").toLowerCase().includes("switchbot")) : -1;
1058
1025
  if (platformIdx === -1) {
1059
1026
  throw new Error("SwitchBot platform config not found");
1060
1027
  }
@@ -1084,13 +1051,48 @@ async function discoverDevices2() {
1084
1051
  }
1085
1052
  }
1086
1053
  const ensureDiscoveryControls = async () => {
1054
+ const selectAllBtn = document.createElement("button");
1055
+ selectAllBtn.textContent = "Select All";
1056
+ selectAllBtn.style.fontSize = "13px";
1057
+ selectAllBtn.style.padding = "6px 18px";
1058
+ selectAllBtn.style.borderRadius = "6px";
1059
+ selectAllBtn.style.background = "#f3f4f6";
1060
+ selectAllBtn.style.color = "#1d4ed8";
1061
+ selectAllBtn.style.border = "1px solid #d1d5db";
1062
+ selectAllBtn.style.cursor = "pointer";
1063
+ selectAllBtn.style.marginRight = "8px";
1064
+ selectAllBtn.onclick = () => {
1065
+ for (const d of discoveredDevices) {
1066
+ selectedIds.add(normalizeId(d.id));
1067
+ }
1068
+ window.dispatchEvent(new Event("discovery-selection-changed"));
1069
+ void updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds);
1070
+ };
1071
+ const deselectAllBtn = document.createElement("button");
1072
+ deselectAllBtn.textContent = "Deselect All";
1073
+ deselectAllBtn.style.fontSize = "13px";
1074
+ deselectAllBtn.style.padding = "6px 18px";
1075
+ deselectAllBtn.style.borderRadius = "6px";
1076
+ deselectAllBtn.style.background = "#f3f4f6";
1077
+ deselectAllBtn.style.color = "#ef4444";
1078
+ deselectAllBtn.style.border = "1px solid #d1d5db";
1079
+ deselectAllBtn.style.cursor = "pointer";
1080
+ deselectAllBtn.onclick = () => {
1081
+ for (const d of discoveredDevices) {
1082
+ selectedIds.delete(normalizeId(d.id));
1083
+ }
1084
+ window.dispatchEvent(new Event("discovery-selection-changed"));
1085
+ void updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds);
1086
+ };
1087
+ const selectControlsRow = document.createElement("div");
1088
+ selectControlsRow.style.display = "flex";
1089
+ selectControlsRow.style.gap = "10px";
1090
+ selectControlsRow.style.margin = "0 0 10px 0";
1091
+ selectControlsRow.appendChild(selectAllBtn);
1092
+ selectControlsRow.appendChild(deselectAllBtn);
1087
1093
  if (controlsInitialized) {
1088
1094
  return;
1089
1095
  }
1090
- if (!window._discoverySelectedIds) {
1091
- window._discoverySelectedIds = /* @__PURE__ */ new Set();
1092
- }
1093
- const selectedIds2 = window._discoverySelectedIds;
1094
1096
  const controlsDiv = document.createElement("div");
1095
1097
  controlsDiv.style.cssText = "margin-bottom: 12px; display: flex; gap: 12px; flex-wrap: wrap; align-items: center;";
1096
1098
  const filterLabel = document.createElement("label");
@@ -1120,7 +1122,7 @@ async function discoverDevices2() {
1120
1122
  filterBtn.onclick = () => {
1121
1123
  preferences.connectionType = option.value;
1122
1124
  setDiscoveryPreferences(preferences);
1123
- void updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds2);
1125
+ void updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds);
1124
1126
  Array.prototype.forEach.call(filterGroup.querySelectorAll("button"), (b) => {
1125
1127
  b.style.border = "1px solid #ccc";
1126
1128
  b.style.backgroundColor = "#fff";
@@ -1157,18 +1159,27 @@ async function discoverDevices2() {
1157
1159
  sortSelect.onchange = () => {
1158
1160
  preferences.sortBy = sortSelect.value;
1159
1161
  setDiscoveryPreferences(preferences);
1160
- void updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds2);
1162
+ void updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds);
1161
1163
  };
1162
- const groupLabel = document.createElement("label");
1163
- groupLabel.style.fontSize = "12px";
1164
- groupLabel.style.fontWeight = "500";
1165
- groupLabel.style.marginLeft = "8px";
1166
- groupLabel.textContent = "Group:";
1167
1164
  const groupSelect = document.createElement("select");
1168
1165
  groupSelect.style.fontSize = "11px";
1169
1166
  groupSelect.style.padding = "4px 8px";
1170
1167
  groupSelect.style.borderRadius = "3px";
1171
- groupSelect.value = groupBy;
1168
+ if (!localStorage.getItem(DISCOVERY_GROUP_BY_KEY)) {
1169
+ groupSelect.value = "type";
1170
+ } else {
1171
+ groupSelect.value = groupBy;
1172
+ }
1173
+ const groupLabel = document.createElement("label");
1174
+ groupLabel.style.fontSize = "12px";
1175
+ groupLabel.style.fontWeight = "500";
1176
+ groupLabel.style.marginLeft = "8px";
1177
+ const groupLabelTextMap = {
1178
+ connection: "Connection",
1179
+ hub: "Hub",
1180
+ type: "Device Type"
1181
+ };
1182
+ groupLabel.textContent = `Group: ${groupLabelTextMap[groupSelect.value] || "Connection"}`;
1172
1183
  const groupOptions = [
1173
1184
  { label: "Connection", value: "connection" },
1174
1185
  { label: "Hub", value: "hub" },
@@ -1183,7 +1194,8 @@ async function discoverDevices2() {
1183
1194
  groupSelect.onchange = () => {
1184
1195
  groupBy = groupSelect.value;
1185
1196
  setDiscoveryGroupByPreference(groupBy);
1186
- void updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds2);
1197
+ groupLabel.textContent = `Group: ${groupLabelTextMap[groupSelect.value] || "Connection"}`;
1198
+ void updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds);
1187
1199
  };
1188
1200
  const hideAddedLabel = document.createElement("label");
1189
1201
  hideAddedLabel.style.display = "inline-flex";
@@ -1203,7 +1215,7 @@ async function discoverDevices2() {
1203
1215
  hideAddedCheckbox.onchange = () => {
1204
1216
  hideAdded = hideAddedCheckbox.checked;
1205
1217
  setDiscoveryHideAddedPreference(hideAdded);
1206
- void updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds2);
1218
+ void updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds);
1207
1219
  };
1208
1220
  const searchInput = document.createElement("input");
1209
1221
  searchInput.type = "text";
@@ -1223,7 +1235,7 @@ async function discoverDevices2() {
1223
1235
  searchTimeout = setTimeout(() => {
1224
1236
  preferences.searchQuery = searchInput.value;
1225
1237
  setDiscoveryPreferences(preferences);
1226
- void updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds2);
1238
+ void updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds);
1227
1239
  }, 300);
1228
1240
  };
1229
1241
  const actionBtnStyle = {
@@ -1248,14 +1260,14 @@ async function discoverDevices2() {
1248
1260
  Object.assign(addSelectedBtn.style, actionBtnStyle);
1249
1261
  addSelectedBtn.disabled = true;
1250
1262
  addSelectedBtn.onclick = async () => {
1251
- if (!selectedIds2.size) {
1263
+ if (!selectedIds.size) {
1252
1264
  return;
1253
1265
  }
1254
1266
  addSelectedBtn.disabled = true;
1255
1267
  addSelectedBtn.textContent = "Adding...";
1256
1268
  try {
1257
1269
  showBusyUi();
1258
- const selectedDevices = discoveredDevices.filter((d) => selectedIds2.has(normalizeId(d.id)));
1270
+ const selectedDevices = discoveredDevices.filter((d) => selectedIds.has(normalizeId(d.id)));
1259
1271
  const bulkResult = await addDevicesInBulk(selectedDevices.map((d) => ({
1260
1272
  deviceId: d.id,
1261
1273
  name: d.name,
@@ -1272,10 +1284,10 @@ async function discoverDevices2() {
1272
1284
  const skippedCount = bulkResult?.skippedCount ?? bulkResult?.data?.skippedCount ?? 0;
1273
1285
  toastSuccess(`Added ${addedCount} device(s)${skippedCount > 0 ? ` (${skippedCount} skipped)` : ""}`);
1274
1286
  await loadConfiguredDevices();
1275
- selectedIds2.clear();
1287
+ selectedIds.clear();
1276
1288
  addSelectedBtn.disabled = true;
1277
1289
  addSelectedBtn.textContent = "Add Selected";
1278
- await updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds2);
1290
+ await updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds);
1279
1291
  } catch (e) {
1280
1292
  uiLog.error("Batch add error:", e);
1281
1293
  toastError(e instanceof Error ? e.message : "Failed to add devices");
@@ -1290,19 +1302,19 @@ async function discoverDevices2() {
1290
1302
  Object.assign(enableSelectedBtn.style, actionBtnStyle);
1291
1303
  enableSelectedBtn.disabled = true;
1292
1304
  enableSelectedBtn.onclick = async () => {
1293
- if (!selectedIds2.size) {
1305
+ if (!selectedIds.size) {
1294
1306
  return;
1295
1307
  }
1296
1308
  enableSelectedBtn.disabled = true;
1297
1309
  enableSelectedBtn.textContent = "Enabling...";
1298
1310
  try {
1299
1311
  showBusyUi();
1300
- await batchSetDeviceEnabled2(selectedIds2, true);
1312
+ await batchSetDeviceEnabled(selectedIds, true);
1301
1313
  toastSuccess("Selected devices enabled");
1302
1314
  await loadConfiguredDevices();
1303
1315
  enableSelectedBtn.disabled = true;
1304
1316
  enableSelectedBtn.textContent = "Enable Selected";
1305
- await updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds2);
1317
+ await updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds);
1306
1318
  } catch (e) {
1307
1319
  uiLog.error("Batch enable error:", e);
1308
1320
  toastError(e instanceof Error ? e.message : "Failed to enable devices");
@@ -1317,19 +1329,19 @@ async function discoverDevices2() {
1317
1329
  Object.assign(disableSelectedBtn.style, actionBtnStyle);
1318
1330
  disableSelectedBtn.disabled = true;
1319
1331
  disableSelectedBtn.onclick = async () => {
1320
- if (!selectedIds2.size) {
1332
+ if (!selectedIds.size) {
1321
1333
  return;
1322
1334
  }
1323
1335
  disableSelectedBtn.disabled = true;
1324
1336
  disableSelectedBtn.textContent = "Disabling...";
1325
1337
  try {
1326
1338
  showBusyUi();
1327
- await batchSetDeviceEnabled2(selectedIds2, false);
1339
+ await batchSetDeviceEnabled(selectedIds, false);
1328
1340
  toastSuccess("Selected devices disabled");
1329
1341
  await loadConfiguredDevices();
1330
1342
  disableSelectedBtn.disabled = true;
1331
1343
  disableSelectedBtn.textContent = "Disable Selected";
1332
- await updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds2);
1344
+ await updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds);
1333
1345
  } catch (e) {
1334
1346
  uiLog.error("Batch disable error:", e);
1335
1347
  toastError(e instanceof Error ? e.message : "Failed to disable devices");
@@ -1356,6 +1368,7 @@ async function discoverDevices2() {
1356
1368
  topActionRow.appendChild(enableSelectedBtn);
1357
1369
  topActionRow.appendChild(disableSelectedBtn);
1358
1370
  list.innerHTML = "";
1371
+ list.appendChild(selectControlsRow);
1359
1372
  list.appendChild(topActionRow);
1360
1373
  list.appendChild(controlsDiv);
1361
1374
  let deviceListContainer = document.getElementById("discoveredDevices");
@@ -1372,7 +1385,7 @@ async function discoverDevices2() {
1372
1385
  list.style.display = "block";
1373
1386
  controlsInitialized = true;
1374
1387
  const updateActionButtons = () => {
1375
- const hasSelection = selectedIds2.size > 0;
1388
+ const hasSelection = selectedIds.size > 0;
1376
1389
  addSelectedBtn.disabled = !hasSelection;
1377
1390
  enableSelectedBtn.disabled = !hasSelection;
1378
1391
  disableSelectedBtn.disabled = !hasSelection;
@@ -1690,21 +1703,10 @@ async function updateDiscoveryView(allDevices, preferences, groupBy, hideAdded,
1690
1703
  toastError("Discovery UI error: device list container missing. Please reload the page.");
1691
1704
  }
1692
1705
  }
1693
- const legacyAddSelectedBtns = [];
1694
- const allBtns = document.querySelectorAll("button");
1695
- for (let i = 0; i < allBtns.length; i++) {
1696
- const btn = allBtns[i];
1697
- if (btn.textContent && btn.textContent.trim() === "Add Selected") {
1698
- legacyAddSelectedBtns.push(btn);
1699
- }
1700
- }
1701
- for (let i = 0; i < legacyAddSelectedBtns.length; i++) {
1702
- legacyAddSelectedBtns[i].remove();
1703
- }
1704
1706
  function updateBatchButtonStates() {
1705
1707
  const addSelectedBtn = document.getElementById("addSelectedBtn");
1706
- const enableSelectedBtn = document.querySelector("button")?.parentElement?.querySelector('button[aria-label="Enable Selected"]');
1707
- const disableSelectedBtn = document.querySelector("button")?.parentElement?.querySelector('button[aria-label="Disable Selected"]');
1708
+ const enableSelectedBtn = document.getElementById("enableSelectedBtn");
1709
+ const disableSelectedBtn = document.getElementById("disableSelectedBtn");
1708
1710
  const hasSelection = selectedIds.size > 0;
1709
1711
  if (addSelectedBtn) {
1710
1712
  addSelectedBtn.disabled = !hasSelection;
@@ -1718,161 +1720,7 @@ async function updateDiscoveryView(allDevices, preferences, groupBy, hideAdded,
1718
1720
  }
1719
1721
  window.removeEventListener("discovery-selection-changed", updateBatchButtonStates);
1720
1722
  window.addEventListener("discovery-selection-changed", updateBatchButtonStates);
1721
- let batchControls = document.getElementById("batchImportControls");
1722
- if (!batchControls) {
1723
- batchControls = document.createElement("div");
1724
- batchControls.id = "batchImportControls";
1725
- batchControls.style.display = "flex";
1726
- batchControls.style.flexWrap = "wrap";
1727
- batchControls.style.alignItems = "center";
1728
- batchControls.style.gap = "18px";
1729
- batchControls.style.margin = "8px 0 18px 0";
1730
- batchControls.style.width = "100%";
1731
- const buttonRow = document.createElement("div");
1732
- buttonRow.style.display = "flex";
1733
- buttonRow.style.flexWrap = "nowrap";
1734
- buttonRow.style.alignItems = "center";
1735
- buttonRow.style.justifyContent = "center";
1736
- buttonRow.style.gap = "16px";
1737
- buttonRow.style.width = "100%";
1738
- const addSelectedBtn = document.createElement("button");
1739
- addSelectedBtn.id = "addSelectedBtn";
1740
- addSelectedBtn.textContent = "Add Selected to Config";
1741
- addSelectedBtn.disabled = selectedIds.size === 0;
1742
- addSelectedBtn.style.fontWeight = "600";
1743
- const redButtonStyle = {
1744
- width: "220px",
1745
- padding: "12px 0",
1746
- fontSize: "17px",
1747
- marginBottom: "0",
1748
- boxShadow: "0 2px 8px 0 rgba(220,38,38,0.10)",
1749
- borderRadius: "8px"
1750
- };
1751
- Object.assign(addSelectedBtn.style, redButtonStyle);
1752
- addSelectedBtn.style.background = "#ef4444";
1753
- addSelectedBtn.style.color = "white";
1754
- addSelectedBtn.style.transition = "background 0.2s, box-shadow 0.2s";
1755
- addSelectedBtn.onmouseenter = function() {
1756
- addSelectedBtn.style.background = "#dc2626";
1757
- };
1758
- addSelectedBtn.onmouseleave = function() {
1759
- addSelectedBtn.style.background = "#ef4444";
1760
- };
1761
- addSelectedBtn.onclick = async () => {
1762
- if (!selectedIds.size) {
1763
- return;
1764
- }
1765
- addSelectedBtn.disabled = true;
1766
- addSelectedBtn.textContent = "Adding...";
1767
- try {
1768
- const selectedDevices = visibleDevices.filter((d) => selectedIds.has(normalizeId(d.id)));
1769
- const bulkResult = await addDevicesInBulk(selectedDevices.map((d) => ({
1770
- deviceId: d.id,
1771
- name: d.name,
1772
- type: d.type,
1773
- rssi: d.rssi,
1774
- address: d.address,
1775
- model: d.model
1776
- })));
1777
- uiLog.info("Batch add response:", bulkResult);
1778
- if (!bulkResult || bulkResult.success === false) {
1779
- throw new Error(bulkResult?.data?.message || "Batch add failed");
1780
- }
1781
- const addedCount = bulkResult?.addedCount ?? bulkResult?.data?.addedCount ?? 0;
1782
- const skippedCount = bulkResult?.skippedCount ?? bulkResult?.data?.skippedCount ?? 0;
1783
- toastSuccess(`Added ${addedCount} device(s)${skippedCount > 0 ? ` (${skippedCount} skipped)` : ""}`);
1784
- await loadConfiguredDevices();
1785
- selectedIds.clear();
1786
- addSelectedBtn.disabled = true;
1787
- addSelectedBtn.textContent = "Add Selected to Config";
1788
- await updateDiscoveryView(allDevices, preferences, groupBy, hideAdded, selectedIds);
1789
- } catch (e) {
1790
- uiLog.error("Batch add error:", e);
1791
- toastError(e instanceof Error ? e.message : "Failed to add devices");
1792
- addSelectedBtn.disabled = false;
1793
- addSelectedBtn.textContent = "Add Selected to Config";
1794
- }
1795
- };
1796
- const enableSelectedBtn = document.createElement("button");
1797
- enableSelectedBtn.textContent = "Enable Selected";
1798
- enableSelectedBtn.style.fontWeight = "600";
1799
- Object.assign(enableSelectedBtn.style, redButtonStyle);
1800
- enableSelectedBtn.style.background = "#ef4444";
1801
- enableSelectedBtn.style.color = "white";
1802
- enableSelectedBtn.style.transition = "background 0.2s, box-shadow 0.2s";
1803
- enableSelectedBtn.onmouseenter = function() {
1804
- enableSelectedBtn.style.background = "#dc2626";
1805
- };
1806
- enableSelectedBtn.onmouseleave = function() {
1807
- enableSelectedBtn.style.background = "#ef4444";
1808
- };
1809
- enableSelectedBtn.disabled = selectedIds.size === 0;
1810
- enableSelectedBtn.onclick = async () => {
1811
- if (!selectedIds.size) {
1812
- return;
1813
- }
1814
- enableSelectedBtn.disabled = true;
1815
- try {
1816
- showBusyUi();
1817
- await batchSetDeviceEnabled(selectedIds, true);
1818
- toastSuccess("Selected devices enabled");
1819
- await loadConfiguredDevices();
1820
- await updateDiscoveryView(allDevices, preferences, groupBy, hideAdded, selectedIds);
1821
- } catch (e) {
1822
- uiLog.error("Batch enable error:", e);
1823
- toastError(e instanceof Error ? e.message : "Failed to enable devices");
1824
- } finally {
1825
- hideBusyUi();
1826
- enableSelectedBtn.disabled = false;
1827
- }
1828
- };
1829
- const disableSelectedBtn = document.createElement("button");
1830
- disableSelectedBtn.textContent = "Disable Selected";
1831
- disableSelectedBtn.style.fontWeight = "600";
1832
- Object.assign(disableSelectedBtn.style, redButtonStyle);
1833
- disableSelectedBtn.style.background = "#ef4444";
1834
- disableSelectedBtn.style.color = "white";
1835
- disableSelectedBtn.style.transition = "background 0.2s, box-shadow 0.2s";
1836
- disableSelectedBtn.onmouseenter = function() {
1837
- disableSelectedBtn.style.background = "#dc2626";
1838
- };
1839
- disableSelectedBtn.onmouseleave = function() {
1840
- disableSelectedBtn.style.background = "#ef4444";
1841
- };
1842
- disableSelectedBtn.disabled = selectedIds.size === 0;
1843
- disableSelectedBtn.onclick = async () => {
1844
- if (!selectedIds.size) {
1845
- return;
1846
- }
1847
- disableSelectedBtn.disabled = true;
1848
- try {
1849
- showBusyUi();
1850
- await batchSetDeviceEnabled(selectedIds, false);
1851
- toastSuccess("Selected devices disabled");
1852
- await loadConfiguredDevices();
1853
- await updateDiscoveryView(allDevices, preferences, groupBy, hideAdded, selectedIds);
1854
- } catch (e) {
1855
- uiLog.error("Batch disable error:", e);
1856
- toastError(e instanceof Error ? e.message : "Failed to disable devices");
1857
- } finally {
1858
- hideBusyUi();
1859
- disableSelectedBtn.disabled = false;
1860
- }
1861
- };
1862
- buttonRow.appendChild(addSelectedBtn);
1863
- buttonRow.appendChild(enableSelectedBtn);
1864
- buttonRow.appendChild(disableSelectedBtn);
1865
- batchControls.appendChild(buttonRow);
1866
- const listContainer = document.getElementById("discoveredList");
1867
- if (listContainer) {
1868
- listContainer.insertBefore(batchControls, listContainer.firstChild);
1869
- }
1870
- } else {
1871
- const addSelectedBtn = document.getElementById("addSelectedBtn");
1872
- if (addSelectedBtn) {
1873
- addSelectedBtn.disabled = selectedIds.size === 0;
1874
- }
1875
- }
1723
+ updateBatchButtonStates();
1876
1724
  const status = document.getElementById("discoverStatus");
1877
1725
  if (status) {
1878
1726
  const totalCount = allDevices.length;
@@ -3020,6 +2868,7 @@ function renderDeviceDetailsPanel(device) {
3020
2868
  }
3021
2869
  }
3022
2870
  const rows = [
2871
+ { label: "Name", value: String(device?.name || device?.configDeviceName || "N/A") },
3023
2872
  { label: "Device ID", value: String(device?.id || device?.deviceId || "N/A"), copyable: !!(device?.id || device?.deviceId) },
3024
2873
  { label: "MAC Address", value: String(device?.address || "N/A"), copyable: !!device?.address },
3025
2874
  { label: "Device Type", value: String(device?.type || device?.configDeviceType || "N/A") },
@@ -3641,7 +3490,7 @@ function renderDeviceList(list) {
3641
3490
  deleteBtn.style.background = "#ef4444";
3642
3491
  deleteBtn.onclick = async () => {
3643
3492
  const { deleteDeviceFromConfig: deleteDeviceFromConfig2 } = await Promise.resolve().then(() => (init_devices_delete(), devices_delete_exports));
3644
- await deleteDeviceFromConfig2(d.id, d.name || d.id);
3493
+ await deleteDeviceFromConfig2(d.id || d.deviceId, d.name || d.id || d.deviceId);
3645
3494
  };
3646
3495
  buttons.appendChild(editBtn);
3647
3496
  buttons.appendChild(copyBtn);
@@ -3717,6 +3566,7 @@ async function addDeviceToConfig2(device, options = {}) {
3717
3566
  }
3718
3567
  }
3719
3568
  if (refresh) {
3569
+ await syncParentPluginConfigFromDisk(true);
3720
3570
  await loadConfiguredDevices();
3721
3571
  }
3722
3572
  return { added: !alreadyExists };