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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (610) hide show
  1. package/.changeset/config.json +14 -0
  2. package/.github/ISSUE_TEMPLATE/e2e-verification.md +36 -0
  3. package/.github/copilot-instructions.md +39 -0
  4. package/.github/workflows/ci.yml +32 -0
  5. package/.github/workflows/manual-e2e.yml +115 -0
  6. package/.github/workflows/release.yml +0 -4
  7. package/.husky/pre-push +15 -0
  8. package/CHANGELOG.md +35 -0
  9. package/E2E-VERIFICATION.md +121 -0
  10. package/MIGRATION.md +54 -0
  11. package/README.md +136 -4
  12. package/TODO.md +263 -0
  13. package/config.schema.json +284 -14787
  14. package/dist/SwitchBotHAPPlatform.d.ts +133 -0
  15. package/dist/SwitchBotHAPPlatform.d.ts.map +1 -0
  16. package/dist/SwitchBotHAPPlatform.js +555 -0
  17. package/dist/SwitchBotHAPPlatform.js.map +1 -0
  18. package/dist/SwitchBotMatterPlatform.d.ts +141 -0
  19. package/dist/SwitchBotMatterPlatform.d.ts.map +1 -0
  20. package/dist/SwitchBotMatterPlatform.js +536 -0
  21. package/dist/SwitchBotMatterPlatform.js.map +1 -0
  22. package/dist/device-types.d.ts +31 -0
  23. package/dist/device-types.d.ts.map +1 -0
  24. package/dist/device-types.js +246 -0
  25. package/dist/device-types.js.map +1 -0
  26. package/dist/deviceCommandMapper.d.ts +10 -0
  27. package/dist/deviceCommandMapper.d.ts.map +1 -0
  28. package/dist/deviceCommandMapper.js +319 -0
  29. package/dist/deviceCommandMapper.js.map +1 -0
  30. package/dist/deviceFactory.d.ts +14 -0
  31. package/dist/deviceFactory.d.ts.map +1 -0
  32. package/dist/deviceFactory.js +159 -0
  33. package/dist/deviceFactory.js.map +1 -0
  34. package/dist/devices/deviceBase.d.ts +50 -0
  35. package/dist/devices/deviceBase.d.ts.map +1 -0
  36. package/dist/devices/deviceBase.js +119 -0
  37. package/dist/devices/deviceBase.js.map +1 -0
  38. package/dist/devices/genericDevice.d.ts +320 -0
  39. package/dist/devices/genericDevice.d.ts.map +1 -0
  40. package/dist/devices/genericDevice.js +1363 -0
  41. package/dist/devices/genericDevice.js.map +1 -0
  42. package/dist/errors.d.ts +38 -0
  43. package/dist/errors.d.ts.map +1 -0
  44. package/dist/errors.js +32 -0
  45. package/dist/errors.js.map +1 -0
  46. package/dist/homebridge-ui/endpoints/config.d.ts +3 -0
  47. package/dist/homebridge-ui/endpoints/config.d.ts.map +1 -0
  48. package/dist/homebridge-ui/endpoints/config.js +90 -0
  49. package/dist/homebridge-ui/endpoints/config.js.map +1 -0
  50. package/dist/homebridge-ui/endpoints/devices.d.ts +6 -0
  51. package/dist/homebridge-ui/endpoints/devices.d.ts.map +1 -0
  52. package/dist/homebridge-ui/endpoints/devices.js +81 -0
  53. package/dist/homebridge-ui/endpoints/devices.js.map +1 -0
  54. package/dist/homebridge-ui/endpoints/discovery.d.ts +7 -0
  55. package/dist/homebridge-ui/endpoints/discovery.d.ts.map +1 -0
  56. package/dist/homebridge-ui/endpoints/discovery.js +212 -0
  57. package/dist/homebridge-ui/endpoints/discovery.js.map +1 -0
  58. package/dist/homebridge-ui/public/css/styles.css +472 -0
  59. package/dist/homebridge-ui/public/index.html +210 -244
  60. package/dist/homebridge-ui/public/js/advanced-settings.d.ts +3 -0
  61. package/dist/homebridge-ui/public/js/advanced-settings.d.ts.map +1 -0
  62. package/dist/homebridge-ui/public/js/advanced-settings.js +95 -0
  63. package/dist/homebridge-ui/public/js/advanced-settings.js.map +1 -0
  64. package/dist/homebridge-ui/public/js/advanced-settings.ts +94 -0
  65. package/dist/homebridge-ui/public/js/api.d.ts +66 -0
  66. package/dist/homebridge-ui/public/js/api.d.ts.map +1 -0
  67. package/dist/homebridge-ui/public/js/api.js +281 -0
  68. package/dist/homebridge-ui/public/js/api.js.map +1 -0
  69. package/dist/homebridge-ui/public/js/api.ts +342 -0
  70. package/dist/homebridge-ui/public/js/app.d.ts +2 -0
  71. package/dist/homebridge-ui/public/js/app.d.ts.map +1 -0
  72. package/dist/homebridge-ui/public/js/app.js +3863 -0
  73. package/dist/homebridge-ui/public/js/app.js.map +7 -0
  74. package/dist/homebridge-ui/public/js/app.ts +22 -0
  75. package/dist/homebridge-ui/public/js/constants.d.ts +2 -0
  76. package/dist/homebridge-ui/public/js/constants.d.ts.map +1 -0
  77. package/dist/homebridge-ui/public/js/constants.js +2 -0
  78. package/dist/homebridge-ui/public/js/constants.js.map +1 -0
  79. package/dist/homebridge-ui/public/js/constants.ts +1 -0
  80. package/dist/homebridge-ui/public/js/credentials.d.ts +3 -0
  81. package/dist/homebridge-ui/public/js/credentials.d.ts.map +1 -0
  82. package/dist/homebridge-ui/public/js/credentials.js +99 -0
  83. package/dist/homebridge-ui/public/js/credentials.js.map +1 -0
  84. package/dist/homebridge-ui/public/js/credentials.ts +105 -0
  85. package/dist/homebridge-ui/public/js/devices-delete.d.ts +3 -0
  86. package/dist/homebridge-ui/public/js/devices-delete.d.ts.map +1 -0
  87. package/dist/homebridge-ui/public/js/devices-delete.js +199 -0
  88. package/dist/homebridge-ui/public/js/devices-delete.js.map +1 -0
  89. package/dist/homebridge-ui/public/js/devices-delete.ts +227 -0
  90. package/dist/homebridge-ui/public/js/devices.d.ts +9 -0
  91. package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -0
  92. package/dist/homebridge-ui/public/js/devices.js +96 -0
  93. package/dist/homebridge-ui/public/js/devices.js.map +1 -0
  94. package/dist/homebridge-ui/public/js/devices.ts +105 -0
  95. package/dist/homebridge-ui/public/js/discovery.d.ts +4 -0
  96. package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -0
  97. package/dist/homebridge-ui/public/js/discovery.js +1374 -0
  98. package/dist/homebridge-ui/public/js/discovery.js.map +1 -0
  99. package/dist/homebridge-ui/public/js/discovery.ts +1498 -0
  100. package/dist/homebridge-ui/public/js/logger.d.ts +7 -0
  101. package/dist/homebridge-ui/public/js/logger.d.ts.map +1 -0
  102. package/dist/homebridge-ui/public/js/logger.js +17 -0
  103. package/dist/homebridge-ui/public/js/logger.js.map +1 -0
  104. package/dist/homebridge-ui/public/js/logger.ts +17 -0
  105. package/dist/homebridge-ui/public/js/modal.d.ts +5 -0
  106. package/dist/homebridge-ui/public/js/modal.d.ts.map +1 -0
  107. package/dist/homebridge-ui/public/js/modal.js +26 -0
  108. package/dist/homebridge-ui/public/js/modal.js.map +1 -0
  109. package/dist/homebridge-ui/public/js/modal.ts +28 -0
  110. package/dist/homebridge-ui/public/js/modals.d.ts +15 -0
  111. package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -0
  112. package/dist/homebridge-ui/public/js/modals.js +673 -0
  113. package/dist/homebridge-ui/public/js/modals.js.map +1 -0
  114. package/dist/homebridge-ui/public/js/modals.ts +761 -0
  115. package/dist/homebridge-ui/public/js/render.d.ts +71 -0
  116. package/dist/homebridge-ui/public/js/render.d.ts.map +1 -0
  117. package/dist/homebridge-ui/public/js/render.js +953 -0
  118. package/dist/homebridge-ui/public/js/render.js.map +1 -0
  119. package/dist/homebridge-ui/public/js/render.ts +1077 -0
  120. package/dist/homebridge-ui/public/js/toast.d.ts +6 -0
  121. package/dist/homebridge-ui/public/js/toast.d.ts.map +1 -0
  122. package/dist/homebridge-ui/public/js/toast.js +29 -0
  123. package/dist/homebridge-ui/public/js/toast.js.map +1 -0
  124. package/dist/homebridge-ui/public/js/toast.ts +37 -0
  125. package/dist/homebridge-ui/public/js/types.d.ts +23 -0
  126. package/dist/homebridge-ui/public/js/types.d.ts.map +1 -0
  127. package/dist/homebridge-ui/public/js/types.js +2 -0
  128. package/dist/homebridge-ui/public/js/types.js.map +1 -0
  129. package/dist/homebridge-ui/public/js/types.ts +26 -0
  130. package/dist/homebridge-ui/server.js +9 -41
  131. package/dist/homebridge-ui/server.js.map +1 -1
  132. package/dist/homebridge-ui/utils/config-parser.d.ts +35 -0
  133. package/dist/homebridge-ui/utils/config-parser.d.ts.map +1 -0
  134. package/dist/homebridge-ui/utils/config-parser.js +87 -0
  135. package/dist/homebridge-ui/utils/config-parser.js.map +1 -0
  136. package/dist/homebridge-ui/utils/device-migration.d.ts +35 -0
  137. package/dist/homebridge-ui/utils/device-migration.d.ts.map +1 -0
  138. package/dist/homebridge-ui/utils/device-migration.js +111 -0
  139. package/dist/homebridge-ui/utils/device-migration.js.map +1 -0
  140. package/dist/homebridge-ui/utils/logger.d.ts +7 -0
  141. package/dist/homebridge-ui/utils/logger.d.ts.map +1 -0
  142. package/dist/homebridge-ui/utils/logger.js +17 -0
  143. package/dist/homebridge-ui/utils/logger.js.map +1 -0
  144. package/dist/index.d.ts +10 -0
  145. package/dist/index.d.ts.map +1 -1
  146. package/dist/index.js +12 -4
  147. package/dist/index.js.map +1 -1
  148. package/dist/settings.d.ts +11 -249
  149. package/dist/settings.d.ts.map +1 -1
  150. package/dist/settings.js +6 -30
  151. package/dist/settings.js.map +1 -1
  152. package/dist/switchbotClient.d.ts +28 -0
  153. package/dist/switchbotClient.d.ts.map +1 -0
  154. package/dist/switchbotClient.js +175 -0
  155. package/dist/switchbotClient.js.map +1 -0
  156. package/dist/utils.d.ts +92 -115
  157. package/dist/utils.d.ts.map +1 -1
  158. package/dist/utils.js +1117 -902
  159. package/dist/utils.js.map +1 -1
  160. package/docs/assets/highlight.css +28 -0
  161. package/docs/assets/icons.js +1 -1
  162. package/docs/assets/icons.svg +1 -1
  163. package/docs/assets/main.js +2 -2
  164. package/docs/assets/style.css +3 -3
  165. package/docs/index.html +148 -13
  166. package/docs/variables/default.html +3 -1
  167. package/eslint.config.js +7 -8
  168. package/nodemon.json +2 -2
  169. package/package.json +36 -32
  170. package/scripts/build-ui.js +37 -0
  171. package/scripts/e2e/README.md +25 -0
  172. package/scripts/e2e/curtain-e2e.sh +70 -0
  173. package/scripts/e2e/fan-e2e.sh +75 -0
  174. package/scripts/e2e/light-advanced-e2e.sh +97 -0
  175. package/scripts/e2e/light-e2e.sh +75 -0
  176. package/scripts/e2e/list-accessories.sh +19 -0
  177. package/scripts/e2e/lock-e2e.sh +65 -0
  178. package/scripts/free-dev-ports.mjs +105 -0
  179. package/scripts/generate-matter-maps.js +77 -0
  180. package/scripts/run-e2e-local.sh +14 -0
  181. package/scripts/sync-device-types.mjs +31 -0
  182. package/src/SwitchBotHAPPlatform.ts +558 -0
  183. package/src/SwitchBotMatterPlatform.ts +538 -0
  184. package/src/device-types.ts +261 -0
  185. package/src/deviceCommandMapper.ts +333 -0
  186. package/src/deviceFactory.ts +201 -0
  187. package/src/devices/deviceBase.ts +141 -0
  188. package/src/devices/genericDevice.ts +1337 -0
  189. package/src/errors.ts +35 -0
  190. package/src/homebridge-ui/endpoints/config.ts +110 -0
  191. package/src/homebridge-ui/endpoints/devices.ts +88 -0
  192. package/src/homebridge-ui/endpoints/discovery.ts +233 -0
  193. package/src/homebridge-ui/public/css/styles.css +472 -0
  194. package/src/homebridge-ui/public/index.html +210 -244
  195. package/src/homebridge-ui/public/js/advanced-settings.ts +94 -0
  196. package/src/homebridge-ui/public/js/api.ts +342 -0
  197. package/src/homebridge-ui/public/js/app.ts +22 -0
  198. package/src/homebridge-ui/public/js/constants.ts +1 -0
  199. package/src/homebridge-ui/public/js/credentials.ts +105 -0
  200. package/src/homebridge-ui/public/js/devices-delete.ts +227 -0
  201. package/src/homebridge-ui/public/js/devices.ts +105 -0
  202. package/src/homebridge-ui/public/js/discovery.ts +1498 -0
  203. package/src/homebridge-ui/public/js/logger.ts +17 -0
  204. package/src/homebridge-ui/public/js/modal.ts +28 -0
  205. package/src/homebridge-ui/public/js/modals.ts +761 -0
  206. package/src/homebridge-ui/public/js/render.ts +1077 -0
  207. package/src/homebridge-ui/public/js/toast.ts +37 -0
  208. package/src/homebridge-ui/public/js/types.ts +26 -0
  209. package/src/homebridge-ui/server.ts +9 -43
  210. package/src/homebridge-ui/utils/config-parser.ts +108 -0
  211. package/src/homebridge-ui/utils/device-migration.ts +144 -0
  212. package/src/homebridge-ui/utils/logger.ts +17 -0
  213. package/src/index.ts +12 -4
  214. package/src/settings.ts +14 -277
  215. package/src/switchbotClient.ts +181 -0
  216. package/src/utils.ts +1106 -900
  217. package/test/client/switchbot-client-debounce.spec.ts +35 -0
  218. package/test/client/switchbot-client-openapi.spec.ts +19 -0
  219. package/test/client/switchbotClient.spec.ts +23 -0
  220. package/test/device/device-mapping.spec.ts +23 -0
  221. package/test/device/deviceBase.spec.ts +26 -0
  222. package/test/device/deviceFactory-edge.spec.ts +15 -0
  223. package/test/device/deviceFactory.spec.ts +33 -0
  224. package/test/device/fan-swing.spec.ts +34 -0
  225. package/test/device/genericDevice-blepoll.spec.ts +47 -0
  226. package/test/device/irdevice.spec.ts +9 -0
  227. package/test/device/lock-users.spec.ts +35 -0
  228. package/test/device/matter-descriptors.spec.ts +22 -0
  229. package/test/device/matter-device-state.spec.ts +37 -0
  230. package/test/e2e/run-e2e.spec.ts +48 -0
  231. package/test/errors/errors.spec.ts +10 -0
  232. package/test/helpers/matter-harness.ts +64 -0
  233. package/test/homebridge-ui/server.spec.ts +9 -0
  234. package/test/platform/accessory-restore.spec.ts +37 -0
  235. package/test/platform/matter-childbridge.spec.ts +34 -0
  236. package/test/platform/matter-integration.spec.ts +33 -0
  237. package/test/platform/platform-edge.spec.ts +73 -0
  238. package/test/platform/platform.integration.spec.ts +34 -0
  239. package/test/utils/utils-extra.spec.ts +10 -0
  240. package/test/utils/utils.spec.ts +53 -0
  241. package/todo/TODO.md +80 -0
  242. package/vitest.config.ts +7 -0
  243. package/coverage/base.css +0 -224
  244. package/coverage/block-navigation.js +0 -87
  245. package/coverage/clover.xml +0 -15847
  246. package/coverage/coverage-final.json +0 -42
  247. package/coverage/docs/assets/dmt/dmt-component-data.js.html +0 -85
  248. package/coverage/docs/assets/dmt/dmt-components.js.html +0 -286
  249. package/coverage/docs/assets/dmt/index.html +0 -131
  250. package/coverage/docs/assets/hierarchy.js.html +0 -85
  251. package/coverage/docs/assets/icons.js.html +0 -136
  252. package/coverage/docs/assets/index.html +0 -146
  253. package/coverage/docs/assets/main.js.html +0 -265
  254. package/coverage/favicon.png +0 -0
  255. package/coverage/index.html +0 -191
  256. package/coverage/prettify.css +0 -1
  257. package/coverage/prettify.js +0 -2
  258. package/coverage/sort-arrow-sprite.png +0 -0
  259. package/coverage/sorter.js +0 -196
  260. package/coverage/src/device/blindtilt.ts.html +0 -3238
  261. package/coverage/src/device/bot.ts.html +0 -2803
  262. package/coverage/src/device/ceilinglight.ts.html +0 -2338
  263. package/coverage/src/device/colorbulb.ts.html +0 -2824
  264. package/coverage/src/device/contact.ts.html +0 -1465
  265. package/coverage/src/device/curtain.ts.html +0 -2869
  266. package/coverage/src/device/device.ts.html +0 -2500
  267. package/coverage/src/device/fan.ts.html +0 -2242
  268. package/coverage/src/device/hub.ts.html +0 -1408
  269. package/coverage/src/device/humidifier.ts.html +0 -2116
  270. package/coverage/src/device/index.html +0 -416
  271. package/coverage/src/device/iosensor.ts.html +0 -1375
  272. package/coverage/src/device/lightstrip.ts.html +0 -2617
  273. package/coverage/src/device/lock.ts.html +0 -1963
  274. package/coverage/src/device/meter.ts.html +0 -1372
  275. package/coverage/src/device/meterplus.ts.html +0 -1384
  276. package/coverage/src/device/meterpro.ts.html +0 -1618
  277. package/coverage/src/device/motion.ts.html +0 -1264
  278. package/coverage/src/device/plug.ts.html +0 -1372
  279. package/coverage/src/device/relayswitch.ts.html +0 -2284
  280. package/coverage/src/device/robotvacuumcleaner.ts.html +0 -1810
  281. package/coverage/src/device/waterdetector.ts.html +0 -1294
  282. package/coverage/src/homebridge-ui/index.html +0 -116
  283. package/coverage/src/homebridge-ui/server.ts.html +0 -229
  284. package/coverage/src/index.html +0 -161
  285. package/coverage/src/index.ts.html +0 -124
  286. package/coverage/src/irdevice/airconditioner.ts.html +0 -1687
  287. package/coverage/src/irdevice/airpurifier.ts.html +0 -844
  288. package/coverage/src/irdevice/camera.ts.html +0 -475
  289. package/coverage/src/irdevice/fan.ts.html +0 -766
  290. package/coverage/src/irdevice/index.html +0 -251
  291. package/coverage/src/irdevice/irdevice.ts.html +0 -1117
  292. package/coverage/src/irdevice/light.ts.html +0 -826
  293. package/coverage/src/irdevice/other.ts.html +0 -2458
  294. package/coverage/src/irdevice/tv.ts.html +0 -1222
  295. package/coverage/src/irdevice/vacuumcleaner.ts.html +0 -466
  296. package/coverage/src/irdevice/waterheater.ts.html +0 -469
  297. package/coverage/src/platform.ts.html +0 -8776
  298. package/coverage/src/settings.ts.html +0 -934
  299. package/coverage/src/utils.ts.html +0 -2092
  300. package/dist/baseMatterAccessory.test.d.ts +0 -2
  301. package/dist/baseMatterAccessory.test.d.ts.map +0 -1
  302. package/dist/baseMatterAccessory.test.js +0 -71
  303. package/dist/baseMatterAccessory.test.js.map +0 -1
  304. package/dist/devices-hap/airpurifier.d.ts +0 -54
  305. package/dist/devices-hap/airpurifier.d.ts.map +0 -1
  306. package/dist/devices-hap/airpurifier.js +0 -527
  307. package/dist/devices-hap/airpurifier.js.map +0 -1
  308. package/dist/devices-hap/blindtilt.d.ts +0 -90
  309. package/dist/devices-hap/blindtilt.d.ts.map +0 -1
  310. package/dist/devices-hap/blindtilt.js +0 -974
  311. package/dist/devices-hap/blindtilt.js.map +0 -1
  312. package/dist/devices-hap/bot.d.ts +0 -102
  313. package/dist/devices-hap/bot.d.ts.map +0 -1
  314. package/dist/devices-hap/bot.js +0 -811
  315. package/dist/devices-hap/bot.js.map +0 -1
  316. package/dist/devices-hap/ceilinglight.d.ts +0 -85
  317. package/dist/devices-hap/ceilinglight.d.ts.map +0 -1
  318. package/dist/devices-hap/ceilinglight.js +0 -701
  319. package/dist/devices-hap/ceilinglight.js.map +0 -1
  320. package/dist/devices-hap/colorbulb.d.ts +0 -88
  321. package/dist/devices-hap/colorbulb.d.ts.map +0 -1
  322. package/dist/devices-hap/colorbulb.js +0 -881
  323. package/dist/devices-hap/colorbulb.js.map +0 -1
  324. package/dist/devices-hap/contact.d.ts +0 -44
  325. package/dist/devices-hap/contact.d.ts.map +0 -1
  326. package/dist/devices-hap/contact.js +0 -409
  327. package/dist/devices-hap/contact.js.map +0 -1
  328. package/dist/devices-hap/curtain.d.ts +0 -73
  329. package/dist/devices-hap/curtain.d.ts.map +0 -1
  330. package/dist/devices-hap/curtain.js +0 -869
  331. package/dist/devices-hap/curtain.js.map +0 -1
  332. package/dist/devices-hap/device.d.ts +0 -98
  333. package/dist/devices-hap/device.d.ts.map +0 -1
  334. package/dist/devices-hap/device.js +0 -831
  335. package/dist/devices-hap/device.js.map +0 -1
  336. package/dist/devices-hap/fan.d.ts +0 -69
  337. package/dist/devices-hap/fan.d.ts.map +0 -1
  338. package/dist/devices-hap/fan.js +0 -649
  339. package/dist/devices-hap/fan.js.map +0 -1
  340. package/dist/devices-hap/hub.d.ts +0 -37
  341. package/dist/devices-hap/hub.d.ts.map +0 -1
  342. package/dist/devices-hap/hub.js +0 -392
  343. package/dist/devices-hap/hub.js.map +0 -1
  344. package/dist/devices-hap/humidifier.d.ts +0 -68
  345. package/dist/devices-hap/humidifier.d.ts.map +0 -1
  346. package/dist/devices-hap/humidifier.js +0 -628
  347. package/dist/devices-hap/humidifier.js.map +0 -1
  348. package/dist/devices-hap/iosensor.d.ts +0 -42
  349. package/dist/devices-hap/iosensor.d.ts.map +0 -1
  350. package/dist/devices-hap/iosensor.js +0 -382
  351. package/dist/devices-hap/iosensor.js.map +0 -1
  352. package/dist/devices-hap/lightstrip.d.ts +0 -79
  353. package/dist/devices-hap/lightstrip.d.ts.map +0 -1
  354. package/dist/devices-hap/lightstrip.js +0 -797
  355. package/dist/devices-hap/lightstrip.js.map +0 -1
  356. package/dist/devices-hap/lock.d.ts +0 -53
  357. package/dist/devices-hap/lock.d.ts.map +0 -1
  358. package/dist/devices-hap/lock.js +0 -561
  359. package/dist/devices-hap/lock.js.map +0 -1
  360. package/dist/devices-hap/meter.d.ts +0 -37
  361. package/dist/devices-hap/meter.d.ts.map +0 -1
  362. package/dist/devices-hap/meter.js +0 -379
  363. package/dist/devices-hap/meter.js.map +0 -1
  364. package/dist/devices-hap/meterplus.d.ts +0 -42
  365. package/dist/devices-hap/meterplus.d.ts.map +0 -1
  366. package/dist/devices-hap/meterplus.js +0 -384
  367. package/dist/devices-hap/meterplus.js.map +0 -1
  368. package/dist/devices-hap/meterpro.d.ts +0 -43
  369. package/dist/devices-hap/meterpro.d.ts.map +0 -1
  370. package/dist/devices-hap/meterpro.js +0 -468
  371. package/dist/devices-hap/meterpro.js.map +0 -1
  372. package/dist/devices-hap/motion.d.ts +0 -42
  373. package/dist/devices-hap/motion.d.ts.map +0 -1
  374. package/dist/devices-hap/motion.js +0 -345
  375. package/dist/devices-hap/motion.js.map +0 -1
  376. package/dist/devices-hap/plug.d.ts +0 -49
  377. package/dist/devices-hap/plug.d.ts.map +0 -1
  378. package/dist/devices-hap/plug.js +0 -395
  379. package/dist/devices-hap/plug.js.map +0 -1
  380. package/dist/devices-hap/relayswitch.d.ts +0 -96
  381. package/dist/devices-hap/relayswitch.d.ts.map +0 -1
  382. package/dist/devices-hap/relayswitch.js +0 -642
  383. package/dist/devices-hap/relayswitch.js.map +0 -1
  384. package/dist/devices-hap/robotvacuumcleaner.d.ts +0 -54
  385. package/dist/devices-hap/robotvacuumcleaner.d.ts.map +0 -1
  386. package/dist/devices-hap/robotvacuumcleaner.js +0 -523
  387. package/dist/devices-hap/robotvacuumcleaner.js.map +0 -1
  388. package/dist/devices-hap/waterdetector.d.ts +0 -41
  389. package/dist/devices-hap/waterdetector.d.ts.map +0 -1
  390. package/dist/devices-hap/waterdetector.js +0 -356
  391. package/dist/devices-hap/waterdetector.js.map +0 -1
  392. package/dist/devices-matter/BaseMatterAccessory.d.ts +0 -78
  393. package/dist/devices-matter/BaseMatterAccessory.d.ts.map +0 -1
  394. package/dist/devices-matter/BaseMatterAccessory.js +0 -244
  395. package/dist/devices-matter/BaseMatterAccessory.js.map +0 -1
  396. package/dist/devices-matter/ColorLightAccessory.d.ts +0 -20
  397. package/dist/devices-matter/ColorLightAccessory.d.ts.map +0 -1
  398. package/dist/devices-matter/ColorLightAccessory.js +0 -95
  399. package/dist/devices-matter/ColorLightAccessory.js.map +0 -1
  400. package/dist/devices-matter/ColorTemperatureLightAccessory.d.ts +0 -18
  401. package/dist/devices-matter/ColorTemperatureLightAccessory.d.ts.map +0 -1
  402. package/dist/devices-matter/ColorTemperatureLightAccessory.js +0 -76
  403. package/dist/devices-matter/ColorTemperatureLightAccessory.js.map +0 -1
  404. package/dist/devices-matter/ContactSensorAccessory.d.ts +0 -12
  405. package/dist/devices-matter/ContactSensorAccessory.d.ts.map +0 -1
  406. package/dist/devices-matter/ContactSensorAccessory.js +0 -34
  407. package/dist/devices-matter/ContactSensorAccessory.js.map +0 -1
  408. package/dist/devices-matter/DimmableLightAccessory.d.ts +0 -58
  409. package/dist/devices-matter/DimmableLightAccessory.d.ts.map +0 -1
  410. package/dist/devices-matter/DimmableLightAccessory.js +0 -167
  411. package/dist/devices-matter/DimmableLightAccessory.js.map +0 -1
  412. package/dist/devices-matter/DoorLockAccessory.d.ts +0 -14
  413. package/dist/devices-matter/DoorLockAccessory.d.ts.map +0 -1
  414. package/dist/devices-matter/DoorLockAccessory.js +0 -50
  415. package/dist/devices-matter/DoorLockAccessory.js.map +0 -1
  416. package/dist/devices-matter/ExtendedColorLightAccessory.d.ts +0 -21
  417. package/dist/devices-matter/ExtendedColorLightAccessory.d.ts.map +0 -1
  418. package/dist/devices-matter/ExtendedColorLightAccessory.js +0 -106
  419. package/dist/devices-matter/ExtendedColorLightAccessory.js.map +0 -1
  420. package/dist/devices-matter/FanAccessory.d.ts +0 -16
  421. package/dist/devices-matter/FanAccessory.d.ts.map +0 -1
  422. package/dist/devices-matter/FanAccessory.js +0 -81
  423. package/dist/devices-matter/FanAccessory.js.map +0 -1
  424. package/dist/devices-matter/HumiditySensorAccessory.d.ts +0 -12
  425. package/dist/devices-matter/HumiditySensorAccessory.d.ts.map +0 -1
  426. package/dist/devices-matter/HumiditySensorAccessory.js +0 -34
  427. package/dist/devices-matter/HumiditySensorAccessory.js.map +0 -1
  428. package/dist/devices-matter/LeakSensorAccessory.d.ts +0 -12
  429. package/dist/devices-matter/LeakSensorAccessory.d.ts.map +0 -1
  430. package/dist/devices-matter/LeakSensorAccessory.js +0 -33
  431. package/dist/devices-matter/LeakSensorAccessory.js.map +0 -1
  432. package/dist/devices-matter/LightSensorAccessory.d.ts +0 -12
  433. package/dist/devices-matter/LightSensorAccessory.d.ts.map +0 -1
  434. package/dist/devices-matter/LightSensorAccessory.js +0 -34
  435. package/dist/devices-matter/LightSensorAccessory.js.map +0 -1
  436. package/dist/devices-matter/OccupancySensorAccessory.d.ts +0 -12
  437. package/dist/devices-matter/OccupancySensorAccessory.d.ts.map +0 -1
  438. package/dist/devices-matter/OccupancySensorAccessory.js +0 -39
  439. package/dist/devices-matter/OccupancySensorAccessory.js.map +0 -1
  440. package/dist/devices-matter/OnOffLightAccessory.d.ts +0 -38
  441. package/dist/devices-matter/OnOffLightAccessory.d.ts.map +0 -1
  442. package/dist/devices-matter/OnOffLightAccessory.js +0 -110
  443. package/dist/devices-matter/OnOffLightAccessory.js.map +0 -1
  444. package/dist/devices-matter/OnOffOutletAccessory.d.ts +0 -14
  445. package/dist/devices-matter/OnOffOutletAccessory.d.ts.map +0 -1
  446. package/dist/devices-matter/OnOffOutletAccessory.js +0 -43
  447. package/dist/devices-matter/OnOffOutletAccessory.js.map +0 -1
  448. package/dist/devices-matter/OnOffSwitchAccessory.d.ts +0 -14
  449. package/dist/devices-matter/OnOffSwitchAccessory.d.ts.map +0 -1
  450. package/dist/devices-matter/OnOffSwitchAccessory.js +0 -42
  451. package/dist/devices-matter/OnOffSwitchAccessory.js.map +0 -1
  452. package/dist/devices-matter/RoboticVacuumAccessory.d.ts +0 -68
  453. package/dist/devices-matter/RoboticVacuumAccessory.d.ts.map +0 -1
  454. package/dist/devices-matter/RoboticVacuumAccessory.js +0 -334
  455. package/dist/devices-matter/RoboticVacuumAccessory.js.map +0 -1
  456. package/dist/devices-matter/SmokeCOAlarmAccessory.d.ts +0 -11
  457. package/dist/devices-matter/SmokeCOAlarmAccessory.d.ts.map +0 -1
  458. package/dist/devices-matter/SmokeCOAlarmAccessory.js +0 -49
  459. package/dist/devices-matter/SmokeCOAlarmAccessory.js.map +0 -1
  460. package/dist/devices-matter/TemperatureSensorAccessory.d.ts +0 -12
  461. package/dist/devices-matter/TemperatureSensorAccessory.d.ts.map +0 -1
  462. package/dist/devices-matter/TemperatureSensorAccessory.js +0 -36
  463. package/dist/devices-matter/TemperatureSensorAccessory.js.map +0 -1
  464. package/dist/devices-matter/ThermostatAccessory.d.ts +0 -19
  465. package/dist/devices-matter/ThermostatAccessory.d.ts.map +0 -1
  466. package/dist/devices-matter/ThermostatAccessory.js +0 -95
  467. package/dist/devices-matter/ThermostatAccessory.js.map +0 -1
  468. package/dist/devices-matter/VenetianBlindAccessory.d.ts +0 -19
  469. package/dist/devices-matter/VenetianBlindAccessory.d.ts.map +0 -1
  470. package/dist/devices-matter/VenetianBlindAccessory.js +0 -99
  471. package/dist/devices-matter/VenetianBlindAccessory.js.map +0 -1
  472. package/dist/devices-matter/WindowBlindAccessory.d.ts +0 -17
  473. package/dist/devices-matter/WindowBlindAccessory.d.ts.map +0 -1
  474. package/dist/devices-matter/WindowBlindAccessory.js +0 -80
  475. package/dist/devices-matter/WindowBlindAccessory.js.map +0 -1
  476. package/dist/devices-matter/custom/PowerStripAccessory.d.ts +0 -97
  477. package/dist/devices-matter/custom/PowerStripAccessory.d.ts.map +0 -1
  478. package/dist/devices-matter/custom/PowerStripAccessory.js +0 -265
  479. package/dist/devices-matter/custom/PowerStripAccessory.js.map +0 -1
  480. package/dist/devices-matter/custom/index.d.ts +0 -8
  481. package/dist/devices-matter/custom/index.d.ts.map +0 -1
  482. package/dist/devices-matter/custom/index.js +0 -8
  483. package/dist/devices-matter/custom/index.js.map +0 -1
  484. package/dist/devices-matter/index.d.ts +0 -29
  485. package/dist/devices-matter/index.d.ts.map +0 -1
  486. package/dist/devices-matter/index.js +0 -28
  487. package/dist/devices-matter/index.js.map +0 -1
  488. package/dist/index.test.d.ts +0 -2
  489. package/dist/index.test.d.ts.map +0 -1
  490. package/dist/index.test.js +0 -19
  491. package/dist/index.test.js.map +0 -1
  492. package/dist/irdevice/airconditioner.d.ts +0 -61
  493. package/dist/irdevice/airconditioner.d.ts.map +0 -1
  494. package/dist/irdevice/airconditioner.js +0 -472
  495. package/dist/irdevice/airconditioner.js.map +0 -1
  496. package/dist/irdevice/airpurifier.d.ts +0 -50
  497. package/dist/irdevice/airpurifier.d.ts.map +0 -1
  498. package/dist/irdevice/airpurifier.js +0 -213
  499. package/dist/irdevice/airpurifier.js.map +0 -1
  500. package/dist/irdevice/camera.d.ts +0 -32
  501. package/dist/irdevice/camera.d.ts.map +0 -1
  502. package/dist/irdevice/camera.js +0 -107
  503. package/dist/irdevice/camera.js.map +0 -1
  504. package/dist/irdevice/fan.d.ts +0 -36
  505. package/dist/irdevice/fan.d.ts.map +0 -1
  506. package/dist/irdevice/fan.js +0 -200
  507. package/dist/irdevice/fan.js.map +0 -1
  508. package/dist/irdevice/irdevice.d.ts +0 -68
  509. package/dist/irdevice/irdevice.d.ts.map +0 -1
  510. package/dist/irdevice/irdevice.js +0 -398
  511. package/dist/irdevice/irdevice.js.map +0 -1
  512. package/dist/irdevice/light.d.ts +0 -36
  513. package/dist/irdevice/light.d.ts.map +0 -1
  514. package/dist/irdevice/light.js +0 -206
  515. package/dist/irdevice/light.js.map +0 -1
  516. package/dist/irdevice/other.d.ts +0 -57
  517. package/dist/irdevice/other.d.ts.map +0 -1
  518. package/dist/irdevice/other.js +0 -778
  519. package/dist/irdevice/other.js.map +0 -1
  520. package/dist/irdevice/tv.d.ts +0 -45
  521. package/dist/irdevice/tv.d.ts.map +0 -1
  522. package/dist/irdevice/tv.js +0 -327
  523. package/dist/irdevice/tv.js.map +0 -1
  524. package/dist/irdevice/vacuumcleaner.d.ts +0 -28
  525. package/dist/irdevice/vacuumcleaner.d.ts.map +0 -1
  526. package/dist/irdevice/vacuumcleaner.js +0 -104
  527. package/dist/irdevice/vacuumcleaner.js.map +0 -1
  528. package/dist/irdevice/waterheater.d.ts +0 -30
  529. package/dist/irdevice/waterheater.d.ts.map +0 -1
  530. package/dist/irdevice/waterheater.js +0 -105
  531. package/dist/irdevice/waterheater.js.map +0 -1
  532. package/dist/platform-hap.d.ts +0 -145
  533. package/dist/platform-hap.d.ts.map +0 -1
  534. package/dist/platform-hap.js +0 -2823
  535. package/dist/platform-hap.js.map +0 -1
  536. package/dist/platform-matter.d.ts +0 -131
  537. package/dist/platform-matter.d.ts.map +0 -1
  538. package/dist/platform-matter.js +0 -1002
  539. package/dist/platform-matter.js.map +0 -1
  540. package/dist/utils.test.d.ts +0 -2
  541. package/dist/utils.test.d.ts.map +0 -1
  542. package/dist/utils.test.js +0 -95
  543. package/dist/utils.test.js.map +0 -1
  544. package/dist/verifyconfig.test.d.ts +0 -2
  545. package/dist/verifyconfig.test.d.ts.map +0 -1
  546. package/dist/verifyconfig.test.js +0 -167
  547. package/dist/verifyconfig.test.js.map +0 -1
  548. package/src/baseMatterAccessory.test.ts +0 -88
  549. package/src/custom.d.ts +0 -7
  550. package/src/devices-hap/airpurifier.ts +0 -563
  551. package/src/devices-hap/blindtilt.ts +0 -1049
  552. package/src/devices-hap/bot.ts +0 -900
  553. package/src/devices-hap/ceilinglight.ts +0 -742
  554. package/src/devices-hap/colorbulb.ts +0 -904
  555. package/src/devices-hap/contact.ts +0 -457
  556. package/src/devices-hap/curtain.ts +0 -944
  557. package/src/devices-hap/device.ts +0 -884
  558. package/src/devices-hap/fan.ts +0 -711
  559. package/src/devices-hap/hub.ts +0 -439
  560. package/src/devices-hap/humidifier.ts +0 -669
  561. package/src/devices-hap/iosensor.ts +0 -427
  562. package/src/devices-hap/lightstrip.ts +0 -836
  563. package/src/devices-hap/lock.ts +0 -620
  564. package/src/devices-hap/meter.ts +0 -426
  565. package/src/devices-hap/meterplus.ts +0 -430
  566. package/src/devices-hap/meterpro.ts +0 -522
  567. package/src/devices-hap/motion.ts +0 -390
  568. package/src/devices-hap/plug.ts +0 -423
  569. package/src/devices-hap/relayswitch.ts +0 -727
  570. package/src/devices-hap/robotvacuumcleaner.ts +0 -568
  571. package/src/devices-hap/waterdetector.ts +0 -400
  572. package/src/devices-matter/BaseMatterAccessory.ts +0 -273
  573. package/src/devices-matter/ColorLightAccessory.ts +0 -110
  574. package/src/devices-matter/ColorTemperatureLightAccessory.ts +0 -90
  575. package/src/devices-matter/ContactSensorAccessory.ts +0 -41
  576. package/src/devices-matter/DimmableLightAccessory.ts +0 -192
  577. package/src/devices-matter/DoorLockAccessory.ts +0 -60
  578. package/src/devices-matter/ExtendedColorLightAccessory.ts +0 -122
  579. package/src/devices-matter/FanAccessory.ts +0 -95
  580. package/src/devices-matter/HumiditySensorAccessory.ts +0 -41
  581. package/src/devices-matter/LeakSensorAccessory.ts +0 -40
  582. package/src/devices-matter/LightSensorAccessory.ts +0 -41
  583. package/src/devices-matter/OccupancySensorAccessory.ts +0 -48
  584. package/src/devices-matter/OnOffLightAccessory.ts +0 -125
  585. package/src/devices-matter/OnOffOutletAccessory.ts +0 -51
  586. package/src/devices-matter/OnOffSwitchAccessory.ts +0 -51
  587. package/src/devices-matter/RoboticVacuumAccessory.ts +0 -407
  588. package/src/devices-matter/SmokeCOAlarmAccessory.ts +0 -59
  589. package/src/devices-matter/TemperatureSensorAccessory.ts +0 -43
  590. package/src/devices-matter/ThermostatAccessory.ts +0 -110
  591. package/src/devices-matter/VenetianBlindAccessory.ts +0 -115
  592. package/src/devices-matter/WindowBlindAccessory.ts +0 -92
  593. package/src/devices-matter/custom/PowerStripAccessory.ts +0 -309
  594. package/src/devices-matter/custom/index.ts +0 -8
  595. package/src/devices-matter/index.ts +0 -29
  596. package/src/index.test.ts +0 -24
  597. package/src/irdevice/airconditioner.ts +0 -533
  598. package/src/irdevice/airpurifier.ts +0 -252
  599. package/src/irdevice/camera.ts +0 -129
  600. package/src/irdevice/fan.ts +0 -226
  601. package/src/irdevice/irdevice.ts +0 -435
  602. package/src/irdevice/light.ts +0 -246
  603. package/src/irdevice/other.ts +0 -790
  604. package/src/irdevice/tv.ts +0 -378
  605. package/src/irdevice/vacuumcleaner.ts +0 -126
  606. package/src/irdevice/waterheater.ts +0 -127
  607. package/src/platform-hap.ts +0 -2952
  608. package/src/platform-matter.ts +0 -1129
  609. package/src/utils.test.ts +0 -96
  610. package/src/verifyconfig.test.ts +0 -198
