@switchbot/homebridge-switchbot 5.0.0-beta.15 → 5.0.0-beta.150

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 (610) hide show
  1. package/.changeset/config.json +14 -0
  2. package/.github/ISSUE_TEMPLATE/e2e-verification.md +36 -0
  3. package/.github/copilot-instructions.md +39 -0
  4. package/.github/workflows/ci.yml +32 -0
  5. package/.github/workflows/manual-e2e.yml +115 -0
  6. package/.github/workflows/release.yml +0 -4
  7. package/.husky/pre-push +15 -0
  8. package/CHANGELOG.md +35 -0
  9. package/E2E-VERIFICATION.md +121 -0
  10. package/MIGRATION.md +54 -0
  11. package/README.md +136 -4
  12. package/TODO.md +263 -0
  13. package/config.schema.json +284 -14787
  14. package/dist/SwitchBotHAPPlatform.d.ts +133 -0
  15. package/dist/SwitchBotHAPPlatform.d.ts.map +1 -0
  16. package/dist/SwitchBotHAPPlatform.js +555 -0
  17. package/dist/SwitchBotHAPPlatform.js.map +1 -0
  18. package/dist/SwitchBotMatterPlatform.d.ts +141 -0
  19. package/dist/SwitchBotMatterPlatform.d.ts.map +1 -0
  20. package/dist/SwitchBotMatterPlatform.js +536 -0
  21. package/dist/SwitchBotMatterPlatform.js.map +1 -0
  22. package/dist/device-types.d.ts +31 -0
  23. package/dist/device-types.d.ts.map +1 -0
  24. package/dist/device-types.js +246 -0
  25. package/dist/device-types.js.map +1 -0
  26. package/dist/deviceCommandMapper.d.ts +10 -0
  27. package/dist/deviceCommandMapper.d.ts.map +1 -0
  28. package/dist/deviceCommandMapper.js +319 -0
  29. package/dist/deviceCommandMapper.js.map +1 -0
  30. package/dist/deviceFactory.d.ts +14 -0
  31. package/dist/deviceFactory.d.ts.map +1 -0
  32. package/dist/deviceFactory.js +159 -0
  33. package/dist/deviceFactory.js.map +1 -0
  34. package/dist/devices/deviceBase.d.ts +50 -0
  35. package/dist/devices/deviceBase.d.ts.map +1 -0
  36. package/dist/devices/deviceBase.js +119 -0
  37. package/dist/devices/deviceBase.js.map +1 -0
  38. package/dist/devices/genericDevice.d.ts +320 -0
  39. package/dist/devices/genericDevice.d.ts.map +1 -0
  40. package/dist/devices/genericDevice.js +1363 -0
  41. package/dist/devices/genericDevice.js.map +1 -0
  42. package/dist/errors.d.ts +38 -0
  43. package/dist/errors.d.ts.map +1 -0
  44. package/dist/errors.js +32 -0
  45. package/dist/errors.js.map +1 -0
  46. package/dist/homebridge-ui/endpoints/config.d.ts +3 -0
  47. package/dist/homebridge-ui/endpoints/config.d.ts.map +1 -0
  48. package/dist/homebridge-ui/endpoints/config.js +90 -0
  49. package/dist/homebridge-ui/endpoints/config.js.map +1 -0
  50. package/dist/homebridge-ui/endpoints/devices.d.ts +6 -0
  51. package/dist/homebridge-ui/endpoints/devices.d.ts.map +1 -0
  52. package/dist/homebridge-ui/endpoints/devices.js +81 -0
  53. package/dist/homebridge-ui/endpoints/devices.js.map +1 -0
  54. package/dist/homebridge-ui/endpoints/discovery.d.ts +7 -0
  55. package/dist/homebridge-ui/endpoints/discovery.d.ts.map +1 -0
  56. package/dist/homebridge-ui/endpoints/discovery.js +212 -0
  57. package/dist/homebridge-ui/endpoints/discovery.js.map +1 -0
  58. package/dist/homebridge-ui/public/css/styles.css +472 -0
  59. package/dist/homebridge-ui/public/index.html +210 -244
  60. package/dist/homebridge-ui/public/js/advanced-settings.d.ts +3 -0
  61. package/dist/homebridge-ui/public/js/advanced-settings.d.ts.map +1 -0
  62. package/dist/homebridge-ui/public/js/advanced-settings.js +95 -0
  63. package/dist/homebridge-ui/public/js/advanced-settings.js.map +1 -0
  64. package/dist/homebridge-ui/public/js/advanced-settings.ts +94 -0
  65. package/dist/homebridge-ui/public/js/api.d.ts +66 -0
  66. package/dist/homebridge-ui/public/js/api.d.ts.map +1 -0
  67. package/dist/homebridge-ui/public/js/api.js +281 -0
  68. package/dist/homebridge-ui/public/js/api.js.map +1 -0
  69. package/dist/homebridge-ui/public/js/api.ts +342 -0
  70. package/dist/homebridge-ui/public/js/app.d.ts +2 -0
  71. package/dist/homebridge-ui/public/js/app.d.ts.map +1 -0
  72. package/dist/homebridge-ui/public/js/app.js +3863 -0
  73. package/dist/homebridge-ui/public/js/app.js.map +7 -0
  74. package/dist/homebridge-ui/public/js/app.ts +22 -0
  75. package/dist/homebridge-ui/public/js/constants.d.ts +2 -0
  76. package/dist/homebridge-ui/public/js/constants.d.ts.map +1 -0
  77. package/dist/homebridge-ui/public/js/constants.js +2 -0
  78. package/dist/homebridge-ui/public/js/constants.js.map +1 -0
  79. package/dist/homebridge-ui/public/js/constants.ts +1 -0
  80. package/dist/homebridge-ui/public/js/credentials.d.ts +3 -0
  81. package/dist/homebridge-ui/public/js/credentials.d.ts.map +1 -0
  82. package/dist/homebridge-ui/public/js/credentials.js +99 -0
  83. package/dist/homebridge-ui/public/js/credentials.js.map +1 -0
  84. package/dist/homebridge-ui/public/js/credentials.ts +105 -0
  85. package/dist/homebridge-ui/public/js/devices-delete.d.ts +3 -0
  86. package/dist/homebridge-ui/public/js/devices-delete.d.ts.map +1 -0
  87. package/dist/homebridge-ui/public/js/devices-delete.js +199 -0
  88. package/dist/homebridge-ui/public/js/devices-delete.js.map +1 -0
  89. package/dist/homebridge-ui/public/js/devices-delete.ts +227 -0
  90. package/dist/homebridge-ui/public/js/devices.d.ts +9 -0
  91. package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -0
  92. package/dist/homebridge-ui/public/js/devices.js +96 -0
  93. package/dist/homebridge-ui/public/js/devices.js.map +1 -0
  94. package/dist/homebridge-ui/public/js/devices.ts +105 -0
  95. package/dist/homebridge-ui/public/js/discovery.d.ts +4 -0
  96. package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -0
  97. package/dist/homebridge-ui/public/js/discovery.js +1374 -0
  98. package/dist/homebridge-ui/public/js/discovery.js.map +1 -0
  99. package/dist/homebridge-ui/public/js/discovery.ts +1498 -0
  100. package/dist/homebridge-ui/public/js/logger.d.ts +7 -0
  101. package/dist/homebridge-ui/public/js/logger.d.ts.map +1 -0
  102. package/dist/homebridge-ui/public/js/logger.js +17 -0
  103. package/dist/homebridge-ui/public/js/logger.js.map +1 -0
  104. package/dist/homebridge-ui/public/js/logger.ts +17 -0
  105. package/dist/homebridge-ui/public/js/modal.d.ts +5 -0
  106. package/dist/homebridge-ui/public/js/modal.d.ts.map +1 -0
  107. package/dist/homebridge-ui/public/js/modal.js +26 -0
  108. package/dist/homebridge-ui/public/js/modal.js.map +1 -0
  109. package/dist/homebridge-ui/public/js/modal.ts +28 -0
  110. package/dist/homebridge-ui/public/js/modals.d.ts +15 -0
  111. package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -0
  112. package/dist/homebridge-ui/public/js/modals.js +673 -0
  113. package/dist/homebridge-ui/public/js/modals.js.map +1 -0
  114. package/dist/homebridge-ui/public/js/modals.ts +761 -0
  115. package/dist/homebridge-ui/public/js/render.d.ts +71 -0
  116. package/dist/homebridge-ui/public/js/render.d.ts.map +1 -0
  117. package/dist/homebridge-ui/public/js/render.js +953 -0
  118. package/dist/homebridge-ui/public/js/render.js.map +1 -0
  119. package/dist/homebridge-ui/public/js/render.ts +1077 -0
  120. package/dist/homebridge-ui/public/js/toast.d.ts +6 -0
  121. package/dist/homebridge-ui/public/js/toast.d.ts.map +1 -0
  122. package/dist/homebridge-ui/public/js/toast.js +29 -0
  123. package/dist/homebridge-ui/public/js/toast.js.map +1 -0
  124. package/dist/homebridge-ui/public/js/toast.ts +37 -0
  125. package/dist/homebridge-ui/public/js/types.d.ts +23 -0
  126. package/dist/homebridge-ui/public/js/types.d.ts.map +1 -0
  127. package/dist/homebridge-ui/public/js/types.js +2 -0
  128. package/dist/homebridge-ui/public/js/types.js.map +1 -0
  129. package/dist/homebridge-ui/public/js/types.ts +26 -0
  130. package/dist/homebridge-ui/server.js +9 -41
  131. package/dist/homebridge-ui/server.js.map +1 -1
  132. package/dist/homebridge-ui/utils/config-parser.d.ts +35 -0
  133. package/dist/homebridge-ui/utils/config-parser.d.ts.map +1 -0
  134. package/dist/homebridge-ui/utils/config-parser.js +87 -0
  135. package/dist/homebridge-ui/utils/config-parser.js.map +1 -0
  136. package/dist/homebridge-ui/utils/device-migration.d.ts +35 -0
  137. package/dist/homebridge-ui/utils/device-migration.d.ts.map +1 -0
  138. package/dist/homebridge-ui/utils/device-migration.js +111 -0
  139. package/dist/homebridge-ui/utils/device-migration.js.map +1 -0
  140. package/dist/homebridge-ui/utils/logger.d.ts +7 -0
  141. package/dist/homebridge-ui/utils/logger.d.ts.map +1 -0
  142. package/dist/homebridge-ui/utils/logger.js +17 -0
  143. package/dist/homebridge-ui/utils/logger.js.map +1 -0
  144. package/dist/index.d.ts +10 -0
  145. package/dist/index.d.ts.map +1 -1
  146. package/dist/index.js +12 -4
  147. package/dist/index.js.map +1 -1
  148. package/dist/settings.d.ts +11 -249
  149. package/dist/settings.d.ts.map +1 -1
  150. package/dist/settings.js +6 -30
  151. package/dist/settings.js.map +1 -1
  152. package/dist/switchbotClient.d.ts +28 -0
  153. package/dist/switchbotClient.d.ts.map +1 -0
  154. package/dist/switchbotClient.js +175 -0
  155. package/dist/switchbotClient.js.map +1 -0
  156. package/dist/utils.d.ts +92 -115
  157. package/dist/utils.d.ts.map +1 -1
  158. package/dist/utils.js +1117 -902
  159. package/dist/utils.js.map +1 -1
  160. package/docs/assets/highlight.css +28 -0
  161. package/docs/assets/icons.js +1 -1
  162. package/docs/assets/icons.svg +1 -1
  163. package/docs/assets/main.js +2 -2
  164. package/docs/assets/style.css +3 -3
  165. package/docs/index.html +148 -13
  166. package/docs/variables/default.html +3 -1
  167. package/eslint.config.js +7 -8
  168. package/nodemon.json +2 -2
  169. package/package.json +36 -32
  170. package/scripts/build-ui.js +37 -0
  171. package/scripts/e2e/README.md +25 -0
  172. package/scripts/e2e/curtain-e2e.sh +70 -0
  173. package/scripts/e2e/fan-e2e.sh +75 -0
  174. package/scripts/e2e/light-advanced-e2e.sh +97 -0
  175. package/scripts/e2e/light-e2e.sh +75 -0
  176. package/scripts/e2e/list-accessories.sh +19 -0
  177. package/scripts/e2e/lock-e2e.sh +65 -0
  178. package/scripts/free-dev-ports.mjs +105 -0
  179. package/scripts/generate-matter-maps.js +77 -0
  180. package/scripts/run-e2e-local.sh +14 -0
  181. package/scripts/sync-device-types.mjs +31 -0
  182. package/src/SwitchBotHAPPlatform.ts +558 -0
  183. package/src/SwitchBotMatterPlatform.ts +538 -0
  184. package/src/device-types.ts +261 -0
  185. package/src/deviceCommandMapper.ts +333 -0
  186. package/src/deviceFactory.ts +201 -0
  187. package/src/devices/deviceBase.ts +141 -0
  188. package/src/devices/genericDevice.ts +1337 -0
  189. package/src/errors.ts +35 -0
  190. package/src/homebridge-ui/endpoints/config.ts +110 -0
  191. package/src/homebridge-ui/endpoints/devices.ts +88 -0
  192. package/src/homebridge-ui/endpoints/discovery.ts +233 -0
  193. package/src/homebridge-ui/public/css/styles.css +472 -0
  194. package/src/homebridge-ui/public/index.html +210 -244
  195. package/src/homebridge-ui/public/js/advanced-settings.ts +94 -0
  196. package/src/homebridge-ui/public/js/api.ts +342 -0
  197. package/src/homebridge-ui/public/js/app.ts +22 -0
  198. package/src/homebridge-ui/public/js/constants.ts +1 -0
  199. package/src/homebridge-ui/public/js/credentials.ts +105 -0
  200. package/src/homebridge-ui/public/js/devices-delete.ts +227 -0
  201. package/src/homebridge-ui/public/js/devices.ts +105 -0
  202. package/src/homebridge-ui/public/js/discovery.ts +1498 -0
  203. package/src/homebridge-ui/public/js/logger.ts +17 -0
  204. package/src/homebridge-ui/public/js/modal.ts +28 -0
  205. package/src/homebridge-ui/public/js/modals.ts +761 -0
  206. package/src/homebridge-ui/public/js/render.ts +1077 -0
  207. package/src/homebridge-ui/public/js/toast.ts +37 -0
  208. package/src/homebridge-ui/public/js/types.ts +26 -0
  209. package/src/homebridge-ui/server.ts +9 -43
  210. package/src/homebridge-ui/utils/config-parser.ts +108 -0
  211. package/src/homebridge-ui/utils/device-migration.ts +144 -0
  212. package/src/homebridge-ui/utils/logger.ts +17 -0
  213. package/src/index.ts +12 -4
  214. package/src/settings.ts +14 -277
  215. package/src/switchbotClient.ts +181 -0
  216. package/src/utils.ts +1106 -900
  217. package/test/client/switchbot-client-debounce.spec.ts +35 -0
  218. package/test/client/switchbot-client-openapi.spec.ts +19 -0
  219. package/test/client/switchbotClient.spec.ts +23 -0
  220. package/test/device/device-mapping.spec.ts +23 -0
  221. package/test/device/deviceBase.spec.ts +26 -0
  222. package/test/device/deviceFactory-edge.spec.ts +15 -0
  223. package/test/device/deviceFactory.spec.ts +33 -0
  224. package/test/device/fan-swing.spec.ts +34 -0
  225. package/test/device/genericDevice-blepoll.spec.ts +47 -0
  226. package/test/device/irdevice.spec.ts +9 -0
  227. package/test/device/lock-users.spec.ts +35 -0
  228. package/test/device/matter-descriptors.spec.ts +22 -0
  229. package/test/device/matter-device-state.spec.ts +37 -0
  230. package/test/e2e/run-e2e.spec.ts +48 -0
  231. package/test/errors/errors.spec.ts +10 -0
  232. package/test/helpers/matter-harness.ts +64 -0
  233. package/test/homebridge-ui/server.spec.ts +9 -0
  234. package/test/platform/accessory-restore.spec.ts +37 -0
  235. package/test/platform/matter-childbridge.spec.ts +34 -0
  236. package/test/platform/matter-integration.spec.ts +33 -0
  237. package/test/platform/platform-edge.spec.ts +73 -0
  238. package/test/platform/platform.integration.spec.ts +34 -0
  239. package/test/utils/utils-extra.spec.ts +10 -0
  240. package/test/utils/utils.spec.ts +53 -0
  241. package/todo/TODO.md +80 -0
  242. package/vitest.config.ts +7 -0
  243. package/coverage/base.css +0 -224
  244. package/coverage/block-navigation.js +0 -87
  245. package/coverage/clover.xml +0 -15847
  246. package/coverage/coverage-final.json +0 -42
  247. package/coverage/docs/assets/dmt/dmt-component-data.js.html +0 -85
  248. package/coverage/docs/assets/dmt/dmt-components.js.html +0 -286
  249. package/coverage/docs/assets/dmt/index.html +0 -131
  250. package/coverage/docs/assets/hierarchy.js.html +0 -85
  251. package/coverage/docs/assets/icons.js.html +0 -136
  252. package/coverage/docs/assets/index.html +0 -146
  253. package/coverage/docs/assets/main.js.html +0 -265
  254. package/coverage/favicon.png +0 -0
  255. package/coverage/index.html +0 -191
  256. package/coverage/prettify.css +0 -1
  257. package/coverage/prettify.js +0 -2
  258. package/coverage/sort-arrow-sprite.png +0 -0
  259. package/coverage/sorter.js +0 -196
  260. package/coverage/src/device/blindtilt.ts.html +0 -3238
  261. package/coverage/src/device/bot.ts.html +0 -2803
  262. package/coverage/src/device/ceilinglight.ts.html +0 -2338
  263. package/coverage/src/device/colorbulb.ts.html +0 -2824
  264. package/coverage/src/device/contact.ts.html +0 -1465
  265. package/coverage/src/device/curtain.ts.html +0 -2869
  266. package/coverage/src/device/device.ts.html +0 -2500
  267. package/coverage/src/device/fan.ts.html +0 -2242
  268. package/coverage/src/device/hub.ts.html +0 -1408
  269. package/coverage/src/device/humidifier.ts.html +0 -2116
  270. package/coverage/src/device/index.html +0 -416
  271. package/coverage/src/device/iosensor.ts.html +0 -1375
  272. package/coverage/src/device/lightstrip.ts.html +0 -2617
  273. package/coverage/src/device/lock.ts.html +0 -1963
  274. package/coverage/src/device/meter.ts.html +0 -1372
  275. package/coverage/src/device/meterplus.ts.html +0 -1384
  276. package/coverage/src/device/meterpro.ts.html +0 -1618
  277. package/coverage/src/device/motion.ts.html +0 -1264
  278. package/coverage/src/device/plug.ts.html +0 -1372
  279. package/coverage/src/device/relayswitch.ts.html +0 -2284
  280. package/coverage/src/device/robotvacuumcleaner.ts.html +0 -1810
  281. package/coverage/src/device/waterdetector.ts.html +0 -1294
  282. package/coverage/src/homebridge-ui/index.html +0 -116
  283. package/coverage/src/homebridge-ui/server.ts.html +0 -229
  284. package/coverage/src/index.html +0 -161
  285. package/coverage/src/index.ts.html +0 -124
  286. package/coverage/src/irdevice/airconditioner.ts.html +0 -1687
  287. package/coverage/src/irdevice/airpurifier.ts.html +0 -844
  288. package/coverage/src/irdevice/camera.ts.html +0 -475
  289. package/coverage/src/irdevice/fan.ts.html +0 -766
  290. package/coverage/src/irdevice/index.html +0 -251
  291. package/coverage/src/irdevice/irdevice.ts.html +0 -1117
  292. package/coverage/src/irdevice/light.ts.html +0 -826
  293. package/coverage/src/irdevice/other.ts.html +0 -2458
  294. package/coverage/src/irdevice/tv.ts.html +0 -1222
  295. package/coverage/src/irdevice/vacuumcleaner.ts.html +0 -466
  296. package/coverage/src/irdevice/waterheater.ts.html +0 -469
  297. package/coverage/src/platform.ts.html +0 -8776
  298. package/coverage/src/settings.ts.html +0 -934
  299. package/coverage/src/utils.ts.html +0 -2092
  300. package/dist/baseMatterAccessory.test.d.ts +0 -2
  301. package/dist/baseMatterAccessory.test.d.ts.map +0 -1
  302. package/dist/baseMatterAccessory.test.js +0 -71
  303. package/dist/baseMatterAccessory.test.js.map +0 -1
  304. package/dist/devices-hap/airpurifier.d.ts +0 -54
  305. package/dist/devices-hap/airpurifier.d.ts.map +0 -1
  306. package/dist/devices-hap/airpurifier.js +0 -527
  307. package/dist/devices-hap/airpurifier.js.map +0 -1
  308. package/dist/devices-hap/blindtilt.d.ts +0 -90
  309. package/dist/devices-hap/blindtilt.d.ts.map +0 -1
  310. package/dist/devices-hap/blindtilt.js +0 -974
  311. package/dist/devices-hap/blindtilt.js.map +0 -1
  312. package/dist/devices-hap/bot.d.ts +0 -102
  313. package/dist/devices-hap/bot.d.ts.map +0 -1
  314. package/dist/devices-hap/bot.js +0 -811
  315. package/dist/devices-hap/bot.js.map +0 -1
  316. package/dist/devices-hap/ceilinglight.d.ts +0 -85
  317. package/dist/devices-hap/ceilinglight.d.ts.map +0 -1
  318. package/dist/devices-hap/ceilinglight.js +0 -701
  319. package/dist/devices-hap/ceilinglight.js.map +0 -1
  320. package/dist/devices-hap/colorbulb.d.ts +0 -88
  321. package/dist/devices-hap/colorbulb.d.ts.map +0 -1
  322. package/dist/devices-hap/colorbulb.js +0 -881
  323. package/dist/devices-hap/colorbulb.js.map +0 -1
  324. package/dist/devices-hap/contact.d.ts +0 -44
  325. package/dist/devices-hap/contact.d.ts.map +0 -1
  326. package/dist/devices-hap/contact.js +0 -409
  327. package/dist/devices-hap/contact.js.map +0 -1
  328. package/dist/devices-hap/curtain.d.ts +0 -73
  329. package/dist/devices-hap/curtain.d.ts.map +0 -1
  330. package/dist/devices-hap/curtain.js +0 -869
  331. package/dist/devices-hap/curtain.js.map +0 -1
  332. package/dist/devices-hap/device.d.ts +0 -98
  333. package/dist/devices-hap/device.d.ts.map +0 -1
  334. package/dist/devices-hap/device.js +0 -831
  335. package/dist/devices-hap/device.js.map +0 -1
  336. package/dist/devices-hap/fan.d.ts +0 -69
  337. package/dist/devices-hap/fan.d.ts.map +0 -1
  338. package/dist/devices-hap/fan.js +0 -649
  339. package/dist/devices-hap/fan.js.map +0 -1
  340. package/dist/devices-hap/hub.d.ts +0 -37
  341. package/dist/devices-hap/hub.d.ts.map +0 -1
  342. package/dist/devices-hap/hub.js +0 -392
  343. package/dist/devices-hap/hub.js.map +0 -1
  344. package/dist/devices-hap/humidifier.d.ts +0 -68
  345. package/dist/devices-hap/humidifier.d.ts.map +0 -1
  346. package/dist/devices-hap/humidifier.js +0 -628
  347. package/dist/devices-hap/humidifier.js.map +0 -1
  348. package/dist/devices-hap/iosensor.d.ts +0 -42
  349. package/dist/devices-hap/iosensor.d.ts.map +0 -1
  350. package/dist/devices-hap/iosensor.js +0 -382
  351. package/dist/devices-hap/iosensor.js.map +0 -1
  352. package/dist/devices-hap/lightstrip.d.ts +0 -79
  353. package/dist/devices-hap/lightstrip.d.ts.map +0 -1
  354. package/dist/devices-hap/lightstrip.js +0 -797
  355. package/dist/devices-hap/lightstrip.js.map +0 -1
  356. package/dist/devices-hap/lock.d.ts +0 -53
  357. package/dist/devices-hap/lock.d.ts.map +0 -1
  358. package/dist/devices-hap/lock.js +0 -561
  359. package/dist/devices-hap/lock.js.map +0 -1
  360. package/dist/devices-hap/meter.d.ts +0 -37
  361. package/dist/devices-hap/meter.d.ts.map +0 -1
  362. package/dist/devices-hap/meter.js +0 -379
  363. package/dist/devices-hap/meter.js.map +0 -1
  364. package/dist/devices-hap/meterplus.d.ts +0 -42
  365. package/dist/devices-hap/meterplus.d.ts.map +0 -1
  366. package/dist/devices-hap/meterplus.js +0 -384
  367. package/dist/devices-hap/meterplus.js.map +0 -1
  368. package/dist/devices-hap/meterpro.d.ts +0 -43
  369. package/dist/devices-hap/meterpro.d.ts.map +0 -1
  370. package/dist/devices-hap/meterpro.js +0 -468
  371. package/dist/devices-hap/meterpro.js.map +0 -1
  372. package/dist/devices-hap/motion.d.ts +0 -42
  373. package/dist/devices-hap/motion.d.ts.map +0 -1
  374. package/dist/devices-hap/motion.js +0 -345
  375. package/dist/devices-hap/motion.js.map +0 -1
  376. package/dist/devices-hap/plug.d.ts +0 -49
  377. package/dist/devices-hap/plug.d.ts.map +0 -1
  378. package/dist/devices-hap/plug.js +0 -395
  379. package/dist/devices-hap/plug.js.map +0 -1
  380. package/dist/devices-hap/relayswitch.d.ts +0 -96
  381. package/dist/devices-hap/relayswitch.d.ts.map +0 -1
  382. package/dist/devices-hap/relayswitch.js +0 -642
  383. package/dist/devices-hap/relayswitch.js.map +0 -1
  384. package/dist/devices-hap/robotvacuumcleaner.d.ts +0 -54
  385. package/dist/devices-hap/robotvacuumcleaner.d.ts.map +0 -1
  386. package/dist/devices-hap/robotvacuumcleaner.js +0 -523
  387. package/dist/devices-hap/robotvacuumcleaner.js.map +0 -1
  388. package/dist/devices-hap/waterdetector.d.ts +0 -41
  389. package/dist/devices-hap/waterdetector.d.ts.map +0 -1
  390. package/dist/devices-hap/waterdetector.js +0 -356
  391. package/dist/devices-hap/waterdetector.js.map +0 -1
  392. package/dist/devices-matter/BaseMatterAccessory.d.ts +0 -78
  393. package/dist/devices-matter/BaseMatterAccessory.d.ts.map +0 -1
  394. package/dist/devices-matter/BaseMatterAccessory.js +0 -244
  395. package/dist/devices-matter/BaseMatterAccessory.js.map +0 -1
  396. package/dist/devices-matter/ColorLightAccessory.d.ts +0 -20
  397. package/dist/devices-matter/ColorLightAccessory.d.ts.map +0 -1
  398. package/dist/devices-matter/ColorLightAccessory.js +0 -95
  399. package/dist/devices-matter/ColorLightAccessory.js.map +0 -1
  400. package/dist/devices-matter/ColorTemperatureLightAccessory.d.ts +0 -18
  401. package/dist/devices-matter/ColorTemperatureLightAccessory.d.ts.map +0 -1
  402. package/dist/devices-matter/ColorTemperatureLightAccessory.js +0 -76
  403. package/dist/devices-matter/ColorTemperatureLightAccessory.js.map +0 -1
  404. package/dist/devices-matter/ContactSensorAccessory.d.ts +0 -12
  405. package/dist/devices-matter/ContactSensorAccessory.d.ts.map +0 -1
  406. package/dist/devices-matter/ContactSensorAccessory.js +0 -34
  407. package/dist/devices-matter/ContactSensorAccessory.js.map +0 -1
  408. package/dist/devices-matter/DimmableLightAccessory.d.ts +0 -58
  409. package/dist/devices-matter/DimmableLightAccessory.d.ts.map +0 -1
  410. package/dist/devices-matter/DimmableLightAccessory.js +0 -167
  411. package/dist/devices-matter/DimmableLightAccessory.js.map +0 -1
  412. package/dist/devices-matter/DoorLockAccessory.d.ts +0 -14
  413. package/dist/devices-matter/DoorLockAccessory.d.ts.map +0 -1
  414. package/dist/devices-matter/DoorLockAccessory.js +0 -50
  415. package/dist/devices-matter/DoorLockAccessory.js.map +0 -1
  416. package/dist/devices-matter/ExtendedColorLightAccessory.d.ts +0 -21
  417. package/dist/devices-matter/ExtendedColorLightAccessory.d.ts.map +0 -1
  418. package/dist/devices-matter/ExtendedColorLightAccessory.js +0 -106
  419. package/dist/devices-matter/ExtendedColorLightAccessory.js.map +0 -1
  420. package/dist/devices-matter/FanAccessory.d.ts +0 -16
  421. package/dist/devices-matter/FanAccessory.d.ts.map +0 -1
  422. package/dist/devices-matter/FanAccessory.js +0 -81
  423. package/dist/devices-matter/FanAccessory.js.map +0 -1
  424. package/dist/devices-matter/HumiditySensorAccessory.d.ts +0 -12
  425. package/dist/devices-matter/HumiditySensorAccessory.d.ts.map +0 -1
  426. package/dist/devices-matter/HumiditySensorAccessory.js +0 -34
  427. package/dist/devices-matter/HumiditySensorAccessory.js.map +0 -1
  428. package/dist/devices-matter/LeakSensorAccessory.d.ts +0 -12
  429. package/dist/devices-matter/LeakSensorAccessory.d.ts.map +0 -1
  430. package/dist/devices-matter/LeakSensorAccessory.js +0 -33
  431. package/dist/devices-matter/LeakSensorAccessory.js.map +0 -1
  432. package/dist/devices-matter/LightSensorAccessory.d.ts +0 -12
  433. package/dist/devices-matter/LightSensorAccessory.d.ts.map +0 -1
  434. package/dist/devices-matter/LightSensorAccessory.js +0 -34
  435. package/dist/devices-matter/LightSensorAccessory.js.map +0 -1
  436. package/dist/devices-matter/OccupancySensorAccessory.d.ts +0 -12
  437. package/dist/devices-matter/OccupancySensorAccessory.d.ts.map +0 -1
  438. package/dist/devices-matter/OccupancySensorAccessory.js +0 -39
  439. package/dist/devices-matter/OccupancySensorAccessory.js.map +0 -1
  440. package/dist/devices-matter/OnOffLightAccessory.d.ts +0 -38
  441. package/dist/devices-matter/OnOffLightAccessory.d.ts.map +0 -1
  442. package/dist/devices-matter/OnOffLightAccessory.js +0 -110
  443. package/dist/devices-matter/OnOffLightAccessory.js.map +0 -1
  444. package/dist/devices-matter/OnOffOutletAccessory.d.ts +0 -14
  445. package/dist/devices-matter/OnOffOutletAccessory.d.ts.map +0 -1
  446. package/dist/devices-matter/OnOffOutletAccessory.js +0 -43
  447. package/dist/devices-matter/OnOffOutletAccessory.js.map +0 -1
  448. package/dist/devices-matter/OnOffSwitchAccessory.d.ts +0 -14
  449. package/dist/devices-matter/OnOffSwitchAccessory.d.ts.map +0 -1
  450. package/dist/devices-matter/OnOffSwitchAccessory.js +0 -42
  451. package/dist/devices-matter/OnOffSwitchAccessory.js.map +0 -1
  452. package/dist/devices-matter/RoboticVacuumAccessory.d.ts +0 -68
  453. package/dist/devices-matter/RoboticVacuumAccessory.d.ts.map +0 -1
  454. package/dist/devices-matter/RoboticVacuumAccessory.js +0 -334
  455. package/dist/devices-matter/RoboticVacuumAccessory.js.map +0 -1
  456. package/dist/devices-matter/SmokeCOAlarmAccessory.d.ts +0 -11
  457. package/dist/devices-matter/SmokeCOAlarmAccessory.d.ts.map +0 -1
  458. package/dist/devices-matter/SmokeCOAlarmAccessory.js +0 -49
  459. package/dist/devices-matter/SmokeCOAlarmAccessory.js.map +0 -1
  460. package/dist/devices-matter/TemperatureSensorAccessory.d.ts +0 -12
  461. package/dist/devices-matter/TemperatureSensorAccessory.d.ts.map +0 -1
  462. package/dist/devices-matter/TemperatureSensorAccessory.js +0 -36
  463. package/dist/devices-matter/TemperatureSensorAccessory.js.map +0 -1
  464. package/dist/devices-matter/ThermostatAccessory.d.ts +0 -19
  465. package/dist/devices-matter/ThermostatAccessory.d.ts.map +0 -1
  466. package/dist/devices-matter/ThermostatAccessory.js +0 -95
  467. package/dist/devices-matter/ThermostatAccessory.js.map +0 -1
  468. package/dist/devices-matter/VenetianBlindAccessory.d.ts +0 -19
  469. package/dist/devices-matter/VenetianBlindAccessory.d.ts.map +0 -1
  470. package/dist/devices-matter/VenetianBlindAccessory.js +0 -99
  471. package/dist/devices-matter/VenetianBlindAccessory.js.map +0 -1
  472. package/dist/devices-matter/WindowBlindAccessory.d.ts +0 -17
  473. package/dist/devices-matter/WindowBlindAccessory.d.ts.map +0 -1
  474. package/dist/devices-matter/WindowBlindAccessory.js +0 -80
  475. package/dist/devices-matter/WindowBlindAccessory.js.map +0 -1
  476. package/dist/devices-matter/custom/PowerStripAccessory.d.ts +0 -97
  477. package/dist/devices-matter/custom/PowerStripAccessory.d.ts.map +0 -1
  478. package/dist/devices-matter/custom/PowerStripAccessory.js +0 -265
  479. package/dist/devices-matter/custom/PowerStripAccessory.js.map +0 -1
  480. package/dist/devices-matter/custom/index.d.ts +0 -8
  481. package/dist/devices-matter/custom/index.d.ts.map +0 -1
  482. package/dist/devices-matter/custom/index.js +0 -8
  483. package/dist/devices-matter/custom/index.js.map +0 -1
  484. package/dist/devices-matter/index.d.ts +0 -29
  485. package/dist/devices-matter/index.d.ts.map +0 -1
  486. package/dist/devices-matter/index.js +0 -28
  487. package/dist/devices-matter/index.js.map +0 -1
  488. package/dist/index.test.d.ts +0 -2
  489. package/dist/index.test.d.ts.map +0 -1
  490. package/dist/index.test.js +0 -19
  491. package/dist/index.test.js.map +0 -1
  492. package/dist/irdevice/airconditioner.d.ts +0 -61
  493. package/dist/irdevice/airconditioner.d.ts.map +0 -1
  494. package/dist/irdevice/airconditioner.js +0 -472
  495. package/dist/irdevice/airconditioner.js.map +0 -1
  496. package/dist/irdevice/airpurifier.d.ts +0 -50
  497. package/dist/irdevice/airpurifier.d.ts.map +0 -1
  498. package/dist/irdevice/airpurifier.js +0 -213
  499. package/dist/irdevice/airpurifier.js.map +0 -1
  500. package/dist/irdevice/camera.d.ts +0 -32
  501. package/dist/irdevice/camera.d.ts.map +0 -1
  502. package/dist/irdevice/camera.js +0 -107
  503. package/dist/irdevice/camera.js.map +0 -1
  504. package/dist/irdevice/fan.d.ts +0 -36
  505. package/dist/irdevice/fan.d.ts.map +0 -1
  506. package/dist/irdevice/fan.js +0 -200
  507. package/dist/irdevice/fan.js.map +0 -1
  508. package/dist/irdevice/irdevice.d.ts +0 -68
  509. package/dist/irdevice/irdevice.d.ts.map +0 -1
  510. package/dist/irdevice/irdevice.js +0 -398
  511. package/dist/irdevice/irdevice.js.map +0 -1
  512. package/dist/irdevice/light.d.ts +0 -36
  513. package/dist/irdevice/light.d.ts.map +0 -1
  514. package/dist/irdevice/light.js +0 -206
  515. package/dist/irdevice/light.js.map +0 -1
  516. package/dist/irdevice/other.d.ts +0 -57
  517. package/dist/irdevice/other.d.ts.map +0 -1
  518. package/dist/irdevice/other.js +0 -778
  519. package/dist/irdevice/other.js.map +0 -1
  520. package/dist/irdevice/tv.d.ts +0 -45
  521. package/dist/irdevice/tv.d.ts.map +0 -1
  522. package/dist/irdevice/tv.js +0 -327
  523. package/dist/irdevice/tv.js.map +0 -1
  524. package/dist/irdevice/vacuumcleaner.d.ts +0 -28
  525. package/dist/irdevice/vacuumcleaner.d.ts.map +0 -1
  526. package/dist/irdevice/vacuumcleaner.js +0 -104
  527. package/dist/irdevice/vacuumcleaner.js.map +0 -1
  528. package/dist/irdevice/waterheater.d.ts +0 -30
  529. package/dist/irdevice/waterheater.d.ts.map +0 -1
  530. package/dist/irdevice/waterheater.js +0 -105
  531. package/dist/irdevice/waterheater.js.map +0 -1
  532. package/dist/platform-hap.d.ts +0 -145
  533. package/dist/platform-hap.d.ts.map +0 -1
  534. package/dist/platform-hap.js +0 -2823
  535. package/dist/platform-hap.js.map +0 -1
  536. package/dist/platform-matter.d.ts +0 -131
  537. package/dist/platform-matter.d.ts.map +0 -1
  538. package/dist/platform-matter.js +0 -1002
  539. package/dist/platform-matter.js.map +0 -1
  540. package/dist/utils.test.d.ts +0 -2
  541. package/dist/utils.test.d.ts.map +0 -1
  542. package/dist/utils.test.js +0 -95
  543. package/dist/utils.test.js.map +0 -1
  544. package/dist/verifyconfig.test.d.ts +0 -2
  545. package/dist/verifyconfig.test.d.ts.map +0 -1
  546. package/dist/verifyconfig.test.js +0 -167
  547. package/dist/verifyconfig.test.js.map +0 -1
  548. package/src/baseMatterAccessory.test.ts +0 -88
  549. package/src/custom.d.ts +0 -7
  550. package/src/devices-hap/airpurifier.ts +0 -563
  551. package/src/devices-hap/blindtilt.ts +0 -1049
  552. package/src/devices-hap/bot.ts +0 -900
  553. package/src/devices-hap/ceilinglight.ts +0 -742
  554. package/src/devices-hap/colorbulb.ts +0 -904
  555. package/src/devices-hap/contact.ts +0 -457
  556. package/src/devices-hap/curtain.ts +0 -944
  557. package/src/devices-hap/device.ts +0 -884
  558. package/src/devices-hap/fan.ts +0 -711
  559. package/src/devices-hap/hub.ts +0 -439
  560. package/src/devices-hap/humidifier.ts +0 -669
  561. package/src/devices-hap/iosensor.ts +0 -427
  562. package/src/devices-hap/lightstrip.ts +0 -836
  563. package/src/devices-hap/lock.ts +0 -620
  564. package/src/devices-hap/meter.ts +0 -426
  565. package/src/devices-hap/meterplus.ts +0 -430
  566. package/src/devices-hap/meterpro.ts +0 -522
  567. package/src/devices-hap/motion.ts +0 -390
  568. package/src/devices-hap/plug.ts +0 -423
  569. package/src/devices-hap/relayswitch.ts +0 -727
  570. package/src/devices-hap/robotvacuumcleaner.ts +0 -568
  571. package/src/devices-hap/waterdetector.ts +0 -400
  572. package/src/devices-matter/BaseMatterAccessory.ts +0 -273
  573. package/src/devices-matter/ColorLightAccessory.ts +0 -110
  574. package/src/devices-matter/ColorTemperatureLightAccessory.ts +0 -90
  575. package/src/devices-matter/ContactSensorAccessory.ts +0 -41
  576. package/src/devices-matter/DimmableLightAccessory.ts +0 -192
  577. package/src/devices-matter/DoorLockAccessory.ts +0 -60
  578. package/src/devices-matter/ExtendedColorLightAccessory.ts +0 -122
  579. package/src/devices-matter/FanAccessory.ts +0 -95
  580. package/src/devices-matter/HumiditySensorAccessory.ts +0 -41
  581. package/src/devices-matter/LeakSensorAccessory.ts +0 -40
  582. package/src/devices-matter/LightSensorAccessory.ts +0 -41
  583. package/src/devices-matter/OccupancySensorAccessory.ts +0 -48
  584. package/src/devices-matter/OnOffLightAccessory.ts +0 -125
  585. package/src/devices-matter/OnOffOutletAccessory.ts +0 -51
  586. package/src/devices-matter/OnOffSwitchAccessory.ts +0 -51
  587. package/src/devices-matter/RoboticVacuumAccessory.ts +0 -407
  588. package/src/devices-matter/SmokeCOAlarmAccessory.ts +0 -59
  589. package/src/devices-matter/TemperatureSensorAccessory.ts +0 -43
  590. package/src/devices-matter/ThermostatAccessory.ts +0 -110
  591. package/src/devices-matter/VenetianBlindAccessory.ts +0 -115
  592. package/src/devices-matter/WindowBlindAccessory.ts +0 -92
  593. package/src/devices-matter/custom/PowerStripAccessory.ts +0 -309
  594. package/src/devices-matter/custom/index.ts +0 -8
  595. package/src/devices-matter/index.ts +0 -29
  596. package/src/index.test.ts +0 -24
  597. package/src/irdevice/airconditioner.ts +0 -533
  598. package/src/irdevice/airpurifier.ts +0 -252
  599. package/src/irdevice/camera.ts +0 -129
  600. package/src/irdevice/fan.ts +0 -226
  601. package/src/irdevice/irdevice.ts +0 -435
  602. package/src/irdevice/light.ts +0 -246
  603. package/src/irdevice/other.ts +0 -790
  604. package/src/irdevice/tv.ts +0 -378
  605. package/src/irdevice/vacuumcleaner.ts +0 -126
  606. package/src/irdevice/waterheater.ts +0 -127
  607. package/src/platform-hap.ts +0 -2952
  608. package/src/platform-matter.ts +0 -1129
  609. package/src/utils.test.ts +0 -96
  610. package/src/verifyconfig.test.ts +0 -198
