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

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 +146 -0
  19. package/dist/deviceFactory.js.map +1 -0
  20. package/dist/devices/deviceBase.d.ts +50 -0
  21. package/dist/devices/deviceBase.d.ts.map +1 -0
  22. package/dist/devices/deviceBase.js +119 -0
  23. package/dist/devices/deviceBase.js.map +1 -0
  24. package/dist/devices/genericDevice.d.ts +276 -0
  25. package/dist/devices/genericDevice.d.ts.map +1 -0
  26. package/dist/devices/genericDevice.js +1320 -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 +190 -0
  172. package/src/devices/deviceBase.ts +141 -0
  173. package/src/devices/genericDevice.ts +1258 -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,1258 @@
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
+ async createMatterAccessory(api: any): Promise<any> {
431
+ // Determine device type and features
432
+ const typeKey = (this.opts.type || '').toLowerCase()
433
+ // Dynamic detection: check config/state for lift/tilt support
434
+ const hasConfigLift = !!this.opts.lift || typeof this.opts.lift === 'number'
435
+ const hasConfigTilt = !!this.opts.tilt || typeof this.opts.tilt === 'number'
436
+ let hasLiftAttrs = false
437
+ let hasTiltAttrs = false
438
+ if (typeof this.getState === 'function') {
439
+ let state: any
440
+ try {
441
+ state = await this.getState()
442
+ } catch (e) {
443
+ state = undefined
444
+ }
445
+ if (state) {
446
+ hasLiftAttrs = typeof state.position === 'number'
447
+ hasTiltAttrs = typeof state.tilt === 'number'
448
+ }
449
+ }
450
+ // Default supportsLift to true for curtain/rollershade/blindtilt unless config or state disables it
451
+ let supportsLift = hasConfigLift || hasLiftAttrs
452
+ if (!supportsLift && (typeKey.includes('curtain') || typeKey.includes('rollershade') || typeKey.includes('blindtilt'))) {
453
+ supportsLift = true
454
+ }
455
+ const supportsTilt = hasConfigTilt || hasTiltAttrs || typeKey.includes('blindtilt') || typeKey.includes('blind tilt')
456
+
457
+ // If neither lift nor tilt is supported, set type to Curtain (4)
458
+ let matterType: number
459
+ if (!supportsLift && !supportsTilt) {
460
+ matterType = 4 // Curtain
461
+ } else if (supportsLift && !supportsTilt) {
462
+ matterType = 0 // RollerShade (lift-only)
463
+ } else if (supportsLift && supportsTilt) {
464
+ matterType = 7 // BlindTilt (lift+tilt)
465
+ } else if (!supportsLift && supportsTilt) {
466
+ matterType = 8 // Tilt-only (if supported by Matter)
467
+ } else {
468
+ // fallback to config or default
469
+ try {
470
+ const clusterConf = DEVICE_MATTER_CLUSTERS[typeKey] || DEVICE_MATTER_CLUSTERS.curtain
471
+ if (clusterConf && clusterConf.windowCovering && typeof clusterConf.windowCovering.type === 'number') {
472
+ matterType = clusterConf.windowCovering.type
473
+ } else {
474
+ matterType = 4 // Curtain as safe fallback
475
+ }
476
+ } catch (e) {
477
+ matterType = 4
478
+ }
479
+ }
480
+
481
+ // Build attributes for Matter WindowCovering
482
+ const attributes: Record<string, any> = {
483
+ type: { read: async () => matterType, write: undefined },
484
+ currentPosition: { read: async () => { const s = await this.getState(); return typeof s.position === 'number' ? s.position : 0 }, write: undefined },
485
+ [MATTER_ATTRIBUTE_IDS.WindowCovering.CurrentPosition]: { read: async () => { const s = await this.getState(); return typeof s.position === 'number' ? s.position : 0 }, write: undefined },
486
+ 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) }) },
487
+ [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) }) },
488
+ }
489
+ if (supportsTilt) {
490
+ // Expose tilt attributes for BlindTilt
491
+ attributes.currentTilt = { read: async () => { const s = await this.getState(); return typeof s.tilt === 'number' ? s.tilt : 0 }, write: undefined }
492
+ 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) }) }
493
+ }
494
+
495
+ return {
496
+ id: this.opts.id,
497
+ name: this.opts.name ?? this.opts.type,
498
+ protocol: 'matter',
499
+ clusters: [
500
+ {
501
+ // Shade cluster
502
+ type: 'Shade',
503
+ clusterId: MATTER_CLUSTER_IDS.WindowCovering,
504
+ attributes,
505
+ },
506
+ ],
507
+ }
508
+ }
509
+ }
510
+
511
+ export class FanDevice extends GenericDevice {
512
+ createHAPAccessory(api: any) {
513
+ return {
514
+ services: [
515
+ {
516
+ type: 'Fan',
517
+ characteristics: {
518
+ On: {
519
+ get: async () => {
520
+ const s = await this.getState()
521
+ return !!(s && (s.on === true || s.state === 'on'))
522
+ },
523
+ set: async (v: any) => {
524
+ await this.setState({ on: !!v })
525
+ },
526
+ },
527
+ RotationSpeed: {
528
+ get: async () => {
529
+ const s = await this.getState()
530
+ return typeof s.speed === 'number' ? s.speed : 0
531
+ },
532
+ set: async (v: any) => {
533
+ await this.setState({ speed: Number(v) })
534
+ },
535
+ },
536
+ },
537
+ },
538
+ ],
539
+ }
540
+ }
541
+
542
+ async setState(change: any): Promise<any> {
543
+ if (!this.client) { return { success: false, reason: 'no client' } }
544
+
545
+ // Oscillation support
546
+ if (typeof change.oscillate === 'boolean') {
547
+ const body = { command: 'setOscillation', parameter: change.oscillate ? 'on' : 'off', commandType: 'command' }
548
+ try {
549
+ return await this.client.setDeviceState(this.opts.id, body)
550
+ } catch (err) {
551
+ try {
552
+ if (typeof this.client.sendCommand === 'function') { return await this.client.sendCommand(this.opts.id, body) }
553
+ } catch (e) {}
554
+ const e = err as any
555
+ return { success: false, reason: e?.message ?? String(e) }
556
+ }
557
+ }
558
+
559
+ // Swing / sweep support (angle or mode)
560
+ if (change && (typeof change.swing === 'boolean' || typeof change.swingAngle === 'number' || typeof change.swingMode === 'string')) {
561
+ let param: string = 'default'
562
+ if (typeof change.swingMode === 'string') { param = change.swingMode } else if (typeof change.swingAngle === 'number') { param = String(Number(change.swingAngle)) } else {
563
+ param = change.swing ? 'on' : 'off'
564
+ }
565
+
566
+ const body = { command: 'setSwing', parameter: param, commandType: 'command' }
567
+ try {
568
+ return await this.client.setDeviceState(this.opts.id, body)
569
+ } catch (err) {
570
+ try {
571
+ if (typeof this.client.sendCommand === 'function') { return await this.client.sendCommand(this.opts.id, body) }
572
+ } catch (e) {}
573
+ const e = err as any
574
+ return { success: false, reason: e?.message ?? String(e) }
575
+ }
576
+ }
577
+
578
+ return super.setState(change)
579
+ }
580
+
581
+ // Matter-specific descriptor for Fan
582
+ createMatterAccessory(api: any): any {
583
+ return {
584
+ id: this.opts.id,
585
+ name: this.opts.name ?? this.opts.type,
586
+ protocol: 'matter',
587
+ clusters: [
588
+ {
589
+ // OnOff cluster
590
+ type: 'OnOff',
591
+ clusterId: MATTER_CLUSTER_IDS.OnOff,
592
+ attributes: {
593
+ onOff: { read: async () => { const s = await this.getState(); return !!(s && (s.on === true || s.state === 'on')) }, write: async (v: any) => this.setState({ on: !!v }) },
594
+ // numeric attribute id for onOff
595
+ [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 }) },
596
+ },
597
+ },
598
+ {
599
+ // Fan Control cluster
600
+ type: 'FanControl',
601
+ clusterId: MATTER_CLUSTER_IDS.FanControl,
602
+ attributes: {
603
+ 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) }) },
604
+ [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) }) },
605
+ oscillation: { read: async () => { const s = await this.getState(); return !!s?.oscillating }, write: async (v: any) => this.setState({ oscillate: !!v }) },
606
+ swingMode: { read: async () => { const s = await this.getState(); return s?.swingMode ?? null }, write: async (v: any) => this.setState({ swingMode: v }) },
607
+ },
608
+ },
609
+ ],
610
+ }
611
+ }
612
+ }
613
+ export class LightDevice extends GenericDevice {
614
+ createHAPAccessory(api: any) {
615
+ return {
616
+ services: [
617
+ {
618
+ type: 'Lightbulb',
619
+ characteristics: {
620
+ On: {
621
+ get: async () => {
622
+ const s = await this.getState()
623
+ return !!(s && (s.on === true || s.state === 'on' || s.power === 'on'))
624
+ },
625
+ set: async (v: any) => {
626
+ await this.setState({ on: !!v })
627
+ },
628
+ },
629
+ Brightness: {
630
+ props: { minValue: 0, maxValue: 100, minStep: 1 },
631
+ get: async () => {
632
+ const s = await this.getState()
633
+ return typeof s.brightness === 'number' ? s.brightness : 100
634
+ },
635
+ set: async (v: any) => {
636
+ await this.setState({ brightness: Number(v) })
637
+ },
638
+ },
639
+ Hue: {
640
+ props: { minValue: 0, maxValue: 360, minStep: 1 },
641
+ get: async () => {
642
+ const s = await this.getState()
643
+ // prefer explicit hue if provided
644
+ if (s && typeof s.hue === 'number') { return s.hue }
645
+ // try HSV from color hex
646
+ const hex = s?.color || s?.colorHex || s?.colour
647
+ if (typeof hex === 'string' && HEX_COLOR_REGEX.test(hex)) {
648
+ const h = (() => {
649
+ const hsl = (h: number, s: number, l: number) => ({ h, s, l })
650
+ // convert hex -> rgb -> hsv
651
+ const cleaned = hex.replace('#', '')
652
+ const r = Number.parseInt(cleaned.substr(0, 2), 16) / 255
653
+ const g = Number.parseInt(cleaned.substr(2, 2), 16) / 255
654
+ const b = Number.parseInt(cleaned.substr(4, 2), 16) / 255
655
+ const mx = Math.max(r, g, b); const mn = Math.min(r, g, b)
656
+ const d = mx - mn
657
+ if (d === 0) { return 0 }
658
+ let hue = 0
659
+ switch (mx) {
660
+ case r: hue = ((g - b) / d) % 6; break
661
+ case g: hue = (b - r) / d + 2; break
662
+ case b: hue = (r - g) / d + 4; break
663
+ }
664
+ hue = Math.round(hue * 60)
665
+ if (hue < 0) { hue += 360 }
666
+ return hue
667
+ })()
668
+ return h
669
+ }
670
+ return 0
671
+ },
672
+ set: async (v: any) => {
673
+ await this.setState({ hue: Number(v) })
674
+ },
675
+ },
676
+ Saturation: {
677
+ props: { minValue: 0, maxValue: 100, minStep: 1 },
678
+ get: async () => {
679
+ const s = await this.getState()
680
+ if (s && typeof s.saturation === 'number') { return s.saturation }
681
+ // if color hex is available, derive saturation from rgb
682
+ const hex = s?.color || s?.colorHex || s?.colour
683
+ if (typeof hex === 'string' && HEX_COLOR_REGEX.test(hex)) {
684
+ const cleaned = hex.replace('#', '')
685
+ const r = Number.parseInt(cleaned.substr(0, 2), 16) / 255
686
+ const g = Number.parseInt(cleaned.substr(2, 2), 16) / 255
687
+ const b = Number.parseInt(cleaned.substr(4, 2), 16) / 255
688
+ const mx = Math.max(r, g, b); const mn = Math.min(r, g, b)
689
+ const d = mx - mn
690
+ const sat = mx === 0 ? 0 : Math.round((d / mx) * 100)
691
+ return sat
692
+ }
693
+ return 0
694
+ },
695
+ set: async (v: any) => {
696
+ await this.setState({ saturation: Number(v) })
697
+ },
698
+ },
699
+ ColorTemperature: {
700
+ props: { minValue: 153, maxValue: 500, minStep: 1 },
701
+ get: async () => {
702
+ const s = await this.getState()
703
+ // prefer mired if provided
704
+ if (s && typeof s.colorTemperature === 'number') { return s.colorTemperature }
705
+ if (s && typeof s.color_temp === 'number') { return s.color_temp }
706
+ // some devices provide kelvin
707
+ if (s && typeof s.kelvin === 'number' && s.kelvin > 0) {
708
+ return Math.round(1000000 / s.kelvin)
709
+ }
710
+ return 400
711
+ },
712
+ set: async (v: any) => {
713
+ await this.setState({ colorTemperature: Number(v) })
714
+ },
715
+ },
716
+ },
717
+ },
718
+ ],
719
+ }
720
+ }
721
+
722
+ async setState(change: any): Promise<any> {
723
+ if (!this.client) { return { success: false, reason: 'no client' } }
724
+
725
+ // Color temperature (mired) or brightness/hue/sat
726
+ if (typeof change.colorTemperature === 'number' || typeof change.color_temp === 'number') {
727
+ const v = String(Number(change.colorTemperature ?? change.color_temp))
728
+ const body = { command: 'setColorTemperature', parameter: v, 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
+ if (typeof change.hue === 'number' && typeof change.saturation === 'number') {
741
+ const body = { command: 'setColor', parameter: `${Number(change.hue)},${Number(change.saturation)}`, commandType: 'command' }
742
+ try {
743
+ return await this.client.setDeviceState(this.opts.id, body)
744
+ } catch (err) {
745
+ try {
746
+ if (typeof this.client.sendCommand === 'function') { return await this.client.sendCommand(this.opts.id, body) }
747
+ } catch (e) {}
748
+ const e = err as any
749
+ return { success: false, reason: e?.message ?? String(e) }
750
+ }
751
+ }
752
+
753
+ if (change && typeof change.color === 'string') {
754
+ const body = { command: 'setColor', parameter: change.color, commandType: 'command' }
755
+ try {
756
+ return await this.client.setDeviceState(this.opts.id, body)
757
+ } catch (err) {
758
+ try {
759
+ if (typeof this.client.sendCommand === 'function') { return await this.client.sendCommand(this.opts.id, body) }
760
+ } catch (e) {}
761
+ const e = err as any
762
+ return { success: false, reason: e?.message ?? String(e) }
763
+ }
764
+ }
765
+
766
+ // Fallback to generic handler (brightness/on)
767
+ return super.setState(change)
768
+ }
769
+
770
+ // Matter-specific descriptor for lights (OnOff + Level + Color)
771
+ createMatterAccessory(api: any): any {
772
+ return {
773
+ id: this.opts.id,
774
+ name: this.opts.name ?? this.opts.type,
775
+ protocol: 'matter',
776
+ clusters: [
777
+ {
778
+ // OnOff cluster
779
+ type: 'OnOff',
780
+ clusterId: MATTER_CLUSTER_IDS.OnOff,
781
+ attributes: {
782
+ 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 }) },
783
+ [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 }) },
784
+ },
785
+ },
786
+ {
787
+ // Level Control cluster
788
+ type: 'LevelControl',
789
+ clusterId: MATTER_CLUSTER_IDS.LevelControl,
790
+ attributes: {
791
+ 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) }) },
792
+ [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) }) },
793
+ },
794
+ },
795
+ {
796
+ // Color Control cluster
797
+ type: 'ColorControl',
798
+ clusterId: MATTER_CLUSTER_IDS.ColorControl,
799
+ attributes: {
800
+ // Required colorMode attribute for Matter conformance (0 = currentHueAndSaturation)
801
+ colorMode: { read: async () => 0 },
802
+ 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) }) },
803
+ [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) }) },
804
+ 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) }) },
805
+ [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) }) },
806
+ colorTemperature: { read: async () => {
807
+ 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
808
+ }, write: async (v: any) => this.setState({ colorTemperature: Number(v) }) },
809
+ [MATTER_ATTRIBUTE_IDS.ColorControl.ColorTemperatureMireds]: { read: async () => {
810
+ 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
811
+ }, write: async (v: any) => this.setState({ colorTemperature: Number(v) }) },
812
+ },
813
+ },
814
+ ],
815
+ }
816
+ }
817
+ }
818
+
819
+ export class LightStripDevice extends LightDevice {}
820
+
821
+ export class MotionSensorDevice extends GenericDevice {
822
+ createHAPAccessory(api: any) {
823
+ return {
824
+ services: [
825
+ {
826
+ type: 'MotionSensor',
827
+ characteristics: {
828
+ MotionDetected: {
829
+ get: async () => {
830
+ const s = await this.getState()
831
+ return !!(s && s.motion === true)
832
+ },
833
+ },
834
+ },
835
+ },
836
+ ],
837
+ }
838
+ }
839
+
840
+ async setState(change: any): Promise<any> {
841
+ if (!this.client) { return { success: false, reason: 'no client' } }
842
+
843
+ // Oscillation support
844
+ if (typeof change.oscillate === 'boolean') {
845
+ const body = { command: 'setOscillation', parameter: change.oscillate ? 'on' : 'off', commandType: 'command' }
846
+ try {
847
+ return await this.client.setDeviceState(this.opts.id, body)
848
+ } catch (err) {
849
+ try {
850
+ if (typeof this.client.sendCommand === 'function') { return await this.client.sendCommand(this.opts.id, body) }
851
+ } catch (e) {}
852
+ const e = err as any
853
+ return { success: false, reason: e?.message ?? String(e) }
854
+ }
855
+ }
856
+
857
+ // Swing / sweep support (angle or mode)
858
+ if (change && (typeof change.swing === 'boolean' || typeof change.swingAngle === 'number' || typeof change.swingMode === 'string')) {
859
+ let param: string = 'default'
860
+ if (typeof change.swingMode === 'string') { param = change.swingMode } else if (typeof change.swingAngle === 'number') { param = String(Number(change.swingAngle)) } else {
861
+ param = change.swing ? 'on' : 'off'
862
+ }
863
+
864
+ const body = { command: 'setSwing', parameter: param, commandType: 'command' }
865
+ try {
866
+ return await this.client.setDeviceState(this.opts.id, body)
867
+ } catch (err) {
868
+ try {
869
+ if (typeof this.client.sendCommand === 'function') { return await this.client.sendCommand(this.opts.id, body) }
870
+ } catch (e) {}
871
+ const e = err as any
872
+ return { success: false, reason: e?.message ?? String(e) }
873
+ }
874
+ }
875
+
876
+ return super.setState(change)
877
+ }
878
+ }
879
+
880
+ export class ContactSensorDevice extends GenericDevice {
881
+ createHAPAccessory(api: any) {
882
+ return {
883
+ services: [
884
+ {
885
+ type: 'ContactSensor',
886
+ characteristics: {
887
+ ContactSensorState: {
888
+ get: async () => {
889
+ const s = await this.getState()
890
+ return s && s.open ? 1 : 0
891
+ },
892
+ },
893
+ },
894
+ },
895
+ ],
896
+ }
897
+ }
898
+ }
899
+
900
+ export class VacuumDevice extends GenericDevice {
901
+ // Use DeviceBase defaults (Switch-style) — no override needed
902
+ createHAPAccessory(api: any) {
903
+ return super.createHAPAccessory(api)
904
+ }
905
+ }
906
+
907
+ export class LockDevice extends GenericDevice {
908
+ createHAPAccessory(api: any) {
909
+ return {
910
+ services: [
911
+ {
912
+ type: 'LockMechanism',
913
+ characteristics: {
914
+ LockCurrentState: {
915
+ get: async () => {
916
+ const s = await this.getState()
917
+ return s && s.locked ? 1 : 0
918
+ },
919
+ },
920
+ LockTargetState: {
921
+ get: async () => {
922
+ const s = await this.getState()
923
+ return s && s.locked ? 1 : 0
924
+ },
925
+ set: async (v: any) => {
926
+ await this.setState({ locked: !!v })
927
+ },
928
+ },
929
+ },
930
+ },
931
+ ],
932
+ }
933
+ }
934
+
935
+ async setState(change: any): Promise<any> {
936
+ if (!this.client) { return { success: false, reason: 'no client' } }
937
+
938
+ // User management actions: add/remove/list users, unlock with pin
939
+ if (change && typeof change.action === 'string') {
940
+ const action = change.action
941
+ try {
942
+ if (action === 'addUser' && (change.user || change.userId) && (change.pin || change.code)) {
943
+ const user = change.user ?? change.userId
944
+ const p = String(change.pin ?? change.code)
945
+ const body = { command: 'addUserCode', parameter: `${user}:${p}`, commandType: 'command' }
946
+ return await this.client.setDeviceState(this.opts.id, body)
947
+ }
948
+
949
+ if (action === 'removeUser' && (change.user || change.userId)) {
950
+ const user = change.user ?? change.userId
951
+ const body = { command: 'removeUserCode', parameter: String(user), commandType: 'command' }
952
+ return await this.client.setDeviceState(this.opts.id, body)
953
+ }
954
+
955
+ if (action === 'listUsers') {
956
+ const body = { command: 'listUsers', parameter: 'default', commandType: 'command' }
957
+ return await this.client.setDeviceState(this.opts.id, body)
958
+ }
959
+
960
+ if (action === 'unlockWithPin' && (change.pin || change.code)) {
961
+ const p = String(change.pin ?? change.code)
962
+ const body = { command: 'unlockWithPin', parameter: p, commandType: 'command' }
963
+ return await this.client.setDeviceState(this.opts.id, body)
964
+ }
965
+ } catch (err) {
966
+ try {
967
+ if (typeof this.client.sendCommand === 'function') { return await this.client.sendCommand(this.opts.id, { command: action, parameter: change.parameter ?? 'default', commandType: 'command' }) }
968
+ } catch (e) {}
969
+ const e = err as any
970
+ return { success: false, reason: e?.message ?? String(e) }
971
+ }
972
+ }
973
+
974
+ // Support setting lock PIN/passcode via `pin` or `passcode` (fallback)
975
+ const pin = change?.pin ?? change?.passcode ?? change?.code
976
+ if (typeof pin === 'string' || typeof pin === 'number') {
977
+ const body = { command: 'setLockPin', parameter: String(pin), commandType: 'command' }
978
+ try {
979
+ return await this.client.setDeviceState(this.opts.id, body)
980
+ } catch (err) {
981
+ try {
982
+ if (typeof this.client.sendCommand === 'function') { return await this.client.sendCommand(this.opts.id, body) }
983
+ } catch (e) {}
984
+ const e = err as any
985
+ return { success: false, reason: e?.message ?? String(e) }
986
+ }
987
+ }
988
+
989
+ return super.setState(change)
990
+ }
991
+
992
+ // Matter DoorLock descriptor including simple user-management actions
993
+ createMatterAccessory(api: any): any {
994
+ return {
995
+ id: this.opts.id,
996
+ name: this.opts.name ?? this.opts.type,
997
+ protocol: 'matter',
998
+ clusters: [
999
+ {
1000
+ // DoorLock cluster
1001
+ type: 'DoorLock',
1002
+ clusterId: MATTER_CLUSTER_IDS.DoorLock,
1003
+ attributes: {
1004
+ lockState: { read: async () => { const s = await this.getState(); return !!(s && s.locked) }, write: async (v: any) => this.setState({ locked: !!v }) },
1005
+ [MATTER_ATTRIBUTE_IDS.DoorLock.LockState]: { read: async () => { const s = await this.getState(); return !!(s && s.locked) }, write: async (v: any) => this.setState({ locked: !!v }) },
1006
+ },
1007
+ },
1008
+ {
1009
+ // DoorLock user mgmt cluster (conceptual)
1010
+ type: 'DoorLockUserManagement',
1011
+ clusterId: 0x0301,
1012
+ attributes: {
1013
+ addUser: { write: async (v: any) => this.setState({ action: 'addUser', user: v?.user, pin: v?.pin }) },
1014
+ removeUser: { write: async (v: any) => this.setState({ action: 'removeUser', user: v?.user }) },
1015
+ listUsers: { read: async () => { const r = await this.setState({ action: 'listUsers' }); return r }, write: undefined },
1016
+ unlockWithPin: { write: async (v: any) => this.setState({ action: 'unlockWithPin', pin: v?.pin }) },
1017
+ },
1018
+ },
1019
+ ],
1020
+ }
1021
+ }
1022
+ }
1023
+
1024
+ export class HumidifierDevice extends GenericDevice {
1025
+ createHAPAccessory(api: any) {
1026
+ return {
1027
+ services: [
1028
+ {
1029
+ type: 'HumidifierDehumidifier',
1030
+ characteristics: {
1031
+ Active: {
1032
+ get: async () => {
1033
+ const s = await this.getState()
1034
+ return s && s.on ? 1 : 0
1035
+ },
1036
+ set: async (v: any) => {
1037
+ await this.setState({ on: v === 1 })
1038
+ },
1039
+ },
1040
+ CurrentHumidifierDehumidifierState: {
1041
+ get: async () => {
1042
+ const s = await this.getState()
1043
+ return s && s.on ? 2 : 0 // 0 = Inactive, 2 = Humidifying
1044
+ },
1045
+ },
1046
+ TargetHumidifierDehumidifierState: {
1047
+ get: async () => 1, // 1 = Humidifier
1048
+ set: async (v: any) => {
1049
+ // Support humidifier mode
1050
+ },
1051
+ },
1052
+ CurrentRelativeHumidity: {
1053
+ get: async () => {
1054
+ const s = await this.getState()
1055
+ return typeof s.humidity === 'number' ? s.humidity : 0
1056
+ },
1057
+ },
1058
+ RelativeHumidityHumidifierThreshold: {
1059
+ get: async () => {
1060
+ const s = await this.getState()
1061
+ return typeof s.targetHumidity === 'number' ? s.targetHumidity : 50
1062
+ },
1063
+ set: async (v: any) => {
1064
+ await this.setState({ humidity: Number(v) })
1065
+ },
1066
+ },
1067
+ },
1068
+ },
1069
+ ],
1070
+ }
1071
+ }
1072
+
1073
+ async setState(change: any): Promise<any> {
1074
+ if (!this.client) { return { success: false, reason: 'no client' } }
1075
+
1076
+ if (typeof change.humidity === 'number') {
1077
+ const v = String(Number(change.humidity))
1078
+ const body = { command: 'setHumidity', parameter: v, commandType: 'command' }
1079
+ try {
1080
+ return await this.client.setDeviceState(this.opts.id, body)
1081
+ } catch (err) {
1082
+ try {
1083
+ if (typeof this.client.sendCommand === 'function') { return await this.client.sendCommand(this.opts.id, body) }
1084
+ } catch (e) {}
1085
+ const e = err as any
1086
+ return { success: false, reason: e?.message ?? String(e) }
1087
+ }
1088
+ }
1089
+
1090
+ if (typeof change.dry === 'boolean') {
1091
+ const body = { command: 'setDry', parameter: change.dry ? 'on' : 'off', commandType: 'command' }
1092
+ try {
1093
+ return await this.client.setDeviceState(this.opts.id, body)
1094
+ } catch (err) {
1095
+ try {
1096
+ if (typeof this.client.sendCommand === 'function') { return await this.client.sendCommand(this.opts.id, body) }
1097
+ } catch (e) {}
1098
+ const e = err as any
1099
+ return { success: false, reason: e?.message ?? String(e) }
1100
+ }
1101
+ }
1102
+
1103
+ return super.setState(change)
1104
+ }
1105
+ }
1106
+
1107
+ // Provide Matter descriptor for humidifier (humidity and on/off)
1108
+ export class HumidifierMatterDevice extends HumidifierDevice {
1109
+ async createMatterAccessory(api: any): Promise<any> {
1110
+ return {
1111
+ id: this.opts.id,
1112
+ name: this.opts.name ?? this.opts.type,
1113
+ protocol: 'matter',
1114
+ clusters: [
1115
+ {
1116
+ // Relative Humidity Sensor cluster
1117
+ type: 'RelativeHumiditySensor',
1118
+ clusterId: MATTER_CLUSTER_IDS.RelativeHumidityMeasurement,
1119
+ attributes: {
1120
+ currentRelativeHumidity: { read: async () => { const s = await this.getState(); return typeof s.humidity === 'number' ? s.humidity : 0 }, write: undefined },
1121
+ [MATTER_ATTRIBUTE_IDS.RelativeHumidityMeasurement.MeasuredValue]: { read: async () => { const s = await this.getState(); return typeof s.humidity === 'number' ? s.humidity : 0 }, write: undefined },
1122
+ },
1123
+ },
1124
+ {
1125
+ type: 'OnOff',
1126
+ clusterId: MATTER_CLUSTER_IDS.OnOff,
1127
+ attributes: {
1128
+ onOff: { read: async () => { const s = await this.getState(); return !!s?.on }, write: async (v: any) => this.setState({ on: !!v }) },
1129
+ [MATTER_ATTRIBUTE_IDS.OnOff.OnOff]: { read: async () => { const s = await this.getState(); return !!s?.on }, write: async (v: any) => this.setState({ on: !!v }) },
1130
+ },
1131
+ },
1132
+ ],
1133
+ }
1134
+ }
1135
+ }
1136
+
1137
+ export class TemperatureSensorDevice extends GenericDevice {
1138
+ createHAPAccessory(api: any) {
1139
+ return {
1140
+ services: [
1141
+ {
1142
+ type: 'TemperatureSensor',
1143
+ characteristics: {
1144
+ CurrentTemperature: {
1145
+ get: async () => {
1146
+ const s = await this.getState()
1147
+ return typeof s.temperature === 'number' ? s.temperature : 0
1148
+ },
1149
+ },
1150
+ },
1151
+ },
1152
+ ],
1153
+ }
1154
+ }
1155
+ }
1156
+
1157
+ // Additional device classes (aliases / specialized variants)
1158
+ export class RelaySwitchDevice extends GenericDevice {}
1159
+ export class RelaySwitch1PMDevice extends GenericDevice {}
1160
+ export class PlugDevice extends GenericDevice {
1161
+ createHAPAccessory(api: any) {
1162
+ return {
1163
+ services: [
1164
+ {
1165
+ type: 'Outlet',
1166
+ characteristics: {
1167
+ On: {
1168
+ get: async () => {
1169
+ const s = await this.getState()
1170
+ return !!(s && (s.on === true || s.state === 'on'))
1171
+ },
1172
+ set: async (v: any) => {
1173
+ await this.setState({ on: !!v })
1174
+ },
1175
+ },
1176
+ OutletInUse: {
1177
+ get: async () => {
1178
+ const s = await this.getState()
1179
+ return s && s.inUse ? 1 : 0
1180
+ },
1181
+ },
1182
+ },
1183
+ },
1184
+ ],
1185
+ }
1186
+ }
1187
+ }
1188
+
1189
+ export class PlugMiniDevice extends PlugDevice {}
1190
+
1191
+ export class BlindTiltDevice extends CurtainDevice {}
1192
+ export class Curtain3Device extends CurtainDevice {}
1193
+ export class RollerShadeDevice extends CurtainDevice {}
1194
+
1195
+ export class Hub2Device extends GenericDevice {}
1196
+
1197
+ export class MeterDevice extends GenericDevice {
1198
+ createHAPAccessory(api: any) {
1199
+ return {
1200
+ services: [
1201
+ {
1202
+ type: 'TemperatureSensor',
1203
+ characteristics: {
1204
+ CurrentTemperature: {
1205
+ get: async () => {
1206
+ const s = await this.getState()
1207
+ return typeof s.temperature === 'number' ? s.temperature : 0
1208
+ },
1209
+ },
1210
+ },
1211
+ },
1212
+ {
1213
+ type: 'HumiditySensor',
1214
+ characteristics: {
1215
+ CurrentRelativeHumidity: {
1216
+ get: async () => {
1217
+ const s = await this.getState()
1218
+ return typeof s.humidity === 'number' ? s.humidity : 0
1219
+ },
1220
+ },
1221
+ },
1222
+ },
1223
+ ],
1224
+ }
1225
+ }
1226
+ }
1227
+
1228
+ export class WaterDetectorDevice extends GenericDevice {
1229
+ createHAPAccessory(api: any) {
1230
+ return {
1231
+ services: [
1232
+ {
1233
+ type: 'LeakSensor',
1234
+ characteristics: {
1235
+ LeakDetected: {
1236
+ get: async () => {
1237
+ const s = await this.getState()
1238
+ return s && s.leak ? 1 : 0
1239
+ },
1240
+ },
1241
+ },
1242
+ },
1243
+ ],
1244
+ }
1245
+ }
1246
+ }
1247
+
1248
+ export class SmartFanDevice extends FanDevice {}
1249
+
1250
+ export class StripLightDevice extends LightStripDevice {}
1251
+
1252
+ export class WalletFinderDevice extends GenericDevice {}
1253
+
1254
+ // K10 / WoSweeper variants map to VacuumDevice
1255
+ export class WoSweeperDevice extends VacuumDevice {}
1256
+ export class WoSweeperMiniDevice extends VacuumDevice {}
1257
+ export class WoSweeperMiniProDevice extends VacuumDevice {}
1258
+ // Add missing closing braces for MeterDevice and WaterDetectorDevice