@switchbot/homebridge-switchbot 5.0.0-beta.12 → 5.0.0-beta.120

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 (588) 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 +187 -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 +144 -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 +276 -0
  25. package/dist/devices/genericDevice.d.ts.map +1 -0
  26. package/dist/devices/genericDevice.js +1287 -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 +203 -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 +59 -0
  51. package/dist/homebridge-ui/public/js/api.d.ts.map +1 -0
  52. package/dist/homebridge-ui/public/js/api.js +219 -0
  53. package/dist/homebridge-ui/public/js/api.js.map +1 -0
  54. package/dist/homebridge-ui/public/js/api.ts +272 -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 +3475 -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 +93 -0
  78. package/dist/homebridge-ui/public/js/devices.js.map +1 -0
  79. package/dist/homebridge-ui/public/js/devices.ts +111 -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 +365 -0
  98. package/dist/homebridge-ui/public/js/modals.js.map +1 -0
  99. package/dist/homebridge-ui/public/js/modals.ts +416 -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 +1553 -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 +188 -0
  172. package/src/devices/deviceBase.ts +141 -0
  173. package/src/devices/genericDevice.ts +1232 -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 +225 -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 +272 -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 +111 -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 +416 -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 +1568 -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/devices-hap/airpurifier.d.ts +0 -54
  284. package/dist/devices-hap/airpurifier.d.ts.map +0 -1
  285. package/dist/devices-hap/airpurifier.js +0 -527
  286. package/dist/devices-hap/airpurifier.js.map +0 -1
  287. package/dist/devices-hap/blindtilt.d.ts +0 -90
  288. package/dist/devices-hap/blindtilt.d.ts.map +0 -1
  289. package/dist/devices-hap/blindtilt.js +0 -974
  290. package/dist/devices-hap/blindtilt.js.map +0 -1
  291. package/dist/devices-hap/bot.d.ts +0 -102
  292. package/dist/devices-hap/bot.d.ts.map +0 -1
  293. package/dist/devices-hap/bot.js +0 -811
  294. package/dist/devices-hap/bot.js.map +0 -1
  295. package/dist/devices-hap/ceilinglight.d.ts +0 -85
  296. package/dist/devices-hap/ceilinglight.d.ts.map +0 -1
  297. package/dist/devices-hap/ceilinglight.js +0 -701
  298. package/dist/devices-hap/ceilinglight.js.map +0 -1
  299. package/dist/devices-hap/colorbulb.d.ts +0 -88
  300. package/dist/devices-hap/colorbulb.d.ts.map +0 -1
  301. package/dist/devices-hap/colorbulb.js +0 -881
  302. package/dist/devices-hap/colorbulb.js.map +0 -1
  303. package/dist/devices-hap/contact.d.ts +0 -44
  304. package/dist/devices-hap/contact.d.ts.map +0 -1
  305. package/dist/devices-hap/contact.js +0 -409
  306. package/dist/devices-hap/contact.js.map +0 -1
  307. package/dist/devices-hap/curtain.d.ts +0 -73
  308. package/dist/devices-hap/curtain.d.ts.map +0 -1
  309. package/dist/devices-hap/curtain.js +0 -869
  310. package/dist/devices-hap/curtain.js.map +0 -1
  311. package/dist/devices-hap/device.d.ts +0 -98
  312. package/dist/devices-hap/device.d.ts.map +0 -1
  313. package/dist/devices-hap/device.js +0 -749
  314. package/dist/devices-hap/device.js.map +0 -1
  315. package/dist/devices-hap/fan.d.ts +0 -69
  316. package/dist/devices-hap/fan.d.ts.map +0 -1
  317. package/dist/devices-hap/fan.js +0 -649
  318. package/dist/devices-hap/fan.js.map +0 -1
  319. package/dist/devices-hap/hub.d.ts +0 -37
  320. package/dist/devices-hap/hub.d.ts.map +0 -1
  321. package/dist/devices-hap/hub.js +0 -392
  322. package/dist/devices-hap/hub.js.map +0 -1
  323. package/dist/devices-hap/humidifier.d.ts +0 -68
  324. package/dist/devices-hap/humidifier.d.ts.map +0 -1
  325. package/dist/devices-hap/humidifier.js +0 -628
  326. package/dist/devices-hap/humidifier.js.map +0 -1
  327. package/dist/devices-hap/iosensor.d.ts +0 -42
  328. package/dist/devices-hap/iosensor.d.ts.map +0 -1
  329. package/dist/devices-hap/iosensor.js +0 -382
  330. package/dist/devices-hap/iosensor.js.map +0 -1
  331. package/dist/devices-hap/lightstrip.d.ts +0 -79
  332. package/dist/devices-hap/lightstrip.d.ts.map +0 -1
  333. package/dist/devices-hap/lightstrip.js +0 -797
  334. package/dist/devices-hap/lightstrip.js.map +0 -1
  335. package/dist/devices-hap/lock.d.ts +0 -53
  336. package/dist/devices-hap/lock.d.ts.map +0 -1
  337. package/dist/devices-hap/lock.js +0 -561
  338. package/dist/devices-hap/lock.js.map +0 -1
  339. package/dist/devices-hap/meter.d.ts +0 -37
  340. package/dist/devices-hap/meter.d.ts.map +0 -1
  341. package/dist/devices-hap/meter.js +0 -379
  342. package/dist/devices-hap/meter.js.map +0 -1
  343. package/dist/devices-hap/meterplus.d.ts +0 -42
  344. package/dist/devices-hap/meterplus.d.ts.map +0 -1
  345. package/dist/devices-hap/meterplus.js +0 -384
  346. package/dist/devices-hap/meterplus.js.map +0 -1
  347. package/dist/devices-hap/meterpro.d.ts +0 -43
  348. package/dist/devices-hap/meterpro.d.ts.map +0 -1
  349. package/dist/devices-hap/meterpro.js +0 -468
  350. package/dist/devices-hap/meterpro.js.map +0 -1
  351. package/dist/devices-hap/motion.d.ts +0 -42
  352. package/dist/devices-hap/motion.d.ts.map +0 -1
  353. package/dist/devices-hap/motion.js +0 -345
  354. package/dist/devices-hap/motion.js.map +0 -1
  355. package/dist/devices-hap/plug.d.ts +0 -49
  356. package/dist/devices-hap/plug.d.ts.map +0 -1
  357. package/dist/devices-hap/plug.js +0 -395
  358. package/dist/devices-hap/plug.js.map +0 -1
  359. package/dist/devices-hap/relayswitch.d.ts +0 -96
  360. package/dist/devices-hap/relayswitch.d.ts.map +0 -1
  361. package/dist/devices-hap/relayswitch.js +0 -642
  362. package/dist/devices-hap/relayswitch.js.map +0 -1
  363. package/dist/devices-hap/robotvacuumcleaner.d.ts +0 -54
  364. package/dist/devices-hap/robotvacuumcleaner.d.ts.map +0 -1
  365. package/dist/devices-hap/robotvacuumcleaner.js +0 -523
  366. package/dist/devices-hap/robotvacuumcleaner.js.map +0 -1
  367. package/dist/devices-hap/waterdetector.d.ts +0 -41
  368. package/dist/devices-hap/waterdetector.d.ts.map +0 -1
  369. package/dist/devices-hap/waterdetector.js +0 -356
  370. package/dist/devices-hap/waterdetector.js.map +0 -1
  371. package/dist/devices-matter/BaseMatterAccessory.d.ts +0 -63
  372. package/dist/devices-matter/BaseMatterAccessory.d.ts.map +0 -1
  373. package/dist/devices-matter/BaseMatterAccessory.js +0 -100
  374. package/dist/devices-matter/BaseMatterAccessory.js.map +0 -1
  375. package/dist/devices-matter/ColorLightAccessory.d.ts +0 -20
  376. package/dist/devices-matter/ColorLightAccessory.d.ts.map +0 -1
  377. package/dist/devices-matter/ColorLightAccessory.js +0 -95
  378. package/dist/devices-matter/ColorLightAccessory.js.map +0 -1
  379. package/dist/devices-matter/ColorTemperatureLightAccessory.d.ts +0 -18
  380. package/dist/devices-matter/ColorTemperatureLightAccessory.d.ts.map +0 -1
  381. package/dist/devices-matter/ColorTemperatureLightAccessory.js +0 -78
  382. package/dist/devices-matter/ColorTemperatureLightAccessory.js.map +0 -1
  383. package/dist/devices-matter/ContactSensorAccessory.d.ts +0 -12
  384. package/dist/devices-matter/ContactSensorAccessory.d.ts.map +0 -1
  385. package/dist/devices-matter/ContactSensorAccessory.js +0 -34
  386. package/dist/devices-matter/ContactSensorAccessory.js.map +0 -1
  387. package/dist/devices-matter/DimmableLightAccessory.d.ts +0 -58
  388. package/dist/devices-matter/DimmableLightAccessory.d.ts.map +0 -1
  389. package/dist/devices-matter/DimmableLightAccessory.js +0 -167
  390. package/dist/devices-matter/DimmableLightAccessory.js.map +0 -1
  391. package/dist/devices-matter/DoorLockAccessory.d.ts +0 -14
  392. package/dist/devices-matter/DoorLockAccessory.d.ts.map +0 -1
  393. package/dist/devices-matter/DoorLockAccessory.js +0 -50
  394. package/dist/devices-matter/DoorLockAccessory.js.map +0 -1
  395. package/dist/devices-matter/ExtendedColorLightAccessory.d.ts +0 -21
  396. package/dist/devices-matter/ExtendedColorLightAccessory.d.ts.map +0 -1
  397. package/dist/devices-matter/ExtendedColorLightAccessory.js +0 -107
  398. package/dist/devices-matter/ExtendedColorLightAccessory.js.map +0 -1
  399. package/dist/devices-matter/FanAccessory.d.ts +0 -16
  400. package/dist/devices-matter/FanAccessory.d.ts.map +0 -1
  401. package/dist/devices-matter/FanAccessory.js +0 -81
  402. package/dist/devices-matter/FanAccessory.js.map +0 -1
  403. package/dist/devices-matter/HumiditySensorAccessory.d.ts +0 -12
  404. package/dist/devices-matter/HumiditySensorAccessory.d.ts.map +0 -1
  405. package/dist/devices-matter/HumiditySensorAccessory.js +0 -34
  406. package/dist/devices-matter/HumiditySensorAccessory.js.map +0 -1
  407. package/dist/devices-matter/LeakSensorAccessory.d.ts +0 -12
  408. package/dist/devices-matter/LeakSensorAccessory.d.ts.map +0 -1
  409. package/dist/devices-matter/LeakSensorAccessory.js +0 -33
  410. package/dist/devices-matter/LeakSensorAccessory.js.map +0 -1
  411. package/dist/devices-matter/LightSensorAccessory.d.ts +0 -12
  412. package/dist/devices-matter/LightSensorAccessory.d.ts.map +0 -1
  413. package/dist/devices-matter/LightSensorAccessory.js +0 -34
  414. package/dist/devices-matter/LightSensorAccessory.js.map +0 -1
  415. package/dist/devices-matter/OccupancySensorAccessory.d.ts +0 -12
  416. package/dist/devices-matter/OccupancySensorAccessory.d.ts.map +0 -1
  417. package/dist/devices-matter/OccupancySensorAccessory.js +0 -39
  418. package/dist/devices-matter/OccupancySensorAccessory.js.map +0 -1
  419. package/dist/devices-matter/OnOffLightAccessory.d.ts +0 -38
  420. package/dist/devices-matter/OnOffLightAccessory.d.ts.map +0 -1
  421. package/dist/devices-matter/OnOffLightAccessory.js +0 -118
  422. package/dist/devices-matter/OnOffLightAccessory.js.map +0 -1
  423. package/dist/devices-matter/OnOffOutletAccessory.d.ts +0 -12
  424. package/dist/devices-matter/OnOffOutletAccessory.d.ts.map +0 -1
  425. package/dist/devices-matter/OnOffOutletAccessory.js +0 -40
  426. package/dist/devices-matter/OnOffOutletAccessory.js.map +0 -1
  427. package/dist/devices-matter/OnOffSwitchAccessory.d.ts +0 -14
  428. package/dist/devices-matter/OnOffSwitchAccessory.d.ts.map +0 -1
  429. package/dist/devices-matter/OnOffSwitchAccessory.js +0 -42
  430. package/dist/devices-matter/OnOffSwitchAccessory.js.map +0 -1
  431. package/dist/devices-matter/RoboticVacuumAccessory.d.ts +0 -68
  432. package/dist/devices-matter/RoboticVacuumAccessory.d.ts.map +0 -1
  433. package/dist/devices-matter/RoboticVacuumAccessory.js +0 -334
  434. package/dist/devices-matter/RoboticVacuumAccessory.js.map +0 -1
  435. package/dist/devices-matter/SmokeCOAlarmAccessory.d.ts +0 -11
  436. package/dist/devices-matter/SmokeCOAlarmAccessory.d.ts.map +0 -1
  437. package/dist/devices-matter/SmokeCOAlarmAccessory.js +0 -49
  438. package/dist/devices-matter/SmokeCOAlarmAccessory.js.map +0 -1
  439. package/dist/devices-matter/TemperatureSensorAccessory.d.ts +0 -12
  440. package/dist/devices-matter/TemperatureSensorAccessory.d.ts.map +0 -1
  441. package/dist/devices-matter/TemperatureSensorAccessory.js +0 -36
  442. package/dist/devices-matter/TemperatureSensorAccessory.js.map +0 -1
  443. package/dist/devices-matter/ThermostatAccessory.d.ts +0 -19
  444. package/dist/devices-matter/ThermostatAccessory.d.ts.map +0 -1
  445. package/dist/devices-matter/ThermostatAccessory.js +0 -95
  446. package/dist/devices-matter/ThermostatAccessory.js.map +0 -1
  447. package/dist/devices-matter/VenetianBlindAccessory.d.ts +0 -19
  448. package/dist/devices-matter/VenetianBlindAccessory.d.ts.map +0 -1
  449. package/dist/devices-matter/VenetianBlindAccessory.js +0 -99
  450. package/dist/devices-matter/VenetianBlindAccessory.js.map +0 -1
  451. package/dist/devices-matter/WindowBlindAccessory.d.ts +0 -17
  452. package/dist/devices-matter/WindowBlindAccessory.d.ts.map +0 -1
  453. package/dist/devices-matter/WindowBlindAccessory.js +0 -80
  454. package/dist/devices-matter/WindowBlindAccessory.js.map +0 -1
  455. package/dist/devices-matter/custom/PowerStripAccessory.d.ts +0 -97
  456. package/dist/devices-matter/custom/PowerStripAccessory.d.ts.map +0 -1
  457. package/dist/devices-matter/custom/PowerStripAccessory.js +0 -265
  458. package/dist/devices-matter/custom/PowerStripAccessory.js.map +0 -1
  459. package/dist/devices-matter/custom/index.d.ts +0 -8
  460. package/dist/devices-matter/custom/index.d.ts.map +0 -1
  461. package/dist/devices-matter/custom/index.js +0 -8
  462. package/dist/devices-matter/custom/index.js.map +0 -1
  463. package/dist/devices-matter/index.d.ts +0 -29
  464. package/dist/devices-matter/index.d.ts.map +0 -1
  465. package/dist/devices-matter/index.js +0 -28
  466. package/dist/devices-matter/index.js.map +0 -1
  467. package/dist/index.test.d.ts +0 -2
  468. package/dist/index.test.d.ts.map +0 -1
  469. package/dist/index.test.js +0 -19
  470. package/dist/index.test.js.map +0 -1
  471. package/dist/irdevice/airconditioner.d.ts +0 -61
  472. package/dist/irdevice/airconditioner.d.ts.map +0 -1
  473. package/dist/irdevice/airconditioner.js +0 -472
  474. package/dist/irdevice/airconditioner.js.map +0 -1
  475. package/dist/irdevice/airpurifier.d.ts +0 -50
  476. package/dist/irdevice/airpurifier.d.ts.map +0 -1
  477. package/dist/irdevice/airpurifier.js +0 -213
  478. package/dist/irdevice/airpurifier.js.map +0 -1
  479. package/dist/irdevice/camera.d.ts +0 -32
  480. package/dist/irdevice/camera.d.ts.map +0 -1
  481. package/dist/irdevice/camera.js +0 -107
  482. package/dist/irdevice/camera.js.map +0 -1
  483. package/dist/irdevice/fan.d.ts +0 -36
  484. package/dist/irdevice/fan.d.ts.map +0 -1
  485. package/dist/irdevice/fan.js +0 -200
  486. package/dist/irdevice/fan.js.map +0 -1
  487. package/dist/irdevice/irdevice.d.ts +0 -68
  488. package/dist/irdevice/irdevice.d.ts.map +0 -1
  489. package/dist/irdevice/irdevice.js +0 -298
  490. package/dist/irdevice/irdevice.js.map +0 -1
  491. package/dist/irdevice/light.d.ts +0 -36
  492. package/dist/irdevice/light.d.ts.map +0 -1
  493. package/dist/irdevice/light.js +0 -206
  494. package/dist/irdevice/light.js.map +0 -1
  495. package/dist/irdevice/other.d.ts +0 -57
  496. package/dist/irdevice/other.d.ts.map +0 -1
  497. package/dist/irdevice/other.js +0 -778
  498. package/dist/irdevice/other.js.map +0 -1
  499. package/dist/irdevice/tv.d.ts +0 -45
  500. package/dist/irdevice/tv.d.ts.map +0 -1
  501. package/dist/irdevice/tv.js +0 -327
  502. package/dist/irdevice/tv.js.map +0 -1
  503. package/dist/irdevice/vacuumcleaner.d.ts +0 -28
  504. package/dist/irdevice/vacuumcleaner.d.ts.map +0 -1
  505. package/dist/irdevice/vacuumcleaner.js +0 -104
  506. package/dist/irdevice/vacuumcleaner.js.map +0 -1
  507. package/dist/irdevice/waterheater.d.ts +0 -30
  508. package/dist/irdevice/waterheater.d.ts.map +0 -1
  509. package/dist/irdevice/waterheater.js +0 -105
  510. package/dist/irdevice/waterheater.js.map +0 -1
  511. package/dist/platform-hap.d.ts +0 -149
  512. package/dist/platform-hap.d.ts.map +0 -1
  513. package/dist/platform-hap.js +0 -2861
  514. package/dist/platform-hap.js.map +0 -1
  515. package/dist/platform-matter.d.ts +0 -120
  516. package/dist/platform-matter.d.ts.map +0 -1
  517. package/dist/platform-matter.js +0 -984
  518. package/dist/platform-matter.js.map +0 -1
  519. package/dist/utils.test.d.ts +0 -2
  520. package/dist/utils.test.d.ts.map +0 -1
  521. package/dist/utils.test.js +0 -95
  522. package/dist/utils.test.js.map +0 -1
  523. package/dist/verifyconfig.test.d.ts +0 -2
  524. package/dist/verifyconfig.test.d.ts.map +0 -1
  525. package/dist/verifyconfig.test.js +0 -167
  526. package/dist/verifyconfig.test.js.map +0 -1
  527. package/src/custom.d.ts +0 -7
  528. package/src/devices-hap/airpurifier.ts +0 -563
  529. package/src/devices-hap/blindtilt.ts +0 -1049
  530. package/src/devices-hap/bot.ts +0 -900
  531. package/src/devices-hap/ceilinglight.ts +0 -742
  532. package/src/devices-hap/colorbulb.ts +0 -904
  533. package/src/devices-hap/contact.ts +0 -457
  534. package/src/devices-hap/curtain.ts +0 -944
  535. package/src/devices-hap/device.ts +0 -811
  536. package/src/devices-hap/fan.ts +0 -711
  537. package/src/devices-hap/hub.ts +0 -439
  538. package/src/devices-hap/humidifier.ts +0 -669
  539. package/src/devices-hap/iosensor.ts +0 -427
  540. package/src/devices-hap/lightstrip.ts +0 -836
  541. package/src/devices-hap/lock.ts +0 -620
  542. package/src/devices-hap/meter.ts +0 -426
  543. package/src/devices-hap/meterplus.ts +0 -430
  544. package/src/devices-hap/meterpro.ts +0 -522
  545. package/src/devices-hap/motion.ts +0 -390
  546. package/src/devices-hap/plug.ts +0 -423
  547. package/src/devices-hap/relayswitch.ts +0 -727
  548. package/src/devices-hap/robotvacuumcleaner.ts +0 -568
  549. package/src/devices-hap/waterdetector.ts +0 -400
  550. package/src/devices-matter/BaseMatterAccessory.ts +0 -131
  551. package/src/devices-matter/ColorLightAccessory.ts +0 -110
  552. package/src/devices-matter/ColorTemperatureLightAccessory.ts +0 -92
  553. package/src/devices-matter/ContactSensorAccessory.ts +0 -41
  554. package/src/devices-matter/DimmableLightAccessory.ts +0 -192
  555. package/src/devices-matter/DoorLockAccessory.ts +0 -60
  556. package/src/devices-matter/ExtendedColorLightAccessory.ts +0 -123
  557. package/src/devices-matter/FanAccessory.ts +0 -95
  558. package/src/devices-matter/HumiditySensorAccessory.ts +0 -41
  559. package/src/devices-matter/LeakSensorAccessory.ts +0 -40
  560. package/src/devices-matter/LightSensorAccessory.ts +0 -41
  561. package/src/devices-matter/OccupancySensorAccessory.ts +0 -48
  562. package/src/devices-matter/OnOffLightAccessory.ts +0 -133
  563. package/src/devices-matter/OnOffOutletAccessory.ts +0 -46
  564. package/src/devices-matter/OnOffSwitchAccessory.ts +0 -51
  565. package/src/devices-matter/RoboticVacuumAccessory.ts +0 -407
  566. package/src/devices-matter/SmokeCOAlarmAccessory.ts +0 -59
  567. package/src/devices-matter/TemperatureSensorAccessory.ts +0 -43
  568. package/src/devices-matter/ThermostatAccessory.ts +0 -110
  569. package/src/devices-matter/VenetianBlindAccessory.ts +0 -115
  570. package/src/devices-matter/WindowBlindAccessory.ts +0 -92
  571. package/src/devices-matter/custom/PowerStripAccessory.ts +0 -309
  572. package/src/devices-matter/custom/index.ts +0 -8
  573. package/src/devices-matter/index.ts +0 -29
  574. package/src/index.test.ts +0 -24
  575. package/src/irdevice/airconditioner.ts +0 -533
  576. package/src/irdevice/airpurifier.ts +0 -252
  577. package/src/irdevice/camera.ts +0 -129
  578. package/src/irdevice/fan.ts +0 -226
  579. package/src/irdevice/irdevice.ts +0 -344
  580. package/src/irdevice/light.ts +0 -246
  581. package/src/irdevice/other.ts +0 -790
  582. package/src/irdevice/tv.ts +0 -378
  583. package/src/irdevice/vacuumcleaner.ts +0 -126
  584. package/src/irdevice/waterheater.ts +0 -127
  585. package/src/platform-hap.ts +0 -2997
  586. package/src/platform-matter.ts +0 -1110
  587. package/src/utils.test.ts +0 -96
  588. package/src/verifyconfig.test.ts +0 -197
