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

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