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

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 +165 -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 +1396 -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 +1587 -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 +208 -0
  172. package/src/devices/deviceBase.ts +141 -0
  173. package/src/devices/genericDevice.ts +1345 -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 +1601 -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 +110 -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,1345 @@
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
+ const windowCoveringCluster = {
499
+ type: 'WindowCovering', // for test compatibility
500
+ matterType, // actual Matter type number
501
+ clusterId: MATTER_CLUSTER_IDS.WindowCovering,
502
+ attributes,
503
+ }
504
+ const clustersArr: any = [windowCoveringCluster]
505
+ // Always set clusters.windowCovering to the WindowCovering cluster by clusterId
506
+ const foundWC = clustersArr.find((c: any) => c && c.clusterId === MATTER_CLUSTER_IDS.WindowCovering)
507
+ clustersArr.windowCovering = foundWC || null
508
+ return {
509
+ id: this.opts.id,
510
+ name: this.opts.name ?? this.opts.type,
511
+ protocol: 'matter',
512
+ clusters: clustersArr,
513
+ }
514
+ }
515
+
516
+ // Only try allowed types for each feature set (per Matter spec Table 8.2)
517
+ // - supportsLift && !supportsTilt: RollerShade (0), NOT Drapery (4)
518
+ // - !supportsLift && supportsTilt: BlindTilt (8)
519
+ // - supportsLift && supportsTilt: RollerShade (0) (or Blind, but 0 is safe)
520
+ // - supportsLift && supportsTilt && typeKey.includes('drapery'): allow Drapery (4)
521
+ // - !supportsLift && !supportsTilt: skip
522
+ interface MatterCombo {
523
+ matterType: number
524
+ supportsLift: boolean
525
+ supportsTilt: boolean
526
+ }
527
+ const combos: MatterCombo[] = []
528
+ if (supportsLift && supportsTilt) {
529
+ // Both supported: allow RollerShade (0), optionally Drapery (4) if typeKey includes 'drapery'
530
+ combos.push({ matterType: 0, supportsLift: true, supportsTilt: true })
531
+ if (typeKey.includes('drapery')) {
532
+ combos.push({ matterType: 4, supportsLift: true, supportsTilt: true })
533
+ }
534
+ } else if (supportsLift && !supportsTilt) {
535
+ // Only lift: for SwitchBot Curtain, use Drapery (4) for conformance
536
+ if (typeKey.includes('curtain')) {
537
+ combos.push({ matterType: 4, supportsLift: true, supportsTilt: false })
538
+ } else {
539
+ combos.push({ matterType: 0, supportsLift: true, supportsTilt: false })
540
+ }
541
+ } else if (!supportsLift && supportsTilt) {
542
+ // Only tilt: allow BlindTilt (8)
543
+ combos.push({ matterType: 8, supportsLift: false, supportsTilt: true })
544
+ }
545
+ let lastError
546
+ for (const combo of combos) {
547
+ try {
548
+ this.log.info(`[Matter/Retry] Trying combo for ${this.opts.name ?? this.opts.id}: matterType=${combo.matterType}, supportsLift=${combo.supportsLift}, supportsTilt=${combo.supportsTilt}`)
549
+ this.log.debug(`[Matter/Debug] ${this.opts.name ?? this.opts.id} trying combo: matterType=${combo.matterType}, supportsLift=${combo.supportsLift}, supportsTilt=${combo.supportsTilt}`)
550
+ // Build attributes for Matter WindowCovering (string keys only)
551
+ const attributes: Record<string, any> = {
552
+ type: {
553
+ read: async () => {
554
+ this.log.debug(`[Matter/Debug] ${this.opts.name ?? this.opts.id} returning type attribute value:`, combo.matterType)
555
+ return combo.matterType
556
+ },
557
+ write: undefined,
558
+ },
559
+ currentPosition: { read: async () => { const s = await this.getState(); return typeof s.position === 'number' ? s.position : 0 }, write: undefined },
560
+ 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) }) },
561
+ }
562
+ if (combo.supportsTilt) {
563
+ attributes.currentTilt = { read: async () => { const s = await this.getState(); return typeof s.tilt === 'number' ? s.tilt : 0 }, write: undefined }
564
+ 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) }) }
565
+ }
566
+ // Log the final attributes object
567
+ try {
568
+ this.log.debug(`[Matter/Debug] ${this.opts.name ?? this.opts.id} attributes:`, JSON.stringify(attributes, null, 2))
569
+ } catch (e) {
570
+ this.log.debug(`[Matter/Debug] ${this.opts.name ?? this.opts.id} attributes: [unserializable]`)
571
+ }
572
+ // Try to register with this combo
573
+ const windowCoveringCluster = {
574
+ type: 'WindowCovering', // for test compatibility
575
+ matterType: combo.matterType, // actual Matter type number
576
+ clusterId: MATTER_CLUSTER_IDS.WindowCovering,
577
+ attributes,
578
+ }
579
+ const clustersArr: any = [windowCoveringCluster]
580
+ // Always set clusters.windowCovering to the WindowCovering cluster by clusterId
581
+ const foundWC = clustersArr.find((c: any) => c && c.clusterId === MATTER_CLUSTER_IDS.WindowCovering)
582
+ clustersArr.windowCovering = foundWC || null
583
+ return {
584
+ id: this.opts.id,
585
+ name: this.opts.name ?? this.opts.type,
586
+ protocol: 'matter',
587
+ clusters: clustersArr,
588
+ }
589
+ } catch (err) {
590
+ lastError = err
591
+ this.log.warn(`[Matter/Retry] Failed combo matterType=${combo.matterType}, supportsLift=${combo.supportsLift}, supportsTilt=${combo.supportsTilt}: ${err}`)
592
+ }
593
+ }
594
+ // If all combos fail, throw last error
595
+ throw lastError || new Error('All Matter window covering type combos failed')
596
+ }
597
+ }
598
+
599
+ export class FanDevice extends GenericDevice {
600
+ createHAPAccessory(api: any) {
601
+ return {
602
+ services: [
603
+ {
604
+ type: 'Fan',
605
+ characteristics: {
606
+ On: {
607
+ get: async () => {
608
+ const s = await this.getState()
609
+ return !!(s && (s.on === true || s.state === 'on'))
610
+ },
611
+ set: async (v: any) => {
612
+ await this.setState({ on: !!v })
613
+ },
614
+ },
615
+ RotationSpeed: {
616
+ get: async () => {
617
+ const s = await this.getState()
618
+ return typeof s.speed === 'number' ? s.speed : 0
619
+ },
620
+ set: async (v: any) => {
621
+ await this.setState({ speed: Number(v) })
622
+ },
623
+ },
624
+ },
625
+ },
626
+ ],
627
+ }
628
+ }
629
+
630
+ async setState(change: any): Promise<any> {
631
+ if (!this.client) { return { success: false, reason: 'no client' } }
632
+
633
+ // Oscillation support
634
+ if (typeof change.oscillate === 'boolean') {
635
+ const body = { command: 'setOscillation', parameter: change.oscillate ? 'on' : 'off', commandType: 'command' }
636
+ try {
637
+ return await this.client.setDeviceState(this.opts.id, body)
638
+ } catch (err) {
639
+ try {
640
+ if (typeof this.client.sendCommand === 'function') { return await this.client.sendCommand(this.opts.id, body) }
641
+ } catch (e) {}
642
+ const e = err as any
643
+ return { success: false, reason: e?.message ?? String(e) }
644
+ }
645
+ }
646
+
647
+ // Swing / sweep support (angle or mode)
648
+ if (change && (typeof change.swing === 'boolean' || typeof change.swingAngle === 'number' || typeof change.swingMode === 'string')) {
649
+ let param: string = 'default'
650
+ if (typeof change.swingMode === 'string') { param = change.swingMode } else if (typeof change.swingAngle === 'number') { param = String(Number(change.swingAngle)) } else {
651
+ param = change.swing ? 'on' : 'off'
652
+ }
653
+
654
+ const body = { command: 'setSwing', parameter: param, commandType: 'command' }
655
+ try {
656
+ return await this.client.setDeviceState(this.opts.id, body)
657
+ } catch (err) {
658
+ try {
659
+ if (typeof this.client.sendCommand === 'function') { return await this.client.sendCommand(this.opts.id, body) }
660
+ } catch (e) {}
661
+ const e = err as any
662
+ return { success: false, reason: e?.message ?? String(e) }
663
+ }
664
+ }
665
+
666
+ return super.setState(change)
667
+ }
668
+
669
+ // Matter-specific descriptor for Fan
670
+ createMatterAccessory(api: any): any {
671
+ return {
672
+ id: this.opts.id,
673
+ name: this.opts.name ?? this.opts.type,
674
+ protocol: 'matter',
675
+ clusters: [
676
+ {
677
+ // OnOff cluster
678
+ type: 'OnOff',
679
+ clusterId: MATTER_CLUSTER_IDS.OnOff,
680
+ attributes: {
681
+ onOff: { read: async () => { const s = await this.getState(); return !!(s && (s.on === true || s.state === 'on')) }, write: async (v: any) => this.setState({ on: !!v }) },
682
+ // numeric attribute id for onOff
683
+ [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 }) },
684
+ },
685
+ },
686
+ {
687
+ // Fan Control cluster
688
+ type: 'FanControl',
689
+ clusterId: MATTER_CLUSTER_IDS.FanControl,
690
+ attributes: {
691
+ 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) }) },
692
+ [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) }) },
693
+ oscillation: { read: async () => { const s = await this.getState(); return !!s?.oscillating }, write: async (v: any) => this.setState({ oscillate: !!v }) },
694
+ swingMode: { read: async () => { const s = await this.getState(); return s?.swingMode ?? null }, write: async (v: any) => this.setState({ swingMode: v }) },
695
+ },
696
+ },
697
+ ],
698
+ }
699
+ }
700
+ }
701
+ export class LightDevice extends GenericDevice {
702
+ createHAPAccessory(api: any) {
703
+ return {
704
+ services: [
705
+ {
706
+ type: 'Lightbulb',
707
+ characteristics: {
708
+ On: {
709
+ get: async () => {
710
+ const s = await this.getState()
711
+ return !!(s && (s.on === true || s.state === 'on' || s.power === 'on'))
712
+ },
713
+ set: async (v: any) => {
714
+ await this.setState({ on: !!v })
715
+ },
716
+ },
717
+ Brightness: {
718
+ props: { minValue: 0, maxValue: 100, minStep: 1 },
719
+ get: async () => {
720
+ const s = await this.getState()
721
+ return typeof s.brightness === 'number' ? s.brightness : 100
722
+ },
723
+ set: async (v: any) => {
724
+ await this.setState({ brightness: Number(v) })
725
+ },
726
+ },
727
+ Hue: {
728
+ props: { minValue: 0, maxValue: 360, minStep: 1 },
729
+ get: async () => {
730
+ const s = await this.getState()
731
+ // prefer explicit hue if provided
732
+ if (s && typeof s.hue === 'number') { return s.hue }
733
+ // try HSV from color hex
734
+ const hex = s?.color || s?.colorHex || s?.colour
735
+ if (typeof hex === 'string' && HEX_COLOR_REGEX.test(hex)) {
736
+ const h = (() => {
737
+ const hsl = (h: number, s: number, l: number) => ({ h, s, l })
738
+ // convert hex -> rgb -> hsv
739
+ const cleaned = hex.replace('#', '')
740
+ const r = Number.parseInt(cleaned.substr(0, 2), 16) / 255
741
+ const g = Number.parseInt(cleaned.substr(2, 2), 16) / 255
742
+ const b = Number.parseInt(cleaned.substr(4, 2), 16) / 255
743
+ const mx = Math.max(r, g, b); const mn = Math.min(r, g, b)
744
+ const d = mx - mn
745
+ if (d === 0) { return 0 }
746
+ let hue = 0
747
+ switch (mx) {
748
+ case r: hue = ((g - b) / d) % 6; break
749
+ case g: hue = (b - r) / d + 2; break
750
+ case b: hue = (r - g) / d + 4; break
751
+ }
752
+ hue = Math.round(hue * 60)
753
+ if (hue < 0) { hue += 360 }
754
+ return hue
755
+ })()
756
+ return h
757
+ }
758
+ return 0
759
+ },
760
+ set: async (v: any) => {
761
+ await this.setState({ hue: Number(v) })
762
+ },
763
+ },
764
+ Saturation: {
765
+ props: { minValue: 0, maxValue: 100, minStep: 1 },
766
+ get: async () => {
767
+ const s = await this.getState()
768
+ if (s && typeof s.saturation === 'number') { return s.saturation }
769
+ // if color hex is available, derive saturation from rgb
770
+ const hex = s?.color || s?.colorHex || s?.colour
771
+ if (typeof hex === 'string' && HEX_COLOR_REGEX.test(hex)) {
772
+ const cleaned = hex.replace('#', '')
773
+ const r = Number.parseInt(cleaned.substr(0, 2), 16) / 255
774
+ const g = Number.parseInt(cleaned.substr(2, 2), 16) / 255
775
+ const b = Number.parseInt(cleaned.substr(4, 2), 16) / 255
776
+ const mx = Math.max(r, g, b); const mn = Math.min(r, g, b)
777
+ const d = mx - mn
778
+ const sat = mx === 0 ? 0 : Math.round((d / mx) * 100)
779
+ return sat
780
+ }
781
+ return 0
782
+ },
783
+ set: async (v: any) => {
784
+ await this.setState({ saturation: Number(v) })
785
+ },
786
+ },
787
+ ColorTemperature: {
788
+ props: { minValue: 153, maxValue: 500, minStep: 1 },
789
+ get: async () => {
790
+ const s = await this.getState()
791
+ // prefer mired if provided
792
+ if (s && typeof s.colorTemperature === 'number') { return s.colorTemperature }
793
+ if (s && typeof s.color_temp === 'number') { return s.color_temp }
794
+ // some devices provide kelvin
795
+ if (s && typeof s.kelvin === 'number' && s.kelvin > 0) {
796
+ return Math.round(1000000 / s.kelvin)
797
+ }
798
+ return 400
799
+ },
800
+ set: async (v: any) => {
801
+ await this.setState({ colorTemperature: Number(v) })
802
+ },
803
+ },
804
+ },
805
+ },
806
+ ],
807
+ }
808
+ }
809
+
810
+ async setState(change: any): Promise<any> {
811
+ if (!this.client) { return { success: false, reason: 'no client' } }
812
+
813
+ // Color temperature (mired) or brightness/hue/sat
814
+ if (typeof change.colorTemperature === 'number' || typeof change.color_temp === 'number') {
815
+ const v = String(Number(change.colorTemperature ?? change.color_temp))
816
+ const body = { command: 'setColorTemperature', parameter: v, commandType: 'command' }
817
+ try {
818
+ return await this.client.setDeviceState(this.opts.id, body)
819
+ } catch (err) {
820
+ try {
821
+ if (typeof this.client.sendCommand === 'function') { return await this.client.sendCommand(this.opts.id, body) }
822
+ } catch (e) {}
823
+ const e = err as any
824
+ return { success: false, reason: e?.message ?? String(e) }
825
+ }
826
+ }
827
+
828
+ if (typeof change.hue === 'number' && typeof change.saturation === 'number') {
829
+ const body = { command: 'setColor', parameter: `${Number(change.hue)},${Number(change.saturation)}`, commandType: 'command' }
830
+ try {
831
+ return await this.client.setDeviceState(this.opts.id, body)
832
+ } catch (err) {
833
+ try {
834
+ if (typeof this.client.sendCommand === 'function') { return await this.client.sendCommand(this.opts.id, body) }
835
+ } catch (e) {}
836
+ const e = err as any
837
+ return { success: false, reason: e?.message ?? String(e) }
838
+ }
839
+ }
840
+
841
+ if (change && typeof change.color === 'string') {
842
+ const body = { command: 'setColor', parameter: change.color, commandType: 'command' }
843
+ try {
844
+ return await this.client.setDeviceState(this.opts.id, body)
845
+ } catch (err) {
846
+ try {
847
+ if (typeof this.client.sendCommand === 'function') { return await this.client.sendCommand(this.opts.id, body) }
848
+ } catch (e) {}
849
+ const e = err as any
850
+ return { success: false, reason: e?.message ?? String(e) }
851
+ }
852
+ }
853
+
854
+ // Fallback to generic handler (brightness/on)
855
+ return super.setState(change)
856
+ }
857
+
858
+ // Matter-specific descriptor for lights (OnOff + Level + Color)
859
+ createMatterAccessory(api: any): any {
860
+ return {
861
+ id: this.opts.id,
862
+ name: this.opts.name ?? this.opts.type,
863
+ protocol: 'matter',
864
+ clusters: [
865
+ {
866
+ // OnOff cluster
867
+ type: 'OnOff',
868
+ clusterId: MATTER_CLUSTER_IDS.OnOff,
869
+ attributes: {
870
+ 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 }) },
871
+ [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 }) },
872
+ },
873
+ },
874
+ {
875
+ // Level Control cluster
876
+ type: 'LevelControl',
877
+ clusterId: MATTER_CLUSTER_IDS.LevelControl,
878
+ attributes: {
879
+ 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) }) },
880
+ [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) }) },
881
+ },
882
+ },
883
+ {
884
+ // Color Control cluster
885
+ type: 'ColorControl',
886
+ clusterId: MATTER_CLUSTER_IDS.ColorControl,
887
+ attributes: {
888
+ // Required colorMode attribute for Matter conformance (0 = currentHueAndSaturation)
889
+ colorMode: { read: async () => 0 },
890
+ 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) }) },
891
+ [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) }) },
892
+ 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) }) },
893
+ [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) }) },
894
+ colorTemperature: { read: async () => {
895
+ 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
896
+ }, write: async (v: any) => this.setState({ colorTemperature: Number(v) }) },
897
+ [MATTER_ATTRIBUTE_IDS.ColorControl.ColorTemperatureMireds]: { read: async () => {
898
+ 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
899
+ }, write: async (v: any) => this.setState({ colorTemperature: Number(v) }) },
900
+ },
901
+ },
902
+ ],
903
+ }
904
+ }
905
+ }
906
+
907
+ export class LightStripDevice extends LightDevice {}
908
+
909
+ export class MotionSensorDevice extends GenericDevice {
910
+ createHAPAccessory(api: any) {
911
+ return {
912
+ services: [
913
+ {
914
+ type: 'MotionSensor',
915
+ characteristics: {
916
+ MotionDetected: {
917
+ get: async () => {
918
+ const s = await this.getState()
919
+ return !!(s && s.motion === true)
920
+ },
921
+ },
922
+ },
923
+ },
924
+ ],
925
+ }
926
+ }
927
+
928
+ async setState(change: any): Promise<any> {
929
+ if (!this.client) { return { success: false, reason: 'no client' } }
930
+
931
+ // Oscillation support
932
+ if (typeof change.oscillate === 'boolean') {
933
+ const body = { command: 'setOscillation', parameter: change.oscillate ? 'on' : 'off', commandType: 'command' }
934
+ try {
935
+ return await this.client.setDeviceState(this.opts.id, body)
936
+ } catch (err) {
937
+ try {
938
+ if (typeof this.client.sendCommand === 'function') { return await this.client.sendCommand(this.opts.id, body) }
939
+ } catch (e) {}
940
+ const e = err as any
941
+ return { success: false, reason: e?.message ?? String(e) }
942
+ }
943
+ }
944
+
945
+ // Swing / sweep support (angle or mode)
946
+ if (change && (typeof change.swing === 'boolean' || typeof change.swingAngle === 'number' || typeof change.swingMode === 'string')) {
947
+ let param: string = 'default'
948
+ if (typeof change.swingMode === 'string') { param = change.swingMode } else if (typeof change.swingAngle === 'number') { param = String(Number(change.swingAngle)) } else {
949
+ param = change.swing ? 'on' : 'off'
950
+ }
951
+
952
+ const body = { command: 'setSwing', parameter: param, commandType: 'command' }
953
+ try {
954
+ return await this.client.setDeviceState(this.opts.id, body)
955
+ } catch (err) {
956
+ try {
957
+ if (typeof this.client.sendCommand === 'function') { return await this.client.sendCommand(this.opts.id, body) }
958
+ } catch (e) {}
959
+ const e = err as any
960
+ return { success: false, reason: e?.message ?? String(e) }
961
+ }
962
+ }
963
+
964
+ return super.setState(change)
965
+ }
966
+ }
967
+
968
+ export class ContactSensorDevice extends GenericDevice {
969
+ createHAPAccessory(api: any) {
970
+ return {
971
+ services: [
972
+ {
973
+ type: 'ContactSensor',
974
+ characteristics: {
975
+ ContactSensorState: {
976
+ get: async () => {
977
+ const s = await this.getState()
978
+ return s && s.open ? 1 : 0
979
+ },
980
+ },
981
+ },
982
+ },
983
+ ],
984
+ }
985
+ }
986
+ }
987
+
988
+ export class VacuumDevice extends GenericDevice {
989
+ // Use DeviceBase defaults (Switch-style) — no override needed
990
+ createHAPAccessory(api: any) {
991
+ return super.createHAPAccessory(api)
992
+ }
993
+ }
994
+
995
+ export class LockDevice extends GenericDevice {
996
+ createHAPAccessory(api: any) {
997
+ return {
998
+ services: [
999
+ {
1000
+ type: 'LockMechanism',
1001
+ characteristics: {
1002
+ LockCurrentState: {
1003
+ get: async () => {
1004
+ const s = await this.getState()
1005
+ return s && s.locked ? 1 : 0
1006
+ },
1007
+ },
1008
+ LockTargetState: {
1009
+ get: async () => {
1010
+ const s = await this.getState()
1011
+ return s && s.locked ? 1 : 0
1012
+ },
1013
+ set: async (v: any) => {
1014
+ await this.setState({ locked: !!v })
1015
+ },
1016
+ },
1017
+ },
1018
+ },
1019
+ ],
1020
+ }
1021
+ }
1022
+
1023
+ async setState(change: any): Promise<any> {
1024
+ if (!this.client) { return { success: false, reason: 'no client' } }
1025
+
1026
+ // User management actions: add/remove/list users, unlock with pin
1027
+ if (change && typeof change.action === 'string') {
1028
+ const action = change.action
1029
+ try {
1030
+ if (action === 'addUser' && (change.user || change.userId) && (change.pin || change.code)) {
1031
+ const user = change.user ?? change.userId
1032
+ const p = String(change.pin ?? change.code)
1033
+ const body = { command: 'addUserCode', parameter: `${user}:${p}`, commandType: 'command' }
1034
+ return await this.client.setDeviceState(this.opts.id, body)
1035
+ }
1036
+
1037
+ if (action === 'removeUser' && (change.user || change.userId)) {
1038
+ const user = change.user ?? change.userId
1039
+ const body = { command: 'removeUserCode', parameter: String(user), commandType: 'command' }
1040
+ return await this.client.setDeviceState(this.opts.id, body)
1041
+ }
1042
+
1043
+ if (action === 'listUsers') {
1044
+ const body = { command: 'listUsers', parameter: 'default', commandType: 'command' }
1045
+ return await this.client.setDeviceState(this.opts.id, body)
1046
+ }
1047
+
1048
+ if (action === 'unlockWithPin' && (change.pin || change.code)) {
1049
+ const p = String(change.pin ?? change.code)
1050
+ const body = { command: 'unlockWithPin', parameter: p, commandType: 'command' }
1051
+ return await this.client.setDeviceState(this.opts.id, body)
1052
+ }
1053
+ } catch (err) {
1054
+ try {
1055
+ if (typeof this.client.sendCommand === 'function') { return await this.client.sendCommand(this.opts.id, { command: action, parameter: change.parameter ?? 'default', commandType: 'command' }) }
1056
+ } catch (e) {}
1057
+ const e = err as any
1058
+ return { success: false, reason: e?.message ?? String(e) }
1059
+ }
1060
+ }
1061
+
1062
+ // Support setting lock PIN/passcode via `pin` or `passcode` (fallback)
1063
+ const pin = change?.pin ?? change?.passcode ?? change?.code
1064
+ if (typeof pin === 'string' || typeof pin === 'number') {
1065
+ const body = { command: 'setLockPin', parameter: String(pin), commandType: 'command' }
1066
+ try {
1067
+ return await this.client.setDeviceState(this.opts.id, body)
1068
+ } catch (err) {
1069
+ try {
1070
+ if (typeof this.client.sendCommand === 'function') { return await this.client.sendCommand(this.opts.id, body) }
1071
+ } catch (e) {}
1072
+ const e = err as any
1073
+ return { success: false, reason: e?.message ?? String(e) }
1074
+ }
1075
+ }
1076
+
1077
+ return super.setState(change)
1078
+ }
1079
+
1080
+ // Matter DoorLock descriptor including simple user-management actions
1081
+ createMatterAccessory(api: any): any {
1082
+ return {
1083
+ id: this.opts.id,
1084
+ name: this.opts.name ?? this.opts.type,
1085
+ protocol: 'matter',
1086
+ clusters: [
1087
+ {
1088
+ // DoorLock cluster
1089
+ type: 'DoorLock',
1090
+ clusterId: MATTER_CLUSTER_IDS.DoorLock,
1091
+ attributes: {
1092
+ lockState: { read: async () => { const s = await this.getState(); return !!(s && s.locked) }, write: async (v: any) => this.setState({ locked: !!v }) },
1093
+ [MATTER_ATTRIBUTE_IDS.DoorLock.LockState]: { read: async () => { const s = await this.getState(); return !!(s && s.locked) }, write: async (v: any) => this.setState({ locked: !!v }) },
1094
+ },
1095
+ },
1096
+ {
1097
+ // DoorLock user mgmt cluster (conceptual)
1098
+ type: 'DoorLockUserManagement',
1099
+ clusterId: 0x0301,
1100
+ attributes: {
1101
+ addUser: { write: async (v: any) => this.setState({ action: 'addUser', user: v?.user, pin: v?.pin }) },
1102
+ removeUser: { write: async (v: any) => this.setState({ action: 'removeUser', user: v?.user }) },
1103
+ listUsers: { read: async () => { const r = await this.setState({ action: 'listUsers' }); return r }, write: undefined },
1104
+ unlockWithPin: { write: async (v: any) => this.setState({ action: 'unlockWithPin', pin: v?.pin }) },
1105
+ },
1106
+ },
1107
+ ],
1108
+ }
1109
+ }
1110
+ }
1111
+
1112
+ export class HumidifierDevice extends GenericDevice {
1113
+ createHAPAccessory(api: any) {
1114
+ return {
1115
+ services: [
1116
+ {
1117
+ type: 'HumidifierDehumidifier',
1118
+ characteristics: {
1119
+ Active: {
1120
+ get: async () => {
1121
+ const s = await this.getState()
1122
+ return s && s.on ? 1 : 0
1123
+ },
1124
+ set: async (v: any) => {
1125
+ await this.setState({ on: v === 1 })
1126
+ },
1127
+ },
1128
+ CurrentHumidifierDehumidifierState: {
1129
+ get: async () => {
1130
+ const s = await this.getState()
1131
+ return s && s.on ? 2 : 0 // 0 = Inactive, 2 = Humidifying
1132
+ },
1133
+ },
1134
+ TargetHumidifierDehumidifierState: {
1135
+ get: async () => 1, // 1 = Humidifier
1136
+ set: async (v: any) => {
1137
+ // Support humidifier mode
1138
+ },
1139
+ },
1140
+ CurrentRelativeHumidity: {
1141
+ get: async () => {
1142
+ const s = await this.getState()
1143
+ return typeof s.humidity === 'number' ? s.humidity : 0
1144
+ },
1145
+ },
1146
+ RelativeHumidityHumidifierThreshold: {
1147
+ get: async () => {
1148
+ const s = await this.getState()
1149
+ return typeof s.targetHumidity === 'number' ? s.targetHumidity : 50
1150
+ },
1151
+ set: async (v: any) => {
1152
+ await this.setState({ humidity: Number(v) })
1153
+ },
1154
+ },
1155
+ },
1156
+ },
1157
+ ],
1158
+ }
1159
+ }
1160
+
1161
+ async setState(change: any): Promise<any> {
1162
+ if (!this.client) { return { success: false, reason: 'no client' } }
1163
+
1164
+ if (typeof change.humidity === 'number') {
1165
+ const v = String(Number(change.humidity))
1166
+ const body = { command: 'setHumidity', parameter: v, commandType: 'command' }
1167
+ try {
1168
+ return await this.client.setDeviceState(this.opts.id, body)
1169
+ } catch (err) {
1170
+ try {
1171
+ if (typeof this.client.sendCommand === 'function') { return await this.client.sendCommand(this.opts.id, body) }
1172
+ } catch (e) {}
1173
+ const e = err as any
1174
+ return { success: false, reason: e?.message ?? String(e) }
1175
+ }
1176
+ }
1177
+
1178
+ if (typeof change.dry === 'boolean') {
1179
+ const body = { command: 'setDry', parameter: change.dry ? 'on' : 'off', commandType: 'command' }
1180
+ try {
1181
+ return await this.client.setDeviceState(this.opts.id, body)
1182
+ } catch (err) {
1183
+ try {
1184
+ if (typeof this.client.sendCommand === 'function') { return await this.client.sendCommand(this.opts.id, body) }
1185
+ } catch (e) {}
1186
+ const e = err as any
1187
+ return { success: false, reason: e?.message ?? String(e) }
1188
+ }
1189
+ }
1190
+
1191
+ return super.setState(change)
1192
+ }
1193
+ }
1194
+
1195
+ // Provide Matter descriptor for humidifier (humidity and on/off)
1196
+ export class HumidifierMatterDevice extends HumidifierDevice {
1197
+ async createMatterAccessory(api: any): Promise<any> {
1198
+ return {
1199
+ id: this.opts.id,
1200
+ name: this.opts.name ?? this.opts.type,
1201
+ protocol: 'matter',
1202
+ clusters: [
1203
+ {
1204
+ // Relative Humidity Sensor cluster
1205
+ type: 'RelativeHumiditySensor',
1206
+ clusterId: MATTER_CLUSTER_IDS.RelativeHumidityMeasurement,
1207
+ attributes: {
1208
+ currentRelativeHumidity: { read: async () => { const s = await this.getState(); return typeof s.humidity === 'number' ? s.humidity : 0 }, write: undefined },
1209
+ [MATTER_ATTRIBUTE_IDS.RelativeHumidityMeasurement.MeasuredValue]: { read: async () => { const s = await this.getState(); return typeof s.humidity === 'number' ? s.humidity : 0 }, write: undefined },
1210
+ },
1211
+ },
1212
+ {
1213
+ type: 'OnOff',
1214
+ clusterId: MATTER_CLUSTER_IDS.OnOff,
1215
+ attributes: {
1216
+ onOff: { read: async () => { const s = await this.getState(); return !!s?.on }, write: async (v: any) => this.setState({ on: !!v }) },
1217
+ [MATTER_ATTRIBUTE_IDS.OnOff.OnOff]: { read: async () => { const s = await this.getState(); return !!s?.on }, write: async (v: any) => this.setState({ on: !!v }) },
1218
+ },
1219
+ },
1220
+ ],
1221
+ }
1222
+ }
1223
+ }
1224
+
1225
+ export class TemperatureSensorDevice extends GenericDevice {
1226
+ createHAPAccessory(api: any) {
1227
+ return {
1228
+ services: [
1229
+ {
1230
+ type: 'TemperatureSensor',
1231
+ characteristics: {
1232
+ CurrentTemperature: {
1233
+ get: async () => {
1234
+ const s = await this.getState()
1235
+ return typeof s.temperature === 'number' ? s.temperature : 0
1236
+ },
1237
+ },
1238
+ },
1239
+ },
1240
+ ],
1241
+ }
1242
+ }
1243
+ }
1244
+
1245
+ // Additional device classes (aliases / specialized variants)
1246
+ export class RelaySwitchDevice extends GenericDevice {}
1247
+ export class RelaySwitch1PMDevice extends GenericDevice {}
1248
+ export class PlugDevice extends GenericDevice {
1249
+ createHAPAccessory(api: any) {
1250
+ return {
1251
+ services: [
1252
+ {
1253
+ type: 'Outlet',
1254
+ characteristics: {
1255
+ On: {
1256
+ get: async () => {
1257
+ const s = await this.getState()
1258
+ return !!(s && (s.on === true || s.state === 'on'))
1259
+ },
1260
+ set: async (v: any) => {
1261
+ await this.setState({ on: !!v })
1262
+ },
1263
+ },
1264
+ OutletInUse: {
1265
+ get: async () => {
1266
+ const s = await this.getState()
1267
+ return s && s.inUse ? 1 : 0
1268
+ },
1269
+ },
1270
+ },
1271
+ },
1272
+ ],
1273
+ }
1274
+ }
1275
+ }
1276
+
1277
+ export class PlugMiniDevice extends PlugDevice {}
1278
+
1279
+ export class BlindTiltDevice extends CurtainDevice {}
1280
+ export class Curtain3Device extends CurtainDevice {}
1281
+ export class RollerShadeDevice extends CurtainDevice {}
1282
+
1283
+ export class Hub2Device extends GenericDevice {}
1284
+
1285
+ export class MeterDevice extends GenericDevice {
1286
+ createHAPAccessory(api: any) {
1287
+ return {
1288
+ services: [
1289
+ {
1290
+ type: 'TemperatureSensor',
1291
+ characteristics: {
1292
+ CurrentTemperature: {
1293
+ get: async () => {
1294
+ const s = await this.getState()
1295
+ return typeof s.temperature === 'number' ? s.temperature : 0
1296
+ },
1297
+ },
1298
+ },
1299
+ },
1300
+ {
1301
+ type: 'HumiditySensor',
1302
+ characteristics: {
1303
+ CurrentRelativeHumidity: {
1304
+ get: async () => {
1305
+ const s = await this.getState()
1306
+ return typeof s.humidity === 'number' ? s.humidity : 0
1307
+ },
1308
+ },
1309
+ },
1310
+ },
1311
+ ],
1312
+ }
1313
+ }
1314
+ }
1315
+
1316
+ export class WaterDetectorDevice extends GenericDevice {
1317
+ createHAPAccessory(api: any) {
1318
+ return {
1319
+ services: [
1320
+ {
1321
+ type: 'LeakSensor',
1322
+ characteristics: {
1323
+ LeakDetected: {
1324
+ get: async () => {
1325
+ const s = await this.getState()
1326
+ return s && s.leak ? 1 : 0
1327
+ },
1328
+ },
1329
+ },
1330
+ },
1331
+ ],
1332
+ }
1333
+ }
1334
+ }
1335
+
1336
+ export class SmartFanDevice extends FanDevice {}
1337
+
1338
+ export class StripLightDevice extends LightStripDevice {}
1339
+
1340
+ export class WalletFinderDevice extends GenericDevice {}
1341
+
1342
+ // K10 / WoSweeper variants map to VacuumDevice
1343
+ export class WoSweeperDevice extends VacuumDevice {}
1344
+ export class WoSweeperMiniDevice extends VacuumDevice {}
1345
+ export class WoSweeperMiniProDevice extends VacuumDevice {}