@switchbot/homebridge-switchbot 5.0.0-beta.13 → 5.0.0-beta.130

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 (593) hide show
  1. package/.github/ISSUE_TEMPLATE/e2e-verification.md +36 -0
  2. package/.github/copilot-instructions.md +39 -0
  3. package/.github/workflows/ci.yml +32 -0
  4. package/.github/workflows/manual-e2e.yml +115 -0
  5. package/.github/workflows/release.yml +0 -4
  6. package/CHANGELOG.md +35 -0
  7. package/E2E-VERIFICATION.md +121 -0
  8. package/MIGRATION.md +54 -0
  9. package/README.md +99 -3
  10. package/TODO.md +263 -0
  11. package/config.schema.json +198 -14787
  12. package/dist/device-types.d.ts +31 -0
  13. package/dist/device-types.d.ts.map +1 -0
  14. package/dist/device-types.js +246 -0
  15. package/dist/device-types.js.map +1 -0
  16. package/dist/deviceFactory.d.ts +13 -0
  17. package/dist/deviceFactory.d.ts.map +1 -0
  18. package/dist/deviceFactory.js +146 -0
  19. package/dist/deviceFactory.js.map +1 -0
  20. package/dist/devices/deviceBase.d.ts +50 -0
  21. package/dist/devices/deviceBase.d.ts.map +1 -0
  22. package/dist/devices/deviceBase.js +119 -0
  23. package/dist/devices/deviceBase.js.map +1 -0
  24. package/dist/devices/genericDevice.d.ts +277 -0
  25. package/dist/devices/genericDevice.d.ts.map +1 -0
  26. package/dist/devices/genericDevice.js +1383 -0
  27. package/dist/devices/genericDevice.js.map +1 -0
  28. package/dist/errors.d.ts +5 -0
  29. package/dist/errors.d.ts.map +1 -0
  30. package/dist/errors.js +51 -0
  31. package/dist/errors.js.map +1 -0
  32. package/dist/homebridge-ui/endpoints/config.d.ts +3 -0
  33. package/dist/homebridge-ui/endpoints/config.d.ts.map +1 -0
  34. package/dist/homebridge-ui/endpoints/config.js +107 -0
  35. package/dist/homebridge-ui/endpoints/config.js.map +1 -0
  36. package/dist/homebridge-ui/endpoints/credentials.d.ts +6 -0
  37. package/dist/homebridge-ui/endpoints/credentials.d.ts.map +1 -0
  38. package/dist/homebridge-ui/endpoints/credentials.js +65 -0
  39. package/dist/homebridge-ui/endpoints/credentials.js.map +1 -0
  40. package/dist/homebridge-ui/endpoints/devices.d.ts +6 -0
  41. package/dist/homebridge-ui/endpoints/devices.d.ts.map +1 -0
  42. package/dist/homebridge-ui/endpoints/devices.js +430 -0
  43. package/dist/homebridge-ui/endpoints/devices.js.map +1 -0
  44. package/dist/homebridge-ui/endpoints/discovery.d.ts +7 -0
  45. package/dist/homebridge-ui/endpoints/discovery.d.ts.map +1 -0
  46. package/dist/homebridge-ui/endpoints/discovery.js +212 -0
  47. package/dist/homebridge-ui/endpoints/discovery.js.map +1 -0
  48. package/dist/homebridge-ui/public/css/styles.css +447 -0
  49. package/dist/homebridge-ui/public/index.html +144 -248
  50. package/dist/homebridge-ui/public/js/api.d.ts +62 -0
  51. package/dist/homebridge-ui/public/js/api.d.ts.map +1 -0
  52. package/dist/homebridge-ui/public/js/api.js +225 -0
  53. package/dist/homebridge-ui/public/js/api.js.map +1 -0
  54. package/dist/homebridge-ui/public/js/api.ts +279 -0
  55. package/dist/homebridge-ui/public/js/app.d.ts +2 -0
  56. package/dist/homebridge-ui/public/js/app.d.ts.map +1 -0
  57. package/dist/homebridge-ui/public/js/app.js +3600 -0
  58. package/dist/homebridge-ui/public/js/app.js.map +7 -0
  59. package/dist/homebridge-ui/public/js/app.ts +23 -0
  60. package/dist/homebridge-ui/public/js/constants.d.ts +2 -0
  61. package/dist/homebridge-ui/public/js/constants.d.ts.map +1 -0
  62. package/dist/homebridge-ui/public/js/constants.js +2 -0
  63. package/dist/homebridge-ui/public/js/constants.js.map +1 -0
  64. package/dist/homebridge-ui/public/js/constants.ts +1 -0
  65. package/dist/homebridge-ui/public/js/credentials.d.ts +3 -0
  66. package/dist/homebridge-ui/public/js/credentials.d.ts.map +1 -0
  67. package/dist/homebridge-ui/public/js/credentials.js +96 -0
  68. package/dist/homebridge-ui/public/js/credentials.js.map +1 -0
  69. package/dist/homebridge-ui/public/js/credentials.ts +100 -0
  70. package/dist/homebridge-ui/public/js/devices-delete.d.ts +3 -0
  71. package/dist/homebridge-ui/public/js/devices-delete.d.ts.map +1 -0
  72. package/dist/homebridge-ui/public/js/devices-delete.js +206 -0
  73. package/dist/homebridge-ui/public/js/devices-delete.js.map +1 -0
  74. package/dist/homebridge-ui/public/js/devices-delete.ts +236 -0
  75. package/dist/homebridge-ui/public/js/devices.d.ts +9 -0
  76. package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -0
  77. package/dist/homebridge-ui/public/js/devices.js +94 -0
  78. package/dist/homebridge-ui/public/js/devices.js.map +1 -0
  79. package/dist/homebridge-ui/public/js/devices.ts +114 -0
  80. package/dist/homebridge-ui/public/js/discovery.d.ts +4 -0
  81. package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -0
  82. package/dist/homebridge-ui/public/js/discovery.js +1364 -0
  83. package/dist/homebridge-ui/public/js/discovery.js.map +1 -0
  84. package/dist/homebridge-ui/public/js/discovery.ts +1492 -0
  85. package/dist/homebridge-ui/public/js/logger.d.ts +6 -0
  86. package/dist/homebridge-ui/public/js/logger.d.ts.map +1 -0
  87. package/dist/homebridge-ui/public/js/logger.js +12 -0
  88. package/dist/homebridge-ui/public/js/logger.js.map +1 -0
  89. package/dist/homebridge-ui/public/js/logger.ts +11 -0
  90. package/dist/homebridge-ui/public/js/modal.d.ts +5 -0
  91. package/dist/homebridge-ui/public/js/modal.d.ts.map +1 -0
  92. package/dist/homebridge-ui/public/js/modal.js +26 -0
  93. package/dist/homebridge-ui/public/js/modal.js.map +1 -0
  94. package/dist/homebridge-ui/public/js/modal.ts +28 -0
  95. package/dist/homebridge-ui/public/js/modals.d.ts +7 -0
  96. package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -0
  97. package/dist/homebridge-ui/public/js/modals.js +494 -0
  98. package/dist/homebridge-ui/public/js/modals.js.map +1 -0
  99. package/dist/homebridge-ui/public/js/modals.ts +546 -0
  100. package/dist/homebridge-ui/public/js/render.d.ts +68 -0
  101. package/dist/homebridge-ui/public/js/render.d.ts.map +1 -0
  102. package/dist/homebridge-ui/public/js/render.js +914 -0
  103. package/dist/homebridge-ui/public/js/render.js.map +1 -0
  104. package/dist/homebridge-ui/public/js/render.ts +1026 -0
  105. package/dist/homebridge-ui/public/js/toast.d.ts +6 -0
  106. package/dist/homebridge-ui/public/js/toast.d.ts.map +1 -0
  107. package/dist/homebridge-ui/public/js/toast.js +29 -0
  108. package/dist/homebridge-ui/public/js/toast.js.map +1 -0
  109. package/dist/homebridge-ui/public/js/toast.ts +37 -0
  110. package/dist/homebridge-ui/public/js/types.d.ts +23 -0
  111. package/dist/homebridge-ui/public/js/types.d.ts.map +1 -0
  112. package/dist/homebridge-ui/public/js/types.js +2 -0
  113. package/dist/homebridge-ui/public/js/types.js.map +1 -0
  114. package/dist/homebridge-ui/public/js/types.ts +26 -0
  115. package/dist/homebridge-ui/server.js +11 -41
  116. package/dist/homebridge-ui/server.js.map +1 -1
  117. package/dist/homebridge-ui/utils/config-parser.d.ts +35 -0
  118. package/dist/homebridge-ui/utils/config-parser.d.ts.map +1 -0
  119. package/dist/homebridge-ui/utils/config-parser.js +87 -0
  120. package/dist/homebridge-ui/utils/config-parser.js.map +1 -0
  121. package/dist/homebridge-ui/utils/device-migration.d.ts +35 -0
  122. package/dist/homebridge-ui/utils/device-migration.d.ts.map +1 -0
  123. package/dist/homebridge-ui/utils/device-migration.js +111 -0
  124. package/dist/homebridge-ui/utils/device-migration.js.map +1 -0
  125. package/dist/homebridge-ui/utils/logger.d.ts +7 -0
  126. package/dist/homebridge-ui/utils/logger.d.ts.map +1 -0
  127. package/dist/homebridge-ui/utils/logger.js +17 -0
  128. package/dist/homebridge-ui/utils/logger.js.map +1 -0
  129. package/dist/index.d.ts.map +1 -1
  130. package/dist/index.js +1 -3
  131. package/dist/index.js.map +1 -1
  132. package/dist/platform.d.ts +39 -0
  133. package/dist/platform.d.ts.map +1 -0
  134. package/dist/platform.js +1570 -0
  135. package/dist/platform.js.map +1 -0
  136. package/dist/settings.d.ts +10 -249
  137. package/dist/settings.d.ts.map +1 -1
  138. package/dist/settings.js +5 -30
  139. package/dist/settings.js.map +1 -1
  140. package/dist/switchbotClient.d.ts +32 -0
  141. package/dist/switchbotClient.d.ts.map +1 -0
  142. package/dist/switchbotClient.js +253 -0
  143. package/dist/switchbotClient.js.map +1 -0
  144. package/dist/utils.d.ts +36 -82
  145. package/dist/utils.d.ts.map +1 -1
  146. package/dist/utils.js +40 -786
  147. package/dist/utils.js.map +1 -1
  148. package/docs/assets/highlight.css +21 -0
  149. package/docs/assets/icons.js +1 -1
  150. package/docs/assets/icons.svg +1 -1
  151. package/docs/assets/main.js +2 -2
  152. package/docs/assets/style.css +3 -3
  153. package/docs/index.html +103 -13
  154. package/docs/variables/default.html +1 -1
  155. package/eslint.config.js +7 -8
  156. package/nodemon.json +2 -2
  157. package/package.json +36 -32
  158. package/scripts/build-ui.js +36 -0
  159. package/scripts/e2e/README.md +25 -0
  160. package/scripts/e2e/curtain-e2e.sh +70 -0
  161. package/scripts/e2e/fan-e2e.sh +75 -0
  162. package/scripts/e2e/light-advanced-e2e.sh +97 -0
  163. package/scripts/e2e/light-e2e.sh +75 -0
  164. package/scripts/e2e/list-accessories.sh +19 -0
  165. package/scripts/e2e/lock-e2e.sh +65 -0
  166. package/scripts/free-dev-ports.mjs +105 -0
  167. package/scripts/generate-matter-maps.js +60 -0
  168. package/scripts/run-e2e-local.sh +14 -0
  169. package/scripts/sync-device-types.mjs +28 -0
  170. package/src/device-types.ts +261 -0
  171. package/src/deviceFactory.ts +190 -0
  172. package/src/devices/deviceBase.ts +141 -0
  173. package/src/devices/genericDevice.ts +1333 -0
  174. package/src/errors.ts +43 -0
  175. package/src/homebridge-ui/endpoints/config.ts +126 -0
  176. package/src/homebridge-ui/endpoints/credentials.ts +74 -0
  177. package/src/homebridge-ui/endpoints/devices.ts +464 -0
  178. package/src/homebridge-ui/endpoints/discovery.ts +233 -0
  179. package/src/homebridge-ui/public/css/styles.css +447 -0
  180. package/src/homebridge-ui/public/index.html +144 -248
  181. package/src/homebridge-ui/public/js/api.ts +279 -0
  182. package/src/homebridge-ui/public/js/app.ts +23 -0
  183. package/src/homebridge-ui/public/js/constants.ts +1 -0
  184. package/src/homebridge-ui/public/js/credentials.ts +100 -0
  185. package/src/homebridge-ui/public/js/devices-delete.ts +236 -0
  186. package/src/homebridge-ui/public/js/devices.ts +114 -0
  187. package/src/homebridge-ui/public/js/discovery.ts +1492 -0
  188. package/src/homebridge-ui/public/js/logger.ts +11 -0
  189. package/src/homebridge-ui/public/js/modal.ts +28 -0
  190. package/src/homebridge-ui/public/js/modals.ts +546 -0
  191. package/src/homebridge-ui/public/js/render.ts +1026 -0
  192. package/src/homebridge-ui/public/js/toast.ts +37 -0
  193. package/src/homebridge-ui/public/js/types.ts +26 -0
  194. package/src/homebridge-ui/server.ts +11 -43
  195. package/src/homebridge-ui/utils/config-parser.ts +108 -0
  196. package/src/homebridge-ui/utils/device-migration.ts +144 -0
  197. package/src/homebridge-ui/utils/logger.ts +17 -0
  198. package/src/index.ts +1 -3
  199. package/src/platform.ts +1585 -0
  200. package/src/settings.ts +12 -277
  201. package/src/switchbotClient.ts +251 -0
  202. package/src/utils.ts +48 -812
  203. package/test/accessory-restore.spec.ts +73 -0
  204. package/test/device-mapping.spec.ts +37 -0
  205. package/test/deviceFactory.spec.ts +18 -0
  206. package/test/e2e/run-e2e.spec.ts +52 -0
  207. package/test/fan-swing.spec.ts +29 -0
  208. package/test/helpers/matter-harness.ts +53 -0
  209. package/test/lock-users.spec.ts +44 -0
  210. package/test/matter-childbridge.spec.ts +59 -0
  211. package/test/matter-descriptors.spec.ts +97 -0
  212. package/test/matter-device-state.spec.ts +101 -0
  213. package/test/matter-integration.spec.ts +136 -0
  214. package/test/platform.integration.spec.ts +55 -0
  215. package/test/switchbot-client-debounce.spec.ts +131 -0
  216. package/test/switchbot-client-openapi.spec.ts +56 -0
  217. package/test/switchbotClient.spec.ts +10 -0
  218. package/test/utils.spec.ts +36 -0
  219. package/todo/COMPLETED.md +542 -0
  220. package/todo/README.md +59 -0
  221. package/todo/TODO.md +190 -0
  222. package/todo/mockups/filtering-sorting.svg +112 -0
  223. package/todo/mockups/modal-switchbot-theme.svg +66 -0
  224. package/todo/mockups/signal-strength-visualization.svg +94 -0
  225. package/vitest.config.ts +7 -0
  226. package/coverage/base.css +0 -224
  227. package/coverage/block-navigation.js +0 -87
  228. package/coverage/clover.xml +0 -15847
  229. package/coverage/coverage-final.json +0 -42
  230. package/coverage/docs/assets/dmt/dmt-component-data.js.html +0 -85
  231. package/coverage/docs/assets/dmt/dmt-components.js.html +0 -286
  232. package/coverage/docs/assets/dmt/index.html +0 -131
  233. package/coverage/docs/assets/hierarchy.js.html +0 -85
  234. package/coverage/docs/assets/icons.js.html +0 -136
  235. package/coverage/docs/assets/index.html +0 -146
  236. package/coverage/docs/assets/main.js.html +0 -265
  237. package/coverage/favicon.png +0 -0
  238. package/coverage/index.html +0 -191
  239. package/coverage/prettify.css +0 -1
  240. package/coverage/prettify.js +0 -2
  241. package/coverage/sort-arrow-sprite.png +0 -0
  242. package/coverage/sorter.js +0 -196
  243. package/coverage/src/device/blindtilt.ts.html +0 -3238
  244. package/coverage/src/device/bot.ts.html +0 -2803
  245. package/coverage/src/device/ceilinglight.ts.html +0 -2338
  246. package/coverage/src/device/colorbulb.ts.html +0 -2824
  247. package/coverage/src/device/contact.ts.html +0 -1465
  248. package/coverage/src/device/curtain.ts.html +0 -2869
  249. package/coverage/src/device/device.ts.html +0 -2500
  250. package/coverage/src/device/fan.ts.html +0 -2242
  251. package/coverage/src/device/hub.ts.html +0 -1408
  252. package/coverage/src/device/humidifier.ts.html +0 -2116
  253. package/coverage/src/device/index.html +0 -416
  254. package/coverage/src/device/iosensor.ts.html +0 -1375
  255. package/coverage/src/device/lightstrip.ts.html +0 -2617
  256. package/coverage/src/device/lock.ts.html +0 -1963
  257. package/coverage/src/device/meter.ts.html +0 -1372
  258. package/coverage/src/device/meterplus.ts.html +0 -1384
  259. package/coverage/src/device/meterpro.ts.html +0 -1618
  260. package/coverage/src/device/motion.ts.html +0 -1264
  261. package/coverage/src/device/plug.ts.html +0 -1372
  262. package/coverage/src/device/relayswitch.ts.html +0 -2284
  263. package/coverage/src/device/robotvacuumcleaner.ts.html +0 -1810
  264. package/coverage/src/device/waterdetector.ts.html +0 -1294
  265. package/coverage/src/homebridge-ui/index.html +0 -116
  266. package/coverage/src/homebridge-ui/server.ts.html +0 -229
  267. package/coverage/src/index.html +0 -161
  268. package/coverage/src/index.ts.html +0 -124
  269. package/coverage/src/irdevice/airconditioner.ts.html +0 -1687
  270. package/coverage/src/irdevice/airpurifier.ts.html +0 -844
  271. package/coverage/src/irdevice/camera.ts.html +0 -475
  272. package/coverage/src/irdevice/fan.ts.html +0 -766
  273. package/coverage/src/irdevice/index.html +0 -251
  274. package/coverage/src/irdevice/irdevice.ts.html +0 -1117
  275. package/coverage/src/irdevice/light.ts.html +0 -826
  276. package/coverage/src/irdevice/other.ts.html +0 -2458
  277. package/coverage/src/irdevice/tv.ts.html +0 -1222
  278. package/coverage/src/irdevice/vacuumcleaner.ts.html +0 -466
  279. package/coverage/src/irdevice/waterheater.ts.html +0 -469
  280. package/coverage/src/platform.ts.html +0 -8776
  281. package/coverage/src/settings.ts.html +0 -934
  282. package/coverage/src/utils.ts.html +0 -2092
  283. package/dist/baseMatterAccessory.test.d.ts +0 -2
  284. package/dist/baseMatterAccessory.test.d.ts.map +0 -1
  285. package/dist/baseMatterAccessory.test.js +0 -71
  286. package/dist/baseMatterAccessory.test.js.map +0 -1
  287. package/dist/devices-hap/airpurifier.d.ts +0 -54
  288. package/dist/devices-hap/airpurifier.d.ts.map +0 -1
  289. package/dist/devices-hap/airpurifier.js +0 -527
  290. package/dist/devices-hap/airpurifier.js.map +0 -1
  291. package/dist/devices-hap/blindtilt.d.ts +0 -90
  292. package/dist/devices-hap/blindtilt.d.ts.map +0 -1
  293. package/dist/devices-hap/blindtilt.js +0 -974
  294. package/dist/devices-hap/blindtilt.js.map +0 -1
  295. package/dist/devices-hap/bot.d.ts +0 -102
  296. package/dist/devices-hap/bot.d.ts.map +0 -1
  297. package/dist/devices-hap/bot.js +0 -811
  298. package/dist/devices-hap/bot.js.map +0 -1
  299. package/dist/devices-hap/ceilinglight.d.ts +0 -85
  300. package/dist/devices-hap/ceilinglight.d.ts.map +0 -1
  301. package/dist/devices-hap/ceilinglight.js +0 -701
  302. package/dist/devices-hap/ceilinglight.js.map +0 -1
  303. package/dist/devices-hap/colorbulb.d.ts +0 -88
  304. package/dist/devices-hap/colorbulb.d.ts.map +0 -1
  305. package/dist/devices-hap/colorbulb.js +0 -881
  306. package/dist/devices-hap/colorbulb.js.map +0 -1
  307. package/dist/devices-hap/contact.d.ts +0 -44
  308. package/dist/devices-hap/contact.d.ts.map +0 -1
  309. package/dist/devices-hap/contact.js +0 -409
  310. package/dist/devices-hap/contact.js.map +0 -1
  311. package/dist/devices-hap/curtain.d.ts +0 -73
  312. package/dist/devices-hap/curtain.d.ts.map +0 -1
  313. package/dist/devices-hap/curtain.js +0 -869
  314. package/dist/devices-hap/curtain.js.map +0 -1
  315. package/dist/devices-hap/device.d.ts +0 -98
  316. package/dist/devices-hap/device.d.ts.map +0 -1
  317. package/dist/devices-hap/device.js +0 -749
  318. package/dist/devices-hap/device.js.map +0 -1
  319. package/dist/devices-hap/fan.d.ts +0 -69
  320. package/dist/devices-hap/fan.d.ts.map +0 -1
  321. package/dist/devices-hap/fan.js +0 -649
  322. package/dist/devices-hap/fan.js.map +0 -1
  323. package/dist/devices-hap/hub.d.ts +0 -37
  324. package/dist/devices-hap/hub.d.ts.map +0 -1
  325. package/dist/devices-hap/hub.js +0 -392
  326. package/dist/devices-hap/hub.js.map +0 -1
  327. package/dist/devices-hap/humidifier.d.ts +0 -68
  328. package/dist/devices-hap/humidifier.d.ts.map +0 -1
  329. package/dist/devices-hap/humidifier.js +0 -628
  330. package/dist/devices-hap/humidifier.js.map +0 -1
  331. package/dist/devices-hap/iosensor.d.ts +0 -42
  332. package/dist/devices-hap/iosensor.d.ts.map +0 -1
  333. package/dist/devices-hap/iosensor.js +0 -382
  334. package/dist/devices-hap/iosensor.js.map +0 -1
  335. package/dist/devices-hap/lightstrip.d.ts +0 -79
  336. package/dist/devices-hap/lightstrip.d.ts.map +0 -1
  337. package/dist/devices-hap/lightstrip.js +0 -797
  338. package/dist/devices-hap/lightstrip.js.map +0 -1
  339. package/dist/devices-hap/lock.d.ts +0 -53
  340. package/dist/devices-hap/lock.d.ts.map +0 -1
  341. package/dist/devices-hap/lock.js +0 -561
  342. package/dist/devices-hap/lock.js.map +0 -1
  343. package/dist/devices-hap/meter.d.ts +0 -37
  344. package/dist/devices-hap/meter.d.ts.map +0 -1
  345. package/dist/devices-hap/meter.js +0 -379
  346. package/dist/devices-hap/meter.js.map +0 -1
  347. package/dist/devices-hap/meterplus.d.ts +0 -42
  348. package/dist/devices-hap/meterplus.d.ts.map +0 -1
  349. package/dist/devices-hap/meterplus.js +0 -384
  350. package/dist/devices-hap/meterplus.js.map +0 -1
  351. package/dist/devices-hap/meterpro.d.ts +0 -43
  352. package/dist/devices-hap/meterpro.d.ts.map +0 -1
  353. package/dist/devices-hap/meterpro.js +0 -468
  354. package/dist/devices-hap/meterpro.js.map +0 -1
  355. package/dist/devices-hap/motion.d.ts +0 -42
  356. package/dist/devices-hap/motion.d.ts.map +0 -1
  357. package/dist/devices-hap/motion.js +0 -345
  358. package/dist/devices-hap/motion.js.map +0 -1
  359. package/dist/devices-hap/plug.d.ts +0 -49
  360. package/dist/devices-hap/plug.d.ts.map +0 -1
  361. package/dist/devices-hap/plug.js +0 -395
  362. package/dist/devices-hap/plug.js.map +0 -1
  363. package/dist/devices-hap/relayswitch.d.ts +0 -96
  364. package/dist/devices-hap/relayswitch.d.ts.map +0 -1
  365. package/dist/devices-hap/relayswitch.js +0 -642
  366. package/dist/devices-hap/relayswitch.js.map +0 -1
  367. package/dist/devices-hap/robotvacuumcleaner.d.ts +0 -54
  368. package/dist/devices-hap/robotvacuumcleaner.d.ts.map +0 -1
  369. package/dist/devices-hap/robotvacuumcleaner.js +0 -523
  370. package/dist/devices-hap/robotvacuumcleaner.js.map +0 -1
  371. package/dist/devices-hap/waterdetector.d.ts +0 -41
  372. package/dist/devices-hap/waterdetector.d.ts.map +0 -1
  373. package/dist/devices-hap/waterdetector.js +0 -356
  374. package/dist/devices-hap/waterdetector.js.map +0 -1
  375. package/dist/devices-matter/BaseMatterAccessory.d.ts +0 -78
  376. package/dist/devices-matter/BaseMatterAccessory.d.ts.map +0 -1
  377. package/dist/devices-matter/BaseMatterAccessory.js +0 -220
  378. package/dist/devices-matter/BaseMatterAccessory.js.map +0 -1
  379. package/dist/devices-matter/ColorLightAccessory.d.ts +0 -20
  380. package/dist/devices-matter/ColorLightAccessory.d.ts.map +0 -1
  381. package/dist/devices-matter/ColorLightAccessory.js +0 -95
  382. package/dist/devices-matter/ColorLightAccessory.js.map +0 -1
  383. package/dist/devices-matter/ColorTemperatureLightAccessory.d.ts +0 -18
  384. package/dist/devices-matter/ColorTemperatureLightAccessory.d.ts.map +0 -1
  385. package/dist/devices-matter/ColorTemperatureLightAccessory.js +0 -76
  386. package/dist/devices-matter/ColorTemperatureLightAccessory.js.map +0 -1
  387. package/dist/devices-matter/ContactSensorAccessory.d.ts +0 -12
  388. package/dist/devices-matter/ContactSensorAccessory.d.ts.map +0 -1
  389. package/dist/devices-matter/ContactSensorAccessory.js +0 -34
  390. package/dist/devices-matter/ContactSensorAccessory.js.map +0 -1
  391. package/dist/devices-matter/DimmableLightAccessory.d.ts +0 -58
  392. package/dist/devices-matter/DimmableLightAccessory.d.ts.map +0 -1
  393. package/dist/devices-matter/DimmableLightAccessory.js +0 -167
  394. package/dist/devices-matter/DimmableLightAccessory.js.map +0 -1
  395. package/dist/devices-matter/DoorLockAccessory.d.ts +0 -14
  396. package/dist/devices-matter/DoorLockAccessory.d.ts.map +0 -1
  397. package/dist/devices-matter/DoorLockAccessory.js +0 -50
  398. package/dist/devices-matter/DoorLockAccessory.js.map +0 -1
  399. package/dist/devices-matter/ExtendedColorLightAccessory.d.ts +0 -21
  400. package/dist/devices-matter/ExtendedColorLightAccessory.d.ts.map +0 -1
  401. package/dist/devices-matter/ExtendedColorLightAccessory.js +0 -106
  402. package/dist/devices-matter/ExtendedColorLightAccessory.js.map +0 -1
  403. package/dist/devices-matter/FanAccessory.d.ts +0 -16
  404. package/dist/devices-matter/FanAccessory.d.ts.map +0 -1
  405. package/dist/devices-matter/FanAccessory.js +0 -81
  406. package/dist/devices-matter/FanAccessory.js.map +0 -1
  407. package/dist/devices-matter/HumiditySensorAccessory.d.ts +0 -12
  408. package/dist/devices-matter/HumiditySensorAccessory.d.ts.map +0 -1
  409. package/dist/devices-matter/HumiditySensorAccessory.js +0 -34
  410. package/dist/devices-matter/HumiditySensorAccessory.js.map +0 -1
  411. package/dist/devices-matter/LeakSensorAccessory.d.ts +0 -12
  412. package/dist/devices-matter/LeakSensorAccessory.d.ts.map +0 -1
  413. package/dist/devices-matter/LeakSensorAccessory.js +0 -33
  414. package/dist/devices-matter/LeakSensorAccessory.js.map +0 -1
  415. package/dist/devices-matter/LightSensorAccessory.d.ts +0 -12
  416. package/dist/devices-matter/LightSensorAccessory.d.ts.map +0 -1
  417. package/dist/devices-matter/LightSensorAccessory.js +0 -34
  418. package/dist/devices-matter/LightSensorAccessory.js.map +0 -1
  419. package/dist/devices-matter/OccupancySensorAccessory.d.ts +0 -12
  420. package/dist/devices-matter/OccupancySensorAccessory.d.ts.map +0 -1
  421. package/dist/devices-matter/OccupancySensorAccessory.js +0 -39
  422. package/dist/devices-matter/OccupancySensorAccessory.js.map +0 -1
  423. package/dist/devices-matter/OnOffLightAccessory.d.ts +0 -38
  424. package/dist/devices-matter/OnOffLightAccessory.d.ts.map +0 -1
  425. package/dist/devices-matter/OnOffLightAccessory.js +0 -110
  426. package/dist/devices-matter/OnOffLightAccessory.js.map +0 -1
  427. package/dist/devices-matter/OnOffOutletAccessory.d.ts +0 -14
  428. package/dist/devices-matter/OnOffOutletAccessory.d.ts.map +0 -1
  429. package/dist/devices-matter/OnOffOutletAccessory.js +0 -43
  430. package/dist/devices-matter/OnOffOutletAccessory.js.map +0 -1
  431. package/dist/devices-matter/OnOffSwitchAccessory.d.ts +0 -14
  432. package/dist/devices-matter/OnOffSwitchAccessory.d.ts.map +0 -1
  433. package/dist/devices-matter/OnOffSwitchAccessory.js +0 -42
  434. package/dist/devices-matter/OnOffSwitchAccessory.js.map +0 -1
  435. package/dist/devices-matter/RoboticVacuumAccessory.d.ts +0 -68
  436. package/dist/devices-matter/RoboticVacuumAccessory.d.ts.map +0 -1
  437. package/dist/devices-matter/RoboticVacuumAccessory.js +0 -334
  438. package/dist/devices-matter/RoboticVacuumAccessory.js.map +0 -1
  439. package/dist/devices-matter/SmokeCOAlarmAccessory.d.ts +0 -11
  440. package/dist/devices-matter/SmokeCOAlarmAccessory.d.ts.map +0 -1
  441. package/dist/devices-matter/SmokeCOAlarmAccessory.js +0 -49
  442. package/dist/devices-matter/SmokeCOAlarmAccessory.js.map +0 -1
  443. package/dist/devices-matter/TemperatureSensorAccessory.d.ts +0 -12
  444. package/dist/devices-matter/TemperatureSensorAccessory.d.ts.map +0 -1
  445. package/dist/devices-matter/TemperatureSensorAccessory.js +0 -36
  446. package/dist/devices-matter/TemperatureSensorAccessory.js.map +0 -1
  447. package/dist/devices-matter/ThermostatAccessory.d.ts +0 -19
  448. package/dist/devices-matter/ThermostatAccessory.d.ts.map +0 -1
  449. package/dist/devices-matter/ThermostatAccessory.js +0 -95
  450. package/dist/devices-matter/ThermostatAccessory.js.map +0 -1
  451. package/dist/devices-matter/VenetianBlindAccessory.d.ts +0 -19
  452. package/dist/devices-matter/VenetianBlindAccessory.d.ts.map +0 -1
  453. package/dist/devices-matter/VenetianBlindAccessory.js +0 -99
  454. package/dist/devices-matter/VenetianBlindAccessory.js.map +0 -1
  455. package/dist/devices-matter/WindowBlindAccessory.d.ts +0 -17
  456. package/dist/devices-matter/WindowBlindAccessory.d.ts.map +0 -1
  457. package/dist/devices-matter/WindowBlindAccessory.js +0 -80
  458. package/dist/devices-matter/WindowBlindAccessory.js.map +0 -1
  459. package/dist/devices-matter/custom/PowerStripAccessory.d.ts +0 -97
  460. package/dist/devices-matter/custom/PowerStripAccessory.d.ts.map +0 -1
  461. package/dist/devices-matter/custom/PowerStripAccessory.js +0 -265
  462. package/dist/devices-matter/custom/PowerStripAccessory.js.map +0 -1
  463. package/dist/devices-matter/custom/index.d.ts +0 -8
  464. package/dist/devices-matter/custom/index.d.ts.map +0 -1
  465. package/dist/devices-matter/custom/index.js +0 -8
  466. package/dist/devices-matter/custom/index.js.map +0 -1
  467. package/dist/devices-matter/index.d.ts +0 -29
  468. package/dist/devices-matter/index.d.ts.map +0 -1
  469. package/dist/devices-matter/index.js +0 -28
  470. package/dist/devices-matter/index.js.map +0 -1
  471. package/dist/index.test.d.ts +0 -2
  472. package/dist/index.test.d.ts.map +0 -1
  473. package/dist/index.test.js +0 -19
  474. package/dist/index.test.js.map +0 -1
  475. package/dist/irdevice/airconditioner.d.ts +0 -61
  476. package/dist/irdevice/airconditioner.d.ts.map +0 -1
  477. package/dist/irdevice/airconditioner.js +0 -472
  478. package/dist/irdevice/airconditioner.js.map +0 -1
  479. package/dist/irdevice/airpurifier.d.ts +0 -50
  480. package/dist/irdevice/airpurifier.d.ts.map +0 -1
  481. package/dist/irdevice/airpurifier.js +0 -213
  482. package/dist/irdevice/airpurifier.js.map +0 -1
  483. package/dist/irdevice/camera.d.ts +0 -32
  484. package/dist/irdevice/camera.d.ts.map +0 -1
  485. package/dist/irdevice/camera.js +0 -107
  486. package/dist/irdevice/camera.js.map +0 -1
  487. package/dist/irdevice/fan.d.ts +0 -36
  488. package/dist/irdevice/fan.d.ts.map +0 -1
  489. package/dist/irdevice/fan.js +0 -200
  490. package/dist/irdevice/fan.js.map +0 -1
  491. package/dist/irdevice/irdevice.d.ts +0 -68
  492. package/dist/irdevice/irdevice.d.ts.map +0 -1
  493. package/dist/irdevice/irdevice.js +0 -298
  494. package/dist/irdevice/irdevice.js.map +0 -1
  495. package/dist/irdevice/light.d.ts +0 -36
  496. package/dist/irdevice/light.d.ts.map +0 -1
  497. package/dist/irdevice/light.js +0 -206
  498. package/dist/irdevice/light.js.map +0 -1
  499. package/dist/irdevice/other.d.ts +0 -57
  500. package/dist/irdevice/other.d.ts.map +0 -1
  501. package/dist/irdevice/other.js +0 -778
  502. package/dist/irdevice/other.js.map +0 -1
  503. package/dist/irdevice/tv.d.ts +0 -45
  504. package/dist/irdevice/tv.d.ts.map +0 -1
  505. package/dist/irdevice/tv.js +0 -327
  506. package/dist/irdevice/tv.js.map +0 -1
  507. package/dist/irdevice/vacuumcleaner.d.ts +0 -28
  508. package/dist/irdevice/vacuumcleaner.d.ts.map +0 -1
  509. package/dist/irdevice/vacuumcleaner.js +0 -104
  510. package/dist/irdevice/vacuumcleaner.js.map +0 -1
  511. package/dist/irdevice/waterheater.d.ts +0 -30
  512. package/dist/irdevice/waterheater.d.ts.map +0 -1
  513. package/dist/irdevice/waterheater.js +0 -105
  514. package/dist/irdevice/waterheater.js.map +0 -1
  515. package/dist/platform-hap.d.ts +0 -149
  516. package/dist/platform-hap.d.ts.map +0 -1
  517. package/dist/platform-hap.js +0 -2861
  518. package/dist/platform-hap.js.map +0 -1
  519. package/dist/platform-matter.d.ts +0 -120
  520. package/dist/platform-matter.d.ts.map +0 -1
  521. package/dist/platform-matter.js +0 -999
  522. package/dist/platform-matter.js.map +0 -1
  523. package/dist/utils.test.d.ts +0 -2
  524. package/dist/utils.test.d.ts.map +0 -1
  525. package/dist/utils.test.js +0 -95
  526. package/dist/utils.test.js.map +0 -1
  527. package/dist/verifyconfig.test.d.ts +0 -2
  528. package/dist/verifyconfig.test.d.ts.map +0 -1
  529. package/dist/verifyconfig.test.js +0 -167
  530. package/dist/verifyconfig.test.js.map +0 -1
  531. package/src/baseMatterAccessory.test.ts +0 -88
  532. package/src/custom.d.ts +0 -7
  533. package/src/devices-hap/airpurifier.ts +0 -563
  534. package/src/devices-hap/blindtilt.ts +0 -1049
  535. package/src/devices-hap/bot.ts +0 -900
  536. package/src/devices-hap/ceilinglight.ts +0 -742
  537. package/src/devices-hap/colorbulb.ts +0 -904
  538. package/src/devices-hap/contact.ts +0 -457
  539. package/src/devices-hap/curtain.ts +0 -944
  540. package/src/devices-hap/device.ts +0 -811
  541. package/src/devices-hap/fan.ts +0 -711
  542. package/src/devices-hap/hub.ts +0 -439
  543. package/src/devices-hap/humidifier.ts +0 -669
  544. package/src/devices-hap/iosensor.ts +0 -427
  545. package/src/devices-hap/lightstrip.ts +0 -836
  546. package/src/devices-hap/lock.ts +0 -620
  547. package/src/devices-hap/meter.ts +0 -426
  548. package/src/devices-hap/meterplus.ts +0 -430
  549. package/src/devices-hap/meterpro.ts +0 -522
  550. package/src/devices-hap/motion.ts +0 -390
  551. package/src/devices-hap/plug.ts +0 -423
  552. package/src/devices-hap/relayswitch.ts +0 -727
  553. package/src/devices-hap/robotvacuumcleaner.ts +0 -568
  554. package/src/devices-hap/waterdetector.ts +0 -400
  555. package/src/devices-matter/BaseMatterAccessory.ts +0 -249
  556. package/src/devices-matter/ColorLightAccessory.ts +0 -110
  557. package/src/devices-matter/ColorTemperatureLightAccessory.ts +0 -90
  558. package/src/devices-matter/ContactSensorAccessory.ts +0 -41
  559. package/src/devices-matter/DimmableLightAccessory.ts +0 -192
  560. package/src/devices-matter/DoorLockAccessory.ts +0 -60
  561. package/src/devices-matter/ExtendedColorLightAccessory.ts +0 -122
  562. package/src/devices-matter/FanAccessory.ts +0 -95
  563. package/src/devices-matter/HumiditySensorAccessory.ts +0 -41
  564. package/src/devices-matter/LeakSensorAccessory.ts +0 -40
  565. package/src/devices-matter/LightSensorAccessory.ts +0 -41
  566. package/src/devices-matter/OccupancySensorAccessory.ts +0 -48
  567. package/src/devices-matter/OnOffLightAccessory.ts +0 -125
  568. package/src/devices-matter/OnOffOutletAccessory.ts +0 -51
  569. package/src/devices-matter/OnOffSwitchAccessory.ts +0 -51
  570. package/src/devices-matter/RoboticVacuumAccessory.ts +0 -407
  571. package/src/devices-matter/SmokeCOAlarmAccessory.ts +0 -59
  572. package/src/devices-matter/TemperatureSensorAccessory.ts +0 -43
  573. package/src/devices-matter/ThermostatAccessory.ts +0 -110
  574. package/src/devices-matter/VenetianBlindAccessory.ts +0 -115
  575. package/src/devices-matter/WindowBlindAccessory.ts +0 -92
  576. package/src/devices-matter/custom/PowerStripAccessory.ts +0 -309
  577. package/src/devices-matter/custom/index.ts +0 -8
  578. package/src/devices-matter/index.ts +0 -29
  579. package/src/index.test.ts +0 -24
  580. package/src/irdevice/airconditioner.ts +0 -533
  581. package/src/irdevice/airpurifier.ts +0 -252
  582. package/src/irdevice/camera.ts +0 -129
  583. package/src/irdevice/fan.ts +0 -226
  584. package/src/irdevice/irdevice.ts +0 -344
  585. package/src/irdevice/light.ts +0 -246
  586. package/src/irdevice/other.ts +0 -790
  587. package/src/irdevice/tv.ts +0 -378
  588. package/src/irdevice/vacuumcleaner.ts +0 -126
  589. package/src/irdevice/waterheater.ts +0 -127
  590. package/src/platform-hap.ts +0 -2997
  591. package/src/platform-matter.ts +0 -1124
  592. package/src/utils.test.ts +0 -96
  593. package/src/verifyconfig.test.ts +0 -197