package/src/utils.ts CHANGED
@@ -1,972 +1,1178 @@
1
- /* Copyright(C) 2017-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved.
1
+ import type { SwitchBotPluginConfig } from './settings.js'
2
+ import type { Logger, PlatformConfig } from 'homebridge'
3
+ /**
4
+ * Indicates which device types should prefer Matter if available.
5
+ * Based on HAP service mappings: device implementations use specific HomeKit services
6
+ * that map to corresponding Matter clusters when Matter is enabled.
2
7
  *
3
- * util.ts: @switchbot/homebridge-switchbot platform class.
8
+ * @property {boolean} [deviceType] - True if the device type supports Matter, false otherwise.
9
+ * @example
10
+ * DEVICE_MATTER_SUPPORTED['bot'] // true
11
+ */
12
+ /**
13
+ * Factory function to create Matter handlers with Homebridge logger integration.
14
+ * Returns handler objects for supported device types, mapping Matter cluster actions to SwitchBot API calls.
15
+ *
16
+ * @param log - Homebridge logger instance
17
+ * @param deviceId - SwitchBot device ID
18
+ * @param type - Device type string
19
+ * @param client - SwitchBot client instance
20
+ * @returns Handler object for Matter clusters
4
21
  */
5
- import type { API, Logging } from 'homebridge'
6
- import type { blindTilt, curtain, curtain3, device } from 'node-switchbot'
7
22
 