@@ -0,0 +1,1077 @@
1
+ // Move regexes to module scope to avoid re-compilation on every call
2
+ // import type { DEVICE_TYPES } from './constants.js' // Removed unused import
3
+
4
+ const SPACES_REGEX = /\s/g
5
+ const CAMELCASE_REGEX = /([A-Z])/g
6
+ const FIRST_CHAR_REGEX = /^./
7
+
8
+ /**
9
+ * Get RSSI signal quality level and color based on dBm value
10
+ * @param rssi Signal strength in dBm (typically -30 to -90)
11
+ * @returns Object with quality level, color, and description
12
+ */
13
+ export function getRssiSignalQuality(rssi: number | undefined): {
14
+ level: 'excellent' | 'good' | 'fair' | 'poor' | 'unknown'
15
+ color: string
16
+ bgColor: string
17
+ description: string
18
+ bars: number
19
+ } {
20
+ if (!rssi || rssi === 0) {
21
+ return {
22
+ level: 'unknown',
23
+ color: '#999',
24
+ bgColor: '#f5f5f5',
25
+ description: 'Signal strength unknown',
26
+ bars: 0,
27
+ }
28
+ }
29
+
30
+ const dbm = Math.floor(rssi)
31
+
32
+ if (dbm > -60) {
33
+ return {
34
+ level: 'excellent',
35
+ color: '#34a853',
36
+ bgColor: '#e8f5e9',
37
+ description: `Excellent (${dbm} dBm)`,
38
+ bars: 4,
39
+ }
40
+ } else if (dbm > -75) {
41
+ return {
42
+ level: 'good',
43
+ color: '#fbbc04',
44
+ bgColor: '#fffde7',
45
+ description: `Good (${dbm} dBm)`,
46
+ bars: 3,
47
+ }
48
+ } else if (dbm > -85) {
49
+ return {
50
+ level: 'fair',
51
+ color: '#ff9800',
52
+ bgColor: '#fff3e0',
53
+ description: `Fair (${dbm} dBm)`,
54
+ bars: 2,
55
+ }
56
+ } else {
57
+ return {
58
+ level: 'poor',
59
+ color: '#ea4335',
60
+ bgColor: '#ffebee',
61
+ description: `Poor (${dbm} dBm) - unreliable`,
62
+ bars: 1,
63
+ }
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Create visual signal strength indicator bars
69
+ * @param rssi Signal strength in dBm
70
+ * @returns HTML element showing filled bars
71
+ */
72
+ export function renderSignalBars(rssi: number | undefined): HTMLElement {
73
+ const quality = getRssiSignalQuality(rssi)
74
+
75
+ const container = document.createElement('span')
76
+ container.style.display = 'inline-flex'
77
+ container.style.gap = '2px'
78
+ container.style.alignItems = 'center'
79
+ container.style.marginLeft = '8px'
80
+ container.style.fontSize = '12px'
81
+
82
+ // Create 4 bars
83
+ for (let i = 1; i <= 4; i++) {
84
+ const bar = document.createElement('span')
85
+ bar.style.height = `${i * 3}px`
86
+ bar.style.width = '3px'
87
+ bar.style.borderRadius = '1px'
88
+ bar.style.border = `1px solid ${quality.color}`
89
+
90
+ if (i <= quality.bars) {
91
+ bar.style.backgroundColor = quality.color
92
+ } else {
93
+ bar.style.backgroundColor = 'transparent'
94
+ }
95
+
96
+ container.appendChild(bar)
97
+ }
98
+
99
+ // Add tooltip
100
+ container.title = quality.description
101
+
102
+ return container
103
+ }
104
+
105
+ /**
106
+ * Create signal quality badge with color
107
+ * @param rssi Signal strength in dBm
108
+ * @returns HTML element showing quality level
109
+ */
110
+ export function renderSignalQualityBadge(rssi: number | undefined): HTMLElement {
111
+ const quality = getRssiSignalQuality(rssi)
112
+
113
+ const badge = document.createElement('span')
114
+ badge.textContent = quality.level.charAt(0).toUpperCase() + quality.level.slice(1)
115
+ badge.style.cssText = `
116
+ background: ${quality.color};
117
+ color: white;
118
+ padding: 2px 6px;
119
+ border-radius: 3px;
120
+ font-size: 10px;
121
+ font-weight: 600;
122
+ margin-left: 8px;
123
+ `
124
+ badge.title = quality.description
125
+
126
+ return badge
127
+ }
128
+
129
+ export function renderBadge(text: string, style: string): HTMLElement {
130
+ const badge = document.createElement('span')
131
+ badge.textContent = text
132
+ badge.style.cssText = style
133
+ return badge
134
+ }
135
+
136
+ export function renderConnectionBadge(connectionType: string): HTMLElement | null {
137
+ if (!connectionType) {
138
+ return null
139
+ }
140
+
141
+ const badge = renderBadge(connectionType, '')
142
+
143
+ if (connectionType === 'BLE') {
144
+ badge.style.cssText
145
+ = 'background: #4285f4; color: white; padding: 2px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 8px;'
146
+ } else if (connectionType === 'Both') {
147
+ badge.style.cssText
148
+ = 'background: #34a853; color: white; padding: 2px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 8px;'
149
+ } else {
150
+ badge.style.cssText
151
+ = 'background: #9e9e9e; color: white; padding: 2px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 8px;'
152
+ }
153
+
154
+ return badge
155
+ }
156
+
157
+ export function renderIRBadge(): HTMLElement {
158
+ return renderBadge(
159
+ 'IR',
160
+ 'background: #ff6b35; color: white; padding: 2px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 8px;',
161
+ )
162
+ }
163
+
164
+ function normalizeId(value: any): string {
165
+ return String(value ?? '').trim().toLowerCase()
166
+ }
167
+
168
+ function scrollToConfiguredDevice(deviceId: string): void {
169
+ const normalizedId = normalizeId(deviceId)
170
+ const target = document.querySelector(`[data-device-id="${normalizedId}"]`) as HTMLElement | null
171
+ if (!target) {
172
+ return
173
+ }
174
+
175
+ target.scrollIntoView({ behavior: 'smooth', block: 'center' })
176
+ const originalOutline = target.style.outline
177
+ const originalBackground = target.style.background
178
+ target.style.outline = '2px solid var(--switchbot-red, #ef4444)'
179
+ target.style.background = 'rgba(239, 68, 68, 0.08)'
180
+ setTimeout(() => {
181
+ target.style.outline = originalOutline
182
+ target.style.background = originalBackground
183
+ }, 1800)
184
+ }
185
+
186
+ function createConnectionTestControls(device: any): HTMLElement {
187
+ const controls = document.createElement('div')
188
+ controls.style.display = 'inline-flex'
189
+ controls.style.alignItems = 'center'
190
+ controls.style.gap = '6px'
191
+
192
+ const button = document.createElement('button')
193
+ button.textContent = 'Test Connection'
194
+ button.className = 'secondary'
195
+ button.style.padding = '4px 9px'
196
+ button.style.fontSize = '11px'
197
+
198
+ const status = document.createElement('span')
199
+ status.style.fontSize = '10px'
200
+ status.style.opacity = '0.85'
201
+ status.style.whiteSpace = 'normal'
202
+ status.style.overflowWrap = 'anywhere'
203
+
204
+ button.onclick = async () => {
205
+ const startedAt = Date.now()
206
+ button.disabled = true
207
+ button.textContent = 'Testing...'
208
+ status.textContent = 'Checking...'
209
+ status.style.color = '#6b7280'
210
+
211
+ try {
212
+ const { testDeviceConnection } = await import('./api.js')
213
+ const result = await testDeviceConnection({
214
+ deviceId: String(device?.id || device?.deviceId || ''),
215
+ connectionType: device?.connectionType,
216
+ address: device?.address,
217
+ })
218
+
219
+ const measuredLatency = Number(result?.latencyMs) > 0
220
+ ? Number(result.latencyMs)
221
+ : Date.now() - startedAt
222
+
223
+ if (result?.success) {
224
+ const method = result?.method || 'Auto'
225
+ status.textContent = `✓ ${method} · ${measuredLatency}ms`
226
+ status.style.color = '#16a34a'
227
+ } else {
228
+ const detail = result?.message ? ` · ${result.message}` : ''
229
+ status.textContent = `✗ Failed · ${measuredLatency}ms${detail}`
230
+ status.style.color = '#dc2626'
231
+ }
232
+ } catch (e) {
233
+ status.textContent = `✗ Failed · ${Date.now() - startedAt}ms`
234
+ status.style.color = '#dc2626'
235
+ } finally {
236
+ button.disabled = false
237
+ button.textContent = 'Test Connection'
238
+ }
239
+ }
240
+
241
+ controls.appendChild(button)
242
+ controls.appendChild(status)
243
+ return controls
244
+ }
245
+
246
+ function formatLastSeen(value: any): string {
247
+ if (!value) {
248
+ return 'N/A'
249
+ }
250
+ try {
251
+ const date = new Date(value)
252
+ return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString()
253
+ } catch (_e) {
254
+ return String(value)
255
+ }
256
+ }
257
+
258
+ export function renderDeviceDetailsPanel(device: any): HTMLElement {
259
+ const details = document.createElement('div')
260
+ details.style.cssText = `
261
+ border-top: 1px solid #ddd;
262
+ padding: 8px;
263
+ background: #f9fafb;
264
+ border-radius: 4px;
265
+ font-size: 12px;
266
+ margin-top: 4px;
267
+ `
268
+
269
+ // --- Battery history trending ---
270
+ // Persist battery readings in localStorage per device
271
+ const batteryHistoryKey = `batteryHistory:${device?.id || device?.deviceId}`
272
+ let batteryHistory: Array<{ value: number, ts: number }> = []
273
+ try {
274
+ const raw = localStorage.getItem(batteryHistoryKey)
275
+ if (raw) {
276
+ batteryHistory = JSON.parse(raw)
277
+ }
278
+ } catch (e) {
279
+ // Optionally log or handle error
280
+ }
281
+ const now = Date.now()
282
+ if (typeof device?.battery === 'number') {
283
+ // Only add if different from last or >1h since last
284
+ const last = batteryHistory.at(-1)
285
+ if (!last || last.value !== device.battery || now - last.ts > 60 * 60 * 1000) {
286
+ batteryHistory.push({ value: device.battery, ts: now })
287
+ // Keep only last 30 entries (about a month if daily)
288
+ if (batteryHistory.length > 30) {
289
+ batteryHistory = batteryHistory.slice(-30)
290
+ }
291
+ try {
292
+ localStorage.setItem(batteryHistoryKey, JSON.stringify(batteryHistory))
293
+ } catch (e) {
294
+ // Optionally log or handle error
295
+ }
296
+ }
297
+ }
298
+
299
+ const rows: Array<{ label: string, value: string, copyable?: boolean }> = [
300
+ { label: 'Device ID', value: String(device?.id || device?.deviceId || 'N/A'), copyable: !!(device?.id || device?.deviceId) },
301
+ { label: 'MAC Address', value: String(device?.address || 'N/A'), copyable: !!device?.address },
302
+ { label: 'Device Type', value: String(device?.type || device?.configDeviceType || 'N/A') },
303
+ { label: 'Model', value: String(device?.model || 'N/A') },
304
+ { label: 'Hub ID', value: String(device?.hubDeviceId || 'N/A') },
305
+ { label: 'Battery', value: device?.battery !== undefined && device?.battery !== null ? `${device.battery}%` : 'N/A' },
306
+ { label: 'Firmware', value: String(device?.version || device?.firmware || 'N/A') },
307
+ { label: 'Cloud Service', value: device?.enabled === false ? 'Disabled' : 'Enabled' },
308
+ { label: 'Last Seen', value: formatLastSeen(device?.lastSeen || device?.lastseen || device?.updatedAt) },
309
+ ]
310
+
311
+ for (const row of rows) {
312
+ const line = document.createElement('div')
313
+ line.style.display = 'flex'
314
+ line.style.alignItems = 'center'
315
+ line.style.justifyContent = 'space-between'
316
+ line.style.gap = '8px'
317
+ line.style.padding = '2px 0'
318
+
319
+ const label = document.createElement('span')
320
+ label.style.fontWeight = '600'
321
+ label.style.minWidth = '110px'
322
+ label.textContent = `${row.label}:`
323
+
324
+ const valueWrap = document.createElement('span')
325
+ valueWrap.style.display = 'inline-flex'
326
+ valueWrap.style.alignItems = 'center'
327
+ valueWrap.style.gap = '6px'
328
+ valueWrap.style.flex = '1'
329
+ valueWrap.style.justifyContent = 'flex-end'
330
+ valueWrap.style.minWidth = '0'
331
+
332
+ const value = document.createElement('span')
333
+ value.style.fontFamily = 'monospace'
334
+ value.style.fontSize = '11px'
335
+ value.style.opacity = '0.9'
336
+ value.style.whiteSpace = 'normal'
337
+ value.style.overflowWrap = 'anywhere'
338
+ value.style.wordBreak = 'break-word'
339
+ value.style.textAlign = 'right'
340
+ value.textContent = row.value
341
+
342
+ valueWrap.appendChild(value)
343
+
344
+ if (row.copyable && row.value && row.value !== 'N/A') {
345
+ const copyBtn = document.createElement('button')
346
+ copyBtn.textContent = '📋'
347
+ copyBtn.title = `Copy ${row.label}`
348
+ copyBtn.style.padding = '2px 6px'
349
+ copyBtn.style.fontSize = '10px'
350
+ copyBtn.style.lineHeight = '1'
351
+ copyBtn.style.background = '#e5e7eb'
352
+ copyBtn.style.color = '#111827'
353
+
354
+ copyBtn.onclick = async () => {
355
+ try {
356
+ await navigator.clipboard.writeText(row.value)
357
+ copyBtn.textContent = '✓'
358
+ setTimeout(() => {
359
+ copyBtn.textContent = '📋'
360
+ }, 1200)
361
+ } catch (_e) {
362
+ copyBtn.textContent = '!'
363
+ setTimeout(() => {
364
+ copyBtn.textContent = '📋'
365
+ }, 1200)
366
+ }
367
+ }
368
+
369
+ valueWrap.appendChild(copyBtn)
370
+ }
371
+
372
+ line.appendChild(label)
373
+ line.appendChild(valueWrap)
374
+ details.appendChild(line)
375
+
376
+ // If this is the Battery row, add a sparkline chart below
377
+ if (row.label === 'Battery' && Array.isArray(batteryHistory) && batteryHistory.length > 1) {
378
+ const chart = document.createElement('div')
379
+ chart.style.margin = '2px 0 8px 0'
380
+ chart.style.width = '100%'
381
+ chart.style.height = '28px'
382
+ chart.style.display = 'flex'
383
+ // SVG sparkline
384
+ const w = 120
385
+ const h = 24
386
+ const pad = 2
387
+ const min = Math.min(...batteryHistory.map(b => b.value), 100)
388
+ const max = Math.max(...batteryHistory.map(b => b.value), 0)
389
+ const range = max - min || 1
390
+ const points = batteryHistory.map((b, i) => {
391
+ const x = pad + (i * (w - 2 * pad)) / (batteryHistory.length - 1)
392
+ const y = pad + (h - 2 * pad) * (1 - (b.value - min) / range)
393
+ return `${x},${y}`
394
+ }).join(' ')
395
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
396
+ svg.setAttribute('width', String(w))
397
+ svg.setAttribute('height', String(h))
398
+ svg.setAttribute('viewBox', `0 0 ${w} ${h}`)
399
+ svg.style.display = 'block'
400
+ svg.style.background = '#f3f4f6'
401
+ svg.style.borderRadius = '3px'
402
+ svg.style.marginTop = '2px'
403
+ svg.style.boxShadow = '0 1px 2px #0001'
404
+ // Polyline for trend
405
+ const polyline = document.createElementNS('http://www.w3.org/2000/svg', 'polyline')
406
+ polyline.setAttribute('points', points)
407
+ polyline.setAttribute('fill', 'none')
408
+ polyline.setAttribute('stroke', '#2563eb')
409
+ polyline.setAttribute('stroke-width', '2')
410
+ svg.appendChild(polyline)
411
+ // Dots for each point
412
+ batteryHistory.forEach((b, i) => {
413
+ const x = pad + (i * (w - 2 * pad)) / (batteryHistory.length - 1)
414
+ const y = pad + (h - 2 * pad) * (1 - (b.value - min) / range)
415
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle')
416
+ circle.setAttribute('cx', String(x))
417
+ circle.setAttribute('cy', String(y))
418
+ circle.setAttribute('r', '2.5')
419
+ circle.setAttribute('fill', '#2563eb')
420
+ svg.appendChild(circle)
421
+ })
422
+ // Min/max labels
423
+ const minLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text')
424
+ minLabel.setAttribute('x', '2')
425
+ minLabel.setAttribute('y', String(h - 2))
426
+ minLabel.setAttribute('font-size', '9')
427
+ minLabel.setAttribute('fill', '#888')
428
+ minLabel.textContent = `${min}%`
429
+ svg.appendChild(minLabel)
430
+ const maxLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text')
431
+ maxLabel.setAttribute('x', String(w - 18))
432
+ maxLabel.setAttribute('y', '10')
433
+ maxLabel.setAttribute('font-size', '9')
434
+ maxLabel.setAttribute('fill', '#888')
435
+ maxLabel.textContent = `${max}%`
436
+ svg.appendChild(maxLabel)
437
+ chart.appendChild(svg)
438
+ details.appendChild(chart)
439
+ }
440
+ }
441
+
442
+ // --- Expose advanced/extra features dynamically ---
443
+ const featureKeys = [
444
+ 'airQuality',
445
+ 'pm25',
446
+ 'pm10',
447
+ 'voc',
448
+ 'co2',
449
+ 'humidity',
450
+ 'temperature',
451
+ 'preset',
452
+ 'mode',
453
+ 'presetMode',
454
+ 'direction',
455
+ 'calibration',
456
+ 'multiCommand',
457
+ 'extendedInfo',
458
+ 'segmentedControl',
459
+ 'features',
460
+ 'capabilities',
461
+ 'state',
462
+ ]
463
+ const shown = new Set(rows.map(r => r.label.toLowerCase().replace(SPACES_REGEX, '')))
464
+ for (const key of featureKeys) {
465
+ if (device && device[key] !== undefined && !shown.has(key.toLowerCase())) {
466
+ const line = document.createElement('div')
467
+ line.style.display = 'flex'
468
+ line.style.alignItems = 'center'
469
+ line.style.justifyContent = 'space-between'
470
+ line.style.gap = '8px'
471
+ line.style.padding = '2px 0'
472
+
473
+ const label = document.createElement('span')
474
+ label.style.fontWeight = '600'
475
+ label.style.minWidth = '110px'
476
+ label.textContent = `${key.replace(CAMELCASE_REGEX, ' $1').replace(FIRST_CHAR_REGEX, s => s.toUpperCase())}:`
477
+
478
+ const value = document.createElement('span')
479
+ value.style.fontFamily = 'monospace'
480
+ value.style.fontSize = '11px'
481
+ value.style.opacity = '0.9'
482
+ value.style.whiteSpace = 'normal'
483
+ value.style.overflowWrap = 'anywhere'
484
+ value.style.wordBreak = 'break-word'
485
+ value.style.textAlign = 'right'
486
+ value.textContent = typeof device[key] === 'object' ? JSON.stringify(device[key]) : String(device[key])
487
+
488
+ line.appendChild(label)
489
+ line.appendChild(value)
490
+ details.appendChild(line)
491
+ }
492
+ }
493
+
494
+ return details
495
+ }
496
+
497
+ export async function renderDiscoveredDevices(
498
+ devices: any[],
499
+ options: {
500
+ configuredIds?: Set<string>
501
+ selectedIds?: Set<string>
502
+ onToggleSelect?: (device: any, selected: boolean) => void
503
+ } = {},
504
+ ): Promise<HTMLElement> {
505
+ const ul = document.createElement('ul')
506
+ ul.className = 'device-grid'
507
+ ul.style.maxHeight = '400px'
508
+ ul.style.overflowY = 'auto'
509
+ ul.style.marginTop = '12px'
510
+ ul.style.padding = '0'
511
+ ul.style.listStyle = 'none'
512
+
513
+ const { addDeviceToConfig } = await import('./discovery.js')
514
+ const { loadConfiguredDevices } = await import('./devices.js')
515
+ const configuredIds = options.configuredIds ?? new Set<string>()
516
+ const selectedIds = options.selectedIds ?? new Set<string>()
517
+ const onToggleSelect = options.onToggleSelect
518
+
519
+ for (const d of devices) {
520
+ const deviceId = normalizeId(d.id)
521
+ const alreadyAdded = configuredIds.has(deviceId)
522
+
523
+ const li = document.createElement('li')
524
+ li.className = 'device-item'
525
+ li.style.display = 'flex'
526
+ li.style.flexDirection = 'column'
527
+ li.style.alignItems = 'stretch'
528
+ li.style.justifyContent = 'flex-start'
529
+ li.style.padding = '5px 8px'
530
+ li.style.marginBottom = '0'
531
+ li.style.borderRadius = '5px'
532
+ li.style.transition = 'all 0.2s ease'
533
+
534
+ const info = document.createElement('div')
535
+ info.style.flex = '1 1 auto'
536
+ info.style.width = '100%'
537
+ info.style.minWidth = '0'
538
+
539
+ const nameContainer = document.createElement('div')
540
+ nameContainer.style.display = 'flex'
541
+ nameContainer.style.alignItems = 'center'
542
+ nameContainer.style.marginBottom = '0'
543
+ nameContainer.style.flexWrap = 'wrap'
544
+ nameContainer.style.gap = '4px'
545
+
546
+ const name = document.createElement('div')
547
+ name.style.fontWeight = '500'
548
+ name.style.fontSize = '13px'
549
+ name.textContent = d.name || d.id
550
+
551
+ const selectCheckbox = document.createElement('input')
552
+ selectCheckbox.type = 'checkbox'
553
+ selectCheckbox.style.width = 'auto'
554
+ selectCheckbox.style.margin = '0 2px 0 0'
555
+ selectCheckbox.checked = selectedIds.has(deviceId)
556
+ if (alreadyAdded) {
557
+ selectCheckbox.disabled = true
558
+ selectCheckbox.title = 'Already configured'
559
+ }
560
+ selectCheckbox.onchange = () => {
561
+ onToggleSelect?.(d, selectCheckbox.checked)
562
+ // Notify listeners (e.g., batch buttons) of selection change
563
+ window.dispatchEvent(new CustomEvent('discovery-selection-changed'))
564
+ }
565
+
566
+ nameContainer.appendChild(selectCheckbox)
567
+
568
+ nameContainer.appendChild(name)
569
+
570
+ // Show firmware update available indicator if present
571
+ if (d.firmwareUpdateAvailable) {
572
+ const fwBadge = document.createElement('span')
573
+ fwBadge.textContent = 'Update Available'
574
+ fwBadge.style.cssText = 'background: #fb923c; color: #111; padding: 1px 7px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;'
575
+ fwBadge.title = 'A firmware update is available for this device.'
576
+ nameContainer.appendChild(fwBadge)
577
+ }
578
+
579
+ // Show offline/unreachable indicator if device is offline
580
+ let offline = false
581
+ const lastSeen = d.lastSeen || d.lastseen || d.updatedAt
582
+ if (typeof d.offline === 'boolean') {
583
+ offline = d.offline
584
+ } else if (lastSeen) {
585
+ try {
586
+ const last = new Date(lastSeen).getTime()
587
+ if (!Number.isNaN(last)) {
588
+ if (Date.now() - last > 1000 * 60 * 60) { // 1 hour
589
+ offline = true
590
+ }
591
+ }
592
+ } catch {}
593
+ }
594
+ if (offline) {
595
+ const offlineBadge = document.createElement('span')
596
+ offlineBadge.textContent = 'Offline'
597
+ offlineBadge.style.cssText = 'background: #dc2626; color: white; padding: 1px 7px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;'
598
+ offlineBadge.title = 'Device is offline or unreachable.'
599
+ nameContainer.appendChild(offlineBadge)
600
+ }
601
+
602
+ const expandedDetails = document.createElement('div')
603
+ expandedDetails.style.display = 'none'
604
+ expandedDetails.appendChild(renderDeviceDetailsPanel(d))
605
+
606
+ const expandBtn = document.createElement('button')
607
+ expandBtn.textContent = '▾'
608
+ expandBtn.title = 'Show details'
609
+ expandBtn.style.padding = '2px 6px'
610
+ expandBtn.style.fontSize = '11px'
611
+ expandBtn.style.marginLeft = '4px'
612
+ expandBtn.style.background = '#e5e7eb'
613
+ expandBtn.style.color = '#111827'
614
+ expandBtn.style.transition = 'transform 0.2s ease'
615
+ expandBtn.onclick = () => {
616
+ const isHidden = expandedDetails.style.display === 'none'
617
+ expandedDetails.style.display = isHidden ? 'block' : 'none'
618
+ expandBtn.style.transform = isHidden ? 'rotate(180deg)' : 'rotate(0deg)'
619
+ }
620
+ nameContainer.appendChild(expandBtn)
621
+
622
+ const duplicateBadge = document.createElement('span')
623
+ duplicateBadge.textContent = alreadyAdded ? '✓ Already Added' : '➕ New Device'
624
+ duplicateBadge.style.cssText = alreadyAdded
625
+ ? 'background: #16a34a; color: white; padding: 1px 5px; border-radius: 3px; font-size: 9px; font-weight: 600;'
626
+ : 'background: #2563eb; color: white; padding: 1px 5px; border-radius: 3px; font-size: 9px; font-weight: 600;'
627
+ nameContainer.appendChild(duplicateBadge)
628
+
629
+ // Add connection type badge
630
+ if (d.connectionType) {
631
+ const badge = renderConnectionBadge(d.connectionType)
632
+ if (badge) {
633
+ nameContainer.appendChild(badge)
634
+ }
635
+ }
636
+
637
+ // Add IR badge if it's an IR device
638
+ if (d.isIR) {
639
+ nameContainer.appendChild(renderIRBadge())
640
+ }
641
+
642
+ // Add signal strength visualization (only for BLE/wireless devices)
643
+ if (d.rssi !== undefined && d.rssi !== null && d.rssi !== 0) {
644
+ nameContainer.appendChild(renderSignalBars(d.rssi))
645
+ nameContainer.appendChild(renderSignalQualityBadge(d.rssi))
646
+ }
647
+
648
+ // Add battery warning indicator if battery < 20%
649
+ if (typeof d.battery === 'number' && d.battery < 20) {
650
+ const batteryWarn = document.createElement('span')
651
+ batteryWarn.textContent = `⚠️ ${d.battery}%`
652
+ batteryWarn.style.cssText
653
+ = d.battery < 10
654
+ ? 'background: #dc2626; color: white; padding: 1px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;'
655
+ : 'background: #fbbf24; color: #111; padding: 1px 6px; border-radius: 3px; font-size: 10px; font-weight: 600; margin-left: 6px;'
656
+ batteryWarn.title = d.battery < 10 ? 'Battery critically low' : 'Battery low'
657
+ nameContainer.appendChild(batteryWarn)
658
+ }
659
+
660
+ const details = document.createElement('div')
661
+ details.style.fontSize = '10px'
662
+ details.style.opacity = '0.7'
663
+ details.style.marginTop = '0'
664
+ details.style.fontFamily = 'monospace'
665
+ details.style.whiteSpace = 'normal'
666
+ details.style.overflowWrap = 'anywhere'
667
+ details.style.wordBreak = 'break-word'
668
+
669
+ let detailsText = `ID: ${d.id} | Type: ${d.type} | Model: ${d.model || 'N/A'}`
670
+ if (d.hubDeviceId) {
671
+ detailsText += ` | Hub: ${d.hubDeviceId}`
672
+ }
673
+ if (d.address) {
674
+ detailsText += ` | MAC: ${d.address}`
675
+ }
676
+ details.textContent = detailsText
677
+
678
+ info.appendChild(nameContainer)
679
+ info.appendChild(details)
680
+ info.appendChild(expandedDetails)
681
+
682
+ const addBtn = document.createElement('button')
683
+ addBtn.textContent = alreadyAdded ? 'Already Added' : 'Add to Config'
684
+ addBtn.style.marginLeft = '0'
685
+ addBtn.style.marginTop = '2px'
686
+ addBtn.style.padding = '4px 9px'
687
+ addBtn.style.fontSize = '11px'
688
+ addBtn.style.whiteSpace = 'nowrap'
689
+ addBtn.style.flexShrink = '0'
690
+ addBtn.disabled = alreadyAdded
691
+ if (alreadyAdded) {
692
+ addBtn.style.opacity = '0.65'
693
+ addBtn.style.cursor = 'not-allowed'
694
+ addBtn.style.background = '#6b7280'
695
+ }
696
+ addBtn.onclick = async () => {
697
+ if (alreadyAdded) {
698
+ return
699
+ }
700
+ await addDeviceToConfig(d)
701
+ }
702
+
703
+ if (alreadyAdded) {
704
+ const viewBtn = document.createElement('button')
705
+ viewBtn.textContent = 'View in Config'
706
+ viewBtn.className = 'secondary'
707
+ viewBtn.style.marginLeft = '0'
708
+ viewBtn.style.padding = '4px 9px'
709
+ viewBtn.style.fontSize = '11px'
710
+ viewBtn.onclick = async () => {
711
+ await loadConfiguredDevices()
712
+ scrollToConfiguredDevice(d.id)
713
+ }
714
+ li.appendChild(info)
715
+ const actions = document.createElement('div')
716
+ actions.className = 'device-actions'
717
+ actions.style.display = 'flex'
718
+ actions.style.alignItems = 'center'
719
+ actions.style.flexWrap = 'wrap'
720
+ actions.style.justifyContent = 'flex-start'
721
+ actions.style.marginLeft = '0'
722
+ actions.style.width = '100%'
723
+ actions.style.marginTop = '2px'
724
+ actions.style.gap = '5px'
725
+ actions.appendChild(viewBtn)
726
+ actions.appendChild(addBtn)
727
+ actions.appendChild(createConnectionTestControls(d))
728
+ li.appendChild(actions)
729
+ ul.appendChild(li)
730
+ continue
731
+ }
732
+
733
+ const actions = document.createElement('div')
734
+ actions.className = 'device-actions'
735
+ actions.style.display = 'flex'
736
+ actions.style.flexWrap = 'wrap'
737
+ actions.style.justifyContent = 'flex-start'
738
+ actions.style.marginLeft = '0'
739
+ actions.style.width = '100%'
740
+ actions.style.marginTop = '2px'
741
+ actions.style.gap = '5px'
742
+ actions.appendChild(addBtn)
743
+ actions.appendChild(createConnectionTestControls(d))
744
+
745
+ li.appendChild(info)
746
+ li.appendChild(actions)
747
+ ul.appendChild(li)
748
+ }
749
+
750
+ return ul
751
+ }
752
+
753
+ /**
754
+ * Filter devices by connection type and search query
755
+ * @param devices Discovered devices array
756
+ * @param connectionType Filter: 'all' | 'ble' | 'api' | 'both' | 'ir'
757
+ * @param searchQuery Search term to match against name/id/type
758
+ * @returns Filtered devices array
759
+ */
760
+ export function filterDevices(
761
+ devices: any[],
762
+ connectionType: 'all' | 'ble' | 'api' | 'both' | 'ir' = 'all',
763
+ searchQuery = '',
764
+ ): any[] {
765
+ let filtered = [...devices]
766
+
767
+ // Filter by connection type
768
+ if (connectionType !== 'all') {
769
+ filtered = filtered.filter((d) => {
770
+ if (connectionType === 'ir') {
771
+ return d.isIR === true
772
+ }
773
+ if (connectionType === 'ble') {
774
+ return d.connectionType === 'BLE' || d.connectionType?.includes('BLE')
775
+ }
776
+ if (connectionType === 'api') {
777
+ return d.connectionType === 'OpenAPI' || d.connectionType === 'API' || d.connectionType?.includes('API')
778
+ }
779
+ if (connectionType === 'both') {
780
+ return d.connectionType === 'Both' || d.connectionType?.includes('Both')
781
+ }
782
+ return true
783
+ })
784
+ }
785
+
786
+ // Filter by search query
787
+ if (searchQuery.trim()) {
788
+ const query = searchQuery.toLowerCase()
789
+ filtered = filtered.filter((d) => {
790
+ const name = (d.name || '').toLowerCase()
791
+ const id = (d.id || '').toLowerCase()
792
+ const type = (d.type || '').toLowerCase()
793
+ const model = (d.model || '').toLowerCase()
794
+ return name.includes(query) || id.includes(query) || type.includes(query) || model.includes(query)
795
+ })
796
+ }
797
+
798
+ return filtered
799
+ }
800
+
801
+ /**
802
+ * Sort devices by specified criteria
803
+ * @param devices Devices array to sort
804
+ * @param sortBy Sort criterion: 'name' | 'signal' | 'type' | 'connection'
805
+ * @returns Sorted devices array
806
+ */
807
+ export function sortDevices(
808
+ devices: any[],
809
+ sortBy: 'name' | 'signal' | 'type' | 'connection' = 'name',
810
+ ): any[] {
811
+ const sorted = [...devices]
812
+
813
+ switch (sortBy) {
814
+ case 'signal': {
815
+ // Sort by RSSI descending (strongest signal first)
816
+ sorted.sort((a, b) => {
817
+ const aRssi = a.rssi || 0
818
+ const bRssi = b.rssi || 0
819
+ return bRssi - aRssi // Descending order (higher is stronger)
820
+ })
821
+ break
822
+ }
823
+ case 'type': {
824
+ // Sort by device type alphabetically
825
+ sorted.sort((a, b) => {
826
+ const aType = (a.type || '').localeCompare(b.type || '')
827
+ return aType
828
+ })
829
+ break
830
+ }
831
+ case 'connection': {
832
+ // Sort by connection type: Both > BLE > OpenAPI > Others
833
+ const connectionOrder: Record<string, number> = {
834
+ Both: 0,
835
+ BLE: 1,
836
+ OpenAPI: 2,
837
+ API: 2,
838
+ Unknown: 3,
839
+ }
840
+ sorted.sort((a, b) => {
841
+ const aOrder = connectionOrder[a.connectionType || 'Unknown'] ?? 3
842
+ const bOrder = connectionOrder[b.connectionType || 'Unknown'] ?? 3
843
+ return aOrder - bOrder
844
+ })
845
+ break
846
+ }
847
+ case 'name':
848
+ default: {
849
+ // Sort by name alphabetically
850
+ sorted.sort((a, b) => (a.name || a.id || '').localeCompare(b.name || b.id || ''))
851
+ break
852
+ }
853
+ }
854
+
855
+ return sorted
856
+ }
857
+
858
+ /**
859
+ * Get filter/sort preferences from localStorage
860
+ * @returns Object with current filter and sort preferences
861
+ */
862
+ export function getDiscoveryPreferences(): {
863
+ connectionType: 'all' | 'ble' | 'api' | 'both' | 'ir'
864
+ sortBy: 'name' | 'signal' | 'type' | 'connection'
865
+ searchQuery: string
866
+ } {
867
+ try {
868
+ const stored = localStorage.getItem('discoveryPreferences')
869
+ if (stored) {
870
+ return JSON.parse(stored)
871
+ }
872
+ } catch (_e) {
873
+ // Ignore parse errors
874
+ }
875
+
876
+ return {
877
+ connectionType: 'all',
878
+ sortBy: 'name',
879
+ searchQuery: '',
880
+ }
881
+ }
882
+
883
+ /**
884
+ * Save filter/sort preferences to localStorage
885
+ * @param preferences Preferences object to save
886
+ * @param preferences.connectionType Connection type filter
887
+ * @param preferences.sortBy Sort criterion
888
+ * @param preferences.searchQuery Search query string
889
+ */
890
+ export function setDiscoveryPreferences(preferences: {
891
+ connectionType: 'all' | 'ble' | 'api' | 'both' | 'ir'
892
+ sortBy: 'name' | 'signal' | 'type' | 'connection'
893
+ searchQuery: string
894
+ }): void {
895
+ try {
896
+ localStorage.setItem('discoveryPreferences', JSON.stringify(preferences))
897
+ } catch (_e) {
898
+ // Ignore storage errors
899
+ }
900
+ }
901
+
902
+ export function renderDeviceList(list: any[]): void {
903
+ const ul = document.getElementById('devices')
904
+ const status = document.getElementById('status')
905
+ const removeAllContainer = document.getElementById('removeAllContainer')
906
+
907
+ if (!ul || !status) {
908
+ return
909
+ }
910
+
911
+ if (!list.length) {
912
+ status.textContent = 'No devices found in config.'
913
+ ul.innerHTML = ''
914
+ // Hide remove all button when no devices
915
+ if (removeAllContainer) {
916
+ removeAllContainer.style.display = 'none'
917
+ }
918
+ return
919
+ }
920
+
921
+ status.textContent = `Found ${list.length} device(s)`
922
+ ul.classList.add('device-grid')
923
+ ul.innerHTML = ''
924
+
925
+ // Show remove all button when devices exist
926
+ if (removeAllContainer) {
927
+ removeAllContainer.style.display = 'block'
928
+ }
929
+
930
+ for (const d of list) {
931
+ const li = document.createElement('li')
932
+ li.className = 'device-item'
933
+ li.setAttribute('data-device-id', normalizeId(d.id))
934
+ li.style.display = 'flex'
935
+ li.style.flexDirection = 'column'
936
+ li.style.alignItems = 'stretch'
937
+ li.style.marginBottom = '0'
938
+
939
+ const info = document.createElement('div')
940
+ info.style.flex = '1 1 auto'
941
+ info.style.width = '100%'
942
+ info.style.minWidth = '0'
943
+
944
+ const nameContainer = document.createElement('div')
945
+ nameContainer.style.display = 'flex'
946
+ nameContainer.style.flexDirection = 'column'
947
+ nameContainer.style.alignItems = 'flex-start'
948
+ nameContainer.style.gap = '0'
949
+
950
+ const name = document.createElement('div')
951
+ name.style.fontWeight = '500'
952
+ name.style.fontSize = '13px'
953
+ name.textContent = d.name || d.id
954
+
955
+ const expandedDetails = document.createElement('div')
956
+ expandedDetails.style.display = 'none'
957
+ expandedDetails.appendChild(renderDeviceDetailsPanel(d))
958
+
959
+ const expandBtn = document.createElement('button')
960
+ expandBtn.textContent = '▾'
961
+ expandBtn.title = 'Show details'
962
+ expandBtn.style.padding = '2px 6px'
963
+ expandBtn.style.fontSize = '11px'
964
+ expandBtn.style.marginLeft = '4px'
965
+ expandBtn.style.background = '#e5e7eb'
966
+ expandBtn.style.color = '#111827'
967
+ expandBtn.style.transition = 'transform 0.2s ease'
968
+ expandBtn.onclick = () => {
969
+ const isHidden = expandedDetails.style.display === 'none'
970
+ expandedDetails.style.display = isHidden ? 'block' : 'none'
971
+ expandBtn.style.transform = isHidden ? 'rotate(180deg)' : 'rotate(0deg)'
972
+ }
973
+
974
+ const code = document.createElement('code')
975
+ code.textContent = d.id
976
+ code.style.fontSize = '10px'
977
+ code.style.opacity = '0.75'
978
+ code.style.marginLeft = '0'
979
+ code.style.whiteSpace = 'normal'
980
+ code.style.overflowWrap = 'anywhere'
981
+ code.style.wordBreak = 'break-word'
982
+ code.style.maxWidth = '100%'
983
+
984
+ const headerRow = document.createElement('div')
985
+ headerRow.style.display = 'inline-flex'
986
+ headerRow.style.alignItems = 'center'
987
+ headerRow.style.gap = '4px'
988
+ headerRow.appendChild(name)
989
+ headerRow.appendChild(expandBtn)
990
+
991
+ nameContainer.appendChild(headerRow)
992
+ nameContainer.appendChild(code)
993
+
994
+ // Add signal strength visualization if RSSI is available
995
+ if (d.rssi !== undefined && d.rssi !== null && d.rssi !== 0) {
996
+ nameContainer.appendChild(renderSignalBars(d.rssi))
997
+ nameContainer.appendChild(renderSignalQualityBadge(d.rssi))
998
+ }
999
+
1000
+ const meta = document.createElement('div')
1001
+ meta.style.opacity = '0.75'
1002
+ meta.style.marginTop = '0'
1003
+ meta.style.fontSize = '11px'
1004
+
1005
+ const typeText = d.type ? `type: ${d.type}` : ''
1006
+ const connText = d.connectionPreference ? `conn: ${d.connectionPreference}` : ''
1007
+ const roomText = d.room ? `room: ${d.room}` : ''
1008
+ meta.textContent = [typeText, connText, roomText].filter(Boolean).join(' | ')
1009
+
1010
+ info.appendChild(nameContainer)
1011
+ info.appendChild(meta)
1012
+ info.appendChild(expandedDetails)
1013
+
1014
+ const buttons = document.createElement('div')
1015
+ buttons.className = 'device-actions'
1016
+ buttons.style.display = 'flex'
1017
+ buttons.style.flexWrap = 'wrap'
1018
+ buttons.style.justifyContent = 'flex-start'
1019
+ buttons.style.marginLeft = '0'
1020
+ buttons.style.width = '100%'
1021
+ buttons.style.marginTop = '2px'
1022
+ buttons.style.gap = '5px'
1023
+
1024
+ const editBtn = document.createElement('button')
1025
+ editBtn.textContent = '✏️ Edit'
1026
+ editBtn.style.padding = '4px 9px'
1027
+ editBtn.style.fontSize = '11px'
1028
+ editBtn.onclick = async () => {
1029
+ const { editDevice } = await import('./modals.js')
1030
+ await editDevice(d)
1031
+ }
1032
+
1033
+ const copyBtn = document.createElement('button')
1034
+ copyBtn.textContent = 'Copy ID'
1035
+ copyBtn.style.padding = '4px 9px'
1036
+ copyBtn.style.fontSize = '11px'
1037
+ copyBtn.addEventListener('click', async () => {
1038
+ try {
1039
+ await navigator.clipboard.writeText(d.id)
1040
+ copyBtn.textContent = 'Copied'
1041
+ copyBtn.classList.add('success')
1042
+ setTimeout(() => {
1043
+ copyBtn.textContent = 'Copy ID'
1044
+ copyBtn.classList.remove('success')
1045
+ }, 1200)
1046
+ } catch (e) {
1047
+ copyBtn.textContent = 'Failed'
1048
+ copyBtn.classList.add('error')
1049
+ setTimeout(() => {
1050
+ copyBtn.textContent = 'Copy ID'
1051
+ copyBtn.classList.remove('error')
1052
+ }, 1200)
1053
+ }
1054
+ })
1055
+
1056
+ const deleteBtn = document.createElement('button')
1057
+ deleteBtn.textContent = '🗑️ Delete'
1058
+ deleteBtn.style.padding = '4px 9px'
1059
+ deleteBtn.style.fontSize = '11px'
1060
+ deleteBtn.style.background = '#ef4444'
1061
+ deleteBtn.onclick = async () => {
1062
+ const { deleteDeviceFromConfig } = await import('./devices-delete.js')
1063
+ await deleteDeviceFromConfig(d.id, d.name || d.id)
1064
+ }
1065
+
1066
+ buttons.appendChild(editBtn)
1067
+ buttons.appendChild(copyBtn)
1068
+ buttons.appendChild(createConnectionTestControls(d))
1069
+ buttons.appendChild(deleteBtn)
1070
+
1071
+ li.appendChild(info)
1072
+ li.appendChild(buttons)
1073
+ ul.appendChild(li)
1074
+ }
1075
+
1076
+ // No return value needed for void function
1077
+ }