@@ -0,0 +1,1232 @@
1
+ // Utility: Validate BLE response length before parsing
2
+ /* eslint-disable style/max-statements-per-line, unused-imports/no-unused-vars */
3
+ import type { SwitchBotPluginConfig } from '../settings.js'
4
+
5
+ import { Buffer } from 'node:buffer'
6
+
7
+ import { DEVICE_MATTER_CLUSTERS } from '../platform.js'
8
+ import { MATTER_ATTRIBUTE_IDS, MATTER_CLUSTER_IDS } from '../utils.js'
9
+ import { DeviceBase } from './deviceBase.js'
10
+
11
+ function validateBLEResponseLength(buf: Buffer | Uint8Array | any[], expected: number, context = ''): boolean {
12
+ if (!buf || typeof buf.length !== 'number' || buf.length !== expected) {
13
+ console.warn(`[BLE] Invalid response length${context ? ` for ${context}` : ''}: expected ${expected}, got ${buf?.length}`)
14
+ return false
15
+ }
16
+ return true
17
+ }
18
+
19
+ // BLE notification handling: per-command notification futures and unsolicited notification logging
20
+ const BLE_NOTIFICATION_HANDLERS = new Map<string, (payload: Buffer) => void>()
21
+
22
+ // Module-scope regex pattern to avoid recompilation
23
+ const HEX_COLOR_REGEX = /^#?[0-9A-F]{6}$/i
24
+
25
+ export class GenericDevice extends DeviceBase {
26
+ constructor(opts: any, cfg: SwitchBotPluginConfig) {
27
+ super(opts, cfg)
28
+ // If BLE encryptionKey/keyId provided, set on node-switchbot device instance if possible
29
+ if (opts.encryptionKey && this.client && typeof this.client.devices?.get === 'function') {
30
+ try {
31
+ const dev = this.client.devices.get(opts.id)
32
+ if (dev && typeof dev.setKey === 'function') {
33
+ dev.setKey({
34
+ encryptionKey: opts.encryptionKey,
35
+ keyId: opts.keyId || undefined,
36
+ })
37
+ }
38
+ } catch (e) {
39
+ // ignore if device not found or setKey not available
40
+ }
41
+ }
42
+
43
+ // Subscribe to BLE notifications if supported (node-switchbot v4+)
44
+ this._subscribeBLENotifications()
45
+ }
46
+
47
+ /**
48
+ * Subscribe to BLE notifications for this device (if supported by node-switchbot)
49
+ * Logs unsolicited notifications and enables per-command notification futures.
50
+ */
51
+ private async _subscribeBLENotifications() {
52
+ if (!this.client || typeof this.client.devices?.get !== 'function') { return }
53
+ const dev = this.client.devices.get(this.opts.id)
54
+ if (!dev || typeof dev.mac !== 'string' || !dev.mac) { return }
55
+ // Only subscribe once per device
56
+ if (BLE_NOTIFICATION_HANDLERS.has(dev.mac)) { return }
57
+ if (typeof dev.subscribeNotifications === 'function') {
58
+ const handler = (payload: Buffer) => {
59
+ // If a per-command notification future is waiting, let node-switchbot handle it
60
+ // Otherwise, log unsolicited notification
61
+ if (payload && payload.length > 0) {
62
+ // Unsolicited notification logging
63
+ // (node-switchbot will resolve per-command futures internally)
64
+ // eslint-disable-next-line no-console
65
+ console.info(`[BLE] Unsolicited notification from ${dev.mac}: ${payload.toString('hex')}`)
66
+ }
67
+ }
68
+ try {
69
+ // Subscribe and remember handler for possible cleanup
70
+ await dev.subscribeNotifications(handler)
71
+ BLE_NOTIFICATION_HANDLERS.set(dev.mac, handler)
72
+ } catch (e) {
73
+ // ignore if subscription fails
74
+ }
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Await a BLE notification for this device (for advanced use in subclasses)
80
+ * Returns the notification payload or throws on timeout.
81
+ */
82
+ protected async _awaitBLENotification(timeoutMs = 5000): Promise<Buffer> {
83
+ if (!this.client || typeof this.client.devices?.get !== 'function') { throw new Error('No BLE client/device') }
84
+ const dev = this.client.devices.get(this.opts.id)
85
+ if (!dev || typeof dev.mac !== 'string' || !dev.mac) { throw new Error('No BLE MAC for device') }
86
+ if (typeof dev.bleConnection?.sendCommand !== 'function') { throw new TypeError('BLE connection does not support sendCommand') }
87
+ // This is a low-level utility; in most cases, node-switchbot handles notification futures for commands
88
+ // Here, we expose a direct await for advanced use
89
+ return new Promise<Buffer>((resolve, reject) => {
90
+ let timer: NodeJS.Timeout | undefined
91
+ const handler = (payload: Buffer) => {
92
+ clearTimeout(timer)
93
+ dev.bleConnection?.unsubscribeNotifications(dev.mac, handler)
94
+ resolve(payload)
95
+ }
96
+ dev.bleConnection?.subscribeNotifications(dev.mac, handler).then(() => {
97
+ timer = setTimeout(() => {
98
+ dev.bleConnection?.unsubscribeNotifications(dev.mac, handler)
99
+ reject(new Error('BLE notification timeout'))
100
+ }, timeoutMs)
101
+ }).catch(reject)
102
+ })
103
+ }
104
+
105
+ async getState(): Promise<any> {
106
+ // Default: return minimal info; implementations should override
107
+ if (this.client && typeof this.client.getDevice === 'function') {
108
+ try {
109
+ const raw = await this.client.getDevice(this.opts.id)
110
+ // If this is a BLE buffer/array, validate length (common BLE status: 12 bytes, but may vary by device)
111
+ if (raw && (raw instanceof Buffer || Array.isArray(raw) || raw instanceof Uint8Array)) {
112
+ // Default to 12, override per device if needed
113
+ if (!validateBLEResponseLength(raw, 12, this.opts.type)) {
114
+ return { id: this.opts.id, type: this.opts.type, error: 'invalid_ble_response_length', raw }
115
+ }
116
+ }
117
+ // Normalize common response shapes
118
+ try {
119
+ const device = raw?.body ?? raw
120
+ return device
121
+ } catch (e) {
122
+ return raw
123
+ }
124
+ } catch (e) {
125
+ // ignore and fallback
126
+ }
127
+ }
128
+ return { id: this.opts.id, type: this.opts.type }
129
+ }
130
+
131
+ async setState(change: any): Promise<any> {
132
+ // Apply change via SwitchBot API in real implementation
133
+ // Translate common high-level changes into SwitchBot OpenAPI commands
134
+ if (!this.client) {
135
+ return { success: false, reason: 'no client', change }
136
+ }
137
+
138
+ const cmdBody: any = {}
139
+
140
+ if (typeof change.on === 'boolean') {
141
+ cmdBody.command = change.on ? 'turnOn' : 'turnOff'
142
+ cmdBody.parameter = 'default'
143
+ cmdBody.commandType = 'command'
144
+ } else if (typeof change.brightness === 'number') {
145
+ const v = Math.max(0, Math.min(100, Number(change.brightness)))
146
+ cmdBody.command = 'setBrightness'
147
+ cmdBody.parameter = String(v)
148
+ cmdBody.commandType = 'command'
149
+ } else if (typeof change.speed === 'number') {
150
+ const v = Math.max(0, Math.min(100, Number(change.speed)))
151
+ cmdBody.command = 'setFanSpeed'
152
+ cmdBody.parameter = String(v)
153
+ cmdBody.commandType = 'command'
154
+ } else if (typeof change.position === 'number') {
155
+ const v = Math.max(0, Math.min(100, Number(change.position)))
156
+ cmdBody.command = 'setPosition'
157
+ cmdBody.parameter = String(v)
158
+ cmdBody.commandType = 'command'
159
+ } else if (typeof change.locked === 'boolean') {
160
+ cmdBody.command = change.locked ? 'lock' : 'unlock'
161
+ cmdBody.parameter = 'default'
162
+ cmdBody.commandType = 'command'
163
+ } else if (typeof change.start === 'boolean') {
164
+ cmdBody.command = change.start ? 'start' : 'stop'
165
+ cmdBody.parameter = 'default'
166
+ cmdBody.commandType = 'command'
167
+ } else {
168
+ // If caller supplied an explicit command body, pass through
169
+ if (change && typeof change.command === 'string') {
170
+ Object.assign(cmdBody, change)
171
+ } else {
172
+ // Fallback: send raw change to client setDeviceState
173
+ try {
174
+ if (typeof this.client.setDeviceState === 'function') {
175
+ return await this.client.setDeviceState(this.opts.id, change)
176
+ }
177
+ if (typeof this.client.sendCommand === 'function') {
178
+ return await this.client.sendCommand(this.opts.id, change)
179
+ }
180
+ } catch (err) {
181
+ const e = err as any
182
+ return { success: false, reason: e?.message ?? String(e) }
183
+ }
184
+ return { success: false, reason: 'unsupported change', change }
185
+ }
186
+ }
187
+
188
+ try {
189
+ return await this.client.setDeviceState(this.opts.id, cmdBody)
190
+ } catch (err) {
191
+ // try alternative client API if available
192
+ try {
193
+ if (typeof this.client.sendCommand === 'function') {
194
+ return await this.client.sendCommand(this.opts.id, cmdBody)
195
+ }
196
+ } catch (e2) {
197
+ // ignore
198
+ }
199
+ const e = err as any
200
+ return { success: false, reason: e?.message ?? String(e) }
201
+ }
202
+ }
203
+
204
+ createHAPAccessory(api: any): any {
205
+ // Default HAP descriptor: a Switch service with On characteristic
206
+ return {
207
+ services: [
208
+ {
209
+ type: 'Switch',
210
+ characteristics: {
211
+ On: {
212
+ get: async () => {
213
+ const s = await this.getState()
214
+ return !!(s && (s.on === true || s.state === 'on' || s.power === 'on'))
215
+ },
216
+ set: async (v: any) => {
217
+ await this.setState({ on: !!v })
218
+ },
219
+ },
220
+ },
221
+ },
222
+ ],
223
+ }
224
+ }
225
+
226
+ // Default Matter descriptor mirrors HAP descriptor structure so the
227
+ // platform can construct a Matter accessory representation when
228
+ // Homebridge Matter APIs are available. Device subclasses may override
229
+ // this to provide Matter-specific clusters/attributes if desired.
230
+ async createMatterAccessory(api: any): Promise<any> {
231
+ // Dynamically detect features from getState()
232
+ const state = await this.getState()
233
+ const clusters: any[] = []
234
+
235
+ // On/Off (Switch/Plug/Generic)
236
+ if ('on' in state || 'power' in state || 'state' in state) {
237
+ clusters.push({
238
+ type: 'OnOff',
239
+ clusterId: MATTER_CLUSTER_IDS.OnOff,
240
+ attributes: {
241
+ onOff: { read: async () => !!(await this.getState()).on || (await this.getState()).power === 'on' || (await this.getState()).state === 'on', write: async (v: any) => this.setState({ on: !!v }) },
242
+ [MATTER_ATTRIBUTE_IDS.OnOff.OnOff]: { read: async () => !!(await this.getState()).on || (await this.getState()).power === 'on' || (await this.getState()).state === 'on', write: async (v: any) => this.setState({ on: !!v }) },
243
+ },
244
+ })
245
+ }
246
+
247
+ // Brightness (Light)
248
+ if ('brightness' in state) {
249
+ clusters.push({
250
+ type: 'LevelControl',
251
+ clusterId: MATTER_CLUSTER_IDS.LevelControl,
252
+ attributes: {
253
+ currentLevel: { read: async () => (await this.getState()).brightness ?? 100, write: async (v: any) => this.setState({ brightness: Number(v) }) },
254
+ [MATTER_ATTRIBUTE_IDS.LevelControl.CurrentLevel]: { read: async () => (await this.getState()).brightness ?? 100, write: async (v: any) => this.setState({ brightness: Number(v) }) },
255
+ },
256
+ })
257
+ }
258
+
259
+ // Color (Light)
260
+ if ('hue' in state && 'saturation' in state) {
261
+ clusters.push({
262
+ type: 'ColorControl',
263
+ clusterId: MATTER_CLUSTER_IDS.ColorControl,
264
+ attributes: {
265
+ colorMode: { read: async () => 0 },
266
+ colorHue: { read: async () => (await this.getState()).hue ?? 0, write: async (v: any) => this.setState({ hue: Number(v) }) },
267
+ [MATTER_ATTRIBUTE_IDS.ColorControl.CurrentHue]: { read: async () => (await this.getState()).hue ?? 0, write: async (v: any) => this.setState({ hue: Number(v) }) },
268
+ colorSaturation: { read: async () => (await this.getState()).saturation ?? 0, write: async (v: any) => this.setState({ saturation: Number(v) }) },
269
+ [MATTER_ATTRIBUTE_IDS.ColorControl.CurrentSaturation]: { read: async () => (await this.getState()).saturation ?? 0, write: async (v: any) => this.setState({ saturation: Number(v) }) },
270
+ ...(typeof state.colorTemperature === 'number' || typeof state.kelvin === 'number'
271
+ ? {
272
+ colorTemperature: { read: async () => typeof (await this.getState()).colorTemperature === 'number' ? (await this.getState()).colorTemperature : Math.round(1000000 / ((await this.getState()).kelvin ?? 2500)), write: async (v: any) => this.setState({ colorTemperature: Number(v) }) },
273
+ [MATTER_ATTRIBUTE_IDS.ColorControl.ColorTemperatureMireds]: { read: async () => typeof (await this.getState()).colorTemperature === 'number' ? (await this.getState()).colorTemperature : Math.round(1000000 / ((await this.getState()).kelvin ?? 2500)), write: async (v: any) => this.setState({ colorTemperature: Number(v) }) },
274
+ }
275
+ : {}),
276
+ },
277
+ })
278
+ }
279
+
280
+ // Temperature sensor
281
+ if ('temperature' in state) {
282
+ clusters.push({
283
+ type: 'TemperatureMeasurement',
284
+ // No clusterId, not present in MATTER_CLUSTER_IDS
285
+ attributes: {
286
+ measuredValue: { read: async () => (await this.getState()).temperature ?? 0 },
287
+ },
288
+ })
289
+ }
290
+
291
+ // Humidity sensor
292
+ if ('humidity' in state) {
293
+ clusters.push({
294
+ type: 'RelativeHumidityMeasurement',
295
+ clusterId: MATTER_CLUSTER_IDS.RelativeHumidityMeasurement,
296
+ attributes: {
297
+ measuredValue: { read: async () => (await this.getState()).humidity ?? 0 },
298
+ },
299
+ })
300
+ }
301
+
302
+ // CO2 sensor
303
+ if ('CO2' in state) {
304
+ clusters.push({
305
+ type: 'AirQuality',
306
+ attributes: {
307
+ CO2: { read: async () => (await this.getState()).CO2 ?? 0 },
308
+ },
309
+ })
310
+ }
311
+
312
+ // Lock
313
+ if ('lockState' in state || 'locked' in state) {
314
+ clusters.push({
315
+ type: 'DoorLock',
316
+ clusterId: MATTER_CLUSTER_IDS.DoorLock,
317
+ attributes: {
318
+ lockState: { read: async () => (await this.getState()).lockState ?? (await this.getState()).locked ? 1 : 0, write: async (v: any) => this.setState({ locked: !!v }) },
319
+ },
320
+ })
321
+ }
322
+
323
+ // Motion sensor
324
+ if ('moveDetected' in state || 'motion' in state) {
325
+ clusters.push({
326
+ type: 'OccupancySensing',
327
+ // No clusterId, not present in MATTER_CLUSTER_IDS
328
+ attributes: {
329
+ occupancy: { read: async () => (await this.getState()).moveDetected === true || (await this.getState()).motion === true ? 1 : 0 },
330
+ },
331
+ })
332
+ }
333
+
334
+ // Contact sensor
335
+ if ('openState' in state || 'contact' in state || 'open' in state) {
336
+ clusters.push({
337
+ type: 'BooleanState',
338
+ // No clusterId, not present in MATTER_CLUSTER_IDS
339
+ attributes: {
340
+ stateValue: { read: async () => (await this.getState()).openState === 'open' || (await this.getState()).open === true ? 1 : 0 },
341
+ },
342
+ })
343
+ }
344
+
345
+ // Leak sensor
346
+ if ('leak' in state || 'status' in state) {
347
+ clusters.push({
348
+ type: 'LeakSensor',
349
+ attributes: {
350
+ leakDetected: { read: async () => (await this.getState()).leak === true || (await this.getState()).status === 1 ? 1 : 0 },
351
+ },
352
+ })
353
+ }
354
+
355
+ // Energy monitoring (Plug)
356
+ if ('voltage' in state || 'power' in state || 'electricCurrent' in state) {
357
+ clusters.push({
358
+ type: 'ElectricalMeasurement',
359
+ // No clusterId, not present in MATTER_CLUSTER_IDS
360
+ attributes: {
361
+ voltage: { read: async () => (await this.getState()).voltage ?? 0 },
362
+ power: { read: async () => (await this.getState()).power ?? 0 },
363
+ electricCurrent: { read: async () => (await this.getState()).electricCurrent ?? 0 },
364
+ },
365
+ })
366
+ }
367
+
368
+ // Fan
369
+ if ('speed' in state || 'fanSpeed' in state) {
370
+ clusters.push({
371
+ type: 'FanControl',
372
+ clusterId: MATTER_CLUSTER_IDS.FanControl,
373
+ attributes: {
374
+ speedCurrent: { read: async () => (await this.getState()).speed ?? (await this.getState()).fanSpeed ?? 0, write: async (v: any) => this.setState({ speed: Number(v) }) },
375
+ },
376
+ })
377
+ }
378
+
379
+ // Vacuum
380
+ if ('workingStatus' in state) {
381
+ clusters.push({
382
+ type: 'RobotVacuumCleaner',
383
+ attributes: {
384
+ workingStatus: { read: async () => (await this.getState()).workingStatus ?? 'StandBy' },
385
+ },
386
+ })
387
+ }
388
+
389
+ return {
390
+ id: this.opts.id,
391
+ name: this.opts.name ?? this.opts.type,
392
+ protocol: 'matter',
393
+ clusters,
394
+ }
395
+ }
396
+ }
397
+
398
+ // Specific device classes can extend GenericDevice for custom behavior.
399
+ export class BotDevice extends GenericDevice {}
400
+
401
+ export class CurtainDevice extends GenericDevice {
402
+ createHAPAccessory(api: any) {
403
+ return {
404
+ services: [
405
+ {
406
+ type: 'WindowCovering',
407
+ characteristics: {
408
+ CurrentPosition: {
409
+ get: async () => {
410
+ const s = await this.getState()
411
+ return typeof s.position === 'number' ? s.position : 0
412
+ },
413
+ },
414
+ TargetPosition: {
415
+ get: async () => {
416
+ const s = await this.getState()
417
+ return typeof s.position === 'number' ? s.position : 0
418
+ },
419
+ set: async (v: any) => {
420
+ await this.setState({ position: Number(v) })
421
+ },
422
+ },
423
+ },
424
+ },
425
+ ],
426
+ }
427
+ }
428
+
429
+ // Matter-specific descriptor for Curtain (Shade cluster) with conformance type
430
+ createMatterAccessory(api: any): any {
431
+ // Determine device type and features
432
+ const typeKey = (this.opts.type || '').toLowerCase()
433
+ // BlindTilt supports both lift (position) and tilt; Curtain/RollerShade support lift only
434
+ const isBlindTilt = typeKey.includes('blindtilt') || typeKey.includes('blind tilt')
435
+ const supportsLift = true // All SwitchBot curtains/rollershades/blindtilts support lift (position)
436
+ const supportsTilt = isBlindTilt // Only BlindTilt supports tilt
437
+
438
+ // If neither lift nor tilt is supported, skip registration (should not happen for SwitchBot)
439
+ if (!supportsLift && !supportsTilt) {
440
+ // Return null to indicate not to register as Matter WindowCovering
441
+ return null
442
+ }
443
+
444
+ // Set correct Matter type
445
+ let matterType = 0 // Default: RollerShade (lift-only)
446
+ try {
447
+ const clusterConf = DEVICE_MATTER_CLUSTERS[typeKey] || DEVICE_MATTER_CLUSTERS.curtain
448
+ if (clusterConf && clusterConf.windowCovering && typeof clusterConf.windowCovering.type === 'number') {
449
+ matterType = clusterConf.windowCovering.type
450
+ }
451
+ } catch (e) {
452
+ // fallback to default
453
+ }
454
+
455
+ // Build attributes for Matter WindowCovering
456
+ const attributes: Record<string, any> = {
457
+ type: { read: async () => matterType, write: undefined },
458
+ currentPosition: { read: async () => { const s = await this.getState(); return typeof s.position === 'number' ? s.position : 0 }, write: undefined },
459
+ [MATTER_ATTRIBUTE_IDS.WindowCovering.CurrentPosition]: { read: async () => { const s = await this.getState(); return typeof s.position === 'number' ? s.position : 0 }, write: undefined },
460
+ targetPosition: { read: async () => { const s = await this.getState(); return typeof s.position === 'number' ? s.position : 0 }, write: async (v: any) => this.setState({ position: Number(v) }) },
461
+ [MATTER_ATTRIBUTE_IDS.WindowCovering.TargetPosition]: { read: async () => { const s = await this.getState(); return typeof s.position === 'number' ? s.position : 0 }, write: async (v: any) => this.setState({ position: Number(v) }) },
462
+ }
463
+ if (supportsTilt) {
464
+ // Expose tilt attributes for BlindTilt
465
+ attributes.currentTilt = { read: async () => { const s = await this.getState(); return typeof s.tilt === 'number' ? s.tilt : 0 }, write: undefined }
466
+ attributes.targetTilt = { read: async () => { const s = await this.getState(); return typeof s.tilt === 'number' ? s.tilt : 0 }, write: async (v: any) => this.setState({ tilt: Number(v) }) }
467
+ }
468
+
469
+ return {
470
+ id: this.opts.id,
471
+ name: this.opts.name ?? this.opts.type,
472
+ protocol: 'matter',
473
+ clusters: [
474
+ {
475
+ // Shade cluster
476
+ type: 'Shade',
477
+ clusterId: MATTER_CLUSTER_IDS.WindowCovering,
478
+ attributes,
479
+ },
480
+ ],
481
+ }
482
+ }
483
+ }
484
+
485
+ export class FanDevice extends GenericDevice {
486
+ createHAPAccessory(api: any) {
487
+ return {
488
+ services: [
489
+ {
490
+ type: 'Fan',
491
+ characteristics: {
492
+ On: {
493
+ get: async () => {
494
+ const s = await this.getState()
495
+ return !!(s && (s.on === true || s.state === 'on'))
496
+ },
497
+ set: async (v: any) => {
498
+ await this.setState({ on: !!v })
499
+ },
500
+ },
501
+ RotationSpeed: {
502
+ get: async () => {
503
+ const s = await this.getState()
504
+ return typeof s.speed === 'number' ? s.speed : 0
505
+ },
506
+ set: async (v: any) => {
507
+ await this.setState({ speed: Number(v) })
508
+ },
509
+ },
510
+ },
511
+ },
512
+ ],
513
+ }
514
+ }
515
+
516
+ async setState(change: any): Promise<any> {
517
+ if (!this.client) { return { success: false, reason: 'no client' } }
518
+
519
+ // Oscillation support
520
+ if (typeof change.oscillate === 'boolean') {
521
+ const body = { command: 'setOscillation', parameter: change.oscillate ? 'on' : 'off', commandType: 'command' }
522
+ try {
523
+ return await this.client.setDeviceState(this.opts.id, body)
524
+ } catch (err) {
525
+ try {
526
+ if (typeof this.client.sendCommand === 'function') { return await this.client.sendCommand(this.opts.id, body) }
527
+ } catch (e) {}
528
+ const e = err as any
529
+ return { success: false, reason: e?.message ?? String(e) }
530
+ }
531
+ }
532
+
533
+ // Swing / sweep support (angle or mode)
534
+ if (change && (typeof change.swing === 'boolean' || typeof change.swingAngle === 'number' || typeof change.swingMode === 'string')) {
535
+ let param: string = 'default'
536
+ if (typeof change.swingMode === 'string') { param = change.swingMode } else if (typeof change.swingAngle === 'number') { param = String(Number(change.swingAngle)) } else {
537
+ param = change.swing ? 'on' : 'off'
538
+ }
539
+
540
+ const body = { command: 'setSwing', parameter: param, commandType: 'command' }
541
+ try {
542
+ return await this.client.setDeviceState(this.opts.id, body)
543
+ } catch (err) {
544
+ try {
545
+ if (typeof this.client.sendCommand === 'function') { return await this.client.sendCommand(this.opts.id, body) }
546
+ } catch (e) {}
547
+ const e = err as any
548
+ return { success: false, reason: e?.message ?? String(e) }
549
+ }
550
+ }
551
+
552
+ return super.setState(change)
553
+ }
554
+
555
+ // Matter-specific descriptor for Fan
556
+ createMatterAccessory(api: any): any {
557
+ return {
558
+ id: this.opts.id,
559
+ name: this.opts.name ?? this.opts.type,
560
+ protocol: 'matter',
561
+ clusters: [
562
+ {
563
+ // OnOff cluster
564
+ type: 'OnOff',
565
+ clusterId: MATTER_CLUSTER_IDS.OnOff,
566
+ attributes: {
567
+ onOff: { read: async () => { const s = await this.getState(); return !!(s && (s.on === true || s.state === 'on')) }, write: async (v: any) => this.setState({ on: !!v }) },
568
+ // numeric attribute id for onOff
569
+ [MATTER_ATTRIBUTE_IDS.OnOff.OnOff]: { read: async () => { const s = await this.getState(); return !!(s && (s.on === true || s.state === 'on')) }, write: async (v: any) => this.setState({ on: !!v }) },
570
+ },
571
+ },
572
+ {
573
+ // Fan Control cluster
574
+ type: 'FanControl',
575
+ clusterId: MATTER_CLUSTER_IDS.FanControl,
576
+ attributes: {
577
+ rotationSpeed: { read: async () => { const s = await this.getState(); return typeof s.speed === 'number' ? s.speed : 0 }, write: async (v: any) => this.setState({ speed: Number(v) }) },
578
+ [MATTER_ATTRIBUTE_IDS.FanControl.SpeedCurrent]: { read: async () => { const s = await this.getState(); return typeof s.speed === 'number' ? s.speed : 0 }, write: async (v: any) => this.setState({ speed: Number(v) }) },
579
+ oscillation: { read: async () => { const s = await this.getState(); return !!s?.oscillating }, write: async (v: any) => this.setState({ oscillate: !!v }) },
580
+ swingMode: { read: async () => { const s = await this.getState(); return s?.swingMode ?? null }, write: async (v: any) => this.setState({ swingMode: v }) },
581
+ },
582
+ },
583
+ ],
584
+ }
585
+ }
586
+ }
587
+ export class LightDevice extends GenericDevice {
588
+ createHAPAccessory(api: any) {
589
+ return {
590
+ services: [
591
+ {
592
+ type: 'Lightbulb',
593
+ characteristics: {
594
+ On: {
595
+ get: async () => {
596
+ const s = await this.getState()
597
+ return !!(s && (s.on === true || s.state === 'on' || s.power === 'on'))
598
+ },
599
+ set: async (v: any) => {
600
+ await this.setState({ on: !!v })
601
+ },
602
+ },
603
+ Brightness: {
604
+ props: { minValue: 0, maxValue: 100, minStep: 1 },
605
+ get: async () => {
606
+ const s = await this.getState()
607
+ return typeof s.brightness === 'number' ? s.brightness : 100
608
+ },
609
+ set: async (v: any) => {
610
+ await this.setState({ brightness: Number(v) })
611
+ },
612
+ },
613
+ Hue: {
614
+ props: { minValue: 0, maxValue: 360, minStep: 1 },
615
+ get: async () => {
616
+ const s = await this.getState()
617
+ // prefer explicit hue if provided
618
+ if (s && typeof s.hue === 'number') { return s.hue }
619
+ // try HSV from color hex
620
+ const hex = s?.color || s?.colorHex || s?.colour
621
+ if (typeof hex === 'string' && HEX_COLOR_REGEX.test(hex)) {
622
+ const h = (() => {
623
+ const hsl = (h: number, s: number, l: number) => ({ h, s, l })
624
+ // convert hex -> rgb -> hsv
625
+ const cleaned = hex.replace('#', '')
626
+ const r = Number.parseInt(cleaned.substr(0, 2), 16) / 255
627
+ const g = Number.parseInt(cleaned.substr(2, 2), 16) / 255
628
+ const b = Number.parseInt(cleaned.substr(4, 2), 16) / 255
629
+ const mx = Math.max(r, g, b); const mn = Math.min(r, g, b)
630
+ const d = mx - mn
631
+ if (d === 0) { return 0 }
632
+ let hue = 0
633
+ switch (mx) {
634
+ case r: hue = ((g - b) / d) % 6; break
635
+ case g: hue = (b - r) / d + 2; break
636
+ case b: hue = (r - g) / d + 4; break
637
+ }
638
+ hue = Math.round(hue * 60)
639
+ if (hue < 0) { hue += 360 }
640
+ return hue
641
+ })()
642
+ return h
643
+ }
644
+ return 0
645
+ },
646
+ set: async (v: any) => {
647
+ await this.setState({ hue: Number(v) })
648
+ },
649
+ },
650
+ Saturation: {
651
+ props: { minValue: 0, maxValue: 100, minStep: 1 },
652
+ get: async () => {
653
+ const s = await this.getState()
654
+ if (s && typeof s.saturation === 'number') { return s.saturation }
655
+ // if color hex is available, derive saturation from rgb
656
+ const hex = s?.color || s?.colorHex || s?.colour
657
+ if (typeof hex === 'string' && HEX_COLOR_REGEX.test(hex)) {
658
+ const cleaned = hex.replace('#', '')
659
+ const r = Number.parseInt(cleaned.substr(0, 2), 16) / 255
660
+ const g = Number.parseInt(cleaned.substr(2, 2), 16) / 255
661
+ const b = Number.parseInt(cleaned.substr(4, 2), 16) / 255
662
+ const mx = Math.max(r, g, b); const mn = Math.min(r, g, b)
663
+ const d = mx - mn
664
+ const sat = mx === 0 ? 0 : Math.round((d / mx) * 100)
665
+ return sat
666
+ }
667
+ return 0
668
+ },
669
+ set: async (v: any) => {
670
+ await this.setState({ saturation: Number(v) })
671
+ },
672
+ },
673
+ ColorTemperature: {
674
+ props: { minValue: 153, maxValue: 500, minStep: 1 },
675
+ get: async () => {
676
+ const s = await this.getState()
677
+ // prefer mired if provided
678
+ if (s && typeof s.colorTemperature === 'number') { return s.colorTemperature }
679
+ if (s && typeof s.color_temp === 'number') { return s.color_temp }
680
+ // some devices provide kelvin
681
+ if (s && typeof s.kelvin === 'number' && s.kelvin > 0) {
682
+ return Math.round(1000000 / s.kelvin)
683
+ }
684
+ return 400
685
+ },
686
+ set: async (v: any) => {
687
+ await this.setState({ colorTemperature: Number(v) })
688
+ },
689
+ },
690
+ },
691
+ },
692
+ ],
693
+ }
694
+ }
695
+
696
+ async setState(change: any): Promise<any> {
697
+ if (!this.client) { return { success: false, reason: 'no client' } }
698
+
699
+ // Color temperature (mired) or brightness/hue/sat
700
+ if (typeof change.colorTemperature === 'number' || typeof change.color_temp === 'number') {
701
+ const v = String(Number(change.colorTemperature ?? change.color_temp))
702
+ const body = { command: 'setColorTemperature', parameter: v, commandType: 'command' }
703
+ try {
704
+ return await this.client.setDeviceState(this.opts.id, body)
705
+ } catch (err) {
706
+ try {
707
+ if (typeof this.client.sendCommand === 'function') { return await this.client.sendCommand(this.opts.id, body) }
708
+ } catch (e) {}
709
+ const e = err as any
710
+ return { success: false, reason: e?.message ?? String(e) }
711
+ }
712
+ }
713
+
714
+ if (typeof change.hue === 'number' && typeof change.saturation === 'number') {
715
+ const body = { command: 'setColor', parameter: `${Number(change.hue)},${Number(change.saturation)}`, commandType: 'command' }
716
+ try {
717
+ return await this.client.setDeviceState(this.opts.id, body)
718
+ } catch (err) {
719
+ try {
720
+ if (typeof this.client.sendCommand === 'function') { return await this.client.sendCommand(this.opts.id, body) }
721
+ } catch (e) {}
722
+ const e = err as any
723
+ return { success: false, reason: e?.message ?? String(e) }
724
+ }
725
+ }
726
+
727
+ if (change && typeof change.color === 'string') {
728
+ const body = { command: 'setColor', parameter: change.color, commandType: 'command' }
729
+ try {
730
+ return await this.client.setDeviceState(this.opts.id, body)
731
+ } catch (err) {
732
+ try {
733
+ if (typeof this.client.sendCommand === 'function') { return await this.client.sendCommand(this.opts.id, body) }
734
+ } catch (e) {}
735
+ const e = err as any
736
+ return { success: false, reason: e?.message ?? String(e) }
737
+ }
738
+ }
739
+
740
+ // Fallback to generic handler (brightness/on)
741
+ return super.setState(change)
742
+ }
743
+
744
+ // Matter-specific descriptor for lights (OnOff + Level + Color)
745
+ createMatterAccessory(api: any): any {
746
+ return {
747
+ id: this.opts.id,
748
+ name: this.opts.name ?? this.opts.type,
749
+ protocol: 'matter',
750
+ clusters: [
751
+ {
752
+ // OnOff cluster
753
+ type: 'OnOff',
754
+ clusterId: MATTER_CLUSTER_IDS.OnOff,
755
+ attributes: {
756
+ onOff: { read: async () => { const s = await this.getState(); return !!(s && (s.on === true || s.state === 'on' || s.power === 'on')) }, write: async (v: any) => this.setState({ on: !!v }) },
757
+ [MATTER_ATTRIBUTE_IDS.OnOff.OnOff]: { read: async () => { const s = await this.getState(); return !!(s && (s.on === true || s.state === 'on' || s.power === 'on')) }, write: async (v: any) => this.setState({ on: !!v }) },
758
+ },
759
+ },
760
+ {
761
+ // Level Control cluster
762
+ type: 'LevelControl',
763
+ clusterId: MATTER_CLUSTER_IDS.LevelControl,
764
+ attributes: {
765
+ currentLevel: { read: async () => { const s = await this.getState(); return typeof s.brightness === 'number' ? s.brightness : 100 }, write: async (v: any) => this.setState({ brightness: Number(v) }) },
766
+ [MATTER_ATTRIBUTE_IDS.LevelControl.CurrentLevel]: { read: async () => { const s = await this.getState(); return typeof s.brightness === 'number' ? s.brightness : 100 }, write: async (v: any) => this.setState({ brightness: Number(v) }) },
767
+ },
768
+ },
769
+ {
770
+ // Color Control cluster
771
+ type: 'ColorControl',
772
+ clusterId: MATTER_CLUSTER_IDS.ColorControl,
773
+ attributes: {
774
+ // Required colorMode attribute for Matter conformance (0 = currentHueAndSaturation)
775
+ colorMode: { read: async () => 0 },
776
+ colorHue: { read: async () => { const s = await this.getState(); return typeof s.hue === 'number' ? s.hue : 0 }, write: async (v: any) => this.setState({ hue: Number(v) }) },
777
+ [MATTER_ATTRIBUTE_IDS.ColorControl.CurrentHue]: { read: async () => { const s = await this.getState(); return typeof s.hue === 'number' ? s.hue : 0 }, write: async (v: any) => this.setState({ hue: Number(v) }) },
778
+ colorSaturation: { read: async () => { const s = await this.getState(); return typeof s.saturation === 'number' ? s.saturation : 0 }, write: async (v: any) => this.setState({ saturation: Number(v) }) },
779
+ [MATTER_ATTRIBUTE_IDS.ColorControl.CurrentSaturation]: { read: async () => { const s = await this.getState(); return typeof s.saturation === 'number' ? s.saturation : 0 }, write: async (v: any) => this.setState({ saturation: Number(v) }) },
780
+ colorTemperature: { read: async () => {
781
+ const s = await this.getState(); if (typeof s.colorTemperature === 'number') { return s.colorTemperature } if (typeof s.kelvin === 'number') { return Math.round(1000000 / s.kelvin) } return 400
782
+ }, write: async (v: any) => this.setState({ colorTemperature: Number(v) }) },
783
+ [MATTER_ATTRIBUTE_IDS.ColorControl.ColorTemperatureMireds]: { read: async () => {
784
+ const s = await this.getState(); if (typeof s.colorTemperature === 'number') { return s.colorTemperature } if (typeof s.kelvin === 'number') { return Math.round(1000000 / s.kelvin) } return 400
785
+ }, write: async (v: any) => this.setState({ colorTemperature: Number(v) }) },
786
+ },
787
+ },
788
+ ],
789
+ }
790
+ }
791
+ }
792
+
793
+ export class LightStripDevice extends LightDevice {}
794
+
795
+ export class MotionSensorDevice extends GenericDevice {
796
+ createHAPAccessory(api: any) {
797
+ return {
798
+ services: [
799
+ {
800
+ type: 'MotionSensor',
801
+ characteristics: {
802
+ MotionDetected: {
803
+ get: async () => {
804
+ const s = await this.getState()
805
+ return !!(s && s.motion === true)
806
+ },
807
+ },
808
+ },
809
+ },
810
+ ],
811
+ }
812
+ }
813
+
814
+ async setState(change: any): Promise<any> {
815
+ if (!this.client) { return { success: false, reason: 'no client' } }
816
+
817
+ // Oscillation support
818
+ if (typeof change.oscillate === 'boolean') {
819
+ const body = { command: 'setOscillation', parameter: change.oscillate ? 'on' : 'off', commandType: 'command' }
820
+ try {
821
+ return await this.client.setDeviceState(this.opts.id, body)
822
+ } catch (err) {
823
+ try {
824
+ if (typeof this.client.sendCommand === 'function') { return await this.client.sendCommand(this.opts.id, body) }
825
+ } catch (e) {}
826
+ const e = err as any
827
+ return { success: false, reason: e?.message ?? String(e) }
828
+ }
829
+ }
830
+
831
+ // Swing / sweep support (angle or mode)
832
+ if (change && (typeof change.swing === 'boolean' || typeof change.swingAngle === 'number' || typeof change.swingMode === 'string')) {
833
+ let param: string = 'default'
834
+ if (typeof change.swingMode === 'string') { param = change.swingMode } else if (typeof change.swingAngle === 'number') { param = String(Number(change.swingAngle)) } else {
835
+ param = change.swing ? 'on' : 'off'
836
+ }
837
+
838
+ const body = { command: 'setSwing', parameter: param, commandType: 'command' }
839
+ try {
840
+ return await this.client.setDeviceState(this.opts.id, body)
841
+ } catch (err) {
842
+ try {
843
+ if (typeof this.client.sendCommand === 'function') { return await this.client.sendCommand(this.opts.id, body) }
844
+ } catch (e) {}
845
+ const e = err as any
846
+ return { success: false, reason: e?.message ?? String(e) }
847
+ }
848
+ }
849
+
850
+ return super.setState(change)
851
+ }
852
+ }
853
+
854
+ export class ContactSensorDevice extends GenericDevice {
855
+ createHAPAccessory(api: any) {
856
+ return {
857
+ services: [
858
+ {
859
+ type: 'ContactSensor',
860
+ characteristics: {
861
+ ContactSensorState: {
862
+ get: async () => {
863
+ const s = await this.getState()
864
+ return s && s.open ? 1 : 0
865
+ },
866
+ },
867
+ },
868
+ },
869
+ ],
870
+ }
871
+ }
872
+ }
873
+
874
+ export class VacuumDevice extends GenericDevice {
875
+ // Use DeviceBase defaults (Switch-style) — no override needed
876
+ createHAPAccessory(api: any) {
877
+ return super.createHAPAccessory(api)
878
+ }
879
+ }
880
+
881
+ export class LockDevice extends GenericDevice {
882
+ createHAPAccessory(api: any) {
883
+ return {
884
+ services: [
885
+ {
886
+ type: 'LockMechanism',
887
+ characteristics: {
888
+ LockCurrentState: {
889
+ get: async () => {
890
+ const s = await this.getState()
891
+ return s && s.locked ? 1 : 0
892
+ },
893
+ },
894
+ LockTargetState: {
895
+ get: async () => {
896
+ const s = await this.getState()
897
+ return s && s.locked ? 1 : 0
898
+ },
899
+ set: async (v: any) => {
900
+ await this.setState({ locked: !!v })
901
+ },
902
+ },
903
+ },
904
+ },
905
+ ],
906
+ }
907
+ }
908
+
909
+ async setState(change: any): Promise<any> {
910
+ if (!this.client) { return { success: false, reason: 'no client' } }
911
+
912
+ // User management actions: add/remove/list users, unlock with pin
913
+ if (change && typeof change.action === 'string') {
914
+ const action = change.action
915
+ try {
916
+ if (action === 'addUser' && (change.user || change.userId) && (change.pin || change.code)) {
917
+ const user = change.user ?? change.userId
918
+ const p = String(change.pin ?? change.code)
919
+ const body = { command: 'addUserCode', parameter: `${user}:${p}`, commandType: 'command' }
920
+ return await this.client.setDeviceState(this.opts.id, body)
921
+ }
922
+
923
+ if (action === 'removeUser' && (change.user || change.userId)) {
924
+ const user = change.user ?? change.userId
925
+ const body = { command: 'removeUserCode', parameter: String(user), commandType: 'command' }
926
+ return await this.client.setDeviceState(this.opts.id, body)
927
+ }
928
+
929
+ if (action === 'listUsers') {
930
+ const body = { command: 'listUsers', parameter: 'default', commandType: 'command' }
931
+ return await this.client.setDeviceState(this.opts.id, body)
932
+ }
933
+
934
+ if (action === 'unlockWithPin' && (change.pin || change.code)) {
935
+ const p = String(change.pin ?? change.code)
936
+ const body = { command: 'unlockWithPin', parameter: p, commandType: 'command' }
937
+ return await this.client.setDeviceState(this.opts.id, body)
938
+ }
939
+ } catch (err) {
940
+ try {
941
+ if (typeof this.client.sendCommand === 'function') { return await this.client.sendCommand(this.opts.id, { command: action, parameter: change.parameter ?? 'default', commandType: 'command' }) }
942
+ } catch (e) {}
943
+ const e = err as any
944
+ return { success: false, reason: e?.message ?? String(e) }
945
+ }
946
+ }
947
+
948
+ // Support setting lock PIN/passcode via `pin` or `passcode` (fallback)
949
+ const pin = change?.pin ?? change?.passcode ?? change?.code
950
+ if (typeof pin === 'string' || typeof pin === 'number') {
951
+ const body = { command: 'setLockPin', parameter: String(pin), commandType: 'command' }
952
+ try {
953
+ return await this.client.setDeviceState(this.opts.id, body)
954
+ } catch (err) {
955
+ try {
956
+ if (typeof this.client.sendCommand === 'function') { return await this.client.sendCommand(this.opts.id, body) }
957
+ } catch (e) {}
958
+ const e = err as any
959
+ return { success: false, reason: e?.message ?? String(e) }
960
+ }
961
+ }
962
+
963
+ return super.setState(change)
964
+ }
965
+
966
+ // Matter DoorLock descriptor including simple user-management actions
967
+ createMatterAccessory(api: any): any {
968
+ return {
969
+ id: this.opts.id,
970
+ name: this.opts.name ?? this.opts.type,
971
+ protocol: 'matter',
972
+ clusters: [
973
+ {
974
+ // DoorLock cluster
975
+ type: 'DoorLock',
976
+ clusterId: MATTER_CLUSTER_IDS.DoorLock,
977
+ attributes: {
978
+ lockState: { read: async () => { const s = await this.getState(); return !!(s && s.locked) }, write: async (v: any) => this.setState({ locked: !!v }) },
979
+ [MATTER_ATTRIBUTE_IDS.DoorLock.LockState]: { read: async () => { const s = await this.getState(); return !!(s && s.locked) }, write: async (v: any) => this.setState({ locked: !!v }) },
980
+ },
981
+ },
982
+ {
983
+ // DoorLock user mgmt cluster (conceptual)
984
+ type: 'DoorLockUserManagement',
985
+ clusterId: 0x0301,
986
+ attributes: {
987
+ addUser: { write: async (v: any) => this.setState({ action: 'addUser', user: v?.user, pin: v?.pin }) },
988
+ removeUser: { write: async (v: any) => this.setState({ action: 'removeUser', user: v?.user }) },
989
+ listUsers: { read: async () => { const r = await this.setState({ action: 'listUsers' }); return r }, write: undefined },
990
+ unlockWithPin: { write: async (v: any) => this.setState({ action: 'unlockWithPin', pin: v?.pin }) },
991
+ },
992
+ },
993
+ ],
994
+ }
995
+ }
996
+ }
997
+
998
+ export class HumidifierDevice extends GenericDevice {
999
+ createHAPAccessory(api: any) {
1000
+ return {
1001
+ services: [
1002
+ {
1003
+ type: 'HumidifierDehumidifier',
1004
+ characteristics: {
1005
+ Active: {
1006
+ get: async () => {
1007
+ const s = await this.getState()
1008
+ return s && s.on ? 1 : 0
1009
+ },
1010
+ set: async (v: any) => {
1011
+ await this.setState({ on: v === 1 })
1012
+ },
1013
+ },
1014
+ CurrentHumidifierDehumidifierState: {
1015
+ get: async () => {
1016
+ const s = await this.getState()
1017
+ return s && s.on ? 2 : 0 // 0 = Inactive, 2 = Humidifying
1018
+ },
1019
+ },
1020
+ TargetHumidifierDehumidifierState: {
1021
+ get: async () => 1, // 1 = Humidifier
1022
+ set: async (v: any) => {
1023
+ // Support humidifier mode
1024
+ },
1025
+ },
1026
+ CurrentRelativeHumidity: {
1027
+ get: async () => {
1028
+ const s = await this.getState()
1029
+ return typeof s.humidity === 'number' ? s.humidity : 0
1030
+ },
1031
+ },
1032
+ RelativeHumidityHumidifierThreshold: {
1033
+ get: async () => {
1034
+ const s = await this.getState()
1035
+ return typeof s.targetHumidity === 'number' ? s.targetHumidity : 50
1036
+ },
1037
+ set: async (v: any) => {
1038
+ await this.setState({ humidity: Number(v) })
1039
+ },
1040
+ },
1041
+ },
1042
+ },
1043
+ ],
1044
+ }
1045
+ }
1046
+
1047
+ async setState(change: any): Promise<any> {
1048
+ if (!this.client) { return { success: false, reason: 'no client' } }
1049
+
1050
+ if (typeof change.humidity === 'number') {
1051
+ const v = String(Number(change.humidity))
1052
+ const body = { command: 'setHumidity', parameter: v, commandType: 'command' }
1053
+ try {
1054
+ return await this.client.setDeviceState(this.opts.id, body)
1055
+ } catch (err) {
1056
+ try {
1057
+ if (typeof this.client.sendCommand === 'function') { return await this.client.sendCommand(this.opts.id, body) }
1058
+ } catch (e) {}
1059
+ const e = err as any
1060
+ return { success: false, reason: e?.message ?? String(e) }
1061
+ }
1062
+ }
1063
+
1064
+ if (typeof change.dry === 'boolean') {
1065
+ const body = { command: 'setDry', parameter: change.dry ? 'on' : 'off', commandType: 'command' }
1066
+ try {
1067
+ return await this.client.setDeviceState(this.opts.id, body)
1068
+ } catch (err) {
1069
+ try {
1070
+ if (typeof this.client.sendCommand === 'function') { return await this.client.sendCommand(this.opts.id, body) }
1071
+ } catch (e) {}
1072
+ const e = err as any
1073
+ return { success: false, reason: e?.message ?? String(e) }
1074
+ }
1075
+ }
1076
+
1077
+ return super.setState(change)
1078
+ }
1079
+ }
1080
+
1081
+ // Provide Matter descriptor for humidifier (humidity and on/off)
1082
+ export class HumidifierMatterDevice extends HumidifierDevice {
1083
+ async createMatterAccessory(api: any): Promise<any> {
1084
+ return {
1085
+ id: this.opts.id,
1086
+ name: this.opts.name ?? this.opts.type,
1087
+ protocol: 'matter',
1088
+ clusters: [
1089
+ {
1090
+ // Relative Humidity Sensor cluster
1091
+ type: 'RelativeHumiditySensor',
1092
+ clusterId: MATTER_CLUSTER_IDS.RelativeHumidityMeasurement,
1093
+ attributes: {
1094
+ currentRelativeHumidity: { read: async () => { const s = await this.getState(); return typeof s.humidity === 'number' ? s.humidity : 0 }, write: undefined },
1095
+ [MATTER_ATTRIBUTE_IDS.RelativeHumidityMeasurement.MeasuredValue]: { read: async () => { const s = await this.getState(); return typeof s.humidity === 'number' ? s.humidity : 0 }, write: undefined },
1096
+ },
1097
+ },
1098
+ {
1099
+ type: 'OnOff',
1100
+ clusterId: MATTER_CLUSTER_IDS.OnOff,
1101
+ attributes: {
1102
+ onOff: { read: async () => { const s = await this.getState(); return !!s?.on }, write: async (v: any) => this.setState({ on: !!v }) },
1103
+ [MATTER_ATTRIBUTE_IDS.OnOff.OnOff]: { read: async () => { const s = await this.getState(); return !!s?.on }, write: async (v: any) => this.setState({ on: !!v }) },
1104
+ },
1105
+ },
1106
+ ],
1107
+ }
1108
+ }
1109
+ }
1110
+
1111
+ export class TemperatureSensorDevice extends GenericDevice {
1112
+ createHAPAccessory(api: any) {
1113
+ return {
1114
+ services: [
1115
+ {
1116
+ type: 'TemperatureSensor',
1117
+ characteristics: {
1118
+ CurrentTemperature: {
1119
+ get: async () => {
1120
+ const s = await this.getState()
1121
+ return typeof s.temperature === 'number' ? s.temperature : 0
1122
+ },
1123
+ },
1124
+ },
1125
+ },
1126
+ ],
1127
+ }
1128
+ }
1129
+ }
1130
+
1131
+ // Additional device classes (aliases / specialized variants)
1132
+ export class RelaySwitchDevice extends GenericDevice {}
1133
+ export class RelaySwitch1PMDevice extends GenericDevice {}
1134
+ export class PlugDevice extends GenericDevice {
1135
+ createHAPAccessory(api: any) {
1136
+ return {
1137
+ services: [
1138
+ {
1139
+ type: 'Outlet',
1140
+ characteristics: {
1141
+ On: {
1142
+ get: async () => {
1143
+ const s = await this.getState()
1144
+ return !!(s && (s.on === true || s.state === 'on'))
1145
+ },
1146
+ set: async (v: any) => {
1147
+ await this.setState({ on: !!v })
1148
+ },
1149
+ },
1150
+ OutletInUse: {
1151
+ get: async () => {
1152
+ const s = await this.getState()
1153
+ return s && s.inUse ? 1 : 0
1154
+ },
1155
+ },
1156
+ },
1157
+ },
1158
+ ],
1159
+ }
1160
+ }
1161
+ }
1162
+
1163
+ export class PlugMiniDevice extends PlugDevice {}
1164
+
1165
+ export class BlindTiltDevice extends CurtainDevice {}
1166
+ export class Curtain3Device extends CurtainDevice {}
1167
+ export class RollerShadeDevice extends CurtainDevice {}
1168
+
1169
+ export class Hub2Device extends GenericDevice {}
1170
+
1171
+ export class MeterDevice extends GenericDevice {
1172
+ createHAPAccessory(api: any) {
1173
+ return {
1174
+ services: [
1175
+ {
1176
+ type: 'TemperatureSensor',
1177
+ characteristics: {
1178
+ CurrentTemperature: {
1179
+ get: async () => {
1180
+ const s = await this.getState()
1181
+ return typeof s.temperature === 'number' ? s.temperature : 0
1182
+ },
1183
+ },
1184
+ },
1185
+ },
1186
+ {
1187
+ type: 'HumiditySensor',
1188
+ characteristics: {
1189
+ CurrentRelativeHumidity: {
1190
+ get: async () => {
1191
+ const s = await this.getState()
1192
+ return typeof s.humidity === 'number' ? s.humidity : 0
1193
+ },
1194
+ },
1195
+ },
1196
+ },
1197
+ ],
1198
+ }
1199
+ }
1200
+ }
1201
+
1202
+ export class WaterDetectorDevice extends GenericDevice {
1203
+ createHAPAccessory(api: any) {
1204
+ return {
1205
+ services: [
1206
+ {
1207
+ type: 'LeakSensor',
1208
+ characteristics: {
1209
+ LeakDetected: {
1210
+ get: async () => {
1211
+ const s = await this.getState()
1212
+ return s && s.leak ? 1 : 0
1213
+ },
1214
+ },
1215
+ },
1216
+ },
1217
+ ],
1218
+ }
1219
+ }
1220
+ }
1221
+
1222
+ export class SmartFanDevice extends FanDevice {}
1223
+
1224
+ export class StripLightDevice extends LightStripDevice {}
1225
+
1226
+ export class WalletFinderDevice extends GenericDevice {}
1227
+
1228
+ // K10 / WoSweeper variants map to VacuumDevice
1229
+ export class WoSweeperDevice extends VacuumDevice {}
1230
+ export class WoSweeperMiniDevice extends VacuumDevice {}
1231
+ export class WoSweeperMiniProDevice extends VacuumDevice {}
1232
+ // Add missing closing braces for MeterDevice and WaterDetectorDevice