8
- import type { devicesConfig } from './settings.js'
23
+ export const DEVICE_MATTER_SUPPORTED: Record<string, boolean> = {
24
+ // Core devices
25
+ 'bot': true, // Switch → OnOff
26
+ 'curtain': true, // WindowCovering → WindowCovering
27
+ 'fan': true, // Fan → FanControl
28
+ 'light': true, // Lightbulb → OnOff + LevelControl
29
+ 'lightstrip': true, // Lightbulb (color) → OnOff + LevelControl + ColorControl
30
+ 'motion': true, // MotionSensor → OccupancySensing
31
+ 'contact': true, // ContactSensor → BooleanState
32
+ 'vacuum': true, // Switch → RobotVacuumCleaner
33
+ 'lock': true, // LockMechanism → DoorLock
34
+ 'humidifier': true, // Fan + Humidity → OnOff + FanControl + RelativeHumidityMeasurement
35
+ 'temperature': true, // TemperatureSensor → TemperatureMeasurement
9
36
 
10
- export enum BlindTiltMappingMode {
11
- OnlyUp = 'only_up',
12
- OnlyDown = 'only_down',
13
- DownAndUp = 'down_and_up',
14
- UpAndDown = 'up_and_down',
15
- UseTiltForDirection = 'use_tilt_for_direction',
16
- }
37
+ // Switch devices
38
+ 'relay': true, // Switch → OnOff
39
+ 'relay switch 1': true, // Switch → OnOff
40
+ 'relay switch 1pm': true, // Switch → OnOff
41
+ 'plug': true, // Outlet → OnOff
42
+ 'plug mini (jp)': true, // Outlet → OnOff
43
+ 'plug mini (us)': true, // Outlet → OnOff
17
44
 
18
- export function isCurtainDevice(device: device & devicesConfig): device is (curtain | curtain3) & devicesConfig {
19
- return device.deviceType === 'Curtain' || device.deviceType === 'Curtain3'
20
- }
45
+ // Window covering variants
46
+ 'blindtilt': true, // WindowCovering → WindowCovering
47
+ 'blind tilt': true, // WindowCovering → WindowCovering
48
+ 'curtain3': true, // WindowCovering → WindowCovering
49
+ 'rollershade': true, // WindowCovering → WindowCovering
50
+ 'roller shade': true, // WindowCovering → WindowCovering
51
+ 'worollershade': true, // WindowCovering → WindowCovering
52
+ 'wo rollershade': true, // WindowCovering → WindowCovering
21
53
 
22
- export function isBlindTiltDevice(device: device & devicesConfig): device is blindTilt & devicesConfig {
23
- return device.deviceType === 'Blind Tilt'
24
- }
54
+ // Vacuum variants (normalized to 'vacuum' before lookup)
55
+ 'wosweeper': true, // VacuumDevice → RobotVacuumCleaner
56
+ 'wosweepermini': true, // VacuumDevice → RobotVacuumCleaner
57
+ 'wosweeperminipro': true, // VacuumDevice → RobotVacuumCleaner
58
+ 'k10+': true, // VacuumDevice → RobotVacuumCleaner
59
+ 'k10+ pro': true, // VacuumDevice → RobotVacuumCleaner
25
60
 
26
- export function sleep(ms: number): Promise<void> {
27
- return new Promise(resolve => setTimeout(resolve, ms))
28
- }
29
- /**
30
- * Check if the humidity is within the min and max range
31
- * @param humidity - The humidity value
32
- * @param min - The minimum humidity value
33
- * @param max - The maximum humidity value
34
- * @returns The humidity value
35
- */
36
- export function validHumidity(humidity: number, min?: number, max?: number): number {
37
- if (humidity < (min || 0)) {
38
- return min ?? 0
39
- } else if (humidity > (max || 100)) {
40
- return max ?? 100
41
- }
42
- return humidity
43
- }
61
+ // Sensors
62
+ 'meter': true, // TemperatureSensor + HumiditySensor → TemperatureMeasurement + RelativeHumidityMeasurement
63
+ 'meterplus': true, // TemperatureSensor + HumiditySensor → TemperatureMeasurement + RelativeHumidityMeasurement
64
+ 'meter plus (jp)': true, // TemperatureSensor + HumiditySensor → TemperatureMeasurement + RelativeHumidityMeasurement
65
+ 'meterpro': true, // TemperatureSensor + HumiditySensor TemperatureMeasurement + RelativeHumidityMeasurement
66
+ 'meterpro(co2)': true, // TemperatureSensor + HumiditySensor TemperatureMeasurement + RelativeHumidityMeasurement
67
+ 'waterdetector': true, // LeakSensor BooleanState
68
+ 'water detector': true, // LeakSensor BooleanState
44
69
 
45
- /**
46
- * Converts the value to celsius if the temperature units are in Fahrenheit
47
- */
48
- export function convertUnits(value: number, unit: string, convert?: string): number {
49
- if (unit === 'CELSIUS' && convert === 'CELSIUS') {
50
- return Math.round((value * 9) / 5 + 32)
51
- } else if (unit === 'FAHRENHEIT' && convert === 'FAHRENHEIT') {
52
- // celsius should be to the nearest 0.5 degree
53
- return Math.round((5 / 9) * (value - 32) * 2) / 2
54
- }
55
- return value
70
+ // Other devices
71
+ 'smart fan': true, // Fan FanControl
72
+ 'strip light': true, // Lightbulb (color) → OnOff + LevelControl + ColorControl
73
+ 'hub 2': false, // Hub device - not exposed as accessory
74
+ 'walletfinder': false, // Button device - Matter support TBD
56
75
  }
57
76
 
58
77
  /**
59
- * Safely serializes an object to a JSON string, handling circular references.
78
+ * Default Matter cluster configurations by device type.
79
+ * Maps device types to their Matter cluster states (used when device doesn't provide clusters).
80
+ * Note: wosweeper/curtain/plug variants are normalized before cluster lookup (see loadDevices).
60
81
  *
61
- * @param obj - The object to be serialized.
62
- * @returns The JSON string representation of the object.
82
+ * @property {object} [deviceType] - The default cluster state for the device type.
83
+ * @example
84
+ * DEVICE_MATTER_CLUSTERS['bot'] // { onOff: { onOff: false } }
63
85
  */
64
- export function safeStringify(obj: any) {
65
- const seen = new WeakSet()
66
- return JSON.stringify(obj, (_key, value) => {
67
- if (typeof value === 'object' && value !== null) {
68
- if (seen.has(value)) {
69
- return
70
- }
71
- seen.add(value)
72
- }
73
- return value
74
- }, ' ')
75
- }
86
+ export const DEVICE_MATTER_CLUSTERS: Record<string, any> = {
87
+ // Core devices - aligned with HAP service implementations
88
+ bot: { onOff: { onOff: false } }, // Switch → OnOff
89
+ vacuum: {
90
+ rvcRunMode: {
91
+ supportedModes: [
92
+ { label: 'Idle', mode: 0, modeTags: [{ value: 16384 }] },
93
+ { label: 'Cleaning', mode: 1, modeTags: [{ value: 16385 }] },
94
+ ],
95
+ currentMode: 0,
96
+ },
97
+ rvcCleanMode: {
98
+ supportedModes: [
99
+ { label: 'Vacuum', mode: 0, modeTags: [{ value: 16385 }] },
100
+ ],
101
+ currentMode: 0,
102
+ },
103
+ rvcOperationalState: {
104
+ operationalStateList: [
105
+ { operationalStateId: 0 }, // Stopped
106
+ { operationalStateId: 1 }, // Running
107
+ { operationalStateId: 2 }, // Paused
108
+ { operationalStateId: 3 }, // Error (required)
109
+ { operationalStateId: 64 }, // Seeking charger
110
+ { operationalStateId: 65 }, // Charging
111
+ { operationalStateId: 66 }, // Docked
112
+ ],
113
+ operationalState: 66,
114
+ },
115
+ }, // Switch in HAP, RobotVacuumCleaner in Matter
116
+ curtain: {
117
+ windowCovering: {
118
+ currentPositionLiftPercent100ths: 0,
119
+ targetPositionLiftPercent100ths: 0,
120
+ operationalStatus: {
121
+ global: 0,
122
+ lift: 0,
123
+ tilt: 0,
124
+ },
125
+ endProductType: 0,
126
+ configStatus: {
127
+ operational: true,
128
+ onlineReserved: true,
129
+ liftMovementReversed: false,
130
+ liftPositionAware: true,
131
+ tiltPositionAware: false,
132
+ liftEncoderControlled: true,
133
+ tiltEncoderControlled: false,
134
+ },
135
+ },
136
+ }, // WindowCovering → WindowCovering (includes curtain3, rollershade variants via normalization)
137
+ blindtilt: {
138
+ windowCovering: {
139
+ currentPositionLiftPercent100ths: 0,
140
+ targetPositionLiftPercent100ths: 0,
141
+ currentPositionTiltPercent100ths: 0,
142
+ targetPositionTiltPercent100ths: 0,
143
+ operationalStatus: {
144
+ global: 0,
145
+ lift: 0,
146
+ tilt: 0,
147
+ },
148
+ endProductType: 8,
149
+ configStatus: {
150
+ operational: true,
151
+ onlineReserved: true,
152
+ liftMovementReversed: false,
153
+ liftPositionAware: true,
154
+ tiltPositionAware: true,
155
+ liftEncoderControlled: true,
156
+ tiltEncoderControlled: true,
157
+ },
158
+ },
159
+ }, // WindowCovering with tilt → WindowCovering
160
+ fan: {
161
+ onOff: { onOff: false },
162
+ fanControl: {
163
+ fanMode: 0,
164
+ percentCurrent: 0,
165
+ percentSetting: 0,
166
+ speedCurrent: 0,
167
+ speedMax: 100,
168
+ },
169
+ }, // Fan → OnOff + FanControl
170
+ light: {
171
+ onOff: { onOff: false },
172
+ levelControl: {
173
+ currentLevel: 0,
174
+ minLevel: 0,
175
+ maxLevel: 254,
176
+ },
177
+ }, // Lightbulb → OnOff + LevelControl
178
+ lightstrip: {
179
+ onOff: { onOff: false },
180
+ levelControl: {
181
+ currentLevel: 0,
182
+ minLevel: 0,
183
+ maxLevel: 254,
184
+ },
185
+ colorControl: {
186
+ colorMode: 0,
187
+ },
188
+ }, // Lightbulb with color → OnOff + LevelControl + ColorControl
189
+ lock: {
190
+ doorLock: {
191
+ lockState: 0,
192
+ lockType: 0,
193
+ actuatorEnabled: true,
194
+ operatingMode: 0,
195
+ },
196
+ }, // LockMechanism → DoorLock
197
+ motion: {
198
+ occupancySensing: {
199
+ occupancy: 0,
200
+ occupancySensorType: 0,
201
+ },
202
+ }, // MotionSensor → OccupancySensing
203
+ contact: {
204
+ booleanState: {
205
+ stateValue: false,
206
+ },
207
+ }, // ContactSensor → BooleanState
208
+ humidifier: {
209
+ onOff: { onOff: false },
210
+ fanControl: {
211
+ fanMode: 0,
212
+ percentCurrent: 0,
213
+ },
214
+ relativeHumidityMeasurement: {
215
+ measuredValue: 0,
216
+ minMeasuredValue: 0,
217
+ maxMeasuredValue: 100,
218
+ },
219
+ }, // HumidifierDehumidifier → OnOff + FanControl + RelativeHumidityMeasurement
220
+ temperature: {
221
+ temperatureMeasurement: {
222
+ measuredValue: 0,
223
+ minMeasuredValue: -27315,
224
+ maxMeasuredValue: 32767,
225
+ },
226
+ }, // TemperatureSensor → TemperatureMeasurement
76
227
 
77
- /**
78
- * Formats a device ID as a MAC address.
79
- * Ensures the device ID does not already contain colons.
80
- *
81
- * @param deviceId - The device ID to format.
82
- * @param cassSensative - If the MAC address should be case sensitive. Default is false, which will return the MAC address in lowercase.
83
- * @returns The formatted MAC address.
84
- * @throws Will throw an error if the device ID is not a valid MAC address or a 12-character hexadecimal string.
85
- */
86
- export function formatDeviceIdAsMac(deviceId: string, cassSensative?: boolean): string {
87
- if (typeof deviceId !== 'string') {
88
- throw new TypeError('Invalid device ID format. Device ID must be a string.')
89
- }
228
+ // Switch/Outlet devices
229
+ relay: { onOff: { onOff: false } }, // Switch → OnOff
230
+ plug: {
231
+ onOff: { onOff: false },
232
+ electricalMeasurement: {
233
+ activePower: 0,
234
+ rmsCurrent: 0,
235
+ rmsVoltage: 0,
236
+ },
237
+ }, // Outlet OnOff + ElectricalMeasurement (for PM models)
238
+
239
+ // Sensors
240
+ meter: {
241
+ temperatureMeasurement: {
242
+ measuredValue: 0,
243
+ minMeasuredValue: -27315,
244
+ maxMeasuredValue: 32767,
245
+ },
246
+ relativeHumidityMeasurement: {
247
+ measuredValue: 0,
248
+ minMeasuredValue: 0,
249
+ maxMeasuredValue: 100,
250
+ },
251
+ }, // TemperatureSensor + HumiditySensor → TemperatureMeasurement + RelativeHumidityMeasurement
252
+ waterdetector: {
253
+ booleanState: {
254
+ stateValue: false,
255
+ },
256
+ }, // LeakSensor → BooleanState
257
+ }
90
258
 
91
- deviceId = deviceId.trim()
259
+ export function createMatterHandlers(log: Logger, deviceId: string, type: string, client: any): any {
260
+ const lowerType = type.toLowerCase()
92
261
 
93
- const macAddressRegex = /^(?:[0-9a-f]{2}:){5}[0-9a-f]{2}$/i
94
- const hexRegex = /^[0-9a-f]{12}$/i
95
- const vacuumFormatRegex = /^[a-z0-9]{12,18}$/i
262
+ switch (lowerType) {
263
+ case 'vacuum':
264
+ return {
265
+ rvcRunMode: {
266
+ changeToMode: async (request: any) => {
267
+ const modeNames = ['Idle', 'Cleaning', 'Mapping']
268
+ const modeName = modeNames[request?.newMode] || `Unknown (${request?.newMode})`
269
+ log.info(`[${deviceId}] RVC run mode change requested: ${modeName}`)
270
+ if (!client) {
271
+ log.warn(`[${deviceId}] No SwitchBot client available`)
272
+ return { success: false }
273
+ }
274
+ try {
275
+ // For K10+ family: use 'start' to begin cleaning (mode 1 = Cleaning)
276
+ // For older K10+: only supports start/stop/dock
277
+ // For newer K20+/S10/S20: supports startClean with more parameters
278
+ // Map Matter mode to SwitchBot command
279
+ const switchBotCommand = request?.newMode === 1 ? 'start' : 'stop'
280
+ const body = {
281
+ command: switchBotCommand,
282
+ parameter: 'default',
283
+ commandType: 'command',
284
+ }
285
+ log.debug(`[${deviceId}] Sending RVC mode change request:`, JSON.stringify(body))
286
+ const result = await client.setDeviceState(deviceId, body)
287
+ log.debug(`[${deviceId}] RVC mode change API response:`, JSON.stringify(result))
288
+ log.info(`[${deviceId}] RVC mode changed successfully to ${switchBotCommand}`)
289
+ return { success: true, result }
290
+ } catch (e) {
291
+ log.error(`[${deviceId}] Failed to change RVC mode:`, e)
292
+ return { success: false, error: e }
293
+ }
294
+ },
295
+ },
296
+ rvcCleanMode: {
297
+ changeToMode: async (request: any) => {
298
+ const modeName = request?.newMode !== undefined ? `Mode ${request.newMode}` : 'Unknown'
299
+ log.info(`[${deviceId}] RVC clean mode change requested: ${modeName}`)
300
+ if (!client) {
301
+ log.warn(`[${deviceId}] No SwitchBot client available`)
302
+ return { success: false }
303
+ }
304
+ try {
305
+ // Clean mode (vacuum/mop/etc) not directly supported via Matter for K10+
306
+ // K20+ Pro and newer models support via startClean action parameter
307
+ log.info(`[${deviceId}] Clean mode change requires startClean command (not yet implemented for Matter)`)
308
+ return { success: true }
309
+ } catch (e) {
310
+ log.error(`[${deviceId}] Failed to change RVC clean mode:`, e)
311
+ return { success: false, error: e }
312
+ }
313
+ },
314
+ },
315
+ rvcOperationalState: {
316
+ pause: async () => {
317
+ log.info(`[${deviceId}] RVC pause command received`)
318
+ if (!client) {
319
+ log.warn(`[${deviceId}] No SwitchBot client available`)
320
+ return { success: false }
321
+ }
322
+ try {
323
+ const body = {
324
+ command: 'stop',
325
+ parameter: 'default',
326
+ commandType: 'command',
327
+ }
328
+ log.debug(`[${deviceId}] Sending RVC pause request:`, JSON.stringify(body))
329
+ const result = await client.setDeviceState(deviceId, body)
330
+ log.debug(`[${deviceId}] RVC pause API response:`, JSON.stringify(result))
331
+ log.info(`[${deviceId}] RVC paused successfully`)
332
+ return { success: true, result }
333
+ } catch (e) {
334
+ log.error(`[${deviceId}] Failed to pause RVC:`, e)
335
+ return { success: false, error: e }
336
+ }
337
+ },
338
+ resume: async () => {
339
+ log.info(`[${deviceId}] RVC resume command received`)
340
+ if (!client) {
341
+ log.warn(`[${deviceId}] No SwitchBot client available`)
342
+ return { success: false }
343
+ }
344
+ try {
345
+ const body = {
346
+ command: 'start',
347
+ parameter: 'default',
348
+ commandType: 'command',
349
+ }
350
+ log.debug(`[${deviceId}] Sending RVC resume request:`, JSON.stringify(body))
351
+ const result = await client.setDeviceState(deviceId, body)
352
+ log.debug(`[${deviceId}] RVC resume API response:`, JSON.stringify(result))
353
+ log.info(`[${deviceId}] RVC resumed successfully`)
354
+ return { success: true, result }
355
+ } catch (e) {
356
+ log.error(`[${deviceId}] Failed to resume RVC:`, e)
357
+ return { success: false, error: e }
358
+ }
359
+ },
360
+ goHome: async () => {
361
+ log.info(`[${deviceId}] RVC goHome command received`)
362
+ if (!client) {
363
+ log.warn(`[${deviceId}] No SwitchBot client available`)
364
+ return { success: false }
365
+ }
366
+ try {
367
+ const body = {
368
+ command: 'dock',
369
+ parameter: 'default',
370
+ commandType: 'command',
371
+ }
372
+ log.debug(`[${deviceId}] Sending RVC goHome request:`, JSON.stringify(body))
373
+ const result = await client.setDeviceState(deviceId, body)
374
+ log.debug(`[${deviceId}] RVC goHome API response:`, JSON.stringify(result))
375
+ log.info(`[${deviceId}] RVC sent to dock successfully`)
376
+ return { success: true, result }
377
+ } catch (e) {
378
+ log.error(`[${deviceId}] Failed to send goHome command:`, e)
379
+ return { success: false, error: e }
380
+ }
381
+ },
382
+ },
383
+ }
96
384
 
97
- // Check if the deviceId is already in a valid MAC address format
98
- if (macAddressRegex.test(deviceId)) {
99
- return cassSensative ? deviceId : deviceId.toLowerCase()
100
- }
385
+ case 'bot':
386
+ return {
387
+ onOff: {
388
+ on: async () => {
389
+ log.info(`[${deviceId}] Bot ON command received`)
390
+ if (!client) {
391
+ log.warn(`[${deviceId}] No SwitchBot client available`)
392
+ return { success: false }
393
+ }
394
+ try {
395
+ const result = await client.setDeviceState(deviceId, {
396
+ command: 'turnOn',
397
+ parameter: 'default',
398
+ commandType: 'command',
399
+ })
400
+ log.info(`[${deviceId}] Bot turned on successfully`)
401
+ return { success: true, result }
402
+ } catch (e) {
403
+ log.error(`[${deviceId}] Failed to turn on Bot:`, e)
404
+ return { success: false, error: e }
405
+ }
406
+ },
407
+ off: async () => {
408
+ log.info(`[${deviceId}] Bot OFF command received`)
409
+ if (!client) {
410
+ log.warn(`[${deviceId}] No SwitchBot client available`)
411
+ return { success: false }
412
+ }
413
+ try {
414
+ const result = await client.setDeviceState(deviceId, {
415
+ command: 'turnOff',
416
+ parameter: 'default',
417
+ commandType: 'command',
418
+ })
419
+ log.info(`[${deviceId}] Bot turned off successfully`)
420
+ return { success: true, result }
421
+ } catch (e) {
422
+ log.error(`[${deviceId}] Failed to turn off Bot:`, e)
423
+ return { success: false, error: e }
424
+ }
425
+ },
426
+ },
427
+ }
101
428
 
102
- // Check if the deviceId is a valid 12-character hexadecimal string
103
- if (hexRegex.test(deviceId)) {
104
- const formattedDeviceId = deviceId.match(/.{1,2}/g)!.join(':')
105
- return cassSensative ? formattedDeviceId : formattedDeviceId.toLowerCase()
106
- }
429
+ case 'curtain':
430
+ case 'blindtilt':
431
+ return {
432
+ windowCovering: {
433
+ goToLiftPercentage: async (request: any) => {
434
+ const percentage = request?.liftPercent100thsValue
435
+ log.info(`[${deviceId}] Curtain position change requested: ${percentage}`)
436
+ if (!client) {
437
+ log.warn(`[${deviceId}] No SwitchBot client available`)
438
+ return { success: false }
439
+ }
440
+ try {
441
+ // Convert Matter percentage (0-10000) to SwitchBot (0-100)
442
+ const position = Math.max(0, Math.min(100, Math.round((percentage || 0) / 100)))
443
+ const result = await client.setDeviceState(deviceId, {
444
+ command: 'setPosition',
445
+ parameter: String(position),
446
+ commandType: 'command',
447
+ })
448
+ log.info(`[${deviceId}] Curtain position set to ${position}% successfully`)
449
+ return { success: true, result }
450
+ } catch (e) {
451
+ log.error(`[${deviceId}] Failed to set curtain position:`, e)
452
+ return { success: false, error: e }
453
+ }
454
+ },
455
+ upOrOpen: async () => {
456
+ log.info(`[${deviceId}] Curtain open command received`)
457
+ if (!client) {
458
+ log.warn(`[${deviceId}] No SwitchBot client available`)
459
+ return { success: false }
460
+ }
461
+ try {
462
+ const result = await client.setDeviceState(deviceId, {
463
+ command: 'open',
464
+ parameter: 'default',
465
+ commandType: 'command',
466
+ })
467
+ log.info(`[${deviceId}] Curtain opened successfully`)
468
+ return { success: true, result }
469
+ } catch (e) {
470
+ log.error(`[${deviceId}] Failed to open curtain:`, e)
471
+ return { success: false, error: e }
472
+ }
473
+ },
474
+ downOrClose: async () => {
475
+ log.info(`[${deviceId}] Curtain close command received`)
476
+ if (!client) {
477
+ log.warn(`[${deviceId}] No SwitchBot client available`)
478
+ return { success: false }
479
+ }
480
+ try {
481
+ const result = await client.setDeviceState(deviceId, {
482
+ command: 'close',
483
+ parameter: 'default',
484
+ commandType: 'command',
485
+ })
486
+ log.info(`[${deviceId}] Curtain closed successfully`)
487
+ return { success: true, result }
488
+ } catch (e) {
489
+ log.error(`[${deviceId}] Failed to close curtain:`, e)
490
+ return { success: false, error: e }
491
+ }
492
+ },
493
+ stopMotion: async () => {
494
+ log.info(`[${deviceId}] Curtain stop command received`)
495
+ if (!client) {
496
+ log.warn(`[${deviceId}] No SwitchBot client available`)
497
+ return { success: false }
498
+ }
499
+ try {
500
+ const result = await client.setDeviceState(deviceId, {
501
+ command: 'pause',
502
+ parameter: 'default',
503
+ commandType: 'command',
504
+ })
505
+ log.info(`[${deviceId}] Curtain motion stopped successfully`)
506
+ return { success: true, result }
507
+ } catch (e) {
508
+ log.error(`[${deviceId}] Failed to stop curtain:`, e)
509
+ return { success: false, error: e }
510
+ }
511
+ },
512
+ },
513
+ }
107
514
 
108
- // Check if the deviceId matches the custom format
109
- if (vacuumFormatRegex.test(deviceId)) {
110
- const formattedDeviceId = deviceId.slice(-12).match(/.{1,2}/g)!.join(':')
111
- return cassSensative ? formattedDeviceId : formattedDeviceId.toLowerCase()
112
- }
515
+ case 'plug':
516
+ return {
517
+ onOff: {
518
+ on: async () => {
519
+ log.info(`[${deviceId}] Plug ON command received`)
520
+ if (!client) {
521
+ log.warn(`[${deviceId}] No SwitchBot client available`)
522
+ return { success: false }
523
+ }
524
+ try {
525
+ const result = await client.setDeviceState(deviceId, {
526
+ command: 'turnOn',
527
+ parameter: 'default',
528
+ commandType: 'command',
529
+ })
530
+ log.info(`[${deviceId}] Plug turned on successfully`)
531
+ return { success: true, result }
532
+ } catch (e) {
533
+ log.error(`[${deviceId}] Failed to turn on plug:`, e)
534
+ return { success: false, error: e }
535
+ }
536
+ },
537
+ off: async () => {
538
+ log.info(`[${deviceId}] Plug OFF command received`)
539
+ if (!client) {
540
+ log.warn(`[${deviceId}] No SwitchBot client available`)
541
+ return { success: false }
542
+ }
543
+ try {
544
+ const result = await client.setDeviceState(deviceId, {
545
+ command: 'turnOff',
546
+ parameter: 'default',
547
+ commandType: 'command',
548
+ })
549
+ log.info(`[${deviceId}] Plug turned off successfully`)
550
+ return { success: true, result }
551
+ } catch (e) {
552
+ log.error(`[${deviceId}] Failed to turn off plug:`, e)
553
+ return { success: false, error: e }
554
+ }
555
+ },
556
+ },
557
+ }
113
558
 
114
- throw new Error(`Invalid device ID format. Must be a valid MAC address, a 12-character hexadecimal string, or a 12 to 18-character alphanumeric string. Device ID: ${deviceId}`)
115
- }
559
+ case 'lock':
560
+ return {
561
+ doorLock: {
562
+ setLockState: async (request: any) => {
563
+ const state = request?.lockState === 1 ? 'LOCKED' : 'UNLOCKED'
564
+ log.info(`[${deviceId}] Lock state change requested: ${state}`)
565
+ if (!client) {
566
+ log.warn(`[${deviceId}] No SwitchBot client available`)
567
+ return { success: false }
568
+ }
569
+ try {
570
+ const command = request?.lockState === 1 ? 'lock' : 'unlock'
571
+ const result = await client.setDeviceState(deviceId, {
572
+ command,
573
+ parameter: 'default',
574
+ commandType: 'command',
575
+ })
576
+ log.info(`[${deviceId}] Lock ${state} successfully`)
577
+ return { success: true, result }
578
+ } catch (e) {
579
+ log.error(`[${deviceId}] Failed to change lock state:`, e)
580
+ return { success: false, error: e }
581
+ }
582
+ },
583
+ },
584
+ }
116
585
 
117
- export function rgb2hs(r: any, g: any, b: any) {
118
- /**
119
- * Credit:
120
- * https://github.com/WickyNilliams/pure-color
121
- */
122
- r = Number.parseInt(r)
123
- g = Number.parseInt(g)
124
- b = Number.parseInt(b)
125
- const min = Math.min(r, g, b)
126
- const max = Math.max(r, g, b)
127
- const delta = max - min
128
- let h
129
- let s
130
- if (max === 0) {
131
- s = 0
132
- } else {
133
- s = (delta / max) * 100
134
- }
135
- if (max === min) {
136
- h = 0
137
- } else if (r === max) {
138
- h = (g - b) / delta
139
- } else if (g === max) {
140
- h = 2 + (b - r) / delta
141
- } else if (b === max) {
142
- h = 4 + (r - g) / delta
143
- }
144
- h = Math.min(h * 60, 360)
586
+ case 'fan':
587
+ return {
588
+ onOff: {
589
+ on: async () => {
590
+ log.info(`[${deviceId}] Fan ON command received`)
591
+ if (!client) {
592
+ log.warn(`[${deviceId}] No SwitchBot client available`)
593
+ return { success: false }
594
+ }
595
+ try {
596
+ const result = await client.setDeviceState(deviceId, {
597
+ command: 'turnOn',
598
+ parameter: 'default',
599
+ commandType: 'command',
600
+ })
601
+ log.info(`[${deviceId}] Fan turned on successfully`)
602
+ return { success: true, result }
603
+ } catch (e) {
604
+ log.error(`[${deviceId}] Failed to turn on fan:`, e)
605
+ return { success: false, error: e }
606
+ }
607
+ },
608
+ off: async () => {
609
+ log.info(`[${deviceId}] Fan OFF command received`)
610
+ if (!client) {
611
+ log.warn(`[${deviceId}] No SwitchBot client available`)
612
+ return { success: false }
613
+ }
614
+ try {
615
+ const result = await client.setDeviceState(deviceId, {
616
+ command: 'turnOff',
617
+ parameter: 'default',
618
+ commandType: 'command',
619
+ })
620
+ log.info(`[${deviceId}] Fan turned off successfully`)
621
+ return { success: true, result }
622
+ } catch (e) {
623
+ log.error(`[${deviceId}] Failed to turn off fan:`, e)
624
+ return { success: false, error: e }
625
+ }
626
+ },
627
+ },
628
+ fanControl: {
629
+ setFanSpeed: async (request: any) => {
630
+ const speed = request?.percentSetting || 0
631
+ log.info(`[${deviceId}] Fan speed change requested: ${speed}%`)
632
+ if (!client) {
633
+ log.warn(`[${deviceId}] No SwitchBot client available`)
634
+ return { success: false }
635
+ }
636
+ try {
637
+ // Convert percentage to SwitchBot fan speed parameter
638
+ const speedParam = Math.max(1, Math.min(100, speed))
639
+ const result = await client.setDeviceState(deviceId, {
640
+ command: 'setFanSpeed',
641
+ parameter: String(speedParam),
642
+ commandType: 'command',
643
+ })
644
+ log.info(`[${deviceId}] Fan speed set to ${speedParam}% successfully`)
645
+ return { success: true, result }
646
+ } catch (e) {
647
+ log.error(`[${deviceId}] Failed to set fan speed:`, e)
648
+ return { success: false, error: e }
649
+ }
650
+ },
651
+ },
652
+ }
145
653
 
146
- if (h < 0) {
147
- h += 360
148
- }
149
- return [Math.round(h), Math.round(s)]
150
- }
654
+ case 'light':
655
+ return {
656
+ onOff: {
657
+ on: async () => {
658
+ log.info(`[${deviceId}] Light ON command received`)
659
+ if (!client) {
660
+ log.warn(`[${deviceId}] No SwitchBot client available`)
661
+ return { success: false }
662
+ }
663
+ try {
664
+ const result = await client.setDeviceState(deviceId, {
665
+ command: 'turnOn',
666
+ parameter: 'default',
667
+ commandType: 'command',
668
+ })
669
+ log.info(`[${deviceId}] Light turned on successfully`)
670
+ return { success: true, result }
671
+ } catch (e) {
672
+ log.error(`[${deviceId}] Failed to turn on light:`, e)
673
+ return { success: false, error: e }
674
+ }
675
+ },
676
+ off: async () => {
677
+ log.info(`[${deviceId}] Light OFF command received`)
678
+ if (!client) {
679
+ log.warn(`[${deviceId}] No SwitchBot client available`)
680
+ return { success: false }
681
+ }
682
+ try {
683
+ const result = await client.setDeviceState(deviceId, {
684
+ command: 'turnOff',
685
+ parameter: 'default',
686
+ commandType: 'command',
687
+ })
688
+ log.info(`[${deviceId}] Light turned off successfully`)
689
+ return { success: true, result }
690
+ } catch (e) {
691
+ log.error(`[${deviceId}] Failed to turn off light:`, e)
692
+ return { success: false, error: e }
693
+ }
694
+ },
695
+ },
696
+ levelControl: {
697
+ moveToLevel: async (request: any) => {
698
+ const level = request?.level || 0
699
+ // Convert from 0-254 to 0-100
700
+ const brightness = Math.round((level / 254) * 100)
701
+ log.info(`[${deviceId}] Light brightness change requested: ${brightness}%`)
702
+ if (!client) {
703
+ log.warn(`[${deviceId}] No SwitchBot client available`)
704
+ return { success: false }
705
+ }
706
+ try {
707
+ const param = Math.max(0, Math.min(100, brightness))
708
+ const result = await client.setDeviceState(deviceId, {
709
+ command: 'setBrightness',
710
+ parameter: String(param),
711
+ commandType: 'command',
712
+ })
713
+ log.info(`[${deviceId}] Light brightness set to ${param}% successfully`)
714
+ return { success: true, result }
715
+ } catch (e) {
716
+ log.error(`[${deviceId}] Failed to set light brightness:`, e)
717
+ return { success: false, error: e }
718
+ }
719
+ },
720
+ },
721
+ }
151
722
 
152
- export function hs2rgb(h: any, s: any) {
153
- /*
154
- Credit:
155
- https://github.com/WickyNilliams/pure-color
156
- */
157
- h = Number.parseInt(h) / 60
158
- s = Number.parseInt(s) / 100
159
- const f = h - Math.floor(h)
160
- const p = 255 * (1 - s)
161
- const q = 255 * (1 - s * f)
162
- const t = 255 * (1 - s * (1 - f))
163
- let rgb
164
- switch (Math.floor(h) % 6) {
165
- case 0:
166
- rgb = [255, t, p]
167
- break
168
- case 1:
169
- rgb = [q, 255, p]
170
- break
171
- case 2:
172
- rgb = [p, 255, t]
173
- break
174
- case 3:
175
- rgb = [p, q, 255]
176
- break
177
- case 4:
178
- rgb = [t, p, 255]
179
- break
180
- case 5:
181
- rgb = [255, p, q]
182
- break
183
- }
184
- if (rgb[0] === 255) {
185
- rgb[1] *= 0.8
186
- rgb[2] *= 0.8
187
- if (rgb[1] <= 25 && rgb[2] <= 25) {
188
- rgb[1] = 0
189
- rgb[2] = 0
190
- }
191
- }
192
- return [Math.round(rgb[0]), Math.round(rgb[1]), Math.round(rgb[2])]
193
- }
723
+ case 'lightstrip':
724
+ return {
725
+ onOff: {
726
+ on: async () => {
727
+ log.info(`[${deviceId}] Lightstrip ON command received`)
728
+ if (!client) {
729
+ log.warn(`[${deviceId}] No SwitchBot client available`)
730
+ return { success: false }
731
+ }
732
+ try {
733
+ const result = await client.setDeviceState(deviceId, {
734
+ command: 'turnOn',
735
+ parameter: 'default',
736
+ commandType: 'command',
737
+ })
738
+ log.info(`[${deviceId}] Lightstrip turned on successfully`)
739
+ return { success: true, result }
740
+ } catch (e) {
741
+ log.error(`[${deviceId}] Failed to turn on lightstrip:`, e)
742
+ return { success: false, error: e }
743
+ }
744
+ },
745
+ off: async () => {
746
+ log.info(`[${deviceId}] Lightstrip OFF command received`)
747
+ if (!client) {
748
+ log.warn(`[${deviceId}] No SwitchBot client available`)
749
+ return { success: false }
750
+ }
751
+ try {
752
+ const result = await client.setDeviceState(deviceId, {
753
+ command: 'turnOff',
754
+ parameter: 'default',
755
+ commandType: 'command',
756
+ })
757
+ log.info(`[${deviceId}] Lightstrip turned off successfully`)
758
+ return { success: true, result }
759
+ } catch (e) {
760
+ log.error(`[${deviceId}] Failed to turn off lightstrip:`, e)
761
+ return { success: false, error: e }
762
+ }
763
+ },
764
+ },
765
+ levelControl: {
766
+ moveToLevel: async (request: any) => {
767
+ const level = request?.level || 0
768
+ // Convert from 0-254 to 0-100
769
+ const brightness = Math.round((level / 254) * 100)
770
+ log.info(`[${deviceId}] Lightstrip brightness change requested: ${brightness}%`)
771
+ if (!client) {
772
+ log.warn(`[${deviceId}] No SwitchBot client available`)
773
+ return { success: false }
774
+ }
775
+ try {
776
+ const param = Math.max(0, Math.min(100, brightness))
777
+ const result = await client.setDeviceState(deviceId, {
778
+ command: 'setBrightness',
779
+ parameter: String(param),
780
+ commandType: 'command',
781
+ })
782
+ log.info(`[${deviceId}] Lightstrip brightness set to ${param}% successfully`)
783
+ return { success: true, result }
784
+ } catch (e) {
785
+ log.error(`[${deviceId}] Failed to set lightstrip brightness:`, e)
786
+ return { success: false, error: e }
787
+ }
788
+ },
789
+ },
790
+ colorControl: {
791
+ moveToHueAndSaturation: async (request: any) => {
792
+ const hue = request?.hue || 0
793
+ const saturation = request?.saturation || 0
794
+ log.info(`[${deviceId}] Lightstrip color change requested: hue=${hue}, sat=${saturation}`)
795
+ if (!client) {
796
+ log.warn(`[${deviceId}] No SwitchBot client available`)
797
+ return { success: false }
798
+ }
799
+ try {
800
+ // Convert hue (0-254) and saturation (0-254) to combined color parameter
801
+ // SwitchBot typically expects RGB or HSV format as parameter
802
+ const colorParam = `${Math.round(hue)},${Math.round(saturation)}`
803
+ const result = await client.setDeviceState(deviceId, {
804
+ command: 'setColor',
805
+ parameter: colorParam,
806
+ commandType: 'command',
807
+ })
808
+ log.info(`[${deviceId}] Lightstrip color set successfully`)
809
+ return { success: true, result }
810
+ } catch (e) {
811
+ log.error(`[${deviceId}] Failed to set lightstrip color:`, e)
812
+ return { success: false, error: e }
813
+ }
814
+ },
815
+ moveToColorTemperature: async (request: any) => {
816
+ const mireds = request?.colorTemperatureMireds || 400
817
+ // Convert mireds (158-500 typical range) to Kelvin: K = 1000000 / mireds
818
+ const kelvin = Math.round(1000000 / mireds)
819
+ log.info(`[${deviceId}] Lightstrip color temperature change requested: ${mireds} mireds (${kelvin}K)`)
820
+ if (!client) {
821
+ log.warn(`[${deviceId}] No SwitchBot client available`)
822
+ return { success: false }
823
+ }
824
+ try {
825
+ // Map Kelvin to SwitchBot color temperature parameter (typically 0-100 or specific values)
826
+ // Normalize to 0-100 scale where 0=warm (2700K) and 100=cool (6500K)
827
+ const colorTempParam = Math.max(0, Math.min(100, Math.round(((kelvin - 2700) / 3800) * 100)))
828
+ const result = await client.setDeviceState(deviceId, {
829
+ command: 'setColorTemperature',
830
+ parameter: String(colorTempParam),
831
+ commandType: 'command',
832
+ })
833
+ log.info(`[${deviceId}] Lightstrip color temperature set to ${kelvin}K successfully`)
834
+ return { success: true, result }
835
+ } catch (e) {
836
+ log.error(`[${deviceId}] Failed to set lightstrip color temperature:`, e)
837
+ return { success: false, error: e }
838
+ }
839
+ },
840
+ },
841
+ }
194
842
 
195
- export function k2rgb(k: number) {
196
- // Set kelvin to nearest 100, between 2000 and 7100
197
- k = Math.round(k / 100) * 100
198
- k = Math.max(Math.min(k, 7100), 2000)
199
-
200
- // k should now appear in our table of kelvin to rgb
201
- const values = {
202
- 2000: [255, 141, 11],
203
- 2100: [255, 146, 29],
204
- 2200: [255, 147, 44],
205
- 2300: [255, 152, 54],
206
- 2400: [255, 157, 63],
207
- 2500: [255, 166, 69],
208
- 2600: [255, 170, 77],
209
- 2700: [255, 174, 84],
210
- 2800: [255, 173, 94],
211
- 2900: [255, 177, 101],
212
- 3000: [255, 180, 107],
213
- 3100: [255, 189, 111],
214
- 3200: [255, 187, 120],
215
- 3300: [255, 195, 124],
216
- 3400: [255, 198, 130],
217
- 3500: [255, 201, 135],
218
- 3600: [255, 203, 141],
219
- 3700: [255, 206, 146],
220
- 3800: [255, 204, 153],
221
- 3900: [255, 206, 159],
222
- 4000: [255, 213, 161],
223
- 4100: [255, 215, 166],
224
- 4200: [255, 217, 171],
225
- 4300: [255, 219, 175],
226
- 4400: [255, 221, 180],
227
- 4500: [255, 223, 184],
228
- 4600: [255, 225, 188],
229
- 4700: [255, 226, 192],
230
- 4800: [255, 228, 196],
231
- 4900: [255, 229, 200],
232
- 5000: [255, 231, 204],
233
- 5100: [255, 230, 210],
234
- 5200: [255, 234, 211],
235
- 5300: [255, 235, 215],
236
- 5400: [255, 237, 218],
237
- 5500: [255, 236, 224],
238
- 5700: [255, 240, 228],
239
- 5800: [255, 241, 231],
240
- 5900: [255, 243, 234],
241
- 6000: [255, 244, 237],
242
- 6100: [255, 245, 240],
243
- 6200: [255, 246, 243],
244
- 6300: [255, 247, 247],
245
- 6400: [255, 248, 251],
246
- 6500: [255, 249, 253],
247
- 6600: [254, 249, 255],
248
- 6700: [252, 247, 255],
249
- 6800: [249, 246, 255],
250
- 6900: [247, 245, 255],
251
- 7000: [245, 243, 255],
252
- 7100: [243, 242, 255],
253
- }
843
+ case 'humidifier':
844
+ return {
845
+ onOff: {
846
+ on: async () => {
847
+ log.info(`[${deviceId}] Humidifier ON command received`)
848
+ if (!client) {
849
+ log.warn(`[${deviceId}] No SwitchBot client available`)
850
+ return { success: false }
851
+ }
852
+ try {
853
+ const result = await client.setDeviceState(deviceId, {
854
+ command: 'turnOn',
855
+ parameter: 'default',
856
+ commandType: 'command',
857
+ })
858
+ log.info(`[${deviceId}] Humidifier turned on successfully`)
859
+ return { success: true, result }
860
+ } catch (e) {
861
+ log.error(`[${deviceId}] Failed to turn on humidifier:`, e)
862
+ return { success: false, error: e }
863
+ }
864
+ },
865
+ off: async () => {
866
+ log.info(`[${deviceId}] Humidifier OFF command received`)
867
+ if (!client) {
868
+ log.warn(`[${deviceId}] No SwitchBot client available`)
869
+ return { success: false }
870
+ }
871
+ try {
872
+ const result = await client.setDeviceState(deviceId, {
873
+ command: 'turnOff',
874
+ parameter: 'default',
875
+ commandType: 'command',
876
+ })
877
+ log.info(`[${deviceId}] Humidifier turned off successfully`)
878
+ return { success: true, result }
879
+ } catch (e) {
880
+ log.error(`[${deviceId}] Failed to turn off humidifier:`, e)
881
+ return { success: false, error: e }
882
+ }
883
+ },
884
+ },
885
+ fanControl: {
886
+ setFanSpeed: async (request: any) => {
887
+ const speed = request?.percentSetting || 0
888
+ log.info(`[${deviceId}] Humidifier speed change requested: ${speed}%`)
889
+ if (!client) {
890
+ log.warn(`[${deviceId}] No SwitchBot client available`)
891
+ return { success: false }
892
+ }
893
+ try {
894
+ // Convert percentage to SwitchBot humidifier speed parameter
895
+ const speedParam = Math.max(1, Math.min(100, speed))
896
+ const result = await client.setDeviceState(deviceId, {
897
+ command: 'setFanSpeed',
898
+ parameter: String(speedParam),
899
+ commandType: 'command',
900
+ })
901
+ log.info(`[${deviceId}] Humidifier speed set to ${speedParam}% successfully`)
902
+ return { success: true, result }
903
+ } catch (e) {
904
+ log.error(`[${deviceId}] Failed to set humidifier speed:`, e)
905
+ return { success: false, error: e }
906
+ }
907
+ },
908
+ },
909
+ }
254
910
 
255
- // Return the value
256
- return values[k]
257
- }
911
+ case 'relay':
912
+ return {
913
+ onOff: {
914
+ on: async () => {
915
+ log.info(`[${deviceId}] Relay ON command received`)
916
+ if (!client) {
917
+ log.warn(`[${deviceId}] No SwitchBot client available`)
918
+ return { success: false }
919
+ }
920
+ try {
921
+ const result = await client.setDeviceState(deviceId, {
922
+ command: 'turnOn',
923
+ parameter: 'default',
924
+ commandType: 'command',
925
+ })
926
+ log.info(`[${deviceId}] Relay turned on successfully`)
927
+ return { success: true, result }
928
+ } catch (e) {
929
+ log.error(`[${deviceId}] Failed to turn on relay:`, e)
930
+ return { success: false, error: e }
931
+ }
932
+ },
933
+ off: async () => {
934
+ log.info(`[${deviceId}] Relay OFF command received`)
935
+ if (!client) {
936
+ log.warn(`[${deviceId}] No SwitchBot client available`)
937
+ return { success: false }
938
+ }
939
+ try {
940
+ const result = await client.setDeviceState(deviceId, {
941
+ command: 'turnOff',
942
+ parameter: 'default',
943
+ commandType: 'command',
944
+ })
945
+ log.info(`[${deviceId}] Relay turned off successfully`)
946
+ return { success: true, result }
947
+ } catch (e) {
948
+ log.error(`[${deviceId}] Failed to turn off relay:`, e)
949
+ return { success: false, error: e }
950
+ }
951
+ },
952
+ },
953
+ }
258
954
 
259
- export function m2hs(m) {
260
- /*
261
- Credit:
262
- https://github.com/homebridge/HAP-NodeJS
263
- */
264
- const table = {
265
- 100: [19, 222.1],
266
- 101: [18.7, 222.2],
267
- 102: [18.4, 222.3],
268
- 103: [18.2, 222.3],
269
- 104: [17.9, 222.4],
270
- 105: [17.6, 222.5],
271
- 106: [17.3, 222.7],
272
- 107: [17, 222.8],
273
- 108: [16.7, 222.9],
274
- 109: [16.4, 223],
275
- 110: [16.1, 223.2],
276
- 111: [15.8, 223.3],
277
- 112: [15.4, 223.4],
278
- 113: [15.2, 223.6],
279
- 114: [14.9, 223.8],
280
- 115: [14.7, 223.9],
281
- 116: [14.3, 224.1],
282
- 117: [14.1, 224.2],
283
- 118: [13.8, 224.4],
284
- 119: [13.5, 224.6],
285
- 120: [13.2, 224.8],
286
- 121: [12.9, 225],
287
- 122: [12.5, 225.3],
288
- 123: [12.2, 225.6],
289
- 124: [11.8, 225.9],
290
- 125: [11.4, 226.3],
291
- 126: [11.1, 226.7],
292
- 127: [10.7, 227.1],
293
- 128: [10.3, 227.6],
294
- 129: [9.9, 228],
295
- 130: [9.6, 228.5],
296
- 131: [9.3, 229.1],
297
- 132: [8.9, 229.6],
298
- 133: [8.5, 230.2],
299
- 134: [8.2, 230.9],
300
- 135: [7.8, 231.6],
301
- 136: [7.5, 232.5],
302
- 137: [7.1, 233.5],
303
- 138: [6.7, 234.6],
304
- 139: [6.3, 235.8],
305
- 140: [6, 237.1],
306
- 141: [5.6, 238.9],
307
- 142: [5.2, 240.9],
308
- 143: [5, 242.9],
309
- 144: [4.8, 244.9],
310
- 145: [4.6, 246.9],
311
- 146: [4.4, 249.3],
312
- 147: [4.3, 251.9],
313
- 148: [4.1, 254.9],
314
- 149: [3.9, 258],
315
- 150: [3.7, 261.8],
316
- 151: [3.4, 265.9],
317
- 152: [3.2, 271],
318
- 153: [3, 276.4],
319
- 154: [2.8, 283.6],
320
- 155: [2.6, 290.4],
321
- 156: [2.3, 295.3],
322
- 157: [2.1, 300],
323
- 158: [1.9, 300],
324
- 159: [1.6, 300],
325
- 160: [1.4, 195.8],
326
- 161: [1.2, 84.3],
327
- 162: [1.3, 58.2],
328
- 163: [1.5, 55.9],
329
- 164: [1.7, 53.2],
330
- 165: [1.9, 50.2],
331
- 166: [2.1, 47.1],
332
- 167: [2.4, 44.5],
333
- 168: [2.6, 42.6],
334
- 169: [2.9, 40.9],
335
- 170: [3.1, 39.5],
336
- 171: [3.4, 38.3],
337
- 172: [3.7, 37.3],
338
- 173: [3.9, 36.5],
339
- 174: [4.2, 35.7],
340
- 175: [4.4, 35.1],
341
- 176: [4.6, 34.5],
342
- 177: [4.9, 34],
343
- 178: [5.1, 33.5],
344
- 179: [5.3, 33],
345
- 180: [5.6, 32.7],
346
- 181: [5.8, 32.3],
347
- 182: [6, 32],
348
- 183: [6.3, 31.7],
349
- 184: [6.5, 31.4],
350
- 185: [6.7, 31.2],
351
- 186: [7, 30.9],
352
- 187: [7.2, 30.7],
353
- 188: [7.4, 30.5],
354
- 189: [7.6, 30.3],
355
- 190: [7.9, 30.1],
356
- 191: [8.1, 29.9],
357
- 192: [8.4, 29.7],
358
- 193: [8.6, 29.6],
359
- 194: [8.9, 29.5],
360
- 195: [9.1, 29.3],
361
- 196: [9.4, 29.2],
362
- 197: [9.6, 29.1],
363
- 198: [9.8, 29],
364
- 199: [10, 28.9],
365
- 200: [10.2, 28.7],
366
- 201: [10.5, 28.7],
367
- 202: [10.7, 28.6],
368
- 203: [11, 28.5],
369
- 204: [11.2, 28.4],
370
- 205: [11.4, 28.3],
371
- 206: [11.6, 28.3],
372
- 207: [11.8, 28.2],
373
- 208: [12.1, 28.1],
374
- 209: [12.3, 28.1],
375
- 210: [12.5, 28],
376
- 211: [12.7, 28],
377
- 212: [12.9, 27.9],
378
- 213: [13.2, 27.8],
379
- 214: [13.4, 27.8],
380
- 215: [13.6, 27.7],
381
- 216: [13.8, 27.7],
382
- 217: [14, 27.7],
383
- 218: [14.3, 27.6],
384
- 219: [14.5, 27.6],
385
- 220: [14.7, 27.5],
386
- 221: [14.9, 27.5],
387
- 222: [15.1, 27.5],
388
- 223: [15.3, 27.4],
389
- 224: [15.5, 27.4],
390
- 225: [15.8, 27.4],
391
- 226: [16, 27.3],
392
- 227: [16.2, 27.3],
393
- 228: [16.4, 27.3],
394
- 229: [16.6, 27.3],
395
- 230: [16.8, 27.2],
396
- 231: [17, 27.2],
397
- 232: [17.2, 27.2],
398
- 233: [17.4, 27.2],
399
- 234: [17.6, 27.2],
400
- 235: [17.8, 27.1],
401
- 236: [18, 27.1],
402
- 237: [18.2, 27.1],
403
- 238: [18.4, 27.1],
404
- 239: [18.7, 27.1],
405
- 240: [18.8, 27],
406
- 241: [19, 27],
407
- 242: [19.2, 27],
408
- 243: [19.4, 27],
409
- 244: [19.6, 27],
410
- 245: [19.8, 27],
411
- 246: [20, 27],
412
- 247: [20.3, 26.9],
413
- 248: [20.5, 26.9],
414
- 249: [20.6, 26.9],
415
- 250: [20.8, 26.9],
416
- 251: [21, 26.9],
417
- 252: [21.3, 26.9],
418
- 253: [21.5, 26.9],
419
- 254: [21.6, 26.9],
420
- 255: [21.8, 26.8],
421
- 256: [22, 26.8],
422
- 257: [22.2, 26.8],
423
- 258: [22.4, 26.8],
424
- 259: [22.6, 26.8],
425
- 260: [22.8, 26.8],
426
- 261: [23, 26.8],
427
- 262: [23.2, 26.8],
428
- 263: [23.4, 26.8],
429
- 264: [23.6, 26.8],
430
- 265: [23.8, 26.8],
431
- 266: [24, 26.8],
432
- 267: [24.1, 26.8],
433
- 268: [24.3, 26.8],
434
- 269: [24.5, 26.8],
435
- 270: [24.7, 26.8],
436
- 271: [24.8, 26.8],
437
- 272: [25.1, 26.7],
438
- 273: [25.3, 26.7],
439
- 274: [25.4, 26.7],
440
- 275: [25.6, 26.7],
441
- 276: [25.8, 26.7],
442
- 277: [26, 26.7],
443
- 278: [26.1, 26.7],
444
- 279: [26.3, 26.7],
445
- 280: [26.5, 26.7],
446
- 281: [26.7, 26.7],
447
- 282: [26.9, 26.7],
448
- 283: [27.1, 26.7],
449
- 284: [27.3, 26.7],
450
- 285: [27.5, 26.7],
451
- 286: [27.7, 26.7],
452
- 287: [27.8, 26.7],
453
- 288: [28, 26.7],
454
- 289: [28.2, 26.7],
455
- 290: [28.4, 26.7],
456
- 291: [28.6, 26.7],
457
- 292: [28.8, 26.7],
458
- 293: [28.9, 26.7],
459
- 294: [29.1, 26.7],
460
- 295: [29.3, 26.7],
461
- 296: [29.5, 26.7],
462
- 297: [29.6, 26.7],
463
- 298: [29.8, 26.7],
464
- 299: [30, 26.7],
465
- 300: [30.2, 26.7],
466
- 301: [30.4, 26.7],
467
- 302: [30.5, 26.7],
468
- 303: [30.7, 26.7],
469
- 304: [30.9, 26.7],
470
- 305: [31.1, 26.7],
471
- 306: [31.2, 26.7],
472
- 307: [31.4, 26.7],
473
- 308: [31.6, 26.7],
474
- 309: [31.8, 26.8],
475
- 310: [31.9, 26.8],
476
- 311: [32.1, 26.8],
477
- 312: [32.3, 26.8],
478
- 313: [32.5, 26.8],
479
- 314: [32.6, 26.8],
480
- 315: [32.8, 26.8],
481
- 316: [33, 26.8],
482
- 317: [33.2, 26.8],
483
- 318: [33.3, 26.8],
484
- 319: [33.5, 26.8],
485
- 320: [33.7, 26.8],
486
- 321: [33.8, 26.8],
487
- 322: [34, 26.8],
488
- 323: [34.2, 26.8],
489
- 324: [34.4, 26.8],
490
- 325: [34.5, 26.8],
491
- 326: [34.7, 26.8],
492
- 327: [34.9, 26.8],
493
- 328: [35.1, 26.8],
494
- 329: [35.2, 26.8],
495
- 330: [35.4, 26.8],
496
- 331: [35.5, 26.8],
497
- 332: [35.7, 26.8],
498
- 333: [35.9, 26.8],
499
- 334: [36.1, 26.8],
500
- 335: [36.3, 26.9],
501
- 336: [36.5, 26.9],
502
- 337: [36.7, 26.9],
503
- 338: [36.9, 26.9],
504
- 339: [37.1, 26.9],
505
- 340: [37.2, 26.9],
506
- 341: [37.4, 26.9],
507
- 342: [37.5, 26.9],
508
- 343: [37.7, 26.9],
509
- 344: [37.9, 26.9],
510
- 345: [38.1, 26.9],
511
- 346: [38.3, 26.9],
512
- 347: [38.5, 26.9],
513
- 348: [38.7, 26.9],
514
- 349: [38.9, 26.9],
515
- 350: [39, 26.9],
516
- 351: [39.2, 26.9],
517
- 352: [39.3, 27],
518
- 353: [39.5, 27],
519
- 354: [39.7, 27],
520
- 355: [39.9, 27],
521
- 356: [40.1, 27],
522
- 357: [40.2, 27],
523
- 358: [40.4, 27],
524
- 359: [40.6, 27],
525
- 360: [40.8, 27],
526
- 361: [40.9, 27],
527
- 362: [41.1, 27],
528
- 363: [41.2, 27],
529
- 364: [41.4, 27],
530
- 365: [41.6, 27],
531
- 366: [41.8, 27],
532
- 367: [42, 27],
533
- 368: [42.1, 27.1],
534
- 369: [42.3, 27.1],
535
- 370: [42.4, 27.1],
536
- 371: [42.6, 27.1],
537
- 372: [42.8, 27.1],
538
- 373: [43, 27.1],
539
- 374: [43.1, 27.1],
540
- 375: [43.2, 27.1],
541
- 376: [43.4, 27.1],
542
- 377: [43.6, 27.1],
543
- 378: [43.8, 27.1],
544
- 379: [43.9, 27.1],
545
- 380: [44.1, 27.1],
546
- 381: [44.3, 27.2],
547
- 382: [44.4, 27.2],
548
- 383: [44.6, 27.2],
549
- 384: [44.7, 27.2],
550
- 385: [44.9, 27.2],
551
- 386: [45.1, 27.2],
552
- 387: [45.3, 27.2],
553
- 388: [45.5, 27.2],
554
- 389: [45.6, 27.2],
555
- 390: [45.8, 27.2],
556
- 391: [46, 27.2],
557
- 392: [46.2, 27.3],
558
- 393: [46.4, 27.3],
559
- 394: [46.5, 27.3],
560
- 395: [46.7, 27.3],
561
- 396: [46.9, 27.3],
562
- 397: [47.1, 27.3],
563
- 398: [47.2, 27.3],
564
- 399: [47.4, 27.3],
565
- 400: [47.6, 27.3],
566
- 401: [47.7, 27.3],
567
- 402: [47.9, 27.3],
568
- 403: [48.1, 27.3],
569
- 404: [48.3, 27.3],
570
- 405: [48.5, 27.4],
571
- 406: [48.7, 27.4],
572
- 407: [48.8, 27.4],
573
- 408: [49, 27.4],
574
- 409: [49.2, 27.4],
575
- 410: [49.4, 27.4],
576
- 411: [49.6, 27.4],
577
- 412: [49.7, 27.4],
578
- 413: [49.9, 27.4],
579
- 414: [50.1, 27.4],
580
- 415: [50.2, 27.4],
581
- 416: [50.4, 27.4],
582
- 417: [50.6, 27.5],
583
- 418: [50.7, 27.5],
584
- 419: [50.9, 27.5],
585
- 420: [51.1, 27.5],
586
- 421: [51.2, 27.5],
587
- 422: [51.4, 27.5],
588
- 423: [51.6, 27.5],
589
- 424: [51.7, 27.5],
590
- 425: [51.9, 27.5],
591
- 426: [52.1, 27.5],
592
- 427: [51.2, 27.6],
593
- 428: [52.4, 27.6],
594
- 429: [52.5, 27.6],
595
- 430: [52.7, 27.6],
596
- 431: [52.9, 27.6],
597
- 432: [53.1, 27.6],
598
- 433: [53.2, 27.6],
599
- 434: [53.4, 27.6],
600
- 435: [53.6, 27.6],
601
- 436: [53.7, 27.6],
602
- 437: [53.9, 27.6],
603
- 438: [54.1, 27.7],
604
- 439: [54.2, 27.7],
605
- 440: [54.3, 27.7],
606
- 441: [54.5, 27.7],
607
- 442: [54.7, 27.7],
608
- 443: [54.8, 27.7],
609
- 444: [55, 27.7],
610
- 445: [55.2, 27.7],
611
- 446: [55.3, 27.7],
612
- 447: [55.5, 27.7],
613
- 448: [55.7, 27.7],
614
- 449: [55.8, 27.8],
615
- 450: [56, 27.8],
616
- 451: [56.2, 27.8],
617
- 452: [56.3, 27.8],
618
- 453: [56.5, 27.8],
619
- 454: [56.7, 27.8],
620
- 455: [56.8, 27.8],
621
- 456: [57, 27.8],
622
- 457: [57.2, 27.8],
623
- 458: [57.3, 27.9],
624
- 459: [57.4, 27.9],
625
- 460: [57.6, 27.9],
626
- 461: [57.8, 27.9],
627
- 462: [57.9, 27.9],
628
- 463: [58.1, 27.9],
629
- 464: [58.3, 27.9],
630
- 465: [58.4, 27.9],
631
- 466: [58.6, 27.9],
632
- 467: [58.8, 27.9],
633
- 468: [59, 28],
634
- 469: [59.1, 28],
635
- 470: [59.2, 28],
636
- 471: [59.4, 28],
637
- 472: [59.6, 28],
638
- 473: [59.7, 28],
639
- 474: [60, 28],
640
- 475: [60.1, 28],
641
- 476: [60.2, 28],
642
- 477: [60.4, 28],
643
- 478: [60.6, 28.1],
644
- 479: [60.7, 28.1],
645
- 480: [60.9, 28.1],
646
- 481: [60.1, 28.1],
647
- 482: [60.3, 28.1],
648
- 483: [61.4, 28.1],
649
- 484: [61.5, 28.1],
650
- 485: [61.7, 28.1],
651
- 486: [61.9, 28.1],
652
- 487: [62, 28.2],
653
- 488: [62.2, 28.2],
654
- 489: [62.3, 28.2],
655
- 490: [62.5, 28.2],
656
- 491: [62.7, 28.2],
657
- 492: [62.8, 28.2],
658
- 493: [63, 28.2],
659
- 494: [63.2, 28.2],
660
- 495: [63.3, 28.2],
661
- 496: [63.4, 28.2],
662
- 497: [63.6, 28.2],
663
- 498: [63.8, 28.3],
664
- 499: [63.9, 28.3],
665
- 500: [64.1, 28.3],
955
+ default:
956
+ return undefined
666
957
  }
667
- const input = Math.min(Math.max(Math.round(m), 140), 500)
668
- const toReturn = table[input]
669
- return [Math.round(toReturn[1]), Math.round(toReturn[0])]
670
958
  }
671
959
 
672
960
  /**
673
- * Remove deviceConfig entries that contain only default/empty values.
674
- * Returns undefined if nothing meaningful remains.
961
+ * Resolves the Matter device type for a given device, using the Matter API and device type mappings.
962
+ * Used to map normalized device types to Matter device type definitions.
963
+ *
964
+ * @param matterApi - The Matter API object containing deviceTypes
965
+ * @param type - The normalized device type string
966
+ * @param createdDeviceType - Optionally, an already created device type object
967
+ * @param clusters - Optionally, the clusters object for the device
968
+ * @returns The resolved Matter device type object
675
969
  */
676
- export function cleanDeviceConfig(deviceConfig?: Record<string, Record<string, any>>): Record<string, Record<string, any>> | undefined {
677
- if (!deviceConfig || typeof deviceConfig !== 'object') {
678
- return undefined
970
+ const DEVICE_MATTER_DEVICE_TYPE_KEYS: Record<string, string> = {
971
+ bot: 'OnOffSwitch',
972
+ vacuum: 'RoboticVacuumCleaner',
973
+ curtain: 'WindowCovering',
974
+ blindtilt: 'WindowCovering',
975
+ fan: 'Fan',
976
+ light: 'DimmableLight',
977
+ lightstrip: 'ExtendedColorLight',
978
+ lock: 'DoorLock',
979
+ motion: 'MotionSensor',
980
+ contact: 'ContactSensor',
981
+ humidifier: 'Fan',
982
+ temperature: 'TemperatureSensor',
983
+ relay: 'OnOffSwitch',
984
+ plug: 'OnOffOutlet',
985
+ meter: 'TemperatureSensor',
986
+ waterdetector: 'LeakSensor',
987
+ }
988
+
989
+ export function resolveMatterDeviceType(matterApi: any, type: string, createdDeviceType?: any, clusters?: any): any {
990
+ if (createdDeviceType && typeof createdDeviceType === 'object' && typeof createdDeviceType.with === 'function') {
991
+ return createdDeviceType
679
992
  }
680
993
 
681
- const cleaned: Record<string, Record<string, any>> = {}
994
+ const lowerType = (typeof createdDeviceType === 'string' && createdDeviceType) ? createdDeviceType.toLowerCase() : (type || '').toLowerCase()
682
995
 
683
- for (const [deviceName, cfg] of Object.entries(deviceConfig)) {
684
- if (!cfg || typeof cfg !== 'object') {
685
- continue
686
- }
996
+ // Cluster-based upgrade for color lights if descriptor omitted device type.
997
+ const hasColorControl = !!clusters?.colorControl
998
+ const inferredType = hasColorControl && lowerType === 'light'
999
+ ? 'lightstrip'
1000
+ : lowerType
687
1001
 
688
- const hasMeaningful = Object.values(cfg).some((v) => {
689
- if (v === null || v === undefined) {
690
- return false
691
- }
692
- if (typeof v === 'boolean') {
693
- return v === true
694
- }
695
- if (typeof v === 'string') {
696
- return v.trim().length > 0
697
- }
698
- if (typeof v === 'number') {
699
- return Number.isFinite(v)
700
- }
701
- if (Array.isArray(v)) {
702
- return v.length > 0
703
- }
704
- if (typeof v === 'object') {
705
- return Object.keys(v).length > 0
706
- }
707
- return true
708
- })
709
-
710
- if (hasMeaningful) {
711
- cleaned[deviceName] = cfg
712
- }
713
- }
714
-
715
- if (Object.keys(cleaned).length > 0) {
716
- return cleaned
717
- }
718
- return undefined
1002
+ const mappedKey = DEVICE_MATTER_DEVICE_TYPE_KEYS[inferredType] || 'OnOffSwitch'
1003
+ return matterApi?.deviceTypes?.[mappedKey] || matterApi?.deviceTypes?.OnOffSwitch
719
1004
  }
720
1005
 
721
1006
  /**
722
- * Factory that returns a function to send OpenAPI commands using a retry wrapper.
1007
+ * Canonical Matter cluster ID mapping (from matter.js clusters).
1008
+ * Maps cluster names to their numeric cluster IDs.
723
1009
  *
724
- * @param retryCommandFunc - bound function that calls platform.retryCommand(device, bodyChange, maxRetries, delay)
725
- * @param deviceObj - the device object to operate on
726
- * @param opts - optional overrides for maxRetries and delayBetweenRetries
727
- * @param opts.maxRetries - override for maxRetries
728
- * @param opts.delayBetweenRetries - override for delayBetweenRetries
1010
+ * @example
1011
+ * MATTER_CLUSTER_IDS.OnOff // 0x0006
729
1012
  */
730
- export function makeOpenAPISender(retryCommandFunc: any, deviceObj: any, opts?: { maxRetries?: number, delayBetweenRetries?: number }) {
731
- return async (command: string, parameter = 'default') => {
732
- const bodyChange: any = { command, parameter, commandType: 'command' }
733
- return retryCommandFunc(deviceObj, bodyChange, opts?.maxRetries, opts?.delayBetweenRetries)
734
- }
735
- }
1013
+ export const MATTER_CLUSTER_IDS = {
1014
+ OnOff: 0x0006,
1015
+ LevelControl: 0x0008,
1016
+ ColorControl: 0x0300,
1017
+ WindowCovering: 0x0102,
1018
+ DoorLock: 0x0101,
1019
+ FanControl: 0x0202,
1020
+ RelativeHumidityMeasurement: 0x0405,
1021
+ } as const
736
1022
 
737
1023
  /**
738
- * Factory that returns a function to perform BLE actions using a SwitchBotBLE client.
739
- * Handles discovery retries and method invocation on the discovered device instance.
1024
+ * Common Matter attribute IDs grouped by cluster.
1025
+ * Maps cluster names to objects mapping attribute names to their numeric attribute IDs.
740
1026
  *
741
- * @param switchBotBLE - instance of SwitchBotBLE (may be undefined)
742
- * @param deviceObj - the device object (used to obtain bleModel/deviceId)
743
- * @param opts - optional retry settings
744
- * @param opts.bleRetries - number of BLE discovery retries
745
- * @param opts.bleRetryDelay - delay between BLE retries in ms
1027
+ * @example
1028
+ * MATTER_ATTRIBUTE_IDS.OnOff.OnOff // 0x0000
746
1029
  */
747
- export function makeBLESender(switchBotBLE: any, deviceObj: any, opts?: { bleRetries?: number, bleRetryDelay?: number }) {
748
- return async (methodName: string, ...args: any[]) => {
749
- if (!switchBotBLE) {
750
- throw new Error('Platform BLE not available')
751
- }
752
- const id = formatDeviceIdAsMac(deviceObj.deviceId)
753
- const maxRetries = opts?.bleRetries ?? 2
754
- const retryDelay = opts?.bleRetryDelay ?? 500
755
- let attempt = 0
756
- while (attempt < maxRetries) {
757
- try {
758
- const list = await switchBotBLE.discover({ model: (deviceObj as any).bleModel, id })
759
- if (!Array.isArray(list) || list.length === 0) {
760
- throw new Error('BLE device not found')
761
- }
762
- const deviceInst: any = list[0]
763
- if (typeof deviceInst[methodName] !== 'function') {
764
- throw new TypeError(`BLE method ${methodName} not available on device`)
765
- }
766
- return await deviceInst[methodName](...args)
767
- } catch (e: any) {
768
- attempt++
769
- if (attempt >= maxRetries) {
770
- throw e
771
- }
772
- await sleep(retryDelay)
773
- }
774
- }
775
- throw new Error('BLE operation failed')
776
- }
777
- }
1030
+ export const MATTER_ATTRIBUTE_IDS = {
1031
+ OnOff: { OnOff: 0x0000 },
1032
+ LevelControl: { CurrentLevel: 0x0000 },
1033
+ ColorControl: { CurrentHue: 0x0000, CurrentSaturation: 0x0001, ColorTemperatureMireds: 0x0002 },
1034
+ WindowCovering: { CurrentPosition: 0x0000, TargetPosition: 0x0001 },
1035
+ FanControl: { SpeedCurrent: 0x0000 },
1036
+ DoorLock: { LockState: 0x0000 },
1037
+ RelativeHumidityMeasurement: { MeasuredValue: 0x0000 },
1038
+ } as const
778
1039
 
779
1040
  /**
780
- * Decide effective connection type for a device given platform options.
781
- * Mirrors the logic previously in platform-matter.
1041
+ * Normalizes a device type string for Matter integration.
1042
+ * Maps various device type aliases and variants to canonical Matter device types.
1043
+ *
1044
+ * @param {string | undefined | null} typeValue - The device type string to normalize.
1045
+ * @returns {string} The normalized device type string for Matter.
1046
+ *
1047
+ * @example
1048
+ * normalizeTypeForMatter('wosweeper') // 'vacuum'
1049
+ * normalizeTypeForMatter('curtain3') // 'curtain'
1050
+ * normalizeTypeForMatter('plug mini (us)') // 'plug'
782
1051
  */
783
- export function chooseConnectionType(platformOptions: any, deviceObj: any): 'BLE' | 'OpenAPI' {
784
- if (deviceObj?.connectionType) {
785
- return deviceObj.connectionType === 'BLE' ? 'BLE' : 'OpenAPI'
1052
+ export function normalizeTypeForMatter(typeValue: string | undefined | null): string {
1053
+ const raw = String(typeValue || '').trim().toLowerCase()
1054
+ if (!raw) {
1055
+ return 'unknown'
786
1056
  }
787
- if (platformOptions?.BLE && (deviceObj?.bleModel || (typeof deviceObj?.deviceId === 'string' && deviceObj.deviceId.length > 0))) {
788
- return 'BLE'
1057
+
1058
+ // Vacuum variants
1059
+ if (['wosweeper', 'wosweepermini', 'wosweeperminipro', 'k10+', 'k10+ pro'].includes(raw)) {
1060
+ return 'vacuum'
789
1061
  }
790
- return 'OpenAPI'
791
- }
792
1062
 
793
- /**
794
- * Detect whether Matter is enabled/available on the provided Homebridge API object.
795
- * This encapsulates the multi-fallback detection used across the project.
796
- */
797
- /**
798
- * Detect whether Matter is enabled on the provided Homebridge API object.
799
- * Returns an object with an `enabled` boolean and an optional `reason` string
800
- * describing which check matched (useful for diagnostics).
801
- */
802
- export function detectMatter(apiObj: API): { enabled: boolean, reason?: string } {
803
- try {
804
- const maybe = (apiObj as any).isMatterEnabled
805
- if (typeof maybe === 'function') {
806
- return { enabled: Boolean(maybe.call(apiObj)), reason: 'api.isMatterEnabled() returned truthy' }
807
- }
808
- if (typeof maybe !== 'undefined') {
809
- return { enabled: Boolean(maybe), reason: 'api.isMatterEnabled property present' }
810
- }
1063
+ // Window covering variants
1064
+ if (['curtain', 'curtain3', 'rollershade', 'roller shade', 'worollershade', 'wo rollershade'].includes(raw)) {
1065
+ return 'curtain'
1066
+ }
811
1067
 
812
- const server = (apiObj as any).server ?? (apiObj as any).homebridgeServer ?? (apiObj as any).homebridge_server
813
- const serverMaybe = server?.isMatterEnabled
814
- if (typeof serverMaybe === 'function') {
815
- return { enabled: Boolean(serverMaybe.call(server)), reason: 'server.isMatterEnabled() returned truthy' }
816
- }
817
- if (typeof server?.isMatterEnabled !== 'undefined') {
818
- return { enabled: Boolean(server.isMatterEnabled), reason: 'server.isMatterEnabled property present' }
819
- }
820
- } catch (e: any) {
821
- return { enabled: false, reason: `error during detection: ${String(e?.message ?? e)}` }
1068
+ // Blind tilt variants (normalized to 'blindtilt' for Matter since it uses tilt-capable cluster)
1069
+ if (['blindtilt', 'blind tilt'].includes(raw)) {
1070
+ return 'blindtilt'
822
1071
  }
823
- return { enabled: false, reason: 'no isMatterEnabled API or server fallback detected' }
824
- }
825
1072
 
826
- /**
827
- * Backwards-compatible boolean wrapper for detectMatter.
828
- */
829
- export function detectMatterEnabled(apiObj: API): boolean {
830
- return detectMatter(apiObj).enabled
831
- }
1073
+ // Plug variants
1074
+ if (['plug mini (jp)', 'plug mini (us)', 'plug mini (eu)'].includes(raw)) {
1075
+ return 'plug'
1076
+ }
832
1077
 
833
- /**
834
- * Create platform logging helpers used by both HAP and Matter platforms.
835
- *
836
- * getPlatformLogging may be either a synchronous string-returning function or an
837
- * async function that resolves to the current platform logging setting. The
838
- * returned helpers mirror the instance methods previously implemented on the
839
- * HAP platform (infoLog, warnLog, errorLog, debugLog, etc.).
840
- */
841
- export function createPlatformLogger(getPlatformLogging: () => string | Promise<string | undefined>, log: Logging) {
842
- const getPL = async () => {
843
- try {
844
- return await getPlatformLogging()
845
- } catch {
846
- return undefined
847
- }
1078
+ // Meter variants
1079
+ if (['meterplus', 'meter plus', 'meter plus (jp)', 'meterpro', 'meter pro', 'meterpro(co2)', 'meter pro (co2)'].includes(raw)) {
1080
+ return 'meter'
848
1081
  }
849
1082
 
850
- const loggingIsDebug = async () => {
851
- const pl = await getPL()
852
- return pl === 'debugMode' || pl === 'debug'
1083
+ // Relay switch variants
1084
+ if (['relay switch 1', 'relay switch 1pm'].includes(raw)) {
1085
+ return 'relay'
853
1086
  }
854
1087
 
855
- const enablingPlatformLogging = async () => {
856
- const pl = await getPL()
857
- return pl === 'debugMode' || pl === 'debug' || pl === 'standard'
1088
+ // Water detector variants
1089
+ if (['water detector', 'waterdetector'].includes(raw)) {
1090
+ return 'waterdetector'
858
1091
  }
859
1092
 
860
- return {
861
- infoLog: async (...args: any[]) => {
862
- if (await enablingPlatformLogging()) {
863
- log.info(String(...args))
864
- }
865
- },
866
- successLog: async (...args: any[]) => {
867
- if (await enablingPlatformLogging()) {
868
- // Some Logging implementations expose `success` — call if present
869
- ;(log as any).success?.(String(...args)) ?? log.info(String(...args))
870
- }
871
- },
872
- debugSuccessLog: async (...args: any[]) => {
873
- if (await enablingPlatformLogging()) {
874
- if (await loggingIsDebug()) {
875
- ;(log as any).success?.('[DEBUG]', String(...args)) ?? log.info('[DEBUG]', String(...args))
876
- }
877
- }
878
- },
879
- warnLog: async (...args: any[]) => {
880
- if (await enablingPlatformLogging()) {
881
- log.warn(String(...args))
882
- }
883
- },
884
- debugWarnLog: async (...args: any[]) => {
885
- if (await enablingPlatformLogging()) {
886
- if (await loggingIsDebug()) {
887
- log.warn('[DEBUG]', String(...args))
888
- }
889
- }
890
- },
891
- errorLog: async (...args: any[]) => {
892
- if (await enablingPlatformLogging()) {
893
- log.error(String(...args))
894
- }
895
- },
896
- debugErrorLog: async (...args: any[]) => {
897
- if (await enablingPlatformLogging()) {
898
- if (await loggingIsDebug()) {
899
- log.error('[DEBUG]', String(...args))
900
- }
901
- }
902
- },
903
- debugLog: async (...args: any[]) => {
904
- if (await enablingPlatformLogging()) {
905
- const pl = await getPL()
906
- if (pl === 'debug') {
907
- log.info('[DEBUG]', String(...args))
908
- } else if (pl === 'debugMode') {
909
- log.debug(String(...args))
910
- }
911
- }
912
- },
913
- loggingIsDebug,
914
- enablingPlatformLogging,
1093
+ // Fan variants
1094
+ if (['smart fan', 'circulator fan', 'battery circulator fan', 'standing circulator fan'].includes(raw)) {
1095
+ return 'fan'
1096
+ }
1097
+
1098
+ // Light variants
1099
+ if (['strip light', 'strip light 3', 'rgbic neon rope light', 'rgbic neon wire rope light', 'rgbicww floor lamp', 'rgbicww strip light'].includes(raw)) {
1100
+ return 'lightstrip'
1101
+ }
1102
+ if (['color bulb', 'ceiling light', 'ceiling light pro', 'candle warmer lamp', 'floor lamp'].includes(raw)) {
1103
+ return 'light'
1104
+ }
1105
+
1106
+ // Sensor variants
1107
+ if (raw === 'motion sensor') {
1108
+ return 'motion'
1109
+ }
1110
+ if (['contact sensor', 'presence sensor'].includes(raw)) {
1111
+ return 'contact'
1112
+ }
1113
+
1114
+ // Lock variants
1115
+ if (['smart lock', 'smart lock pro', 'smart lock ultra', 'lock lite', 'keypad', 'keypad touch', 'keypad vision', 'keypad vision pro', 'lock vision pro'].includes(raw)) {
1116
+ return 'lock'
1117
+ }
1118
+
1119
+ // Climate variant
1120
+ if (raw === 'humidifier2') {
1121
+ return 'humidifier'
915
1122
  }
1123
+
1124
+ return raw
916
1125
  }
917
1126
 
918
1127
  /**
919
- * Create a Platform proxy class that selects between two platform constructors
920
- * (HAP vs Matter) at runtime using `detectMatter`. Returns a class suitable
921
- * for passing to `api.registerPlatform`.
1128
+ * Normalizes a Homebridge PlatformConfig object to a SwitchBotPluginConfig.
1129
+ *
1130
+ * @param raw The raw Homebridge platform config object.
1131
+ * @returns The normalized plugin config object.
922
1132
  */
923
- export function createPlatformProxy(HAPCtor: any, MatterCtor: any) {
924
- return class PlatformProxy {
925
- delegate: any
926
-
927
- constructor(public readonly log: any, public readonly config: any, public readonly api: API) {
928
- const matterInfo = detectMatter(this.api)
929
- const isMatter = matterInfo.enabled
930
- const reason = matterInfo.reason ? ` Reason: ${matterInfo.reason}` : ''
931
- this.log.info?.(`Homebridge SwitchBot Plugin initializing in ${isMatter ? 'Matter' : 'HAP'} mode.${reason}`)
932
- const PlatformCtor = isMatter ? MatterCtor : HAPCtor
933
- this.delegate = new PlatformCtor(this.log, this.config, this.api)
934
- }
935
-
936
- configureAccessory(accessory: any): void {
937
- try {
938
- if (this.delegate && typeof this.delegate.configureAccessory === 'function') {
939
- return this.delegate.configureAccessory(accessory)
940
- }
941
- } catch (e) {
942
- // swallow — preserve previous behaviour where delegate errors don't bubble here
943
- }
944
- }
1133
+ export function normalizeConfig(raw?: PlatformConfig): SwitchBotPluginConfig {
1134
+ if (!raw) {
1135
+ return {}
1136
+ }
1137
+ return { ...(raw as any) } as SwitchBotPluginConfig
1138
+ }
945
1139
 
946
- configureMatterAccessory?(accessory: any): void {
947
- try {
948
- if (this.delegate && typeof this.delegate.configureMatterAccessory === 'function') {
949
- return this.delegate.configureMatterAccessory(accessory)
950
- }
951
- } catch (e) {
952
- // swallow delegate may not implement this or may throw
953
- }
954
- }
1140
+ // Create a Proxy constructor that instantiates the right platform implementation at runtime.
1141
+ /**
1142
+ * Creates a proxy class that instantiates the correct platform implementation (HAP or Matter) at runtime.
1143
+ *
1144
+ * @param HAPPlatform The HAP platform class constructor.
1145
+ * @param MatterPlatform The Matter platform class constructor.
1146
+ * @returns A proxy class that delegates to the correct platform implementation.
1147
+ *
1148
+ * @class SwitchBotPlatformProxy
1149
+ * @property impl The instantiated platform implementation (HAP or Matter).
1150
+ */
1151
+ export function createPlatformProxy(HAPPlatform: any, MatterPlatform: any): any {
1152
+ return class SwitchBotPlatformProxy {
1153
+ /** The instantiated platform implementation (HAP or Matter) */
1154
+ private impl: any
1155
+ /**
1156
+ * Constructs the proxy and instantiates the correct platform implementation.
1157
+ * @param log Logger instance
1158
+ * @param config Platform config
1159
+ * @param api Homebridge API instance
1160
+ * @returns The instantiated platform implementation
1161
+ */
1162
+ constructor(log: any, config: PlatformConfig, api: any) {
1163
+ const cfg = normalizeConfig(config)
1164
+ const preferMatter = cfg.preferMatter ?? true
1165
+ const enableMatter = cfg.enableMatter ?? true
1166
+ const matterAvailable = !!(api?.isMatterAvailable?.() && api?.isMatterEnabled?.())
955
1167
 
956
- get accessories(): any {
957
- try {
958
- return this.delegate?.accessories
959
- } catch (e) {
960
- return undefined
1168
+ if (enableMatter && preferMatter && MatterPlatform && matterAvailable) {
1169
+ this.impl = new MatterPlatform(log, cfg, api)
1170
+ return this.impl
961
1171
  }
962
- }
963
1172
 
964
- get matterAccessories(): any {
965
- try {
966
- return this.delegate?.matterAccessories
967
- } catch (e) {
968
- return undefined
969
- }
1173
+ // Fallback to HAP
1174
+ this.impl = new HAPPlatform(log, cfg, api)
1175
+ return this.impl
970
1176
  }
971
1177
  }
972
1178
  }