@@ -0,0 +1,3600 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // src/device-types.ts
12
+ function getValidDeviceTypes() {
13
+ const validTypes = /* @__PURE__ */ new Set();
14
+ for (const category of Object.values(DEVICE_TYPES)) {
15
+ for (const type of category) {
16
+ validTypes.add(type);
17
+ }
18
+ }
19
+ return validTypes;
20
+ }
21
+ function normalizeDeviceType(deviceType) {
22
+ if (!deviceType || typeof deviceType !== "string") {
23
+ return null;
24
+ }
25
+ const trimmed = deviceType.trim();
26
+ const lowercase = trimmed.toLowerCase();
27
+ const validTypes = getValidDeviceTypes();
28
+ if (validTypes.has(trimmed)) {
29
+ return trimmed;
30
+ }
31
+ const normalized = DEVICE_TYPE_NORMALIZATION_MAP[lowercase];
32
+ if (normalized && validTypes.has(normalized)) {
33
+ return normalized;
34
+ }
35
+ return null;
36
+ }
37
+ function isValidDeviceType(deviceType) {
38
+ if (!deviceType || typeof deviceType !== "string") {
39
+ return false;
40
+ }
41
+ const validTypes = getValidDeviceTypes();
42
+ return validTypes.has(deviceType.trim());
43
+ }
44
+ var DEVICE_TYPES, DEVICE_TYPE_NORMALIZATION_MAP;
45
+ var init_device_types = __esm({
46
+ "src/device-types.ts"() {
47
+ "use strict";
48
+ DEVICE_TYPES = {
49
+ "Window Coverings": ["Blind Tilt", "Curtain", "Curtain3", "Roller Shade"],
50
+ "Locks & Access": [
51
+ "Keypad",
52
+ "Keypad Touch",
53
+ "Keypad Vision",
54
+ "Keypad Vision Pro",
55
+ "Lock Vision Pro",
56
+ "Lock Lite",
57
+ "Smart Lock",
58
+ "Smart Lock Pro",
59
+ "Smart Lock Ultra",
60
+ "Video Doorbell"
61
+ ],
62
+ "Sensors": ["Contact Sensor", "Motion Sensor", "Presence Sensor", "Water Detector"],
63
+ "Lighting": [
64
+ "Candle Warmer Lamp",
65
+ "Ceiling Light",
66
+ "Ceiling Light Pro",
67
+ "Color Bulb",
68
+ "Floor Lamp",
69
+ "RGBIC Neon Rope Light",
70
+ "RGBIC Neon Wire Rope Light",
71
+ "RGBICWW Floor Lamp",
72
+ "RGBICWW Strip Light",
73
+ "Strip Light",
74
+ "Strip Light 3"
75
+ ],
76
+ "Climate Control": [
77
+ "Air Purifier PM2.5",
78
+ "Air Purifier Table PM2.5",
79
+ "Air Purifier VOC",
80
+ "Air Purifier Table VOC",
81
+ "Battery Circulator Fan",
82
+ "Circulator Fan",
83
+ "Humidifier",
84
+ "Humidifier2",
85
+ "Meter",
86
+ "MeterPlus",
87
+ "Meter Plus",
88
+ "MeterPro",
89
+ "Meter Pro",
90
+ "MeterPro(CO2)",
91
+ "Meter Pro (CO2)",
92
+ "Smart Radiator Thermostat",
93
+ "Standing Circulator Fan",
94
+ "WoIOSensor"
95
+ ],
96
+ "Plugs & Switches": [
97
+ "Garage Door Opener",
98
+ "Plug",
99
+ "Plug Mini (EU)",
100
+ "Plug Mini (JP)",
101
+ "Plug Mini (US)",
102
+ "Relay Switch 1",
103
+ "Relay Switch 1PM",
104
+ "Relay Switch 2PM"
105
+ ],
106
+ "Robot Vacuums": [
107
+ "K10+",
108
+ "K10+ Pro",
109
+ "Robot Vacuum Cleaner K10+ Pro Combo",
110
+ "Robot Vacuum Cleaner K11+",
111
+ "Robot Vacuum Cleaner K20 Plus Pro",
112
+ "Robot Vacuum Cleaner S1",
113
+ "Robot Vacuum Cleaner S1 Plus",
114
+ "Robot Vacuum Cleaner S10",
115
+ "Robot Vacuum Cleaner S20"
116
+ ],
117
+ "Hubs": ["AI Hub", "Hub", "Hub 2", "Hub 3", "Hub Mini", "Hub Plus"],
118
+ "Cameras": [
119
+ "Indoor Cam",
120
+ "Pan/Tilt Cam",
121
+ "Pan/Tilt Cam 2K",
122
+ "Pan/Tilt Cam Plus 2K",
123
+ "Pan/Tilt Cam Plus 3K"
124
+ ],
125
+ "IR Devices": [
126
+ "Air Conditioner",
127
+ "Air Purifier",
128
+ "Camera",
129
+ "DVD",
130
+ "Fan",
131
+ "Light",
132
+ "Others",
133
+ "Projector",
134
+ "Set Top Box",
135
+ "Speaker",
136
+ "Streamer",
137
+ "TV",
138
+ "Vacuum Cleaner",
139
+ "Water Heater"
140
+ ],
141
+ "Other Devices": ["AI Art Frame", "Bot", "Home Climate Panel", "Remote", "remote with screen"]
142
+ };
143
+ DEVICE_TYPE_NORMALIZATION_MAP = {
144
+ // --- node-switchbot beta new types normalization additions ---
145
+ "hub mini": "Hub Mini",
146
+ "hub 3": "Hub 3",
147
+ "keypad": "Keypad",
148
+ // node-switchbot beta
149
+ "plug mini": "Plug Mini (US)",
150
+ // fallback to US if region not specified
151
+ "art frame": "AI Art Frame",
152
+ "rgbicww": "RGBICWW Strip Light",
153
+ "lock vision": "Lock Vision Pro",
154
+ // alias for new lock vision
155
+ "lock pro": "Smart Lock Pro",
156
+ "lock lite": "Lock Lite",
157
+ // node-switchbot beta
158
+ "circulator fan": "Circulator Fan",
159
+ // node-switchbot beta
160
+ "smart thermostat radiator": "Smart Radiator Thermostat",
161
+ "climate panel": "Home Climate Panel",
162
+ "evaporative humidifier": "Humidifier",
163
+ // --- end node-switchbot beta additions ---
164
+ // Only keep the last occurrence for each key, all values canonical
165
+ "air purifier pm2.5": "Air Purifier PM2.5",
166
+ "pan/tilt cam plus 3k": "Pan/Tilt Cam Plus 3K",
167
+ "remote with screen": "Remote with Screen",
168
+ "ai hub": "AI Hub",
169
+ "water detector": "Water Detector",
170
+ "video doorbell": "Video Doorbell",
171
+ "smart radiator thermostat": "Smart Radiator Thermostat",
172
+ "woiosensor": "WoIOSensor",
173
+ "garage door opener": "Garage Door Opener",
174
+ "air purifier table pm2.5": "Air Purifier Table PM2.5",
175
+ "air purifier voc": "Air Purifier VOC",
176
+ "air purifier table voc": "Air Purifier Table VOC",
177
+ "plug mini (eu)": "Plug Mini (EU)",
178
+ // Only last occurrence for each key is kept above. Removed duplicates here.
179
+ // Climate control conversions
180
+ "humidifier2": "Humidifier2",
181
+ "battery circulator fan": "Battery Circulator Fan",
182
+ "standing circulator fan": "Standing Circulator Fan",
183
+ // Lock/keypad conversions
184
+ "smart lock": "Smart Lock",
185
+ "smart lock pro": "Smart Lock Pro",
186
+ "smart lock ultra": "Smart Lock Ultra",
187
+ "keypad touch": "Keypad Touch",
188
+ "keypad vision": "Keypad Vision",
189
+ "keypad vision pro": "Keypad Vision Pro",
190
+ // Light conversions
191
+ "color bulb": "Color Bulb",
192
+ "ceiling light": "Ceiling Light",
193
+ "ceiling light pro": "Ceiling Light Pro",
194
+ "candle warmer lamp": "Candle Warmer Lamp",
195
+ "floor lamp": "Floor Lamp",
196
+ "rgbic neon rope light": "RGBIC Neon Rope Light",
197
+ "rgbic neon wire rope light": "RGBIC Neon Wire Rope Light",
198
+ "rgbicww floor lamp": "RGBICWW Floor Lamp",
199
+ "rgbicww strip light": "RGBICWW Strip Light",
200
+ "strip light": "Strip Light",
201
+ "strip light 3": "Strip Light 3",
202
+ // node-switchbot beta
203
+ // Vacuum conversions
204
+ "robot vacuum cleaner s1": "Robot Vacuum Cleaner S1",
205
+ "robot vacuum cleaner s1 plus": "Robot Vacuum Cleaner S1 Plus",
206
+ "robot vacuum cleaner s10": "Robot Vacuum Cleaner S10",
207
+ "robot vacuum cleaner s20": "Robot Vacuum Cleaner S20",
208
+ "robot vacuum cleaner k10+ pro combo": "Robot Vacuum Cleaner K10+ Pro Combo",
209
+ "robot vacuum cleaner k11+": "Robot Vacuum Cleaner K11+",
210
+ "robot vacuum cleaner k20 plus pro": "Robot Vacuum Cleaner K20 Plus Pro",
211
+ // Exact device type mappings (API format → canonical format)
212
+ "relay switch 1": "Relay Switch 1",
213
+ "blind tilt": "Blind Tilt",
214
+ "roller shade": "Roller Shade",
215
+ "curtain3": "Curtain3",
216
+ "hub 2": "Hub 2",
217
+ "meterplus": "MeterPlus",
218
+ "meterpro": "MeterPro",
219
+ "meterpro(co2)": "MeterPro(CO2)",
220
+ "walletfinder": "WalletFinder",
221
+ "k10+": "K10+",
222
+ "k10+ pro (wosweeperminipro)": "K10+ Pro (wosweeperminipro)",
223
+ // Handle spaced variants from config files (normalize back to canonical type)
224
+ "meter pro": "Meter Pro",
225
+ "meter pro (co2)": "Meter Pro (CO2)",
226
+ "meter plus": "Meter Plus",
227
+ "relay switch 1 pm": "Relay Switch 1PM",
228
+ "relay switch 2 pm": "Relay Switch 2PM",
229
+ "plug mini eu": "Plug Mini (EU)",
230
+ "plug mini jp": "Plug Mini (JP)",
231
+ "plug mini us": "Plug Mini (US)",
232
+ // Migration mappings for invalid/legacy device types
233
+ "lock vision pro": "Lock Vision Pro",
234
+ // Valid alias; map to canonical
235
+ // 'lock vision': 'Keypad Vision', // Invalid type (removed, now node-switchbot beta alias above)
236
+ "lock touch": "Keypad Touch",
237
+ // Invalid type
238
+ // Additional normalization for new/unknown types from logs
239
+ "woplugus": "Plug Mini (US)"
240
+ // Removed duplicate keys below, only last occurrence kept
241
+ // 'plug mini us': 'plug mini (us)', // duplicate, removed
242
+ // 'plug us': 'plug mini (us)', // duplicate, removed
243
+ // 'plug': 'plug', // duplicate, removed
244
+ // 'air purifier pm2.5': 'air purifier pm2.5', // duplicate, removed
245
+ // 'rgbic neon wire rope light': 'rgbic neon wire rope light', // duplicate, removed
246
+ // 'candle warmer lamp': 'candle warmer lamp', // duplicate, removed
247
+ // 'pan/tilt cam plus 3k': 'pan/tilt cam plus 3k', // duplicate, removed
248
+ // 'remote with screen': 'remote with screen', // duplicate, removed
249
+ // 'ai hub': 'ai hub', // duplicate, removed
250
+ // 'lock vision pro': 'lock vision pro', // duplicate, remove this line
251
+ // Add any other device types from logs as needed
252
+ };
253
+ }
254
+ });
255
+
256
+ // src/homebridge-ui/public/js/types.ts
257
+ var init_types = __esm({
258
+ "src/homebridge-ui/public/js/types.ts"() {
259
+ "use strict";
260
+ }
261
+ });
262
+
263
+ // src/homebridge-ui/public/js/logger.ts
264
+ var uiLog;
265
+ var init_logger = __esm({
266
+ "src/homebridge-ui/public/js/logger.ts"() {
267
+ "use strict";
268
+ uiLog = {
269
+ info: (message, ...parameters) => {
270
+ console.warn("[SwitchBot UI]", message, ...parameters);
271
+ },
272
+ warn: (message, ...parameters) => {
273
+ console.warn("[SwitchBot UI]", message, ...parameters);
274
+ },
275
+ error: (message, ...parameters) => {
276
+ console.error("[SwitchBot UI]", message, ...parameters);
277
+ }
278
+ };
279
+ }
280
+ });
281
+
282
+ // src/homebridge-ui/public/js/api.ts
283
+ var api_exports = {};
284
+ __export(api_exports, {
285
+ addDevice: () => addDevice,
286
+ addDevicesInBulk: () => addDevicesInBulk,
287
+ deleteAllDevices: () => deleteAllDevices,
288
+ deleteDevice: () => deleteDevice,
289
+ discoverDevices: () => discoverDevices,
290
+ fetchBluetoothStatus: () => fetchBluetoothStatus,
291
+ fetchCredentialStatus: () => fetchCredentialStatus,
292
+ fetchDevices: () => fetchDevices,
293
+ saveCredentials: () => saveCredentials,
294
+ syncParentPluginConfigFromDisk: () => syncParentPluginConfigFromDisk,
295
+ testDeviceConnection: () => testDeviceConnection,
296
+ updateDevice: () => updateDevice,
297
+ validateAndFixDeviceTypes: () => validateAndFixDeviceTypes
298
+ });
299
+ function validateAndFixDeviceTypes(devices) {
300
+ const errors = [];
301
+ for (const d of devices) {
302
+ if (!isValidDeviceType(d.configDeviceType)) {
303
+ const fixed = normalizeDeviceType(d.configDeviceType);
304
+ if (fixed) {
305
+ d.configDeviceType = fixed;
306
+ } else {
307
+ errors.push({
308
+ deviceId: d.deviceId,
309
+ name: d.configDeviceName,
310
+ type: d.configDeviceType
311
+ });
312
+ }
313
+ }
314
+ }
315
+ return errors;
316
+ }
317
+ function isSwitchBotPlatformConfig(block) {
318
+ const platformName = String(block?.platform || block?.name || "").toLowerCase();
319
+ return platformName === "switchbot" || platformName === "@switchbot/homebridge-switchbot" || platformName.includes("switchbot");
320
+ }
321
+ async function syncParentPluginConfigFromDisk(autoSave = false) {
322
+ try {
323
+ if (typeof homebridge.getPluginConfig !== "function" || typeof homebridge.updatePluginConfig !== "function") {
324
+ uiLog.warn("Parent config sync API not available");
325
+ return false;
326
+ }
327
+ const diskResp = await homebridge.request("/platform-config", {});
328
+ if (!diskResp || diskResp.success === false || !diskResp.data) {
329
+ uiLog.warn("Failed to fetch fresh platform config from disk");
330
+ return false;
331
+ }
332
+ const pluginConfigBlocks = await homebridge.getPluginConfig();
333
+ if (!Array.isArray(pluginConfigBlocks) || !pluginConfigBlocks.length) {
334
+ uiLog.warn("No plugin config blocks returned from Homebridge");
335
+ return false;
336
+ }
337
+ const index = pluginConfigBlocks.findIndex((block) => isSwitchBotPlatformConfig(block));
338
+ if (index < 0) {
339
+ uiLog.warn("SwitchBot platform block not found in Homebridge plugin config");
340
+ return false;
341
+ }
342
+ const errors = validateAndFixDeviceTypes(diskResp.data.devices || []);
343
+ if (errors.length > 0) {
344
+ homebridge.toast?.error?.(`Invalid device types found: ${errors.map((e) => `${e.name} (${e.type})`).join(", ")}`);
345
+ return false;
346
+ }
347
+ pluginConfigBlocks[index] = diskResp.data;
348
+ await homebridge.updatePluginConfig(pluginConfigBlocks);
349
+ if (autoSave && typeof homebridge.savePluginConfig === "function") {
350
+ uiLog.info("Auto-saving config to disk...");
351
+ await homebridge.savePluginConfig();
352
+ uiLog.info("Config saved successfully");
353
+ }
354
+ return true;
355
+ } catch (e) {
356
+ uiLog.warn("Failed to sync parent plugin config cache:", e);
357
+ return false;
358
+ }
359
+ }
360
+ async function fetchCredentialStatus() {
361
+ try {
362
+ const resp = await homebridge.request("/credentials", {});
363
+ uiLog.info("Load credentials response:", resp);
364
+ if (!resp || resp.success === false) {
365
+ uiLog.error("Failed to load credentials:", resp);
366
+ return null;
367
+ }
368
+ return resp.data || {};
369
+ } catch (e) {
370
+ uiLog.error("Error loading credentials:", e);
371
+ return null;
372
+ }
373
+ }
374
+ async function saveCredentials(token, secret) {
375
+ uiLog.info("Saving credentials...");
376
+ const resp = await homebridge.request("/credentials", { token, secret });
377
+ uiLog.info("Save response:", resp);
378
+ if (!resp || resp.success === false) {
379
+ throw new Error(resp?.message || "Save failed");
380
+ }
381
+ return resp.data || resp;
382
+ }
383
+ async function fetchDevices() {
384
+ try {
385
+ const resp = await homebridge.request("/devices", {});
386
+ if (!resp || resp.success === false) {
387
+ const backendMsg = resp?.data?.message || resp?.message;
388
+ throw new Error(backendMsg || "request failed");
389
+ }
390
+ return resp.data || [];
391
+ } catch (e) {
392
+ const msg = e instanceof Error ? e.message : String(e);
393
+ uiLog.error("Error fetching devices:", msg);
394
+ return [];
395
+ }
396
+ }
397
+ async function discoverDevices(mode = "all", options) {
398
+ const resp = await homebridge.request("/discover", { mode, ...options });
399
+ uiLog.info("Discover response:", resp);
400
+ if (!resp || resp.success === false) {
401
+ throw new Error(resp?.data?.message || "Discovery failed");
402
+ }
403
+ return resp.data || [];
404
+ }
405
+ async function fetchBluetoothStatus() {
406
+ try {
407
+ const resp = await homebridge.request("/ble-status", {});
408
+ if (!resp || resp.success === false) {
409
+ return { available: false, message: "Bluetooth status unavailable" };
410
+ }
411
+ return resp.data || { available: false, message: "Bluetooth status unavailable" };
412
+ } catch (_e) {
413
+ return { available: false, message: "Bluetooth status unavailable" };
414
+ }
415
+ }
416
+ async function testDeviceConnection(payload) {
417
+ const resp = await homebridge.request("/test-connection", payload);
418
+ if (!resp || resp.success === false) {
419
+ throw new Error(resp?.data?.message || "Connection test failed");
420
+ }
421
+ return resp.data || {
422
+ success: false,
423
+ deviceId: payload.deviceId,
424
+ method: "Auto",
425
+ latencyMs: 0,
426
+ message: "Connection test failed"
427
+ };
428
+ }
429
+ async function addDevice(deviceId, name, type, options) {
430
+ const payload = { deviceId, name, type };
431
+ if (options?.address) {
432
+ payload.address = options.address;
433
+ }
434
+ if (options?.model) {
435
+ payload.model = options.model;
436
+ }
437
+ if (options?.rssi !== void 0 && options?.rssi !== null && options?.rssi !== 0) {
438
+ payload.rssi = options.rssi;
439
+ }
440
+ if (options?.encryptionKey) {
441
+ payload.encryptionKey = options.encryptionKey;
442
+ }
443
+ if (options?.keyId) {
444
+ payload.keyId = options.keyId;
445
+ }
446
+ if (options?.matterType !== void 0) {
447
+ payload.matterType = options.matterType;
448
+ }
449
+ uiLog.info("Adding device to config:", payload);
450
+ const resp = await homebridge.request("/add-device", payload);
451
+ uiLog.info("Add device response:", resp);
452
+ if (!resp || resp.success === false) {
453
+ throw new Error(resp?.data?.message || "Failed to add device");
454
+ }
455
+ return resp.data || resp;
456
+ }
457
+ async function addDevicesInBulk(devices) {
458
+ const resp = await homebridge.request("/add-devices", { devices });
459
+ uiLog.info("Bulk add response:", resp);
460
+ if (!resp || resp.success === false) {
461
+ throw new Error(resp?.data?.message || "Bulk add failed");
462
+ }
463
+ return resp.data || resp;
464
+ }
465
+ async function updateDevice(deviceId, configDeviceName, configDeviceType, options) {
466
+ const params = { deviceId };
467
+ if (configDeviceName) {
468
+ params.configDeviceName = configDeviceName;
469
+ }
470
+ if (configDeviceType) {
471
+ params.configDeviceType = configDeviceType;
472
+ }
473
+ if (options?.matterType !== void 0) {
474
+ params.matterType = options.matterType;
475
+ }
476
+ uiLog.info("[Update Device] Sending update request with params:", params);
477
+ const resp = await homebridge.request("/update-device", params);
478
+ uiLog.info("[Update Device] Update response:", resp);
479
+ if (!resp || resp.success === false) {
480
+ throw new Error(resp?.data?.message || "Failed to update device");
481
+ }
482
+ return resp.data || resp;
483
+ }
484
+ async function deleteDevice(deviceId) {
485
+ uiLog.info("Sending delete request for deviceId:", deviceId);
486
+ const resp = await homebridge.request("/delete-device", { deviceId });
487
+ uiLog.info("Delete response:", resp);
488
+ if (!resp || resp.success === false) {
489
+ throw new Error(resp?.data?.message || "Failed to delete device");
490
+ }
491
+ return resp.data || resp;
492
+ }
493
+ async function deleteAllDevices() {
494
+ uiLog.info("Sending delete all devices request");
495
+ const resp = await homebridge.request("/delete-all-devices", {});
496
+ uiLog.info("Delete all response:", resp);
497
+ if (!resp || resp.success === false) {
498
+ throw new Error(resp?.data?.message || "Failed to delete all devices");
499
+ }
500
+ return resp.data || resp;
501
+ }
502
+ var init_api = __esm({
503
+ "src/homebridge-ui/public/js/api.ts"() {
504
+ "use strict";
505
+ init_device_types();
506
+ init_types();
507
+ init_logger();
508
+ }
509
+ });
510
+
511
+ // src/homebridge-ui/public/js/modal.ts
512
+ function callUiMethod(name) {
513
+ try {
514
+ const value = homebridge?.[name];
515
+ if (typeof value === "function") {
516
+ const fn = value;
517
+ fn();
518
+ }
519
+ } catch (e) {
520
+ uiLog.warn(`Homebridge UI method ${String(name)} failed:`, e);
521
+ }
522
+ }
523
+ function showBusyUi() {
524
+ callUiMethod("disableSaveButton");
525
+ callUiMethod("showSpinner");
526
+ }
527
+ function hideBusyUi() {
528
+ callUiMethod("hideSpinner");
529
+ callUiMethod("enableSaveButton");
530
+ }
531
+ var init_modal = __esm({
532
+ "src/homebridge-ui/public/js/modal.ts"() {
533
+ "use strict";
534
+ init_types();
535
+ init_logger();
536
+ }
537
+ });
538
+
539
+ // src/homebridge-ui/public/js/toast.ts
540
+ function showToast(method, message, title = "SwitchBot") {
541
+ try {
542
+ const toast = homebridge?.toast;
543
+ const fn = toast?.[method];
544
+ if (typeof fn === "function") {
545
+ fn(message, title);
546
+ return;
547
+ }
548
+ uiLog.info(`[Toast:${method}] ${title} - ${message}`);
549
+ } catch (e) {
550
+ uiLog.warn(`Toast ${method} failed:`, e);
551
+ }
552
+ }
553
+ function toastSuccess(message, title) {
554
+ showToast("success", message, title);
555
+ }
556
+ function toastError(message, title) {
557
+ showToast("error", message, title);
558
+ }
559
+ function toastWarning(message, title) {
560
+ showToast("warning", message, title);
561
+ }
562
+ function toastInfo(message, title) {
563
+ showToast("info", message, title);
564
+ }
565
+ var init_toast = __esm({
566
+ "src/homebridge-ui/public/js/toast.ts"() {
567
+ "use strict";
568
+ init_types();
569
+ init_logger();
570
+ }
571
+ });
572
+
573
+ // src/homebridge-ui/public/js/discovery.ts
574
+ var discovery_exports = {};
575
+ __export(discovery_exports, {
576
+ addDeviceToConfig: () => addDeviceToConfig,
577
+ discoverDevices: () => discoverDevices2,
578
+ initializeDiscoverySettings: () => initializeDiscoverySettings
579
+ });
580
+ async function batchSetDeviceEnabled(selectedIds, enabled) {
581
+ const resp = await homebridge.request("/platform-config", {});
582
+ if (!resp || resp.success === false || !resp.data) {
583
+ throw new Error("Failed to load config");
584
+ }
585
+ const config = resp.data;
586
+ const configArr = Array.isArray(config) ? config : [config];
587
+ const platformIdx = configArr.findIndex((c) => (c.platform || c.name || "").toLowerCase().includes("switchbot"));
588
+ if (platformIdx === -1) {
589
+ throw new Error("SwitchBot platform config not found");
590
+ }
591
+ const platformConfig = configArr[platformIdx];
592
+ if (!Array.isArray(platformConfig.devices)) {
593
+ throw new TypeError("No devices array in config");
594
+ }
595
+ let changed = false;
596
+ for (const dev of platformConfig.devices) {
597
+ const id = String(dev.deviceId || dev.id || "").trim().toLowerCase();
598
+ if (selectedIds.has(id)) {
599
+ if (dev.enabled !== enabled) {
600
+ dev.enabled = enabled;
601
+ changed = true;
602
+ }
603
+ }
604
+ }
605
+ if (changed) {
606
+ if (typeof homebridge.updatePluginConfig === "function") {
607
+ await homebridge.updatePluginConfig(configArr);
608
+ } else {
609
+ throw new TypeError("homebridge.updatePluginConfig is not available");
610
+ }
611
+ if (typeof homebridge.savePluginConfig === "function") {
612
+ await homebridge.savePluginConfig();
613
+ }
614
+ }
615
+ }
616
+ function normalizeId(value) {
617
+ return String(value ?? "").trim().toLowerCase();
618
+ }
619
+ function dedupeById(devices) {
620
+ return devices.filter((d, index, arr) => !!d?.id && arr.findIndex((x) => x?.id === d.id) === index);
621
+ }
622
+ function mergeDiscoveredDevices(existingDevices, incomingDevices) {
623
+ const deviceMap = /* @__PURE__ */ new Map();
624
+ for (const d of dedupeById(existingDevices)) {
625
+ deviceMap.set(d.id, { ...d });
626
+ }
627
+ for (const d of dedupeById(incomingDevices)) {
628
+ const current = deviceMap.get(d.id);
629
+ if (current) {
630
+ const nextConnectionType = current.connectionType === d.connectionType ? current.connectionType : "Both";
631
+ deviceMap.set(d.id, {
632
+ ...current,
633
+ ...d,
634
+ connectionType: nextConnectionType
635
+ });
636
+ } else {
637
+ deviceMap.set(d.id, { ...d });
638
+ }
639
+ }
640
+ return [...deviceMap.values()];
641
+ }
642
+ function setDiscoveryCache(devices) {
643
+ try {
644
+ const payload = { timestamp: Date.now(), devices };
645
+ localStorage.setItem(DISCOVERY_CACHE_KEY, JSON.stringify(payload));
646
+ } catch (_e) {
647
+ }
648
+ }
649
+ function clearDiscoveryCache() {
650
+ try {
651
+ localStorage.removeItem(DISCOVERY_CACHE_KEY);
652
+ } catch (_e) {
653
+ }
654
+ }
655
+ function getDiscoveryCache(validOnly = true) {
656
+ try {
657
+ const stored = localStorage.getItem(DISCOVERY_CACHE_KEY);
658
+ if (!stored) {
659
+ return null;
660
+ }
661
+ const payload = JSON.parse(stored);
662
+ if (!payload || !Array.isArray(payload.devices) || typeof payload.timestamp !== "number") {
663
+ return null;
664
+ }
665
+ const age = Date.now() - payload.timestamp;
666
+ if (validOnly && age > DISCOVERY_CACHE_TTL_MS) {
667
+ return null;
668
+ }
669
+ return payload;
670
+ } catch (_e) {
671
+ return null;
672
+ }
673
+ }
674
+ function getDiscoveryAutoRefreshSeconds() {
675
+ try {
676
+ const stored = localStorage.getItem(DISCOVERY_AUTO_REFRESH_KEY);
677
+ const value = Number(stored || 0);
678
+ return Number.isFinite(value) && value >= 0 ? value : 0;
679
+ } catch (_e) {
680
+ return 0;
681
+ }
682
+ }
683
+ function setDiscoveryAutoRefreshSeconds(value) {
684
+ try {
685
+ localStorage.setItem(DISCOVERY_AUTO_REFRESH_KEY, String(Math.max(0, value)));
686
+ } catch (_e) {
687
+ }
688
+ }
689
+ function getDiscoveryHideAddedPreference() {
690
+ try {
691
+ return localStorage.getItem(DISCOVERY_HIDE_ADDED_KEY) === "true";
692
+ } catch (_e) {
693
+ return false;
694
+ }
695
+ }
696
+ function setDiscoveryHideAddedPreference(value) {
697
+ try {
698
+ localStorage.setItem(DISCOVERY_HIDE_ADDED_KEY, String(value));
699
+ } catch (_e) {
700
+ }
701
+ }
702
+ function formatElapsedShort(ms) {
703
+ const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
704
+ if (totalSeconds < 60) {
705
+ return `${totalSeconds}s ago`;
706
+ }
707
+ const minutes = Math.floor(totalSeconds / 60);
708
+ if (minutes < 60) {
709
+ return `${minutes}m ago`;
710
+ }
711
+ const hours = Math.floor(minutes / 60);
712
+ if (hours < 24) {
713
+ return `${hours}h ago`;
714
+ }
715
+ const days = Math.floor(hours / 24);
716
+ return `${days}d ago`;
717
+ }
718
+ function updateLastScannedStatus() {
719
+ const lastScannedStatus = document.getElementById("lastScannedStatus");
720
+ if (!lastScannedStatus) {
721
+ return;
722
+ }
723
+ const cache = getDiscoveryCache(false);
724
+ if (!cache) {
725
+ lastScannedStatus.textContent = "Last scanned: never";
726
+ return;
727
+ }
728
+ const ageMs = Date.now() - cache.timestamp;
729
+ const timestampText = new Date(cache.timestamp).toLocaleString();
730
+ const stale = ageMs > DISCOVERY_CACHE_TTL_MS;
731
+ lastScannedStatus.textContent = stale ? `Last scanned: ${formatElapsedShort(ageMs)} (${timestampText}, cache expired)` : `Last scanned: ${formatElapsedShort(ageMs)} (${timestampText})`;
732
+ }
733
+ async function renderCachedDiscoveryResults() {
734
+ const cache = getDiscoveryCache(true);
735
+ const list = document.getElementById("discoveredList");
736
+ if (!cache || !list || !cache.devices.length) {
737
+ return;
738
+ }
739
+ list.style.display = "block";
740
+ if (!window._discoverySelectedIds) {
741
+ window._discoverySelectedIds = /* @__PURE__ */ new Set();
742
+ }
743
+ await updateDiscoveryView(
744
+ cache.devices,
745
+ getDiscoveryPreferences(),
746
+ getDiscoveryGroupByPreference(),
747
+ getDiscoveryHideAddedPreference(),
748
+ window._discoverySelectedIds
749
+ );
750
+ }
751
+ function getDiscoveryBleSettings() {
752
+ try {
753
+ const stored = localStorage.getItem(DISCOVERY_BLE_SETTINGS_KEY);
754
+ if (!stored) {
755
+ return { bleEnabled: true, bleScanDurationSeconds: 5, bleTimeoutSeconds: 8 };
756
+ }
757
+ const parsed = JSON.parse(stored);
758
+ return {
759
+ bleEnabled: parsed?.bleEnabled !== false,
760
+ bleScanDurationSeconds: Math.max(3, Math.min(15, Number(parsed?.bleScanDurationSeconds || 5))),
761
+ bleTimeoutSeconds: Math.max(3, Math.min(30, Number(parsed?.bleTimeoutSeconds || 8)))
762
+ };
763
+ } catch (_e) {
764
+ return { bleEnabled: true, bleScanDurationSeconds: 5, bleTimeoutSeconds: 8 };
765
+ }
766
+ }
767
+ function setDiscoveryBleSettings(settings) {
768
+ try {
769
+ localStorage.setItem(DISCOVERY_BLE_SETTINGS_KEY, JSON.stringify(settings));
770
+ } catch (_e) {
771
+ }
772
+ }
773
+ async function initializeDiscoverySettings() {
774
+ const scanSelect = document.getElementById("bleScanDurationSelect");
775
+ const timeoutInput = document.getElementById("bleTimeoutInput");
776
+ const disableBleCheckbox = document.getElementById("disableBleScanCheckbox");
777
+ const scanSetting = document.getElementById("bleScanSetting");
778
+ const timeoutSetting = document.getElementById("bleTimeoutSetting");
779
+ const bluetoothStatus = document.getElementById("bluetoothStatus");
780
+ const autoRefreshSelect = document.getElementById("autoRefreshIntervalSelect");
781
+ const refreshBtn = document.getElementById("refreshDiscoverBtn");
782
+ const current = getDiscoveryBleSettings();
783
+ if (scanSelect) {
784
+ scanSelect.value = String(current.bleScanDurationSeconds);
785
+ }
786
+ if (timeoutInput) {
787
+ timeoutInput.value = String(current.bleTimeoutSeconds);
788
+ }
789
+ if (disableBleCheckbox) {
790
+ disableBleCheckbox.checked = !current.bleEnabled;
791
+ }
792
+ if (autoRefreshSelect) {
793
+ autoRefreshSelect.value = String(getDiscoveryAutoRefreshSeconds());
794
+ }
795
+ const updateBleSettingVisibility = () => {
796
+ const disabled = !!disableBleCheckbox?.checked;
797
+ if (scanSetting) {
798
+ scanSetting.style.display = disabled ? "none" : "inline-flex";
799
+ }
800
+ if (timeoutSetting) {
801
+ timeoutSetting.style.display = disabled ? "none" : "inline-flex";
802
+ }
803
+ };
804
+ const persistFromControls = () => {
805
+ const next = {
806
+ bleEnabled: !(disableBleCheckbox?.checked ?? false),
807
+ bleScanDurationSeconds: Math.max(3, Math.min(15, Number(scanSelect?.value || 5))),
808
+ bleTimeoutSeconds: Math.max(3, Math.min(30, Number(timeoutInput?.value || 8)))
809
+ };
810
+ setDiscoveryBleSettings(next);
811
+ };
812
+ scanSelect?.addEventListener("change", persistFromControls);
813
+ timeoutInput?.addEventListener("change", persistFromControls);
814
+ disableBleCheckbox?.addEventListener("change", () => {
815
+ persistFromControls();
816
+ updateBleSettingVisibility();
817
+ });
818
+ updateBleSettingVisibility();
819
+ if (bluetoothStatus) {
820
+ const status = await fetchBluetoothStatus();
821
+ bluetoothStatus.textContent = status.available ? `Bluetooth: available (${status.message})` : `Bluetooth: unavailable (${status.message})`;
822
+ }
823
+ updateLastScannedStatus();
824
+ if (discoveryLastScannedTimer) {
825
+ clearInterval(discoveryLastScannedTimer);
826
+ }
827
+ discoveryLastScannedTimer = setInterval(updateLastScannedStatus, 15e3);
828
+ refreshBtn?.addEventListener("click", () => {
829
+ void discoverDevices2();
830
+ });
831
+ const applyAutoRefresh = () => {
832
+ const seconds = Math.max(0, Number(autoRefreshSelect?.value || 0));
833
+ setDiscoveryAutoRefreshSeconds(seconds);
834
+ if (discoveryAutoRefreshTimer) {
835
+ clearInterval(discoveryAutoRefreshTimer);
836
+ discoveryAutoRefreshTimer = null;
837
+ }
838
+ if (seconds > 0) {
839
+ discoveryAutoRefreshTimer = setInterval(() => {
840
+ const discoverBtn = document.getElementById("discoverBtn");
841
+ if (!discoverBtn || discoverBtn.disabled || document.hidden) {
842
+ return;
843
+ }
844
+ void discoverDevices2();
845
+ }, seconds * 1e3);
846
+ }
847
+ };
848
+ autoRefreshSelect?.addEventListener("change", applyAutoRefresh);
849
+ applyAutoRefresh();
850
+ await renderCachedDiscoveryResults();
851
+ }
852
+ function getDiscoveryGroupByPreference() {
853
+ try {
854
+ const stored = localStorage.getItem(DISCOVERY_GROUP_BY_KEY);
855
+ if (stored === "hub" || stored === "type") {
856
+ return stored;
857
+ }
858
+ return "connection";
859
+ } catch (_e) {
860
+ return "connection";
861
+ }
862
+ }
863
+ function setDiscoveryGroupByPreference(groupBy) {
864
+ try {
865
+ localStorage.setItem(DISCOVERY_GROUP_BY_KEY, groupBy);
866
+ } catch (_e) {
867
+ }
868
+ }
869
+ function getDiscoveryGroupExpandedState() {
870
+ try {
871
+ const stored = localStorage.getItem(DISCOVERY_GROUP_EXPANDED_KEY);
872
+ if (!stored) {
873
+ return {};
874
+ }
875
+ const parsed = JSON.parse(stored);
876
+ return typeof parsed === "object" && parsed ? parsed : {};
877
+ } catch (_e) {
878
+ return {};
879
+ }
880
+ }
881
+ function setDiscoveryGroupExpandedState(state) {
882
+ try {
883
+ localStorage.setItem(DISCOVERY_GROUP_EXPANDED_KEY, JSON.stringify(state));
884
+ } catch (_e) {
885
+ }
886
+ }
887
+ function isDiscoveryGroupExpanded(groupKey) {
888
+ const state = getDiscoveryGroupExpandedState();
889
+ return state[groupKey] !== false;
890
+ }
891
+ function setDiscoveryGroupExpanded(groupKey, expanded) {
892
+ const state = getDiscoveryGroupExpandedState();
893
+ state[groupKey] = expanded;
894
+ setDiscoveryGroupExpandedState(state);
895
+ }
896
+ async function discoverDevices2() {
897
+ const btn = document.getElementById("discoverBtn");
898
+ const cancelBtn = document.getElementById("cancelDiscoverBtn");
899
+ const status = document.getElementById("discoverStatus");
900
+ const phaseProgress = document.getElementById("discoverPhaseProgress");
901
+ const phaseFill = document.getElementById("discoverPhaseFill");
902
+ const phaseLabel = document.getElementById("discoverPhaseLabel");
903
+ const list = document.getElementById("discoveredList");
904
+ const autoAddAll = document.getElementById("autoAddAllCheckbox")?.checked;
905
+ const scanSelect = document.getElementById("bleScanDurationSelect");
906
+ const timeoutInput = document.getElementById("bleTimeoutInput");
907
+ const disableBleCheckbox = document.getElementById("disableBleScanCheckbox");
908
+ if (!btn || !status || !list) {
909
+ return;
910
+ }
911
+ const spinnerFrames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
912
+ let spinnerIndex = 0;
913
+ const startedAt = Date.now();
914
+ let phaseStartedAt = startedAt;
915
+ let phase = "Preparing discovery...";
916
+ let cancelled = false;
917
+ const bleScanDurationSeconds = scanSelect ? Number(scanSelect.value) : 5;
918
+ const pollIntervalMs = 1e3;
919
+ const scanEndTime = Date.now() + bleScanDurationSeconds * 1e3;
920
+ const setPhase = (nextPhase) => {
921
+ phase = nextPhase;
922
+ phaseStartedAt = Date.now();
923
+ };
924
+ const getPhasePercent = (phaseName) => {
925
+ if (phaseName.includes("Scanning BLE")) {
926
+ return 35;
927
+ }
928
+ if (phaseName.includes("Fetching OpenAPI")) {
929
+ return 75;
930
+ }
931
+ if (phaseName.includes("Complete")) {
932
+ return 100;
933
+ }
934
+ return 10;
935
+ };
936
+ const renderProgress = () => {
937
+ const totalSeconds = Math.max(0, Math.floor((Date.now() - startedAt) / 1e3));
938
+ const phaseSeconds = Math.max(0, Math.floor((Date.now() - phaseStartedAt) / 1e3));
939
+ const frame = spinnerFrames[spinnerIndex % spinnerFrames.length];
940
+ spinnerIndex += 1;
941
+ status.textContent = `${frame} ${phase} (${phaseSeconds}s, ${totalSeconds}s total)`;
942
+ if (phaseProgress) {
943
+ phaseProgress.style.display = "block";
944
+ }
945
+ if (phaseFill) {
946
+ phaseFill.style.width = `${getPhasePercent(phase)}%`;
947
+ }
948
+ if (phaseLabel) {
949
+ phaseLabel.textContent = phase;
950
+ }
951
+ };
952
+ const progressTimer = setInterval(renderProgress, 250);
953
+ let discoveredDevices = [];
954
+ const preferences = getDiscoveryPreferences();
955
+ let groupBy = getDiscoveryGroupByPreference();
956
+ let hideAdded = getDiscoveryHideAddedPreference();
957
+ let controlsInitialized = false;
958
+ if (!window._discoverySelectedIds) {
959
+ window._discoverySelectedIds = /* @__PURE__ */ new Set();
960
+ }
961
+ const selectedIds = window._discoverySelectedIds;
962
+ setPhase("Scanning BLE (real-time RSSI)");
963
+ let pollTimer = null;
964
+ const pollDiscovery = async () => {
965
+ if (cancelled || Date.now() > scanEndTime) {
966
+ if (pollTimer) {
967
+ clearInterval(pollTimer);
968
+ }
969
+ setPhase("Scan Complete");
970
+ try {
971
+ const devices = await discoverDevices("ble");
972
+ discoveredDevices = mergeDiscoveredDevices(discoveredDevices, devices);
973
+ setDiscoveryCache(discoveredDevices);
974
+ await updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds);
975
+ } catch (e) {
976
+ uiLog.error("Final BLE poll failed:", e);
977
+ }
978
+ return;
979
+ }
980
+ try {
981
+ const devices = await discoverDevices("ble");
982
+ discoveredDevices = mergeDiscoveredDevices(discoveredDevices, devices);
983
+ setDiscoveryCache(discoveredDevices);
984
+ await updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds);
985
+ } catch (e) {
986
+ uiLog.error("BLE poll failed:", e);
987
+ }
988
+ };
989
+ pollTimer = setInterval(pollDiscovery, pollIntervalMs);
990
+ pollDiscovery();
991
+ async function batchSetDeviceEnabled2(selectedIds2, enabled) {
992
+ const resp = await homebridge.request("/platform-config", {});
993
+ if (!resp || resp.success === false || !resp.data) {
994
+ throw new Error("Failed to load config");
995
+ }
996
+ const config = resp.data;
997
+ const configArr = Array.isArray(config) ? config : [config];
998
+ const platformIdx = configArr.findIndex((c) => (c.platform || c.name || "").toLowerCase().includes("switchbot"));
999
+ if (platformIdx === -1) {
1000
+ throw new Error("SwitchBot platform config not found");
1001
+ }
1002
+ const platformConfig = configArr[platformIdx];
1003
+ if (!Array.isArray(platformConfig.devices)) {
1004
+ throw new TypeError("No devices array in config");
1005
+ }
1006
+ let changed = false;
1007
+ for (const dev of platformConfig.devices) {
1008
+ const id = String(dev.deviceId || dev.id || "").trim().toLowerCase();
1009
+ if (selectedIds2.has(id)) {
1010
+ if (dev.enabled !== enabled) {
1011
+ dev.enabled = enabled;
1012
+ changed = true;
1013
+ }
1014
+ }
1015
+ }
1016
+ if (changed) {
1017
+ if (typeof homebridge.updatePluginConfig === "function") {
1018
+ await homebridge.updatePluginConfig(configArr);
1019
+ } else {
1020
+ throw new TypeError("homebridge.updatePluginConfig is not available");
1021
+ }
1022
+ if (typeof homebridge.savePluginConfig === "function") {
1023
+ await homebridge.savePluginConfig();
1024
+ }
1025
+ }
1026
+ }
1027
+ const ensureDiscoveryControls = async () => {
1028
+ if (controlsInitialized) {
1029
+ return;
1030
+ }
1031
+ if (!window._discoverySelectedIds) {
1032
+ window._discoverySelectedIds = /* @__PURE__ */ new Set();
1033
+ }
1034
+ const selectedIds2 = window._discoverySelectedIds;
1035
+ const controlsDiv = document.createElement("div");
1036
+ controlsDiv.style.cssText = "margin-bottom: 12px; display: flex; gap: 12px; flex-wrap: wrap; align-items: center;";
1037
+ const filterLabel = document.createElement("label");
1038
+ filterLabel.style.fontSize = "12px";
1039
+ filterLabel.style.fontWeight = "500";
1040
+ filterLabel.textContent = "Filter:";
1041
+ const filterGroup = document.createElement("div");
1042
+ filterGroup.style.display = "flex";
1043
+ filterGroup.style.gap = "4px";
1044
+ const filterOptions = [
1045
+ { label: "All", value: "all" },
1046
+ { label: "BLE", value: "ble" },
1047
+ { label: "API", value: "api" },
1048
+ { label: "Both", value: "both" },
1049
+ { label: "IR", value: "ir" }
1050
+ ];
1051
+ for (const option of filterOptions) {
1052
+ const filterBtn = document.createElement("button");
1053
+ filterBtn.textContent = option.label;
1054
+ filterBtn.style.padding = "4px 8px";
1055
+ filterBtn.style.fontSize = "11px";
1056
+ filterBtn.style.borderRadius = "3px";
1057
+ filterBtn.style.cursor = "pointer";
1058
+ filterBtn.style.border = preferences.connectionType === option.value ? "2px solid #007AFF" : "1px solid #ccc";
1059
+ filterBtn.style.backgroundColor = preferences.connectionType === option.value ? "#f0f7ff" : "#fff";
1060
+ filterBtn.style.color = preferences.connectionType === option.value ? "#1d4ed8" : "#374151";
1061
+ filterBtn.onclick = () => {
1062
+ preferences.connectionType = option.value;
1063
+ setDiscoveryPreferences(preferences);
1064
+ void updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds2);
1065
+ Array.prototype.forEach.call(filterGroup.querySelectorAll("button"), (b) => {
1066
+ b.style.border = "1px solid #ccc";
1067
+ b.style.backgroundColor = "#fff";
1068
+ b.style.color = "#374151";
1069
+ });
1070
+ filterBtn.style.border = "2px solid #007AFF";
1071
+ filterBtn.style.backgroundColor = "#f0f7ff";
1072
+ filterBtn.style.color = "#1d4ed8";
1073
+ };
1074
+ filterGroup.appendChild(filterBtn);
1075
+ }
1076
+ const sortLabel = document.createElement("label");
1077
+ sortLabel.style.fontSize = "12px";
1078
+ sortLabel.style.fontWeight = "500";
1079
+ sortLabel.style.marginLeft = "8px";
1080
+ sortLabel.textContent = "Sort:";
1081
+ const sortSelect = document.createElement("select");
1082
+ sortSelect.style.fontSize = "11px";
1083
+ sortSelect.style.padding = "4px 8px";
1084
+ sortSelect.style.borderRadius = "3px";
1085
+ sortSelect.value = preferences.sortBy;
1086
+ const sortOptions = [
1087
+ { label: "Name", value: "name" },
1088
+ { label: "Signal Strength", value: "signal" },
1089
+ { label: "Type", value: "type" },
1090
+ { label: "Connection", value: "connection" }
1091
+ ];
1092
+ for (const opt of sortOptions) {
1093
+ const sortOption = document.createElement("option");
1094
+ sortOption.value = opt.value;
1095
+ sortOption.textContent = opt.label;
1096
+ sortSelect.appendChild(sortOption);
1097
+ }
1098
+ sortSelect.onchange = () => {
1099
+ preferences.sortBy = sortSelect.value;
1100
+ setDiscoveryPreferences(preferences);
1101
+ void updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds2);
1102
+ };
1103
+ const groupLabel = document.createElement("label");
1104
+ groupLabel.style.fontSize = "12px";
1105
+ groupLabel.style.fontWeight = "500";
1106
+ groupLabel.style.marginLeft = "8px";
1107
+ groupLabel.textContent = "Group:";
1108
+ const groupSelect = document.createElement("select");
1109
+ groupSelect.style.fontSize = "11px";
1110
+ groupSelect.style.padding = "4px 8px";
1111
+ groupSelect.style.borderRadius = "3px";
1112
+ groupSelect.value = groupBy;
1113
+ const groupOptions = [
1114
+ { label: "Connection", value: "connection" },
1115
+ { label: "Hub", value: "hub" },
1116
+ { label: "Device Type", value: "type" }
1117
+ ];
1118
+ for (const opt of groupOptions) {
1119
+ const groupOption = document.createElement("option");
1120
+ groupOption.value = opt.value;
1121
+ groupOption.textContent = opt.label;
1122
+ groupSelect.appendChild(groupOption);
1123
+ }
1124
+ groupSelect.onchange = () => {
1125
+ groupBy = groupSelect.value;
1126
+ setDiscoveryGroupByPreference(groupBy);
1127
+ void updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds2);
1128
+ };
1129
+ const hideAddedLabel = document.createElement("label");
1130
+ hideAddedLabel.style.display = "inline-flex";
1131
+ hideAddedLabel.style.alignItems = "center";
1132
+ hideAddedLabel.style.gap = "4px";
1133
+ hideAddedLabel.style.fontSize = "11px";
1134
+ hideAddedLabel.style.marginLeft = "8px";
1135
+ const hideAddedCheckbox = document.createElement("input");
1136
+ hideAddedCheckbox.type = "checkbox";
1137
+ hideAddedCheckbox.checked = hideAdded;
1138
+ hideAddedCheckbox.style.margin = "0";
1139
+ hideAddedCheckbox.style.width = "auto";
1140
+ const hideAddedText = document.createElement("span");
1141
+ hideAddedText.textContent = "Hide Added";
1142
+ hideAddedLabel.appendChild(hideAddedCheckbox);
1143
+ hideAddedLabel.appendChild(hideAddedText);
1144
+ hideAddedCheckbox.onchange = () => {
1145
+ hideAdded = hideAddedCheckbox.checked;
1146
+ setDiscoveryHideAddedPreference(hideAdded);
1147
+ void updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds2);
1148
+ };
1149
+ const searchInput = document.createElement("input");
1150
+ searchInput.type = "text";
1151
+ searchInput.placeholder = "Search by name, ID, or type...";
1152
+ searchInput.style.fontSize = "13px";
1153
+ searchInput.style.padding = "8px 16px";
1154
+ searchInput.style.borderRadius = "6px";
1155
+ searchInput.style.border = "1px solid #ccc";
1156
+ searchInput.style.flex = "1 1 0%";
1157
+ searchInput.style.minWidth = "120px";
1158
+ searchInput.style.maxWidth = "100%";
1159
+ searchInput.style.width = "100%";
1160
+ searchInput.value = preferences.searchQuery;
1161
+ let searchTimeout;
1162
+ searchInput.oninput = () => {
1163
+ clearTimeout(searchTimeout);
1164
+ searchTimeout = setTimeout(() => {
1165
+ preferences.searchQuery = searchInput.value;
1166
+ setDiscoveryPreferences(preferences);
1167
+ void updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds2);
1168
+ }, 300);
1169
+ };
1170
+ const actionBtnStyle = {
1171
+ fontSize: "16px",
1172
+ padding: "10px 0",
1173
+ borderRadius: "10px",
1174
+ margin: "0 12px 0 0",
1175
+ width: "100%",
1176
+ maxWidth: "220px",
1177
+ fontWeight: "bold",
1178
+ background: "#ef4444",
1179
+ color: "#fff",
1180
+ border: "none",
1181
+ cursor: "pointer",
1182
+ boxShadow: "0 2px 8px #0001",
1183
+ transition: "background 0.2s",
1184
+ outline: "none",
1185
+ display: "block"
1186
+ };
1187
+ const addSelectedBtn = document.createElement("button");
1188
+ addSelectedBtn.textContent = "Add Selected to Config";
1189
+ Object.assign(addSelectedBtn.style, actionBtnStyle);
1190
+ addSelectedBtn.disabled = true;
1191
+ addSelectedBtn.onclick = async () => {
1192
+ if (!selectedIds2.size) {
1193
+ return;
1194
+ }
1195
+ addSelectedBtn.disabled = true;
1196
+ addSelectedBtn.textContent = "Adding...";
1197
+ try {
1198
+ showBusyUi();
1199
+ const selectedDevices = discoveredDevices.filter((d) => selectedIds2.has(normalizeId(d.id)));
1200
+ const bulkResult = await addDevicesInBulk(selectedDevices.map((d) => ({
1201
+ deviceId: d.id,
1202
+ name: d.name,
1203
+ type: d.type,
1204
+ rssi: d.rssi,
1205
+ address: d.address,
1206
+ model: d.model
1207
+ })));
1208
+ uiLog.info("Batch add response:", bulkResult);
1209
+ if (!bulkResult || bulkResult.success === false) {
1210
+ throw new Error(bulkResult?.data?.message || "Batch add failed");
1211
+ }
1212
+ const addedCount = bulkResult?.addedCount ?? bulkResult?.data?.addedCount ?? 0;
1213
+ const skippedCount = bulkResult?.skippedCount ?? bulkResult?.data?.skippedCount ?? 0;
1214
+ toastSuccess(`Added ${addedCount} device(s)${skippedCount > 0 ? ` (${skippedCount} skipped)` : ""}`);
1215
+ await loadConfiguredDevices();
1216
+ selectedIds2.clear();
1217
+ addSelectedBtn.disabled = true;
1218
+ addSelectedBtn.textContent = "Add Selected";
1219
+ await updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds2);
1220
+ } catch (e) {
1221
+ uiLog.error("Batch add error:", e);
1222
+ toastError(e instanceof Error ? e.message : "Failed to add devices");
1223
+ addSelectedBtn.disabled = false;
1224
+ addSelectedBtn.textContent = "Add Selected";
1225
+ } finally {
1226
+ hideBusyUi();
1227
+ }
1228
+ };
1229
+ const enableSelectedBtn = document.createElement("button");
1230
+ enableSelectedBtn.textContent = "Enable Selected";
1231
+ Object.assign(enableSelectedBtn.style, actionBtnStyle);
1232
+ enableSelectedBtn.disabled = true;
1233
+ enableSelectedBtn.onclick = async () => {
1234
+ if (!selectedIds2.size) {
1235
+ return;
1236
+ }
1237
+ enableSelectedBtn.disabled = true;
1238
+ enableSelectedBtn.textContent = "Enabling...";
1239
+ try {
1240
+ showBusyUi();
1241
+ await batchSetDeviceEnabled2(selectedIds2, true);
1242
+ toastSuccess("Selected devices enabled");
1243
+ await loadConfiguredDevices();
1244
+ enableSelectedBtn.disabled = true;
1245
+ enableSelectedBtn.textContent = "Enable Selected";
1246
+ await updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds2);
1247
+ } catch (e) {
1248
+ uiLog.error("Batch enable error:", e);
1249
+ toastError(e instanceof Error ? e.message : "Failed to enable devices");
1250
+ enableSelectedBtn.disabled = false;
1251
+ enableSelectedBtn.textContent = "Enable Selected";
1252
+ } finally {
1253
+ hideBusyUi();
1254
+ }
1255
+ };
1256
+ const disableSelectedBtn = document.createElement("button");
1257
+ disableSelectedBtn.textContent = "Disable Selected";
1258
+ Object.assign(disableSelectedBtn.style, actionBtnStyle);
1259
+ disableSelectedBtn.disabled = true;
1260
+ disableSelectedBtn.onclick = async () => {
1261
+ if (!selectedIds2.size) {
1262
+ return;
1263
+ }
1264
+ disableSelectedBtn.disabled = true;
1265
+ disableSelectedBtn.textContent = "Disabling...";
1266
+ try {
1267
+ showBusyUi();
1268
+ await batchSetDeviceEnabled2(selectedIds2, false);
1269
+ toastSuccess("Selected devices disabled");
1270
+ await loadConfiguredDevices();
1271
+ disableSelectedBtn.disabled = true;
1272
+ disableSelectedBtn.textContent = "Disable Selected";
1273
+ await updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds2);
1274
+ } catch (e) {
1275
+ uiLog.error("Batch disable error:", e);
1276
+ toastError(e instanceof Error ? e.message : "Failed to disable devices");
1277
+ disableSelectedBtn.disabled = false;
1278
+ disableSelectedBtn.textContent = "Disable Selected";
1279
+ } finally {
1280
+ hideBusyUi();
1281
+ }
1282
+ };
1283
+ controlsDiv.appendChild(filterLabel);
1284
+ controlsDiv.appendChild(filterGroup);
1285
+ controlsDiv.appendChild(sortLabel);
1286
+ controlsDiv.appendChild(sortSelect);
1287
+ controlsDiv.appendChild(groupLabel);
1288
+ controlsDiv.appendChild(groupSelect);
1289
+ controlsDiv.appendChild(hideAddedLabel);
1290
+ controlsDiv.appendChild(searchInput);
1291
+ const topActionRow = document.createElement("div");
1292
+ topActionRow.style.display = "flex";
1293
+ topActionRow.style.gap = "20px";
1294
+ topActionRow.style.margin = "18px 0 10px 0";
1295
+ topActionRow.style.justifyContent = "flex-start";
1296
+ topActionRow.appendChild(addSelectedBtn);
1297
+ topActionRow.appendChild(enableSelectedBtn);
1298
+ topActionRow.appendChild(disableSelectedBtn);
1299
+ list.insertBefore(topActionRow, controlsDiv);
1300
+ list.innerHTML = "";
1301
+ list.appendChild(controlsDiv);
1302
+ let deviceListContainer = document.getElementById("discoveredDevices");
1303
+ if (!deviceListContainer) {
1304
+ deviceListContainer = document.createElement("ul");
1305
+ deviceListContainer.id = "discoveredDevices";
1306
+ deviceListContainer.style.maxHeight = "400px";
1307
+ deviceListContainer.style.overflowY = "auto";
1308
+ deviceListContainer.style.marginTop = "12px";
1309
+ deviceListContainer.style.padding = "0";
1310
+ deviceListContainer.style.listStyle = "none";
1311
+ list.appendChild(deviceListContainer);
1312
+ }
1313
+ list.style.display = "block";
1314
+ controlsInitialized = true;
1315
+ const updateActionButtons = () => {
1316
+ const hasSelection = selectedIds2.size > 0;
1317
+ addSelectedBtn.disabled = !hasSelection;
1318
+ enableSelectedBtn.disabled = !hasSelection;
1319
+ disableSelectedBtn.disabled = !hasSelection;
1320
+ };
1321
+ setInterval(updateActionButtons, 300);
1322
+ };
1323
+ try {
1324
+ const bleSettings = {
1325
+ bleEnabled: !(disableBleCheckbox?.checked ?? false),
1326
+ bleScanDurationSeconds: Math.max(3, Math.min(15, Number(scanSelect?.value || 5))),
1327
+ bleTimeoutSeconds: Math.max(3, Math.min(30, Number(timeoutInput?.value || 8)))
1328
+ };
1329
+ setDiscoveryBleSettings(bleSettings);
1330
+ showBusyUi();
1331
+ btn.disabled = true;
1332
+ btn.textContent = "\u{1F50D} Discovering...";
1333
+ setPhase(bleSettings.bleEnabled ? "Scanning BLE..." : "Skipping BLE scan...");
1334
+ renderProgress();
1335
+ status.classList.remove("error");
1336
+ const devicesFoundDisplay = document.getElementById("discoverDevicesFound");
1337
+ if (devicesFoundDisplay) {
1338
+ devicesFoundDisplay.style.display = "none";
1339
+ devicesFoundDisplay.classList.remove("discovery-scanning-pulse");
1340
+ }
1341
+ if (cancelBtn) {
1342
+ cancelBtn.style.display = "inline-block";
1343
+ cancelBtn.disabled = false;
1344
+ cancelBtn.onclick = () => {
1345
+ cancelled = true;
1346
+ const totalSeconds = Math.max(0, Math.floor((Date.now() - startedAt) / 1e3));
1347
+ status.textContent = `Discovery cancelled (${totalSeconds}s total)`;
1348
+ if (phaseProgress) {
1349
+ phaseProgress.style.display = "none";
1350
+ }
1351
+ if (devicesFoundDisplay) {
1352
+ devicesFoundDisplay.style.display = "none";
1353
+ devicesFoundDisplay.classList.remove("discovery-scanning-pulse");
1354
+ }
1355
+ cancelBtn.style.display = "none";
1356
+ btn.disabled = false;
1357
+ btn.textContent = "\u{1F50D} Discover Devices";
1358
+ hideBusyUi();
1359
+ };
1360
+ }
1361
+ if (bleSettings.bleEnabled) {
1362
+ const bleDevicesRaw = await discoverDevices("ble", bleSettings);
1363
+ if (cancelled) {
1364
+ return;
1365
+ }
1366
+ discoveredDevices = dedupeById(bleDevicesRaw);
1367
+ uiLog.info("BLE discover response:", bleDevicesRaw);
1368
+ if (devicesFoundDisplay && bleDevicesRaw.length > 0) {
1369
+ devicesFoundDisplay.style.display = "inline";
1370
+ devicesFoundDisplay.classList.add("discovery-scanning-pulse");
1371
+ devicesFoundDisplay.textContent = `\u{1F4CA} ${bleDevicesRaw.length} device(s) found (scanning...)`;
1372
+ }
1373
+ } else {
1374
+ discoveredDevices = [];
1375
+ uiLog.info("BLE discovery skipped by user setting");
1376
+ }
1377
+ if (!autoAddAll && discoveredDevices.length > 0) {
1378
+ await ensureDiscoveryControls();
1379
+ await updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds);
1380
+ status.textContent = `Showing ${discoveredDevices.length} device(s) from BLE, fetching OpenAPI...`;
1381
+ }
1382
+ setPhase("Fetching OpenAPI...");
1383
+ renderProgress();
1384
+ try {
1385
+ const openApiDevicesRaw = await discoverDevices("openapi");
1386
+ if (cancelled) {
1387
+ return;
1388
+ }
1389
+ uiLog.info("OpenAPI discover response:", openApiDevicesRaw);
1390
+ discoveredDevices = mergeDiscoveredDevices(discoveredDevices, openApiDevicesRaw);
1391
+ if (devicesFoundDisplay && discoveredDevices.length > 0) {
1392
+ devicesFoundDisplay.textContent = `\u{1F4CA} ${discoveredDevices.length} device(s) found (complete)`;
1393
+ }
1394
+ } catch (openApiError) {
1395
+ uiLog.warn("OpenAPI phase failed during discovery:", openApiError);
1396
+ if (!discoveredDevices.length) {
1397
+ throw openApiError;
1398
+ }
1399
+ if (devicesFoundDisplay) {
1400
+ devicesFoundDisplay.classList.remove("discovery-scanning-pulse");
1401
+ }
1402
+ }
1403
+ setPhase("Complete");
1404
+ renderProgress();
1405
+ uiLog.info("Final merged discover response:", discoveredDevices);
1406
+ if (!discoveredDevices.length) {
1407
+ status.textContent = "No devices found in your SwitchBot account";
1408
+ toastInfo("No devices found in your SwitchBot account");
1409
+ list.style.display = "none";
1410
+ if (devicesFoundDisplay) {
1411
+ devicesFoundDisplay.style.display = "none";
1412
+ devicesFoundDisplay.classList.remove("discovery-scanning-pulse");
1413
+ }
1414
+ clearDiscoveryCache();
1415
+ updateLastScannedStatus();
1416
+ return;
1417
+ }
1418
+ if (autoAddAll) {
1419
+ status.textContent = `Auto-adding ${discoveredDevices.length} device(s)...`;
1420
+ try {
1421
+ const bulkResult = await addDevicesInBulk(
1422
+ discoveredDevices.map((d) => ({
1423
+ deviceId: d.id,
1424
+ name: d.name,
1425
+ type: d.type,
1426
+ rssi: d.rssi,
1427
+ address: d.address,
1428
+ model: d.model
1429
+ }))
1430
+ );
1431
+ uiLog.info("Bulk add response:", bulkResult);
1432
+ if (!bulkResult || bulkResult.success === false) {
1433
+ throw new Error(bulkResult?.data?.message || "Bulk add failed");
1434
+ }
1435
+ const addedCount = bulkResult?.addedCount ?? bulkResult?.data?.addedCount ?? 0;
1436
+ const skippedCount = bulkResult?.skippedCount ?? bulkResult?.data?.skippedCount ?? 0;
1437
+ status.textContent = `\u2713 Added ${addedCount} device(s)${skippedCount > 0 ? ` (${skippedCount} skipped)` : ""}`;
1438
+ if (addedCount > 0) {
1439
+ toastSuccess(`Added ${addedCount} device(s)${skippedCount > 0 ? ` (${skippedCount} skipped)` : ""}`);
1440
+ } else if (skippedCount > 0) {
1441
+ toastWarning(`No new devices were added (${skippedCount} skipped)`);
1442
+ }
1443
+ status.classList.remove("error");
1444
+ list.style.display = "none";
1445
+ if (discoveredDevices.length > 0) {
1446
+ const synced = await syncParentPluginConfigFromDisk(true);
1447
+ status.textContent += synced ? " - Config saved automatically." : " - Warning: config may not persist until you close/reopen settings.";
1448
+ if (synced) {
1449
+ toastSuccess("Configuration synced and saved automatically");
1450
+ } else {
1451
+ toastWarning("Configuration sync failed; close and reopen settings before Save");
1452
+ }
1453
+ }
1454
+ } catch (e) {
1455
+ uiLog.error("Bulk add error:", e);
1456
+ status.textContent = `\u2717 Error: ${e instanceof Error ? e.message : "Failed to add devices"}`;
1457
+ status.classList.add("error");
1458
+ toastError(e instanceof Error ? e.message : "Failed to add devices");
1459
+ }
1460
+ await loadConfiguredDevices();
1461
+ return;
1462
+ }
1463
+ await ensureDiscoveryControls();
1464
+ await updateDiscoveryView(discoveredDevices, preferences, groupBy, hideAdded, selectedIds);
1465
+ setDiscoveryCache(discoveredDevices);
1466
+ updateLastScannedStatus();
1467
+ } catch (e) {
1468
+ if (cancelled) {
1469
+ return;
1470
+ }
1471
+ uiLog.error("Discovery error:", e);
1472
+ status.textContent = `Error: ${e instanceof Error ? e.message : "Discovery failed"}`;
1473
+ status.classList.add("error");
1474
+ toastError(e instanceof Error ? e.message : "Discovery failed");
1475
+ list.style.display = "none";
1476
+ } finally {
1477
+ clearInterval(progressTimer);
1478
+ hideBusyUi();
1479
+ if (phaseProgress) {
1480
+ phaseProgress.style.display = "none";
1481
+ }
1482
+ if (phaseFill) {
1483
+ phaseFill.style.width = "0%";
1484
+ }
1485
+ if (phaseLabel) {
1486
+ phaseLabel.textContent = "";
1487
+ }
1488
+ const devicesFoundDisplay = document.getElementById("discoverDevicesFound");
1489
+ if (devicesFoundDisplay) {
1490
+ devicesFoundDisplay.style.display = "none";
1491
+ devicesFoundDisplay.classList.remove("discovery-scanning-pulse");
1492
+ }
1493
+ btn.disabled = false;
1494
+ btn.textContent = "\u{1F50D} Discover Devices";
1495
+ if (cancelBtn) {
1496
+ cancelBtn.disabled = false;
1497
+ cancelBtn.style.display = "none";
1498
+ cancelBtn.onclick = null;
1499
+ }
1500
+ }
1501
+ }
1502
+ async function updateDiscoveryView(allDevices, preferences, groupBy, hideAdded, selectedIds) {
1503
+ const visibleDevices = allDevices.filter((d) => {
1504
+ if (hideAdded && d.added) {
1505
+ return false;
1506
+ }
1507
+ return true;
1508
+ });
1509
+ const configuredIds = new Set(
1510
+ allDevices.filter((d) => d.added).map((d) => normalizeId(d.id))
1511
+ );
1512
+ const getConnectionGroup = (device) => {
1513
+ if (device?.isIR) {
1514
+ return "IR";
1515
+ }
1516
+ const connectionType = String(device?.connectionType || "").toLowerCase();
1517
+ if (connectionType.includes("both")) {
1518
+ return "Both";
1519
+ }
1520
+ if (connectionType.includes("ble")) {
1521
+ return "BLE";
1522
+ }
1523
+ if (connectionType.includes("api")) {
1524
+ return "OpenAPI";
1525
+ }
1526
+ return "Unknown";
1527
+ };
1528
+ const getHubGroup = (device) => {
1529
+ const hub = String(device?.hubDeviceId || "").trim();
1530
+ return hub ? `Hub ${hub}` : "No Hub";
1531
+ };
1532
+ const getTypeGroup = (device) => {
1533
+ const type = String(device?.type || "").trim();
1534
+ return type || "Unknown Type";
1535
+ };
1536
+ const groupedDevices = /* @__PURE__ */ new Map();
1537
+ for (const d of visibleDevices) {
1538
+ let group = getConnectionGroup(d);
1539
+ if (groupBy === "hub") {
1540
+ group = getHubGroup(d);
1541
+ } else if (groupBy === "type") {
1542
+ group = getTypeGroup(d);
1543
+ }
1544
+ const groupDevices = groupedDevices.get(group) || [];
1545
+ groupDevices.push(d);
1546
+ groupedDevices.set(group, groupDevices);
1547
+ }
1548
+ let orderedGroups = [];
1549
+ if (groupBy === "hub") {
1550
+ const hubGroups = [...groupedDevices.keys()].filter((group) => group !== "No Hub").sort((a, b) => a.localeCompare(b));
1551
+ orderedGroups = groupedDevices.has("No Hub") ? [...hubGroups, "No Hub"] : hubGroups;
1552
+ } else if (groupBy === "type") {
1553
+ const typeGroups = [...groupedDevices.keys()].filter((group) => group !== "Unknown Type").sort((a, b) => a.localeCompare(b));
1554
+ orderedGroups = groupedDevices.has("Unknown Type") ? [...typeGroups, "Unknown Type"] : typeGroups;
1555
+ } else {
1556
+ const groupOrder = ["Both", "BLE", "OpenAPI", "IR", "Unknown"];
1557
+ orderedGroups = groupOrder.filter((group) => groupedDevices.has(group));
1558
+ }
1559
+ const container = document.createElement("div");
1560
+ container.id = "discoveredDevices";
1561
+ container.className = "discovery-groups";
1562
+ if (!visibleDevices.length) {
1563
+ const empty = document.createElement("div");
1564
+ empty.className = "discovery-group-empty";
1565
+ empty.textContent = hideAdded ? "No devices match current filters (or all are already added)." : "No devices match current filters.";
1566
+ container.appendChild(empty);
1567
+ } else {
1568
+ for (const groupName of orderedGroups) {
1569
+ const groupItems = groupedDevices.get(groupName);
1570
+ if (!groupItems?.length) {
1571
+ continue;
1572
+ }
1573
+ const groupSection = document.createElement("section");
1574
+ groupSection.className = "discovery-group";
1575
+ const groupStorageKey = `${groupBy}:${groupName}`;
1576
+ let expanded = isDiscoveryGroupExpanded(groupStorageKey);
1577
+ const groupHeader = document.createElement("button");
1578
+ groupHeader.className = "discovery-group-header-btn";
1579
+ groupHeader.type = "button";
1580
+ const setGroupHeaderText = () => {
1581
+ const marker = expanded ? "\u25BE" : "\u25B8";
1582
+ groupHeader.textContent = `${marker} ${groupName} (${groupItems.length})`;
1583
+ };
1584
+ setGroupHeaderText();
1585
+ groupSection.appendChild(groupHeader);
1586
+ const groupList = await renderDiscoveredDevices(groupItems, {
1587
+ configuredIds,
1588
+ selectedIds,
1589
+ onToggleSelect: (device, selected) => {
1590
+ const id = normalizeId(device.id);
1591
+ if (selected) {
1592
+ selectedIds.add(id);
1593
+ } else {
1594
+ selectedIds.delete(id);
1595
+ }
1596
+ const btn = document.querySelector("button")?.parentElement?.querySelector("button");
1597
+ if (btn && btn.textContent?.includes("Add Selected")) {
1598
+ btn.disabled = selectedIds.size === 0;
1599
+ }
1600
+ }
1601
+ });
1602
+ if (!expanded) {
1603
+ groupList.style.display = "none";
1604
+ }
1605
+ groupHeader.onclick = () => {
1606
+ expanded = !expanded;
1607
+ setDiscoveryGroupExpanded(groupStorageKey, expanded);
1608
+ setGroupHeaderText();
1609
+ groupList.style.display = expanded ? "grid" : "none";
1610
+ };
1611
+ groupSection.appendChild(groupList);
1612
+ container.appendChild(groupSection);
1613
+ }
1614
+ }
1615
+ const existingList = document.getElementById("discoveredDevices");
1616
+ if (existingList && existingList.parentNode) {
1617
+ existingList.replaceWith(container);
1618
+ } else {
1619
+ const listContainer = document.getElementById("discoveredList");
1620
+ if (listContainer) {
1621
+ listContainer.appendChild(container);
1622
+ }
1623
+ }
1624
+ const legacyAddSelectedBtns = [];
1625
+ const allBtns = document.querySelectorAll("button");
1626
+ for (let i = 0; i < allBtns.length; i++) {
1627
+ const btn = allBtns[i];
1628
+ if (btn.textContent && btn.textContent.trim() === "Add Selected") {
1629
+ legacyAddSelectedBtns.push(btn);
1630
+ }
1631
+ }
1632
+ for (let i = 0; i < legacyAddSelectedBtns.length; i++) {
1633
+ legacyAddSelectedBtns[i].remove();
1634
+ }
1635
+ function updateBatchButtonStates() {
1636
+ const addSelectedBtn = document.getElementById("addSelectedBtn");
1637
+ const enableSelectedBtn = document.querySelector("button")?.parentElement?.querySelector('button[aria-label="Enable Selected"]');
1638
+ const disableSelectedBtn = document.querySelector("button")?.parentElement?.querySelector('button[aria-label="Disable Selected"]');
1639
+ const hasSelection = selectedIds.size > 0;
1640
+ if (addSelectedBtn) addSelectedBtn.disabled = !hasSelection;
1641
+ if (enableSelectedBtn) enableSelectedBtn.disabled = !hasSelection;
1642
+ if (disableSelectedBtn) disableSelectedBtn.disabled = !hasSelection;
1643
+ }
1644
+ window.removeEventListener("discovery-selection-changed", updateBatchButtonStates);
1645
+ window.addEventListener("discovery-selection-changed", updateBatchButtonStates);
1646
+ let batchControls = document.getElementById("batchImportControls");
1647
+ if (!batchControls) {
1648
+ batchControls = document.createElement("div");
1649
+ batchControls.id = "batchImportControls";
1650
+ batchControls.style.display = "flex";
1651
+ batchControls.style.flexWrap = "wrap";
1652
+ batchControls.style.alignItems = "center";
1653
+ batchControls.style.gap = "18px";
1654
+ batchControls.style.margin = "8px 0 18px 0";
1655
+ batchControls.style.width = "100%";
1656
+ const buttonRow = document.createElement("div");
1657
+ buttonRow.style.display = "flex";
1658
+ buttonRow.style.flexWrap = "nowrap";
1659
+ buttonRow.style.alignItems = "center";
1660
+ buttonRow.style.justifyContent = "center";
1661
+ buttonRow.style.gap = "16px";
1662
+ buttonRow.style.width = "100%";
1663
+ const addSelectedBtn = document.createElement("button");
1664
+ addSelectedBtn.id = "addSelectedBtn";
1665
+ addSelectedBtn.textContent = "Add Selected to Config";
1666
+ addSelectedBtn.disabled = selectedIds.size === 0;
1667
+ addSelectedBtn.style.fontWeight = "600";
1668
+ const redButtonStyle = {
1669
+ width: "220px",
1670
+ padding: "12px 0",
1671
+ fontSize: "17px",
1672
+ marginBottom: "0",
1673
+ boxShadow: "0 2px 8px 0 rgba(220,38,38,0.10)",
1674
+ borderRadius: "8px"
1675
+ };
1676
+ Object.assign(addSelectedBtn.style, redButtonStyle);
1677
+ addSelectedBtn.style.background = "#ef4444";
1678
+ addSelectedBtn.style.color = "white";
1679
+ addSelectedBtn.style.transition = "background 0.2s, box-shadow 0.2s";
1680
+ addSelectedBtn.onmouseenter = function() {
1681
+ addSelectedBtn.style.background = "#dc2626";
1682
+ };
1683
+ addSelectedBtn.onmouseleave = function() {
1684
+ addSelectedBtn.style.background = "#ef4444";
1685
+ };
1686
+ addSelectedBtn.onclick = async () => {
1687
+ if (!selectedIds.size) {
1688
+ return;
1689
+ }
1690
+ addSelectedBtn.disabled = true;
1691
+ addSelectedBtn.textContent = "Adding...";
1692
+ try {
1693
+ const selectedDevices = visibleDevices.filter((d) => selectedIds.has(normalizeId(d.id)));
1694
+ const bulkResult = await addDevicesInBulk(selectedDevices.map((d) => ({
1695
+ deviceId: d.id,
1696
+ name: d.name,
1697
+ type: d.type,
1698
+ rssi: d.rssi,
1699
+ address: d.address,
1700
+ model: d.model
1701
+ })));
1702
+ uiLog.info("Batch add response:", bulkResult);
1703
+ if (!bulkResult || bulkResult.success === false) {
1704
+ throw new Error(bulkResult?.data?.message || "Batch add failed");
1705
+ }
1706
+ const addedCount = bulkResult?.addedCount ?? bulkResult?.data?.addedCount ?? 0;
1707
+ const skippedCount = bulkResult?.skippedCount ?? bulkResult?.data?.skippedCount ?? 0;
1708
+ toastSuccess(`Added ${addedCount} device(s)${skippedCount > 0 ? ` (${skippedCount} skipped)` : ""}`);
1709
+ await loadConfiguredDevices();
1710
+ selectedIds.clear();
1711
+ addSelectedBtn.disabled = true;
1712
+ addSelectedBtn.textContent = "Add Selected to Config";
1713
+ await updateDiscoveryView(allDevices, preferences, groupBy, hideAdded, selectedIds);
1714
+ } catch (e) {
1715
+ uiLog.error("Batch add error:", e);
1716
+ toastError(e instanceof Error ? e.message : "Failed to add devices");
1717
+ addSelectedBtn.disabled = false;
1718
+ addSelectedBtn.textContent = "Add Selected to Config";
1719
+ }
1720
+ };
1721
+ const enableSelectedBtn = document.createElement("button");
1722
+ enableSelectedBtn.textContent = "Enable Selected";
1723
+ enableSelectedBtn.style.fontWeight = "600";
1724
+ Object.assign(enableSelectedBtn.style, redButtonStyle);
1725
+ enableSelectedBtn.style.background = "#ef4444";
1726
+ enableSelectedBtn.style.color = "white";
1727
+ enableSelectedBtn.style.transition = "background 0.2s, box-shadow 0.2s";
1728
+ enableSelectedBtn.onmouseenter = function() {
1729
+ enableSelectedBtn.style.background = "#dc2626";
1730
+ };
1731
+ enableSelectedBtn.onmouseleave = function() {
1732
+ enableSelectedBtn.style.background = "#ef4444";
1733
+ };
1734
+ enableSelectedBtn.disabled = selectedIds.size === 0;
1735
+ enableSelectedBtn.onclick = async () => {
1736
+ if (!selectedIds.size) {
1737
+ return;
1738
+ }
1739
+ enableSelectedBtn.disabled = true;
1740
+ try {
1741
+ showBusyUi();
1742
+ await batchSetDeviceEnabled(selectedIds, true);
1743
+ toastSuccess("Selected devices enabled");
1744
+ await loadConfiguredDevices();
1745
+ await updateDiscoveryView(allDevices, preferences, groupBy, hideAdded, selectedIds);
1746
+ } catch (e) {
1747
+ uiLog.error("Batch enable error:", e);
1748
+ toastError(e instanceof Error ? e.message : "Failed to enable devices");
1749
+ } finally {
1750
+ hideBusyUi();
1751
+ enableSelectedBtn.disabled = false;
1752
+ }
1753
+ };
1754
+ const disableSelectedBtn = document.createElement("button");
1755
+ disableSelectedBtn.textContent = "Disable Selected";
1756
+ disableSelectedBtn.style.fontWeight = "600";
1757
+ Object.assign(disableSelectedBtn.style, redButtonStyle);
1758
+ disableSelectedBtn.style.background = "#ef4444";
1759
+ disableSelectedBtn.style.color = "white";
1760
+ disableSelectedBtn.style.transition = "background 0.2s, box-shadow 0.2s";
1761
+ disableSelectedBtn.onmouseenter = function() {
1762
+ disableSelectedBtn.style.background = "#dc2626";
1763
+ };
1764
+ disableSelectedBtn.onmouseleave = function() {
1765
+ disableSelectedBtn.style.background = "#ef4444";
1766
+ };
1767
+ disableSelectedBtn.disabled = selectedIds.size === 0;
1768
+ disableSelectedBtn.onclick = async () => {
1769
+ if (!selectedIds.size) {
1770
+ return;
1771
+ }
1772
+ disableSelectedBtn.disabled = true;
1773
+ try {
1774
+ showBusyUi();
1775
+ await batchSetDeviceEnabled(selectedIds, false);
1776
+ toastSuccess("Selected devices disabled");
1777
+ await loadConfiguredDevices();
1778
+ await updateDiscoveryView(allDevices, preferences, groupBy, hideAdded, selectedIds);
1779
+ } catch (e) {
1780
+ uiLog.error("Batch disable error:", e);
1781
+ toastError(e instanceof Error ? e.message : "Failed to disable devices");
1782
+ } finally {
1783
+ hideBusyUi();
1784
+ disableSelectedBtn.disabled = false;
1785
+ }
1786
+ };
1787
+ buttonRow.appendChild(addSelectedBtn);
1788
+ buttonRow.appendChild(enableSelectedBtn);
1789
+ buttonRow.appendChild(disableSelectedBtn);
1790
+ batchControls.appendChild(buttonRow);
1791
+ const listContainer = document.getElementById("discoveredList");
1792
+ if (listContainer) {
1793
+ listContainer.insertBefore(batchControls, listContainer.firstChild);
1794
+ }
1795
+ } else {
1796
+ const addSelectedBtn = document.getElementById("addSelectedBtn");
1797
+ if (addSelectedBtn) {
1798
+ addSelectedBtn.disabled = selectedIds.size === 0;
1799
+ }
1800
+ }
1801
+ const status = document.getElementById("discoverStatus");
1802
+ if (status) {
1803
+ const totalCount = allDevices.length;
1804
+ const filteredCount = visibleDevices.length;
1805
+ status.textContent = filteredCount === totalCount ? `Found ${totalCount} device(s)` : `Showing ${filteredCount} of ${totalCount} device(s)`;
1806
+ }
1807
+ }
1808
+ async function addDeviceToConfig(device) {
1809
+ const { addDeviceToConfig: addDevice2 } = await Promise.resolve().then(() => (init_devices(), devices_exports));
1810
+ await addDevice2(device);
1811
+ }
1812
+ 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;
1813
+ var init_discovery = __esm({
1814
+ "src/homebridge-ui/public/js/discovery.ts"() {
1815
+ "use strict";
1816
+ init_api();
1817
+ init_devices();
1818
+ init_logger();
1819
+ init_modal();
1820
+ init_render();
1821
+ init_toast();
1822
+ DISCOVERY_GROUP_BY_KEY = "discoveryGroupBy";
1823
+ DISCOVERY_GROUP_EXPANDED_KEY = "discoveryGroupExpanded";
1824
+ DISCOVERY_BLE_SETTINGS_KEY = "discoveryBleSettings";
1825
+ DISCOVERY_HIDE_ADDED_KEY = "discoveryHideAdded";
1826
+ DISCOVERY_CACHE_KEY = "discoveryCache";
1827
+ DISCOVERY_AUTO_REFRESH_KEY = "discoveryAutoRefreshSeconds";
1828
+ DISCOVERY_CACHE_TTL_MS = 5 * 60 * 1e3;
1829
+ discoveryAutoRefreshTimer = null;
1830
+ discoveryLastScannedTimer = null;
1831
+ }
1832
+ });
1833
+
1834
+ // src/homebridge-ui/public/js/constants.ts
1835
+ var init_constants = __esm({
1836
+ "src/homebridge-ui/public/js/constants.ts"() {
1837
+ "use strict";
1838
+ init_device_types();
1839
+ }
1840
+ });
1841
+
1842
+ // src/homebridge-ui/public/js/modals.ts
1843
+ var modals_exports = {};
1844
+ __export(modals_exports, {
1845
+ editDevice: () => editDevice,
1846
+ importDiscoveredDevice: () => importDiscoveredDevice
1847
+ });
1848
+ async function importDiscoveredDevice(device) {
1849
+ return new Promise((resolve) => {
1850
+ const div = document.createElement("div");
1851
+ div.style.position = "fixed";
1852
+ div.style.top = "0";
1853
+ div.style.left = "0";
1854
+ div.style.width = "100%";
1855
+ div.style.height = "100%";
1856
+ div.style.background = "rgba(0,0,0,0.7)";
1857
+ div.style.display = "flex";
1858
+ div.style.alignItems = "center";
1859
+ div.style.justifyContent = "center";
1860
+ div.style.zIndex = "9999";
1861
+ const modal = document.createElement("div");
1862
+ modal.style.background = getComputedStyle(document.body).backgroundColor;
1863
+ modal.style.color = getComputedStyle(document.body).color;
1864
+ modal.style.padding = "0";
1865
+ modal.style.borderRadius = "10px";
1866
+ modal.style.minWidth = "440px";
1867
+ modal.style.maxWidth = "90vw";
1868
+ modal.style.boxShadow = "0 8px 32px rgba(0,0,0,0.35)";
1869
+ modal.style.overflow = "hidden";
1870
+ modal.style.borderTop = "3px solid var(--switchbot-red, #ef4444)";
1871
+ const title = document.createElement("h3");
1872
+ title.textContent = "Import Discovered Device";
1873
+ title.style.marginTop = "0";
1874
+ title.style.marginBottom = "16px";
1875
+ title.style.padding = "20px 20px 0";
1876
+ title.style.fontSize = "18px";
1877
+ title.style.fontWeight = "600";
1878
+ title.style.color = "var(--switchbot-red, #ef4444)";
1879
+ title.style.letterSpacing = "-0.02em";
1880
+ const contentDiv = document.createElement("div");
1881
+ contentDiv.style.padding = "0 20px 20px";
1882
+ const nameLabel = document.createElement("label");
1883
+ nameLabel.textContent = "Device Name";
1884
+ nameLabel.style.display = "block";
1885
+ nameLabel.style.marginBottom = "6px";
1886
+ nameLabel.style.fontWeight = "500";
1887
+ nameLabel.style.fontSize = "12px";
1888
+ nameLabel.style.color = "#6b7280";
1889
+ const nameInput = document.createElement("input");
1890
+ nameInput.type = "text";
1891
+ nameInput.value = device.name || device.id || "";
1892
+ nameInput.style.width = "100%";
1893
+ nameInput.style.marginBottom = "12px";
1894
+ nameInput.style.padding = "8px 10px";
1895
+ nameInput.style.borderRadius = "6px";
1896
+ nameInput.style.fontSize = "14px";
1897
+ nameInput.style.boxSizing = "border-box";
1898
+ const typeLabel = document.createElement("label");
1899
+ typeLabel.textContent = "Config Device Type";
1900
+ typeLabel.style.display = "block";
1901
+ typeLabel.style.marginBottom = "6px";
1902
+ typeLabel.style.fontWeight = "500";
1903
+ typeLabel.style.fontSize = "12px";
1904
+ typeLabel.style.color = "#6b7280";
1905
+ const typeSelect = document.createElement("select");
1906
+ typeSelect.style.width = "100%";
1907
+ typeSelect.style.padding = "8px 10px";
1908
+ typeSelect.style.marginBottom = "12px";
1909
+ typeSelect.style.borderRadius = "6px";
1910
+ typeSelect.style.fontSize = "14px";
1911
+ typeSelect.style.background = getComputedStyle(nameInput).background;
1912
+ typeSelect.style.color = getComputedStyle(nameInput).color;
1913
+ typeSelect.style.border = getComputedStyle(nameInput).border;
1914
+ typeSelect.style.boxSizing = "border-box";
1915
+ Object.keys(DEVICE_TYPES).forEach((categoryName) => {
1916
+ const optgroup = document.createElement("optgroup");
1917
+ optgroup.label = categoryName;
1918
+ DEVICE_TYPES[categoryName].forEach((deviceType) => {
1919
+ const opt = document.createElement("option");
1920
+ opt.value = deviceType;
1921
+ opt.text = deviceType;
1922
+ const detectedType = (device.type || "").toLowerCase();
1923
+ opt.selected = deviceType.toLowerCase() === detectedType;
1924
+ optgroup.appendChild(opt);
1925
+ });
1926
+ typeSelect.appendChild(optgroup);
1927
+ });
1928
+ const windowCoveringTypes = ["Roller Shade", "Curtain", "Curtain3", "Blind Tilt"];
1929
+ const matterTypeLabel = document.createElement("label");
1930
+ matterTypeLabel.textContent = "Matter Window Covering Type (Advanced)";
1931
+ matterTypeLabel.style.display = "block";
1932
+ matterTypeLabel.style.marginBottom = "6px";
1933
+ matterTypeLabel.style.fontWeight = "500";
1934
+ matterTypeLabel.style.fontSize = "12px";
1935
+ matterTypeLabel.style.color = "#6b7280";
1936
+ matterTypeLabel.style.marginTop = "2px";
1937
+ const matterTypes = [
1938
+ { value: "", label: "Auto-detect (recommended)" },
1939
+ { value: 0, label: "Roller Shade (0)" },
1940
+ { value: 1, label: "Roman Shade (1)" },
1941
+ { value: 2, label: "Balloon Shade (2)" },
1942
+ { value: 3, label: "Woven Wood (3)" },
1943
+ { value: 4, label: "Drapery (4)" },
1944
+ { value: 5, label: "Awning (5)" },
1945
+ { value: 6, label: "Shutter (6)" },
1946
+ { value: 7, label: "Tilt Blind (7)" },
1947
+ { value: 8, label: "Blind Tilt (8)" },
1948
+ { value: 9, label: "Projector Screen (9)" },
1949
+ { value: 15, label: "Other (15)" }
1950
+ ];
1951
+ const matterTypeSelect = document.createElement("select");
1952
+ matterTypeSelect.style.width = "100%";
1953
+ matterTypeSelect.style.marginBottom = "12px";
1954
+ matterTypeSelect.style.padding = "8px 10px";
1955
+ matterTypeSelect.style.borderRadius = "6px";
1956
+ matterTypeSelect.style.fontSize = "14px";
1957
+ matterTypeSelect.style.boxSizing = "border-box";
1958
+ matterTypes.forEach((opt) => {
1959
+ const option = document.createElement("option");
1960
+ option.value = String(opt.value);
1961
+ option.textContent = opt.label;
1962
+ if (device.matterType != null && String(opt.value) === String(device.matterType)) {
1963
+ option.selected = true;
1964
+ }
1965
+ matterTypeSelect.appendChild(option);
1966
+ });
1967
+ function updateMatterTypeVisibility() {
1968
+ if (windowCoveringTypes.includes(typeSelect.value)) {
1969
+ matterTypeLabel.style.display = "block";
1970
+ matterTypeSelect.style.display = "block";
1971
+ } else {
1972
+ matterTypeLabel.style.display = "none";
1973
+ matterTypeSelect.style.display = "none";
1974
+ }
1975
+ }
1976
+ typeSelect.addEventListener("change", updateMatterTypeVisibility);
1977
+ updateMatterTypeVisibility();
1978
+ const macLabel = document.createElement("label");
1979
+ macLabel.textContent = "BLE MAC Address (optional)";
1980
+ macLabel.style.display = "block";
1981
+ macLabel.style.marginBottom = "6px";
1982
+ macLabel.style.fontWeight = "500";
1983
+ macLabel.style.fontSize = "12px";
1984
+ macLabel.style.color = "#6b7280";
1985
+ const macInput = document.createElement("input");
1986
+ macInput.type = "text";
1987
+ macInput.value = device.address || "";
1988
+ macInput.placeholder = "AA:BB:CC:DD:EE:FF";
1989
+ macInput.style.width = "100%";
1990
+ macInput.style.marginBottom = "12px";
1991
+ macInput.style.padding = "8px 10px";
1992
+ macInput.style.borderRadius = "6px";
1993
+ macInput.style.fontSize = "14px";
1994
+ macInput.style.boxSizing = "border-box";
1995
+ const encryptionKeyLabel = document.createElement("label");
1996
+ encryptionKeyLabel.textContent = "BLE Encryption Key (optional)";
1997
+ encryptionKeyLabel.style.display = "block";
1998
+ encryptionKeyLabel.style.marginBottom = "6px";
1999
+ encryptionKeyLabel.style.fontWeight = "500";
2000
+ encryptionKeyLabel.style.fontSize = "12px";
2001
+ encryptionKeyLabel.style.color = "#6b7280";
2002
+ const encryptionKeyInput = document.createElement("input");
2003
+ encryptionKeyInput.type = "password";
2004
+ encryptionKeyInput.value = device.encryptionKey || "";
2005
+ encryptionKeyInput.placeholder = "Paste device BLE encryption key";
2006
+ encryptionKeyInput.style.width = "100%";
2007
+ encryptionKeyInput.style.marginBottom = "12px";
2008
+ encryptionKeyInput.style.padding = "8px 10px";
2009
+ encryptionKeyInput.style.borderRadius = "6px";
2010
+ encryptionKeyInput.style.fontSize = "14px";
2011
+ encryptionKeyInput.style.boxSizing = "border-box";
2012
+ const keyIdLabel = document.createElement("label");
2013
+ keyIdLabel.textContent = "BLE Key ID (optional)";
2014
+ keyIdLabel.style.display = "block";
2015
+ keyIdLabel.style.marginBottom = "6px";
2016
+ keyIdLabel.style.fontWeight = "500";
2017
+ keyIdLabel.style.fontSize = "12px";
2018
+ keyIdLabel.style.color = "#6b7280";
2019
+ const keyIdInput = document.createElement("input");
2020
+ keyIdInput.type = "text";
2021
+ keyIdInput.value = device.keyId || "";
2022
+ keyIdInput.placeholder = "e.g. ff";
2023
+ keyIdInput.style.width = "100%";
2024
+ keyIdInput.style.marginBottom = "12px";
2025
+ keyIdInput.style.padding = "8px 10px";
2026
+ keyIdInput.style.borderRadius = "6px";
2027
+ keyIdInput.style.fontSize = "14px";
2028
+ keyIdInput.style.boxSizing = "border-box";
2029
+ const buttons = document.createElement("div");
2030
+ buttons.style.display = "flex";
2031
+ buttons.style.gap = "10px";
2032
+ buttons.style.justifyContent = "flex-end";
2033
+ buttons.style.marginTop = "18px";
2034
+ buttons.style.paddingTop = "18px";
2035
+ buttons.style.borderTop = "1px solid rgba(0, 0, 0, 0.08)";
2036
+ const cancelBtn = document.createElement("button");
2037
+ cancelBtn.textContent = "Cancel";
2038
+ cancelBtn.className = "secondary";
2039
+ cancelBtn.style.background = "#6b7280";
2040
+ cancelBtn.style.padding = "8px 16px";
2041
+ cancelBtn.style.fontSize = "13px";
2042
+ const importBtn = document.createElement("button");
2043
+ importBtn.textContent = "Add to Config";
2044
+ importBtn.style.background = "var(--switchbot-red, #ef4444)";
2045
+ importBtn.style.padding = "8px 20px";
2046
+ importBtn.style.fontSize = "13px";
2047
+ const cleanup = (result) => {
2048
+ div.remove();
2049
+ resolve(result);
2050
+ };
2051
+ cancelBtn.onclick = () => cleanup(null);
2052
+ importBtn.onclick = () => cleanup({
2053
+ configDeviceName: nameInput.value || device.name || device.id,
2054
+ configDeviceType: typeSelect.value || device.type,
2055
+ address: macInput.value || void 0,
2056
+ encryptionKey: encryptionKeyInput.value || void 0,
2057
+ keyId: keyIdInput.value || void 0,
2058
+ ...windowCoveringTypes.includes(typeSelect.value) && matterTypeSelect.value !== "" ? { matterType: Number(matterTypeSelect.value) } : {}
2059
+ });
2060
+ div.addEventListener("click", (event) => {
2061
+ if (event.target === div) {
2062
+ cleanup(null);
2063
+ }
2064
+ });
2065
+ buttons.appendChild(cancelBtn);
2066
+ buttons.appendChild(importBtn);
2067
+ contentDiv.appendChild(nameLabel);
2068
+ contentDiv.appendChild(nameInput);
2069
+ contentDiv.appendChild(typeLabel);
2070
+ contentDiv.appendChild(typeSelect);
2071
+ contentDiv.appendChild(matterTypeLabel);
2072
+ contentDiv.appendChild(matterTypeSelect);
2073
+ contentDiv.appendChild(macLabel);
2074
+ contentDiv.appendChild(macInput);
2075
+ contentDiv.appendChild(encryptionKeyLabel);
2076
+ contentDiv.appendChild(encryptionKeyInput);
2077
+ contentDiv.appendChild(keyIdLabel);
2078
+ contentDiv.appendChild(keyIdInput);
2079
+ contentDiv.appendChild(buttons);
2080
+ modal.appendChild(title);
2081
+ modal.appendChild(contentDiv);
2082
+ div.appendChild(modal);
2083
+ document.body.appendChild(div);
2084
+ nameInput.focus();
2085
+ });
2086
+ }
2087
+ async function editDevice(device) {
2088
+ const typeLabel = document.createElement("label");
2089
+ typeLabel.textContent = "Config Device Type";
2090
+ typeLabel.style.display = "block";
2091
+ typeLabel.style.marginBottom = "6px";
2092
+ typeLabel.style.fontWeight = "500";
2093
+ typeLabel.style.fontSize = "12px";
2094
+ typeLabel.style.color = "#6b7280";
2095
+ const typeSelect = document.createElement("select");
2096
+ typeSelect.style.width = "100%";
2097
+ typeSelect.style.padding = "8px 10px";
2098
+ typeSelect.style.marginBottom = "12px";
2099
+ typeSelect.style.borderRadius = "6px";
2100
+ typeSelect.style.fontSize = "14px";
2101
+ typeSelect.style.background = getComputedStyle(document.body).backgroundColor;
2102
+ typeSelect.style.color = getComputedStyle(document.body).color;
2103
+ typeSelect.style.border = "1px solid #ccc";
2104
+ typeSelect.style.boxSizing = "border-box";
2105
+ Object.keys(DEVICE_TYPES).forEach((categoryName) => {
2106
+ const optgroup = document.createElement("optgroup");
2107
+ optgroup.label = categoryName;
2108
+ DEVICE_TYPES[categoryName].forEach((deviceType) => {
2109
+ const opt = document.createElement("option");
2110
+ opt.value = deviceType;
2111
+ opt.text = deviceType;
2112
+ const currentType = device.configDeviceType || device.deviceType || device.type || "";
2113
+ opt.selected = currentType === deviceType;
2114
+ optgroup.appendChild(opt);
2115
+ });
2116
+ typeSelect.appendChild(optgroup);
2117
+ });
2118
+ const windowCoveringTypes = ["Roller Shade", "Curtain", "Curtain3", "Blind Tilt"];
2119
+ const showMatterType = windowCoveringTypes.includes(device.configDeviceType || device.deviceType || device.type || "");
2120
+ const matterTypeLabel = document.createElement("label");
2121
+ matterTypeLabel.textContent = "Matter Window Covering Type (Advanced)";
2122
+ matterTypeLabel.style.display = showMatterType ? "block" : "none";
2123
+ matterTypeLabel.style.marginBottom = "6px";
2124
+ matterTypeLabel.style.fontWeight = "500";
2125
+ matterTypeLabel.style.fontSize = "12px";
2126
+ matterTypeLabel.style.color = "#6b7280";
2127
+ matterTypeLabel.style.marginTop = "2px";
2128
+ const matterTypes = [
2129
+ { value: "", label: "Auto-detect (recommended)" },
2130
+ { value: 0, label: "Roller Shade (0)" },
2131
+ { value: 1, label: "Roman Shade (1)" },
2132
+ { value: 2, label: "Balloon Shade (2)" },
2133
+ { value: 3, label: "Woven Wood (3)" },
2134
+ { value: 4, label: "Drapery (4)" },
2135
+ { value: 5, label: "Awning (5)" },
2136
+ { value: 6, label: "Shutter (6)" },
2137
+ { value: 7, label: "Tilt Blind (7)" },
2138
+ { value: 8, label: "Blind Tilt (8)" },
2139
+ { value: 9, label: "Projector Screen (9)" },
2140
+ { value: 15, label: "Other (15)" }
2141
+ ];
2142
+ const matterTypeSelect = document.createElement("select");
2143
+ matterTypeSelect.style.width = "100%";
2144
+ matterTypeSelect.style.marginBottom = "12px";
2145
+ matterTypeSelect.style.padding = "8px 10px";
2146
+ matterTypeSelect.style.borderRadius = "6px";
2147
+ matterTypeSelect.style.fontSize = "14px";
2148
+ matterTypeSelect.style.boxSizing = "border-box";
2149
+ matterTypes.forEach((opt) => {
2150
+ const option = document.createElement("option");
2151
+ option.value = String(opt.value);
2152
+ option.textContent = opt.label;
2153
+ if (device.matterType != null && String(opt.value) === String(device.matterType)) {
2154
+ option.selected = true;
2155
+ }
2156
+ matterTypeSelect.appendChild(option);
2157
+ });
2158
+ function updateMatterTypeVisibility() {
2159
+ if (windowCoveringTypes.includes(typeSelect.value)) {
2160
+ matterTypeLabel.style.display = "block";
2161
+ matterTypeSelect.style.display = "block";
2162
+ } else {
2163
+ matterTypeLabel.style.display = "none";
2164
+ matterTypeSelect.style.display = "none";
2165
+ }
2166
+ }
2167
+ typeSelect.addEventListener("change", updateMatterTypeVisibility);
2168
+ updateMatterTypeVisibility();
2169
+ const div = document.createElement("div");
2170
+ div.style.position = "fixed";
2171
+ div.style.top = "0";
2172
+ div.style.left = "0";
2173
+ div.style.width = "100%";
2174
+ div.style.height = "100%";
2175
+ div.style.background = "rgba(0,0,0,0.7)";
2176
+ div.style.display = "flex";
2177
+ div.style.alignItems = "center";
2178
+ div.style.justifyContent = "center";
2179
+ div.style.zIndex = "9999";
2180
+ const modal = document.createElement("div");
2181
+ modal.style.background = getComputedStyle(document.body).backgroundColor;
2182
+ modal.style.color = getComputedStyle(document.body).color;
2183
+ modal.style.padding = "0";
2184
+ modal.style.borderRadius = "10px";
2185
+ modal.style.minWidth = "440px";
2186
+ modal.style.maxWidth = "90vw";
2187
+ modal.style.boxShadow = "0 8px 32px rgba(0,0,0,0.35)";
2188
+ modal.style.overflow = "hidden";
2189
+ modal.style.borderTop = "3px solid var(--switchbot-red, #ef4444)";
2190
+ const title = document.createElement("h3");
2191
+ title.textContent = "Edit Device";
2192
+ title.style.marginTop = "0";
2193
+ title.style.marginBottom = "16px";
2194
+ title.style.padding = "20px 20px 0";
2195
+ title.style.fontSize = "18px";
2196
+ title.style.fontWeight = "600";
2197
+ title.style.color = "var(--switchbot-red, #ef4444)";
2198
+ title.style.letterSpacing = "-0.02em";
2199
+ const contentDiv = document.createElement("div");
2200
+ contentDiv.style.padding = "0 20px 20px";
2201
+ const nameLabel = document.createElement("label");
2202
+ nameLabel.textContent = "Device Name";
2203
+ nameLabel.style.display = "block";
2204
+ nameLabel.style.marginBottom = "6px";
2205
+ nameLabel.style.fontWeight = "500";
2206
+ nameLabel.style.fontSize = "12px";
2207
+ nameLabel.style.color = "#6b7280";
2208
+ const nameInput = document.createElement("input");
2209
+ nameInput.type = "text";
2210
+ nameInput.value = device.name || device.id;
2211
+ nameInput.style.width = "100%";
2212
+ nameInput.style.marginBottom = "12px";
2213
+ nameInput.style.padding = "8px 10px";
2214
+ nameInput.style.borderRadius = "6px";
2215
+ nameInput.style.fontSize = "14px";
2216
+ nameInput.style.boxSizing = "border-box";
2217
+ nameInput.style.transition = "border-color 0.2s ease";
2218
+ const apiTypeLabel = document.createElement("label");
2219
+ apiTypeLabel.textContent = "Device Type (API - Read Only)";
2220
+ apiTypeLabel.style.display = "block";
2221
+ apiTypeLabel.style.marginBottom = "6px";
2222
+ apiTypeLabel.style.fontWeight = "500";
2223
+ apiTypeLabel.style.fontSize = "12px";
2224
+ apiTypeLabel.style.color = "#6b7280";
2225
+ const apiTypeInput = document.createElement("input");
2226
+ apiTypeInput.type = "text";
2227
+ apiTypeInput.value = device.deviceType || device.type || "Unknown";
2228
+ apiTypeInput.readOnly = true;
2229
+ apiTypeInput.style.width = "100%";
2230
+ apiTypeInput.style.marginBottom = "12px";
2231
+ apiTypeInput.style.padding = "8px 10px";
2232
+ apiTypeInput.style.borderRadius = "6px";
2233
+ apiTypeInput.style.fontSize = "13px";
2234
+ apiTypeInput.style.opacity = "0.6";
2235
+ apiTypeInput.style.cursor = "not-allowed";
2236
+ apiTypeInput.style.boxSizing = "border-box";
2237
+ apiTypeInput.style.backgroundColor = "#f9fafb";
2238
+ Object.keys(DEVICE_TYPES).forEach((categoryName) => {
2239
+ const optgroup = document.createElement("optgroup");
2240
+ optgroup.label = categoryName;
2241
+ DEVICE_TYPES[categoryName].forEach((deviceType) => {
2242
+ const opt = document.createElement("option");
2243
+ opt.value = deviceType;
2244
+ opt.text = deviceType;
2245
+ const currentType = device.configDeviceType || device.deviceType || device.type || "";
2246
+ opt.selected = currentType === deviceType;
2247
+ optgroup.appendChild(opt);
2248
+ });
2249
+ typeSelect.appendChild(optgroup);
2250
+ });
2251
+ const errorMessage = document.createElement("div");
2252
+ errorMessage.style.color = "var(--switchbot-red, #ef4444)";
2253
+ errorMessage.style.marginBottom = "12px";
2254
+ errorMessage.style.fontSize = "12px";
2255
+ errorMessage.style.display = "none";
2256
+ errorMessage.style.padding = "8px 10px";
2257
+ errorMessage.style.background = "var(--switchbot-red-light, #fee2e2)";
2258
+ errorMessage.style.borderRadius = "6px";
2259
+ errorMessage.style.fontWeight = "500";
2260
+ const buttons = document.createElement("div");
2261
+ buttons.style.display = "flex";
2262
+ buttons.style.gap = "10px";
2263
+ buttons.style.justifyContent = "flex-end";
2264
+ buttons.style.marginTop = "18px";
2265
+ buttons.style.paddingTop = "18px";
2266
+ buttons.style.borderTop = "1px solid rgba(0, 0, 0, 0.08)";
2267
+ const cancelBtn = document.createElement("button");
2268
+ cancelBtn.textContent = "Cancel";
2269
+ cancelBtn.className = "secondary";
2270
+ cancelBtn.style.background = "#6b7280";
2271
+ cancelBtn.style.padding = "8px 16px";
2272
+ cancelBtn.style.fontSize = "13px";
2273
+ cancelBtn.onclick = () => div.remove();
2274
+ const saveBtn = document.createElement("button");
2275
+ saveBtn.textContent = "Save";
2276
+ saveBtn.style.background = "var(--switchbot-red, #ef4444)";
2277
+ saveBtn.style.padding = "8px 20px";
2278
+ saveBtn.style.fontSize = "13px";
2279
+ saveBtn.onclick = async () => {
2280
+ try {
2281
+ const { updateDevice: updateDevice2, syncParentPluginConfigFromDisk: syncParentPluginConfigFromDisk2, fetchDevices: fetchDevices2 } = await Promise.resolve().then(() => (init_api(), api_exports));
2282
+ const { renderDeviceList: renderDeviceList2 } = await Promise.resolve().then(() => (init_render(), render_exports));
2283
+ const params = {
2284
+ deviceId: device.id,
2285
+ configDeviceName: nameInput.value || void 0,
2286
+ configDeviceType: typeSelect.value,
2287
+ matterType: windowCoveringTypes.includes(typeSelect.value) && matterTypeSelect.value !== "" ? Number(matterTypeSelect.value) : void 0
2288
+ };
2289
+ await updateDevice2(params.deviceId, params.configDeviceName, params.configDeviceType, { matterType: params.matterType });
2290
+ await syncParentPluginConfigFromDisk2();
2291
+ uiLog.info("[Edit Device] Refreshing device list after update");
2292
+ const list = await fetchDevices2();
2293
+ renderDeviceList2(list);
2294
+ div.remove();
2295
+ } catch (e) {
2296
+ uiLog.error("Update error:", e);
2297
+ errorMessage.textContent = `Error: ${e instanceof Error ? e.message : "Failed to update device"}`;
2298
+ errorMessage.style.display = "block";
2299
+ }
2300
+ };
2301
+ buttons.appendChild(cancelBtn);
2302
+ buttons.appendChild(saveBtn);
2303
+ contentDiv.appendChild(nameLabel);
2304
+ contentDiv.appendChild(nameInput);
2305
+ contentDiv.appendChild(apiTypeLabel);
2306
+ contentDiv.appendChild(apiTypeInput);
2307
+ contentDiv.appendChild(typeLabel);
2308
+ contentDiv.appendChild(typeSelect);
2309
+ contentDiv.appendChild(matterTypeLabel);
2310
+ contentDiv.appendChild(matterTypeSelect);
2311
+ contentDiv.appendChild(errorMessage);
2312
+ contentDiv.appendChild(buttons);
2313
+ modal.appendChild(title);
2314
+ modal.appendChild(contentDiv);
2315
+ div.appendChild(modal);
2316
+ document.body.appendChild(div);
2317
+ nameInput.focus();
2318
+ }
2319
+ var init_modals = __esm({
2320
+ "src/homebridge-ui/public/js/modals.ts"() {
2321
+ "use strict";
2322
+ init_constants();
2323
+ init_logger();
2324
+ }
2325
+ });
2326
+
2327
+ // src/homebridge-ui/public/js/devices-delete.ts
2328
+ var devices_delete_exports = {};
2329
+ __export(devices_delete_exports, {
2330
+ deleteAllDevicesFromConfig: () => deleteAllDevicesFromConfig,
2331
+ deleteDeviceFromConfig: () => deleteDeviceFromConfig
2332
+ });
2333
+ async function confirmDeleteDialog(deviceNameOrId) {
2334
+ return new Promise((resolve) => {
2335
+ const overlay = document.createElement("div");
2336
+ overlay.style.position = "fixed";
2337
+ overlay.style.top = "0";
2338
+ overlay.style.left = "0";
2339
+ overlay.style.width = "100%";
2340
+ overlay.style.height = "100%";
2341
+ overlay.style.background = "rgba(0,0,0,0.6)";
2342
+ overlay.style.display = "flex";
2343
+ overlay.style.alignItems = "center";
2344
+ overlay.style.justifyContent = "center";
2345
+ overlay.style.zIndex = "10000";
2346
+ const modal = document.createElement("div");
2347
+ modal.style.background = getComputedStyle(document.body).backgroundColor;
2348
+ modal.style.color = getComputedStyle(document.body).color;
2349
+ modal.style.padding = "20px";
2350
+ modal.style.borderRadius = "10px";
2351
+ modal.style.minWidth = "340px";
2352
+ modal.style.maxWidth = "90vw";
2353
+ modal.style.boxShadow = "0 12px 40px rgba(0,0,0,0.35)";
2354
+ const title = document.createElement("h3");
2355
+ title.textContent = "Delete Device";
2356
+ title.style.margin = "0 0 10px 0";
2357
+ const message = document.createElement("p");
2358
+ message.textContent = `Remove "${deviceNameOrId}" from configuration?`;
2359
+ message.style.margin = "0 0 16px 0";
2360
+ const actions = document.createElement("div");
2361
+ actions.style.display = "flex";
2362
+ actions.style.justifyContent = "flex-end";
2363
+ actions.style.gap = "8px";
2364
+ const cancelBtn = document.createElement("button");
2365
+ cancelBtn.textContent = "Cancel";
2366
+ cancelBtn.style.background = "#6b7280";
2367
+ const deleteBtn = document.createElement("button");
2368
+ deleteBtn.textContent = "Delete";
2369
+ deleteBtn.style.background = "#ef4444";
2370
+ const cleanup = (result) => {
2371
+ overlay.remove();
2372
+ resolve(result);
2373
+ };
2374
+ cancelBtn.onclick = () => cleanup(false);
2375
+ deleteBtn.onclick = () => cleanup(true);
2376
+ overlay.addEventListener("click", (event) => {
2377
+ if (event.target === overlay) {
2378
+ cleanup(false);
2379
+ }
2380
+ });
2381
+ actions.appendChild(cancelBtn);
2382
+ actions.appendChild(deleteBtn);
2383
+ modal.appendChild(title);
2384
+ modal.appendChild(message);
2385
+ modal.appendChild(actions);
2386
+ overlay.appendChild(modal);
2387
+ document.body.appendChild(overlay);
2388
+ });
2389
+ }
2390
+ async function deleteDeviceFromConfig(deviceId, deviceName) {
2391
+ uiLog.info("Delete button clicked for device:", deviceId, deviceName);
2392
+ const confirmed = await confirmDeleteDialog(deviceName || deviceId);
2393
+ if (!confirmed) {
2394
+ uiLog.info("Delete cancelled by user");
2395
+ return;
2396
+ }
2397
+ try {
2398
+ showBusyUi();
2399
+ uiLog.info("Sending delete request for deviceId:", deviceId);
2400
+ const resp = await deleteDevice(deviceId);
2401
+ uiLog.info("Delete response:", resp);
2402
+ if (!resp || resp.success === false) {
2403
+ throw new Error(resp?.data?.message || "Failed to delete device");
2404
+ }
2405
+ uiLog.info("Syncing parent config from disk...");
2406
+ const synced = await syncParentPluginConfigFromDisk(true);
2407
+ if (!synced) {
2408
+ toastWarning("Device deleted, but configuration sync failed");
2409
+ }
2410
+ uiLog.info("Refreshing device list...");
2411
+ const list = await fetchDevices();
2412
+ uiLog.info("Rendering devices:", list.length);
2413
+ renderDeviceList(list);
2414
+ uiLog.info("\u2713 Device deleted successfully");
2415
+ toastSuccess(`Device "${deviceName || deviceId}" deleted successfully`);
2416
+ } catch (e) {
2417
+ uiLog.error("Delete error:", e);
2418
+ toastError(e instanceof Error ? e.message : "Failed to delete device");
2419
+ } finally {
2420
+ hideBusyUi();
2421
+ }
2422
+ }
2423
+ async function confirmDeleteAllDialog(deviceCount) {
2424
+ return new Promise((resolve) => {
2425
+ const overlay = document.createElement("div");
2426
+ overlay.style.position = "fixed";
2427
+ overlay.style.top = "0";
2428
+ overlay.style.left = "0";
2429
+ overlay.style.width = "100%";
2430
+ overlay.style.height = "100%";
2431
+ overlay.style.background = "rgba(0,0,0,0.7)";
2432
+ overlay.style.display = "flex";
2433
+ overlay.style.alignItems = "center";
2434
+ overlay.style.justifyContent = "center";
2435
+ overlay.style.zIndex = "10000";
2436
+ const modal = document.createElement("div");
2437
+ modal.style.background = getComputedStyle(document.body).backgroundColor;
2438
+ modal.style.color = getComputedStyle(document.body).color;
2439
+ modal.style.padding = "20px";
2440
+ modal.style.borderRadius = "10px";
2441
+ modal.style.minWidth = "380px";
2442
+ modal.style.maxWidth = "90vw";
2443
+ modal.style.boxShadow = "0 12px 40px rgba(0,0,0,0.35)";
2444
+ modal.style.borderTop = "3px solid #ef4444";
2445
+ const title = document.createElement("h3");
2446
+ title.textContent = "\u26A0\uFE0F Remove All Devices";
2447
+ title.style.margin = "0 0 12px 0";
2448
+ title.style.color = "#ef4444";
2449
+ const message = document.createElement("p");
2450
+ 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.`;
2451
+ message.style.margin = "0 0 18px 0";
2452
+ message.style.lineHeight = "1.5";
2453
+ const actions = document.createElement("div");
2454
+ actions.style.display = "flex";
2455
+ actions.style.justifyContent = "flex-end";
2456
+ actions.style.gap = "10px";
2457
+ const cancelBtn = document.createElement("button");
2458
+ cancelBtn.textContent = "Cancel";
2459
+ cancelBtn.style.background = "#6b7280";
2460
+ cancelBtn.style.padding = "8px 16px";
2461
+ cancelBtn.style.fontSize = "13px";
2462
+ cancelBtn.className = "secondary";
2463
+ const deleteBtn = document.createElement("button");
2464
+ deleteBtn.textContent = "Remove All";
2465
+ deleteBtn.style.background = "#ef4444";
2466
+ deleteBtn.style.padding = "8px 20px";
2467
+ deleteBtn.style.fontSize = "13px";
2468
+ const cleanup = (result) => {
2469
+ overlay.remove();
2470
+ resolve(result);
2471
+ };
2472
+ cancelBtn.onclick = () => cleanup(false);
2473
+ deleteBtn.onclick = () => cleanup(true);
2474
+ overlay.addEventListener("click", (event) => {
2475
+ if (event.target === overlay) {
2476
+ cleanup(false);
2477
+ }
2478
+ });
2479
+ actions.appendChild(cancelBtn);
2480
+ actions.appendChild(deleteBtn);
2481
+ modal.appendChild(title);
2482
+ modal.appendChild(message);
2483
+ modal.appendChild(actions);
2484
+ overlay.appendChild(modal);
2485
+ document.body.appendChild(overlay);
2486
+ });
2487
+ }
2488
+ async function deleteAllDevicesFromConfig() {
2489
+ uiLog.info("Remove All Devices button clicked");
2490
+ try {
2491
+ const list = await fetchDevices();
2492
+ if (!list || list.length === 0) {
2493
+ toastWarning("No devices to remove");
2494
+ return;
2495
+ }
2496
+ const confirmed = await confirmDeleteAllDialog(list.length);
2497
+ if (!confirmed) {
2498
+ uiLog.info("Remove all cancelled by user");
2499
+ return;
2500
+ }
2501
+ showBusyUi();
2502
+ uiLog.info("Sending delete all devices request");
2503
+ const resp = await deleteAllDevices();
2504
+ uiLog.info("Delete all response:", resp);
2505
+ if (!resp || resp.success === false) {
2506
+ throw new Error(resp?.data?.message || "Failed to delete all devices");
2507
+ }
2508
+ uiLog.info("Syncing parent config from disk...");
2509
+ const synced = await syncParentPluginConfigFromDisk(true);
2510
+ if (!synced) {
2511
+ toastWarning("Devices deleted, but configuration sync failed");
2512
+ }
2513
+ uiLog.info("Refreshing device list...");
2514
+ const updatedList = await fetchDevices();
2515
+ uiLog.info("Rendering devices:", updatedList.length);
2516
+ renderDeviceList(updatedList);
2517
+ const deletedCount = resp?.deletedCount || list.length;
2518
+ uiLog.info(`\u2713 Removed ${deletedCount} device(s) successfully`);
2519
+ toastSuccess(`Removed ${deletedCount} device(s) successfully`);
2520
+ } catch (e) {
2521
+ uiLog.error("Delete all error:", e);
2522
+ toastError(e instanceof Error ? e.message : "Failed to delete all devices");
2523
+ } finally {
2524
+ hideBusyUi();
2525
+ }
2526
+ }
2527
+ var init_devices_delete = __esm({
2528
+ "src/homebridge-ui/public/js/devices-delete.ts"() {
2529
+ "use strict";
2530
+ init_api();
2531
+ init_logger();
2532
+ init_modal();
2533
+ init_render();
2534
+ init_toast();
2535
+ }
2536
+ });
2537
+
2538
+ // src/homebridge-ui/public/js/render.ts
2539
+ var render_exports = {};
2540
+ __export(render_exports, {
2541
+ filterDevices: () => filterDevices,
2542
+ getDiscoveryPreferences: () => getDiscoveryPreferences,
2543
+ getRssiSignalQuality: () => getRssiSignalQuality,
2544
+ renderBadge: () => renderBadge,
2545
+ renderConnectionBadge: () => renderConnectionBadge,
2546
+ renderDeviceDetailsPanel: () => renderDeviceDetailsPanel,
2547
+ renderDeviceList: () => renderDeviceList,
2548
+ renderDiscoveredDevices: () => renderDiscoveredDevices,
2549
+ renderIRBadge: () => renderIRBadge,
2550
+ renderSignalBars: () => renderSignalBars,
2551
+ renderSignalQualityBadge: () => renderSignalQualityBadge,
2552
+ setDiscoveryPreferences: () => setDiscoveryPreferences,
2553
+ sortDevices: () => sortDevices
2554
+ });
2555
+ function getRssiSignalQuality(rssi) {
2556
+ if (!rssi || rssi === 0) {
2557
+ return {
2558
+ level: "unknown",
2559
+ color: "#999",
2560
+ bgColor: "#f5f5f5",
2561
+ description: "Signal strength unknown",
2562
+ bars: 0
2563
+ };
2564
+ }
2565
+ const dbm = Math.floor(rssi);
2566
+ if (dbm > -60) {
2567
+ return {
2568
+ level: "excellent",
2569
+ color: "#34a853",
2570
+ bgColor: "#e8f5e9",
2571
+ description: `Excellent (${dbm} dBm)`,
2572
+ bars: 4
2573
+ };
2574
+ } else if (dbm > -75) {
2575
+ return {
2576
+ level: "good",
2577
+ color: "#fbbc04",
2578
+ bgColor: "#fffde7",
2579
+ description: `Good (${dbm} dBm)`,
2580
+ bars: 3
2581
+ };
2582
+ } else if (dbm > -85) {
2583
+ return {
2584
+ level: "fair",
2585
+ color: "#ff9800",
2586
+ bgColor: "#fff3e0",
2587
+ description: `Fair (${dbm} dBm)`,
2588
+ bars: 2
2589
+ };
2590
+ } else {
2591
+ return {
2592
+ level: "poor",
2593
+ color: "#ea4335",
2594
+ bgColor: "#ffebee",
2595
+ description: `Poor (${dbm} dBm) - unreliable`,
2596
+ bars: 1
2597
+ };
2598
+ }
2599
+ }
2600
+ function renderSignalBars(rssi) {
2601
+ const quality = getRssiSignalQuality(rssi);
2602
+ const container = document.createElement("span");
2603
+ container.style.display = "inline-flex";
2604
+ container.style.gap = "2px";
2605
+ container.style.alignItems = "center";
2606
+ container.style.marginLeft = "8px";
2607
+ container.style.fontSize = "12px";
2608
+ for (let i = 1; i <= 4; i++) {
2609
+ const bar = document.createElement("span");
2610
+ bar.style.height = `${i * 3}px`;
2611
+ bar.style.width = "3px";
2612
+ bar.style.borderRadius = "1px";
2613
+ bar.style.border = `1px solid ${quality.color}`;
2614
+ if (i <= quality.bars) {
2615
+ bar.style.backgroundColor = quality.color;
2616
+ } else {
2617
+ bar.style.backgroundColor = "transparent";
2618
+ }
2619
+ container.appendChild(bar);
2620
+ }
2621
+ container.title = quality.description;
2622
+ return container;
2623
+ }
2624
+ function renderSignalQualityBadge(rssi) {
2625
+ const quality = getRssiSignalQuality(rssi);
2626
+ const badge = document.createElement("span");
2627
+ badge.textContent = quality.level.charAt(0).toUpperCase() + quality.level.slice(1);
2628
+ badge.style.cssText = `
2629
+ background: ${quality.color};
2630
+ color: white;
2631
+ padding: 2px 6px;
2632
+ border-radius: 3px;
2633
+ font-size: 10px;
2634
+ font-weight: 600;
2635
+ margin-left: 8px;
2636
+ `;
2637
+ badge.title = quality.description;
2638
+ return badge;
2639
+ }
2640
+ function renderBadge(text, style) {
2641
+ const badge = document.createElement("span");
2642
+ badge.textContent = text;
2643
+ badge.style.cssText = style;
2644
+ return badge;
2645
+ }
2646
+ function renderConnectionBadge(connectionType) {
2647
+ if (!connectionType) {
2648
+ return null;
2649
+ }
2650
+ const badge = renderBadge(connectionType, "");
2651
+ if (connectionType === "BLE") {
2652
+ badge.style.cssText = "background: #4285f4; color: white; padding: 2px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 8px;";
2653
+ } else if (connectionType === "Both") {
2654
+ badge.style.cssText = "background: #34a853; color: white; padding: 2px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 8px;";
2655
+ } else {
2656
+ badge.style.cssText = "background: #9e9e9e; color: white; padding: 2px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 8px;";
2657
+ }
2658
+ return badge;
2659
+ }
2660
+ function renderIRBadge() {
2661
+ return renderBadge(
2662
+ "IR",
2663
+ "background: #ff6b35; color: white; padding: 2px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 8px;"
2664
+ );
2665
+ }
2666
+ function normalizeId2(value) {
2667
+ return String(value ?? "").trim().toLowerCase();
2668
+ }
2669
+ function scrollToConfiguredDevice(deviceId) {
2670
+ const normalizedId = normalizeId2(deviceId);
2671
+ const target = document.querySelector(`[data-device-id="${normalizedId}"]`);
2672
+ if (!target) {
2673
+ return;
2674
+ }
2675
+ target.scrollIntoView({ behavior: "smooth", block: "center" });
2676
+ const originalOutline = target.style.outline;
2677
+ const originalBackground = target.style.background;
2678
+ target.style.outline = "2px solid var(--switchbot-red, #ef4444)";
2679
+ target.style.background = "rgba(239, 68, 68, 0.08)";
2680
+ setTimeout(() => {
2681
+ target.style.outline = originalOutline;
2682
+ target.style.background = originalBackground;
2683
+ }, 1800);
2684
+ }
2685
+ function createConnectionTestControls(device) {
2686
+ const controls = document.createElement("div");
2687
+ controls.style.display = "inline-flex";
2688
+ controls.style.alignItems = "center";
2689
+ controls.style.gap = "6px";
2690
+ const button = document.createElement("button");
2691
+ button.textContent = "Test Connection";
2692
+ button.className = "secondary";
2693
+ button.style.padding = "4px 9px";
2694
+ button.style.fontSize = "11px";
2695
+ const status = document.createElement("span");
2696
+ status.style.fontSize = "10px";
2697
+ status.style.opacity = "0.85";
2698
+ status.style.whiteSpace = "normal";
2699
+ status.style.overflowWrap = "anywhere";
2700
+ button.onclick = async () => {
2701
+ const startedAt = Date.now();
2702
+ button.disabled = true;
2703
+ button.textContent = "Testing...";
2704
+ status.textContent = "Checking...";
2705
+ status.style.color = "#6b7280";
2706
+ try {
2707
+ const { testDeviceConnection: testDeviceConnection2 } = await Promise.resolve().then(() => (init_api(), api_exports));
2708
+ const result = await testDeviceConnection2({
2709
+ deviceId: String(device?.id || device?.deviceId || ""),
2710
+ connectionType: device?.connectionType,
2711
+ address: device?.address
2712
+ });
2713
+ const measuredLatency = Number(result?.latencyMs) > 0 ? Number(result.latencyMs) : Date.now() - startedAt;
2714
+ if (result?.success) {
2715
+ const method = result?.method || "Auto";
2716
+ status.textContent = `\u2713 ${method} \xB7 ${measuredLatency}ms`;
2717
+ status.style.color = "#16a34a";
2718
+ } else {
2719
+ const detail = result?.message ? ` \xB7 ${result.message}` : "";
2720
+ status.textContent = `\u2717 Failed \xB7 ${measuredLatency}ms${detail}`;
2721
+ status.style.color = "#dc2626";
2722
+ }
2723
+ } catch (e) {
2724
+ status.textContent = `\u2717 Failed \xB7 ${Date.now() - startedAt}ms`;
2725
+ status.style.color = "#dc2626";
2726
+ } finally {
2727
+ button.disabled = false;
2728
+ button.textContent = "Test Connection";
2729
+ }
2730
+ };
2731
+ controls.appendChild(button);
2732
+ controls.appendChild(status);
2733
+ return controls;
2734
+ }
2735
+ function formatLastSeen(value) {
2736
+ if (!value) {
2737
+ return "N/A";
2738
+ }
2739
+ try {
2740
+ const date = new Date(value);
2741
+ return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString();
2742
+ } catch (_e) {
2743
+ return String(value);
2744
+ }
2745
+ }
2746
+ function renderDeviceDetailsPanel(device) {
2747
+ const details = document.createElement("div");
2748
+ details.style.cssText = `
2749
+ border-top: 1px solid #ddd;
2750
+ padding: 8px;
2751
+ background: #f9fafb;
2752
+ border-radius: 4px;
2753
+ font-size: 12px;
2754
+ margin-top: 4px;
2755
+ `;
2756
+ const batteryHistoryKey = `batteryHistory:${device?.id || device?.deviceId}`;
2757
+ let batteryHistory = [];
2758
+ try {
2759
+ const raw = localStorage.getItem(batteryHistoryKey);
2760
+ if (raw) batteryHistory = JSON.parse(raw);
2761
+ } catch {
2762
+ }
2763
+ const now = Date.now();
2764
+ if (typeof device?.battery === "number") {
2765
+ const last = batteryHistory[batteryHistory.length - 1];
2766
+ if (!last || last.value !== device.battery || now - last.ts > 60 * 60 * 1e3) {
2767
+ batteryHistory.push({ value: device.battery, ts: now });
2768
+ if (batteryHistory.length > 30) batteryHistory = batteryHistory.slice(-30);
2769
+ try {
2770
+ localStorage.setItem(batteryHistoryKey, JSON.stringify(batteryHistory));
2771
+ } catch {
2772
+ }
2773
+ }
2774
+ }
2775
+ const rows = [
2776
+ { label: "Device ID", value: String(device?.id || device?.deviceId || "N/A"), copyable: !!(device?.id || device?.deviceId) },
2777
+ { label: "MAC Address", value: String(device?.address || "N/A"), copyable: !!device?.address },
2778
+ { label: "Device Type", value: String(device?.type || device?.configDeviceType || "N/A") },
2779
+ { label: "Model", value: String(device?.model || "N/A") },
2780
+ { label: "Hub ID", value: String(device?.hubDeviceId || "N/A") },
2781
+ { label: "Battery", value: device?.battery !== void 0 && device?.battery !== null ? `${device.battery}%` : "N/A" },
2782
+ { label: "Firmware", value: String(device?.version || device?.firmware || "N/A") },
2783
+ { label: "Cloud Service", value: device?.enabled === false ? "Disabled" : "Enabled" },
2784
+ { label: "Last Seen", value: formatLastSeen(device?.lastSeen || device?.lastseen || device?.updatedAt) }
2785
+ ];
2786
+ for (const row of rows) {
2787
+ const line = document.createElement("div");
2788
+ line.style.display = "flex";
2789
+ line.style.alignItems = "center";
2790
+ line.style.justifyContent = "space-between";
2791
+ line.style.gap = "8px";
2792
+ line.style.padding = "2px 0";
2793
+ const label = document.createElement("span");
2794
+ label.style.fontWeight = "600";
2795
+ label.style.minWidth = "110px";
2796
+ label.textContent = `${row.label}:`;
2797
+ const valueWrap = document.createElement("span");
2798
+ valueWrap.style.display = "inline-flex";
2799
+ valueWrap.style.alignItems = "center";
2800
+ valueWrap.style.gap = "6px";
2801
+ valueWrap.style.flex = "1";
2802
+ valueWrap.style.justifyContent = "flex-end";
2803
+ valueWrap.style.minWidth = "0";
2804
+ const value = document.createElement("span");
2805
+ value.style.fontFamily = "monospace";
2806
+ value.style.fontSize = "11px";
2807
+ value.style.opacity = "0.9";
2808
+ value.style.whiteSpace = "normal";
2809
+ value.style.overflowWrap = "anywhere";
2810
+ value.style.wordBreak = "break-word";
2811
+ value.style.textAlign = "right";
2812
+ value.textContent = row.value;
2813
+ valueWrap.appendChild(value);
2814
+ if (row.copyable && row.value && row.value !== "N/A") {
2815
+ const copyBtn = document.createElement("button");
2816
+ copyBtn.textContent = "\u{1F4CB}";
2817
+ copyBtn.title = `Copy ${row.label}`;
2818
+ copyBtn.style.padding = "2px 6px";
2819
+ copyBtn.style.fontSize = "10px";
2820
+ copyBtn.style.lineHeight = "1";
2821
+ copyBtn.style.background = "#e5e7eb";
2822
+ copyBtn.style.color = "#111827";
2823
+ copyBtn.onclick = async () => {
2824
+ try {
2825
+ await navigator.clipboard.writeText(row.value);
2826
+ copyBtn.textContent = "\u2713";
2827
+ setTimeout(() => {
2828
+ copyBtn.textContent = "\u{1F4CB}";
2829
+ }, 1200);
2830
+ } catch (_e) {
2831
+ copyBtn.textContent = "!";
2832
+ setTimeout(() => {
2833
+ copyBtn.textContent = "\u{1F4CB}";
2834
+ }, 1200);
2835
+ }
2836
+ };
2837
+ valueWrap.appendChild(copyBtn);
2838
+ }
2839
+ line.appendChild(label);
2840
+ line.appendChild(valueWrap);
2841
+ details.appendChild(line);
2842
+ if (row.label === "Battery" && Array.isArray(batteryHistory) && batteryHistory.length > 1) {
2843
+ const chart = document.createElement("div");
2844
+ chart.style.margin = "2px 0 8px 0";
2845
+ chart.style.width = "100%";
2846
+ chart.style.height = "28px";
2847
+ chart.style.display = "flex";
2848
+ const w = 120, h = 24, pad = 2;
2849
+ const min = Math.min(...batteryHistory.map((b) => b.value), 100);
2850
+ const max = Math.max(...batteryHistory.map((b) => b.value), 0);
2851
+ const range = max - min || 1;
2852
+ const points = batteryHistory.map((b, i) => {
2853
+ const x = pad + i * (w - 2 * pad) / (batteryHistory.length - 1);
2854
+ const y = pad + (h - 2 * pad) * (1 - (b.value - min) / range);
2855
+ return `${x},${y}`;
2856
+ }).join(" ");
2857
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
2858
+ svg.setAttribute("width", String(w));
2859
+ svg.setAttribute("height", String(h));
2860
+ svg.setAttribute("viewBox", `0 0 ${w} ${h}`);
2861
+ svg.style.display = "block";
2862
+ svg.style.background = "#f3f4f6";
2863
+ svg.style.borderRadius = "3px";
2864
+ svg.style.marginTop = "2px";
2865
+ svg.style.boxShadow = "0 1px 2px #0001";
2866
+ const polyline = document.createElementNS("http://www.w3.org/2000/svg", "polyline");
2867
+ polyline.setAttribute("points", points);
2868
+ polyline.setAttribute("fill", "none");
2869
+ polyline.setAttribute("stroke", "#2563eb");
2870
+ polyline.setAttribute("stroke-width", "2");
2871
+ svg.appendChild(polyline);
2872
+ batteryHistory.forEach((b, i) => {
2873
+ const x = pad + i * (w - 2 * pad) / (batteryHistory.length - 1);
2874
+ const y = pad + (h - 2 * pad) * (1 - (b.value - min) / range);
2875
+ const circle = document.createElementNS("http://www.w3.org/2000/svg", "circle");
2876
+ circle.setAttribute("cx", String(x));
2877
+ circle.setAttribute("cy", String(y));
2878
+ circle.setAttribute("r", "2.5");
2879
+ circle.setAttribute("fill", "#2563eb");
2880
+ svg.appendChild(circle);
2881
+ });
2882
+ const minLabel = document.createElementNS("http://www.w3.org/2000/svg", "text");
2883
+ minLabel.setAttribute("x", "2");
2884
+ minLabel.setAttribute("y", String(h - 2));
2885
+ minLabel.setAttribute("font-size", "9");
2886
+ minLabel.setAttribute("fill", "#888");
2887
+ minLabel.textContent = `${min}%`;
2888
+ svg.appendChild(minLabel);
2889
+ const maxLabel = document.createElementNS("http://www.w3.org/2000/svg", "text");
2890
+ maxLabel.setAttribute("x", String(w - 18));
2891
+ maxLabel.setAttribute("y", "10");
2892
+ maxLabel.setAttribute("font-size", "9");
2893
+ maxLabel.setAttribute("fill", "#888");
2894
+ maxLabel.textContent = `${max}%`;
2895
+ svg.appendChild(maxLabel);
2896
+ chart.appendChild(svg);
2897
+ details.appendChild(chart);
2898
+ }
2899
+ }
2900
+ const featureKeys = [
2901
+ "airQuality",
2902
+ "pm25",
2903
+ "pm10",
2904
+ "voc",
2905
+ "co2",
2906
+ "humidity",
2907
+ "temperature",
2908
+ "preset",
2909
+ "mode",
2910
+ "presetMode",
2911
+ "direction",
2912
+ "calibration",
2913
+ "multiCommand",
2914
+ "extendedInfo",
2915
+ "segmentedControl",
2916
+ "features",
2917
+ "capabilities",
2918
+ "state"
2919
+ ];
2920
+ const shown = new Set(rows.map((r) => r.label.toLowerCase().replace(/\s/g, "")));
2921
+ for (const key of featureKeys) {
2922
+ if (device && device[key] !== void 0 && !shown.has(key.toLowerCase())) {
2923
+ const line = document.createElement("div");
2924
+ line.style.display = "flex";
2925
+ line.style.alignItems = "center";
2926
+ line.style.justifyContent = "space-between";
2927
+ line.style.gap = "8px";
2928
+ line.style.padding = "2px 0";
2929
+ const label = document.createElement("span");
2930
+ label.style.fontWeight = "600";
2931
+ label.style.minWidth = "110px";
2932
+ label.textContent = key.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase()) + ":";
2933
+ const value = document.createElement("span");
2934
+ value.style.fontFamily = "monospace";
2935
+ value.style.fontSize = "11px";
2936
+ value.style.opacity = "0.9";
2937
+ value.style.whiteSpace = "normal";
2938
+ value.style.overflowWrap = "anywhere";
2939
+ value.style.wordBreak = "break-word";
2940
+ value.style.textAlign = "right";
2941
+ value.textContent = typeof device[key] === "object" ? JSON.stringify(device[key]) : String(device[key]);
2942
+ line.appendChild(label);
2943
+ line.appendChild(value);
2944
+ details.appendChild(line);
2945
+ }
2946
+ }
2947
+ return details;
2948
+ }
2949
+ async function renderDiscoveredDevices(devices, options = {}) {
2950
+ const ul = document.createElement("ul");
2951
+ ul.className = "device-grid";
2952
+ ul.style.maxHeight = "400px";
2953
+ ul.style.overflowY = "auto";
2954
+ ul.style.marginTop = "12px";
2955
+ ul.style.padding = "0";
2956
+ ul.style.listStyle = "none";
2957
+ const { addDeviceToConfig: addDeviceToConfig3 } = await Promise.resolve().then(() => (init_discovery(), discovery_exports));
2958
+ const { loadConfiguredDevices: loadConfiguredDevices2 } = await Promise.resolve().then(() => (init_devices(), devices_exports));
2959
+ const configuredIds = options.configuredIds ?? /* @__PURE__ */ new Set();
2960
+ const selectedIds = options.selectedIds ?? /* @__PURE__ */ new Set();
2961
+ const onToggleSelect = options.onToggleSelect;
2962
+ for (const d of devices) {
2963
+ const deviceId = normalizeId2(d.id);
2964
+ const alreadyAdded = configuredIds.has(deviceId);
2965
+ const li = document.createElement("li");
2966
+ li.className = "device-item";
2967
+ li.style.display = "flex";
2968
+ li.style.flexDirection = "column";
2969
+ li.style.alignItems = "stretch";
2970
+ li.style.justifyContent = "flex-start";
2971
+ li.style.padding = "5px 8px";
2972
+ li.style.marginBottom = "0";
2973
+ li.style.borderRadius = "5px";
2974
+ li.style.transition = "all 0.2s ease";
2975
+ const info = document.createElement("div");
2976
+ info.style.flex = "1 1 auto";
2977
+ info.style.width = "100%";
2978
+ info.style.minWidth = "0";
2979
+ const nameContainer = document.createElement("div");
2980
+ nameContainer.style.display = "flex";
2981
+ nameContainer.style.alignItems = "center";
2982
+ nameContainer.style.marginBottom = "0";
2983
+ nameContainer.style.flexWrap = "wrap";
2984
+ nameContainer.style.gap = "4px";
2985
+ const name = document.createElement("div");
2986
+ name.style.fontWeight = "500";
2987
+ name.style.fontSize = "13px";
2988
+ name.textContent = d.name || d.id;
2989
+ const selectCheckbox = document.createElement("input");
2990
+ selectCheckbox.type = "checkbox";
2991
+ selectCheckbox.style.width = "auto";
2992
+ selectCheckbox.style.margin = "0 2px 0 0";
2993
+ selectCheckbox.checked = selectedIds.has(deviceId);
2994
+ if (alreadyAdded) {
2995
+ selectCheckbox.disabled = true;
2996
+ selectCheckbox.title = "Already configured";
2997
+ }
2998
+ selectCheckbox.onchange = () => {
2999
+ onToggleSelect?.(d, selectCheckbox.checked);
3000
+ window.dispatchEvent(new CustomEvent("discovery-selection-changed"));
3001
+ };
3002
+ nameContainer.appendChild(selectCheckbox);
3003
+ nameContainer.appendChild(name);
3004
+ if (d.firmwareUpdateAvailable) {
3005
+ const fwBadge = document.createElement("span");
3006
+ fwBadge.textContent = "Update Available";
3007
+ fwBadge.style.cssText = "background: #fb923c; color: #111; padding: 1px 7px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;";
3008
+ fwBadge.title = "A firmware update is available for this device.";
3009
+ nameContainer.appendChild(fwBadge);
3010
+ }
3011
+ let offline = false;
3012
+ let lastSeen = d.lastSeen || d.lastseen || d.updatedAt;
3013
+ if (typeof d.offline === "boolean") {
3014
+ offline = d.offline;
3015
+ } else if (lastSeen) {
3016
+ try {
3017
+ const last = new Date(lastSeen).getTime();
3018
+ if (!isNaN(last) && Date.now() - last > 1e3 * 60 * 60) {
3019
+ offline = true;
3020
+ }
3021
+ } catch {
3022
+ }
3023
+ }
3024
+ if (offline) {
3025
+ const offlineBadge = document.createElement("span");
3026
+ offlineBadge.textContent = "Offline";
3027
+ offlineBadge.style.cssText = "background: #dc2626; color: white; padding: 1px 7px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;";
3028
+ offlineBadge.title = "Device is offline or unreachable.";
3029
+ nameContainer.appendChild(offlineBadge);
3030
+ }
3031
+ const expandedDetails = document.createElement("div");
3032
+ expandedDetails.style.display = "none";
3033
+ expandedDetails.appendChild(renderDeviceDetailsPanel(d));
3034
+ const expandBtn = document.createElement("button");
3035
+ expandBtn.textContent = "\u25BE";
3036
+ expandBtn.title = "Show details";
3037
+ expandBtn.style.padding = "2px 6px";
3038
+ expandBtn.style.fontSize = "11px";
3039
+ expandBtn.style.marginLeft = "4px";
3040
+ expandBtn.style.background = "#e5e7eb";
3041
+ expandBtn.style.color = "#111827";
3042
+ expandBtn.style.transition = "transform 0.2s ease";
3043
+ expandBtn.onclick = () => {
3044
+ const isHidden = expandedDetails.style.display === "none";
3045
+ expandedDetails.style.display = isHidden ? "block" : "none";
3046
+ expandBtn.style.transform = isHidden ? "rotate(180deg)" : "rotate(0deg)";
3047
+ };
3048
+ nameContainer.appendChild(expandBtn);
3049
+ const duplicateBadge = document.createElement("span");
3050
+ duplicateBadge.textContent = alreadyAdded ? "\u2713 Already Added" : "\u2795 New Device";
3051
+ 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;";
3052
+ nameContainer.appendChild(duplicateBadge);
3053
+ if (d.connectionType) {
3054
+ const badge = renderConnectionBadge(d.connectionType);
3055
+ if (badge) {
3056
+ nameContainer.appendChild(badge);
3057
+ }
3058
+ }
3059
+ if (d.isIR) {
3060
+ nameContainer.appendChild(renderIRBadge());
3061
+ }
3062
+ if (d.rssi !== void 0 && d.rssi !== null && d.rssi !== 0) {
3063
+ nameContainer.appendChild(renderSignalBars(d.rssi));
3064
+ nameContainer.appendChild(renderSignalQualityBadge(d.rssi));
3065
+ }
3066
+ if (typeof d.battery === "number" && d.battery < 20) {
3067
+ const batteryWarn = document.createElement("span");
3068
+ batteryWarn.textContent = `\u26A0\uFE0F ${d.battery}%`;
3069
+ 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;";
3070
+ batteryWarn.title = d.battery < 10 ? "Battery critically low" : "Battery low";
3071
+ nameContainer.appendChild(batteryWarn);
3072
+ }
3073
+ const details = document.createElement("div");
3074
+ details.style.fontSize = "10px";
3075
+ details.style.opacity = "0.7";
3076
+ details.style.marginTop = "0";
3077
+ details.style.fontFamily = "monospace";
3078
+ details.style.whiteSpace = "normal";
3079
+ details.style.overflowWrap = "anywhere";
3080
+ details.style.wordBreak = "break-word";
3081
+ let detailsText = `ID: ${d.id} | Type: ${d.type} | Model: ${d.model || "N/A"}`;
3082
+ if (d.hubDeviceId) {
3083
+ detailsText += ` | Hub: ${d.hubDeviceId}`;
3084
+ }
3085
+ if (d.address) {
3086
+ detailsText += ` | MAC: ${d.address}`;
3087
+ }
3088
+ details.textContent = detailsText;
3089
+ info.appendChild(nameContainer);
3090
+ info.appendChild(details);
3091
+ info.appendChild(expandedDetails);
3092
+ const addBtn = document.createElement("button");
3093
+ addBtn.textContent = alreadyAdded ? "Already Added" : "Add to Config";
3094
+ addBtn.style.marginLeft = "0";
3095
+ addBtn.style.marginTop = "2px";
3096
+ addBtn.style.padding = "4px 9px";
3097
+ addBtn.style.fontSize = "11px";
3098
+ addBtn.style.whiteSpace = "nowrap";
3099
+ addBtn.style.flexShrink = "0";
3100
+ addBtn.disabled = alreadyAdded;
3101
+ if (alreadyAdded) {
3102
+ addBtn.style.opacity = "0.65";
3103
+ addBtn.style.cursor = "not-allowed";
3104
+ addBtn.style.background = "#6b7280";
3105
+ }
3106
+ addBtn.onclick = async () => {
3107
+ if (alreadyAdded) {
3108
+ return;
3109
+ }
3110
+ await addDeviceToConfig3(d);
3111
+ };
3112
+ if (alreadyAdded) {
3113
+ const viewBtn = document.createElement("button");
3114
+ viewBtn.textContent = "View in Config";
3115
+ viewBtn.className = "secondary";
3116
+ viewBtn.style.marginLeft = "0";
3117
+ viewBtn.style.padding = "4px 9px";
3118
+ viewBtn.style.fontSize = "11px";
3119
+ viewBtn.onclick = async () => {
3120
+ await loadConfiguredDevices2();
3121
+ scrollToConfiguredDevice(d.id);
3122
+ };
3123
+ li.appendChild(info);
3124
+ const actions2 = document.createElement("div");
3125
+ actions2.className = "device-actions";
3126
+ actions2.style.display = "flex";
3127
+ actions2.style.alignItems = "center";
3128
+ actions2.style.flexWrap = "wrap";
3129
+ actions2.style.justifyContent = "flex-start";
3130
+ actions2.style.marginLeft = "0";
3131
+ actions2.style.width = "100%";
3132
+ actions2.style.marginTop = "2px";
3133
+ actions2.style.gap = "5px";
3134
+ actions2.appendChild(viewBtn);
3135
+ actions2.appendChild(addBtn);
3136
+ actions2.appendChild(createConnectionTestControls(d));
3137
+ li.appendChild(actions2);
3138
+ ul.appendChild(li);
3139
+ continue;
3140
+ }
3141
+ const actions = document.createElement("div");
3142
+ actions.className = "device-actions";
3143
+ actions.style.display = "flex";
3144
+ actions.style.flexWrap = "wrap";
3145
+ actions.style.justifyContent = "flex-start";
3146
+ actions.style.marginLeft = "0";
3147
+ actions.style.width = "100%";
3148
+ actions.style.marginTop = "2px";
3149
+ actions.style.gap = "5px";
3150
+ actions.appendChild(addBtn);
3151
+ actions.appendChild(createConnectionTestControls(d));
3152
+ li.appendChild(info);
3153
+ li.appendChild(actions);
3154
+ ul.appendChild(li);
3155
+ }
3156
+ return ul;
3157
+ }
3158
+ function filterDevices(devices, connectionType = "all", searchQuery = "") {
3159
+ let filtered = [...devices];
3160
+ if (connectionType !== "all") {
3161
+ filtered = filtered.filter((d) => {
3162
+ if (connectionType === "ir") {
3163
+ return d.isIR === true;
3164
+ }
3165
+ if (connectionType === "ble") {
3166
+ return d.connectionType === "BLE" || d.connectionType?.includes("BLE");
3167
+ }
3168
+ if (connectionType === "api") {
3169
+ return d.connectionType === "OpenAPI" || d.connectionType === "API" || d.connectionType?.includes("API");
3170
+ }
3171
+ if (connectionType === "both") {
3172
+ return d.connectionType === "Both" || d.connectionType?.includes("Both");
3173
+ }
3174
+ return true;
3175
+ });
3176
+ }
3177
+ if (searchQuery.trim()) {
3178
+ const query = searchQuery.toLowerCase();
3179
+ filtered = filtered.filter((d) => {
3180
+ const name = (d.name || "").toLowerCase();
3181
+ const id = (d.id || "").toLowerCase();
3182
+ const type = (d.type || "").toLowerCase();
3183
+ const model = (d.model || "").toLowerCase();
3184
+ return name.includes(query) || id.includes(query) || type.includes(query) || model.includes(query);
3185
+ });
3186
+ }
3187
+ return filtered;
3188
+ }
3189
+ function sortDevices(devices, sortBy = "name") {
3190
+ const sorted = [...devices];
3191
+ switch (sortBy) {
3192
+ case "signal": {
3193
+ sorted.sort((a, b) => {
3194
+ const aRssi = a.rssi || 0;
3195
+ const bRssi = b.rssi || 0;
3196
+ return bRssi - aRssi;
3197
+ });
3198
+ break;
3199
+ }
3200
+ case "type": {
3201
+ sorted.sort((a, b) => {
3202
+ const aType = (a.type || "").localeCompare(b.type || "");
3203
+ return aType;
3204
+ });
3205
+ break;
3206
+ }
3207
+ case "connection": {
3208
+ const connectionOrder = {
3209
+ "Both": 0,
3210
+ "BLE": 1,
3211
+ "OpenAPI": 2,
3212
+ "API": 2,
3213
+ "Unknown": 3
3214
+ };
3215
+ sorted.sort((a, b) => {
3216
+ const aOrder = connectionOrder[a.connectionType || "Unknown"] ?? 3;
3217
+ const bOrder = connectionOrder[b.connectionType || "Unknown"] ?? 3;
3218
+ return aOrder - bOrder;
3219
+ });
3220
+ break;
3221
+ }
3222
+ case "name":
3223
+ default: {
3224
+ sorted.sort((a, b) => (a.name || a.id || "").localeCompare(b.name || b.id || ""));
3225
+ break;
3226
+ }
3227
+ }
3228
+ return sorted;
3229
+ }
3230
+ function getDiscoveryPreferences() {
3231
+ try {
3232
+ const stored = localStorage.getItem("discoveryPreferences");
3233
+ if (stored) {
3234
+ return JSON.parse(stored);
3235
+ }
3236
+ } catch (_e) {
3237
+ }
3238
+ return {
3239
+ connectionType: "all",
3240
+ sortBy: "name",
3241
+ searchQuery: ""
3242
+ };
3243
+ }
3244
+ function setDiscoveryPreferences(preferences) {
3245
+ try {
3246
+ localStorage.setItem("discoveryPreferences", JSON.stringify(preferences));
3247
+ } catch (_e) {
3248
+ }
3249
+ }
3250
+ function renderDeviceList(list) {
3251
+ const ul = document.getElementById("devices");
3252
+ const status = document.getElementById("status");
3253
+ const removeAllContainer = document.getElementById("removeAllContainer");
3254
+ if (!ul || !status) {
3255
+ return;
3256
+ }
3257
+ if (!list.length) {
3258
+ status.textContent = "No devices found in config.";
3259
+ ul.innerHTML = "";
3260
+ if (removeAllContainer) {
3261
+ removeAllContainer.style.display = "none";
3262
+ }
3263
+ return;
3264
+ }
3265
+ status.textContent = `Found ${list.length} device(s)`;
3266
+ ul.classList.add("device-grid");
3267
+ ul.innerHTML = "";
3268
+ if (removeAllContainer) {
3269
+ removeAllContainer.style.display = "block";
3270
+ }
3271
+ for (const d of list) {
3272
+ const li = document.createElement("li");
3273
+ li.className = "device-item";
3274
+ li.setAttribute("data-device-id", normalizeId2(d.id));
3275
+ li.style.display = "flex";
3276
+ li.style.flexDirection = "column";
3277
+ li.style.alignItems = "stretch";
3278
+ li.style.marginBottom = "0";
3279
+ const info = document.createElement("div");
3280
+ info.style.flex = "1 1 auto";
3281
+ info.style.width = "100%";
3282
+ info.style.minWidth = "0";
3283
+ const nameContainer = document.createElement("div");
3284
+ nameContainer.style.display = "flex";
3285
+ nameContainer.style.flexDirection = "column";
3286
+ nameContainer.style.alignItems = "flex-start";
3287
+ nameContainer.style.gap = "0";
3288
+ const name = document.createElement("div");
3289
+ name.style.fontWeight = "500";
3290
+ name.style.fontSize = "13px";
3291
+ name.textContent = d.name || d.id;
3292
+ const expandedDetails = document.createElement("div");
3293
+ expandedDetails.style.display = "none";
3294
+ expandedDetails.appendChild(renderDeviceDetailsPanel(d));
3295
+ const expandBtn = document.createElement("button");
3296
+ expandBtn.textContent = "\u25BE";
3297
+ expandBtn.title = "Show details";
3298
+ expandBtn.style.padding = "2px 6px";
3299
+ expandBtn.style.fontSize = "11px";
3300
+ expandBtn.style.marginLeft = "4px";
3301
+ expandBtn.style.background = "#e5e7eb";
3302
+ expandBtn.style.color = "#111827";
3303
+ expandBtn.style.transition = "transform 0.2s ease";
3304
+ expandBtn.onclick = () => {
3305
+ const isHidden = expandedDetails.style.display === "none";
3306
+ expandedDetails.style.display = isHidden ? "block" : "none";
3307
+ expandBtn.style.transform = isHidden ? "rotate(180deg)" : "rotate(0deg)";
3308
+ };
3309
+ const code = document.createElement("code");
3310
+ code.textContent = d.id;
3311
+ code.style.fontSize = "10px";
3312
+ code.style.opacity = "0.75";
3313
+ code.style.marginLeft = "0";
3314
+ code.style.whiteSpace = "normal";
3315
+ code.style.overflowWrap = "anywhere";
3316
+ code.style.wordBreak = "break-word";
3317
+ code.style.maxWidth = "100%";
3318
+ const headerRow = document.createElement("div");
3319
+ headerRow.style.display = "inline-flex";
3320
+ headerRow.style.alignItems = "center";
3321
+ headerRow.style.gap = "4px";
3322
+ headerRow.appendChild(name);
3323
+ headerRow.appendChild(expandBtn);
3324
+ nameContainer.appendChild(headerRow);
3325
+ nameContainer.appendChild(code);
3326
+ if (d.rssi !== void 0 && d.rssi !== null && d.rssi !== 0) {
3327
+ nameContainer.appendChild(renderSignalBars(d.rssi));
3328
+ nameContainer.appendChild(renderSignalQualityBadge(d.rssi));
3329
+ }
3330
+ const meta = document.createElement("div");
3331
+ meta.style.opacity = "0.75";
3332
+ meta.style.marginTop = "0";
3333
+ meta.style.fontSize = "11px";
3334
+ const typeText = d.type ? `type: ${d.type}` : "";
3335
+ const connText = d.connectionPreference ? `conn: ${d.connectionPreference}` : "";
3336
+ const roomText = d.room ? `room: ${d.room}` : "";
3337
+ meta.textContent = [typeText, connText, roomText].filter(Boolean).join(" | ");
3338
+ info.appendChild(nameContainer);
3339
+ info.appendChild(meta);
3340
+ info.appendChild(expandedDetails);
3341
+ const buttons = document.createElement("div");
3342
+ buttons.className = "device-actions";
3343
+ buttons.style.display = "flex";
3344
+ buttons.style.flexWrap = "wrap";
3345
+ buttons.style.justifyContent = "flex-start";
3346
+ buttons.style.marginLeft = "0";
3347
+ buttons.style.width = "100%";
3348
+ buttons.style.marginTop = "2px";
3349
+ buttons.style.gap = "5px";
3350
+ const editBtn = document.createElement("button");
3351
+ editBtn.textContent = "\u270F\uFE0F Edit";
3352
+ editBtn.style.padding = "4px 9px";
3353
+ editBtn.style.fontSize = "11px";
3354
+ editBtn.onclick = async () => {
3355
+ const { editDevice: editDevice2 } = await Promise.resolve().then(() => (init_modals(), modals_exports));
3356
+ await editDevice2(d);
3357
+ };
3358
+ const copyBtn = document.createElement("button");
3359
+ copyBtn.textContent = "Copy ID";
3360
+ copyBtn.style.padding = "4px 9px";
3361
+ copyBtn.style.fontSize = "11px";
3362
+ copyBtn.addEventListener("click", async () => {
3363
+ try {
3364
+ await navigator.clipboard.writeText(d.id);
3365
+ copyBtn.textContent = "Copied";
3366
+ copyBtn.classList.add("success");
3367
+ setTimeout(() => {
3368
+ copyBtn.textContent = "Copy ID";
3369
+ copyBtn.classList.remove("success");
3370
+ }, 1200);
3371
+ } catch (e) {
3372
+ alert("Failed to copy");
3373
+ }
3374
+ });
3375
+ const deleteBtn = document.createElement("button");
3376
+ deleteBtn.textContent = "\u{1F5D1}\uFE0F Delete";
3377
+ deleteBtn.style.padding = "4px 9px";
3378
+ deleteBtn.style.fontSize = "11px";
3379
+ deleteBtn.style.background = "#ef4444";
3380
+ deleteBtn.onclick = async () => {
3381
+ const { deleteDeviceFromConfig: deleteDeviceFromConfig2 } = await Promise.resolve().then(() => (init_devices_delete(), devices_delete_exports));
3382
+ await deleteDeviceFromConfig2(d.id, d.name || d.id);
3383
+ };
3384
+ buttons.appendChild(editBtn);
3385
+ buttons.appendChild(copyBtn);
3386
+ buttons.appendChild(createConnectionTestControls(d));
3387
+ buttons.appendChild(deleteBtn);
3388
+ li.appendChild(info);
3389
+ li.appendChild(buttons);
3390
+ ul.appendChild(li);
3391
+ }
3392
+ }
3393
+ var init_render = __esm({
3394
+ "src/homebridge-ui/public/js/render.ts"() {
3395
+ "use strict";
3396
+ }
3397
+ });
3398
+
3399
+ // src/homebridge-ui/public/js/devices.ts
3400
+ var devices_exports = {};
3401
+ __export(devices_exports, {
3402
+ addDeviceToConfig: () => addDeviceToConfig2,
3403
+ initRemoveAllButton: () => initRemoveAllButton,
3404
+ loadConfiguredDevices: () => loadConfiguredDevices
3405
+ });
3406
+ async function addDeviceToConfig2(device, options = {}) {
3407
+ const { refresh = true, showStatus = true } = options;
3408
+ try {
3409
+ const { importDiscoveredDevice: importDiscoveredDevice2 } = await Promise.resolve().then(() => (init_modals(), modals_exports));
3410
+ const importValues = await importDiscoveredDevice2(device);
3411
+ if (!importValues) {
3412
+ return { added: false };
3413
+ }
3414
+ showBusyUi();
3415
+ uiLog.info("Adding device to config:", device);
3416
+ const resp = await addDevice(device.id, importValues.configDeviceName, importValues.configDeviceType, {
3417
+ address: importValues.address,
3418
+ model: device.model,
3419
+ rssi: device.rssi,
3420
+ encryptionKey: importValues.encryptionKey,
3421
+ keyId: importValues.keyId,
3422
+ matterType: importValues.matterType
3423
+ });
3424
+ uiLog.info("Add device response:", resp);
3425
+ if (!resp || resp.success === false) {
3426
+ throw new Error(resp?.data?.message || "Failed to add device");
3427
+ }
3428
+ const alreadyExists = !!resp?.alreadyExists;
3429
+ const message = resp?.message || (alreadyExists ? `Device "${importValues.configDeviceName}" already in config` : `Device "${importValues.configDeviceName}" added successfully!`);
3430
+ if (alreadyExists) {
3431
+ toastInfo(message);
3432
+ } else {
3433
+ toastSuccess(message);
3434
+ }
3435
+ if (showStatus) {
3436
+ const status = document.getElementById("discoverStatus");
3437
+ if (status) {
3438
+ status.textContent = (alreadyExists ? "\u2022 " : "\u2713 ") + message;
3439
+ status.classList.remove("error");
3440
+ status.classList.add("success-msg");
3441
+ if (!alreadyExists) {
3442
+ const synced = await syncParentPluginConfigFromDisk(true);
3443
+ status.textContent += synced ? " - Config saved automatically." : " - Warning: config may not persist until you close/reopen settings.";
3444
+ if (synced) {
3445
+ toastSuccess("Configuration synced and saved automatically");
3446
+ } else {
3447
+ toastWarning("Configuration sync failed; close and reopen settings before Save");
3448
+ }
3449
+ }
3450
+ }
3451
+ }
3452
+ if (refresh) {
3453
+ await loadConfiguredDevices();
3454
+ }
3455
+ return { added: !alreadyExists };
3456
+ } catch (e) {
3457
+ const msg = e instanceof Error ? e.message : String(e);
3458
+ uiLog.error("Add device error:", msg);
3459
+ const status = document.getElementById("discoverStatus");
3460
+ if (status) {
3461
+ status.textContent = `\u2717 Error: ${msg}`;
3462
+ status.classList.add("error");
3463
+ }
3464
+ toastError(msg);
3465
+ return { added: false };
3466
+ } finally {
3467
+ hideBusyUi();
3468
+ }
3469
+ }
3470
+ async function initRemoveAllButton() {
3471
+ const removeAllBtn = document.getElementById("removeAllBtn");
3472
+ if (!removeAllBtn) {
3473
+ return;
3474
+ }
3475
+ removeAllBtn.addEventListener("click", async () => {
3476
+ const { deleteAllDevicesFromConfig: deleteAllDevicesFromConfig2 } = await Promise.resolve().then(() => (init_devices_delete(), devices_delete_exports));
3477
+ await deleteAllDevicesFromConfig2();
3478
+ });
3479
+ }
3480
+ async function loadConfiguredDevices() {
3481
+ const list = await fetchDevices();
3482
+ renderDeviceList(list);
3483
+ }
3484
+ var init_devices = __esm({
3485
+ "src/homebridge-ui/public/js/devices.ts"() {
3486
+ "use strict";
3487
+ init_api();
3488
+ init_logger();
3489
+ init_modal();
3490
+ init_render();
3491
+ init_toast();
3492
+ }
3493
+ });
3494
+
3495
+ // src/homebridge-ui/public/js/credentials.ts
3496
+ init_api();
3497
+ init_logger();
3498
+ init_modal();
3499
+ init_toast();
3500
+ async function loadCredentialStatus() {
3501
+ try {
3502
+ const creds = await fetchCredentialStatus();
3503
+ if (!creds) {
3504
+ uiLog.error("Failed to load credentials");
3505
+ return;
3506
+ }
3507
+ const tokenStatus = document.getElementById("tokenStatus");
3508
+ const secretStatus = document.getElementById("secretStatus");
3509
+ if (!tokenStatus || !secretStatus) {
3510
+ return;
3511
+ }
3512
+ if (creds.hasToken) {
3513
+ tokenStatus.textContent = `\u2713 Configured (${creds.tokenLength} characters)`;
3514
+ tokenStatus.classList.add("ok");
3515
+ } else {
3516
+ tokenStatus.textContent = "Not configured";
3517
+ tokenStatus.classList.remove("ok");
3518
+ }
3519
+ if (creds.hasSecret) {
3520
+ secretStatus.textContent = `\u2713 Configured (${creds.secretLength} characters)`;
3521
+ secretStatus.classList.add("ok");
3522
+ } else {
3523
+ secretStatus.textContent = "Not configured";
3524
+ secretStatus.classList.remove("ok");
3525
+ }
3526
+ } catch (e) {
3527
+ uiLog.error("Error loading credentials:", e);
3528
+ }
3529
+ }
3530
+ async function saveCredentials2() {
3531
+ const token = document.getElementById("token")?.value;
3532
+ const secret = document.getElementById("secret")?.value;
3533
+ const saveStatus = document.getElementById("saveStatus");
3534
+ const saveBtn = document.getElementById("saveBtn");
3535
+ if (!saveStatus || !saveBtn) {
3536
+ return;
3537
+ }
3538
+ if (!token || !secret) {
3539
+ saveStatus.textContent = "Please enter both token and secret";
3540
+ saveStatus.classList.add("error");
3541
+ toastWarning("Please enter both token and secret");
3542
+ return;
3543
+ }
3544
+ try {
3545
+ showBusyUi();
3546
+ saveBtn.disabled = true;
3547
+ saveBtn.textContent = "Saving...";
3548
+ uiLog.info("Saving credentials...");
3549
+ const result = await saveCredentials(token, secret);
3550
+ uiLog.info("Save response:", result);
3551
+ const message = result?.message || "Credentials saved successfully";
3552
+ saveStatus.textContent = `\u2713 ${message}`;
3553
+ saveStatus.classList.remove("error");
3554
+ saveStatus.classList.add("success-msg");
3555
+ toastSuccess(message);
3556
+ const synced = await syncParentPluginConfigFromDisk(true);
3557
+ if (synced) {
3558
+ saveStatus.textContent += " - Config saved automatically.";
3559
+ toastSuccess("Configuration synced and saved automatically");
3560
+ } else {
3561
+ toastWarning("Credentials saved, but configuration sync failed");
3562
+ }
3563
+ ;
3564
+ document.getElementById("token").value = "";
3565
+ document.getElementById("secret").value = "";
3566
+ setTimeout(() => loadCredentialStatus(), 500);
3567
+ setTimeout(() => {
3568
+ saveStatus.textContent = "";
3569
+ saveStatus.classList.remove("success-msg");
3570
+ }, 3e3);
3571
+ } catch (e) {
3572
+ uiLog.error("Save error:", e);
3573
+ saveStatus.textContent = `Error: ${e instanceof Error ? e.message : "Failed to save"}`;
3574
+ saveStatus.classList.add("error");
3575
+ toastError(e instanceof Error ? e.message : "Failed to save credentials");
3576
+ } finally {
3577
+ hideBusyUi();
3578
+ saveBtn.disabled = false;
3579
+ saveBtn.textContent = "Save Credentials";
3580
+ }
3581
+ }
3582
+
3583
+ // src/homebridge-ui/public/js/app.ts
3584
+ init_devices();
3585
+ init_discovery();
3586
+ window.loadCredentialStatus = loadCredentialStatus;
3587
+ window.saveCredentials = saveCredentials2;
3588
+ window.discoverDevices = discoverDevices2;
3589
+ async function init() {
3590
+ await loadCredentialStatus();
3591
+ await initializeDiscoverySettings();
3592
+ const list = await loadConfiguredDevices();
3593
+ await initRemoveAllButton();
3594
+ }
3595
+ if (document.readyState === "loading") {
3596
+ document.addEventListener("DOMContentLoaded", init);
3597
+ } else {
3598
+ init();
3599
+ }
3600
+ //# sourceMappingURL=app.js.map