@switchbot/homebridge-switchbot 5.0.0-beta.99 → 5.0.0

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 (282) hide show
  1. package/.changeset/config.json +14 -0
  2. package/.github/copilot-instructions.md +39 -0
  3. package/.github/workflows/ci.yml +4 -1
  4. package/.github/workflows/manual-e2e.yml +6 -3
  5. package/.github/workflows/release.yml +64 -15
  6. package/.github/workflows/stale.yml +2 -4
  7. package/.husky/pre-push +15 -0
  8. package/CHANGELOG.md +126 -134
  9. package/MIGRATION.md +16 -6
  10. package/README.md +84 -3
  11. package/TODO.md +263 -0
  12. package/config.schema.json +229 -36
  13. package/dist/SwitchBotHAPPlatform.d.ts +133 -0
  14. package/dist/SwitchBotHAPPlatform.d.ts.map +1 -0
  15. package/dist/SwitchBotHAPPlatform.js +555 -0
  16. package/dist/SwitchBotHAPPlatform.js.map +1 -0
  17. package/dist/SwitchBotMatterPlatform.d.ts +141 -0
  18. package/dist/SwitchBotMatterPlatform.d.ts.map +1 -0
  19. package/dist/SwitchBotMatterPlatform.js +536 -0
  20. package/dist/SwitchBotMatterPlatform.js.map +1 -0
  21. package/dist/device-types.d.ts +31 -0
  22. package/dist/device-types.d.ts.map +1 -0
  23. package/dist/device-types.js +246 -0
  24. package/dist/device-types.js.map +1 -0
  25. package/dist/deviceCommandMapper.d.ts +10 -0
  26. package/dist/deviceCommandMapper.d.ts.map +1 -0
  27. package/dist/deviceCommandMapper.js +319 -0
  28. package/dist/deviceCommandMapper.js.map +1 -0
  29. package/dist/deviceFactory.d.ts +3 -2
  30. package/dist/deviceFactory.d.ts.map +1 -1
  31. package/dist/deviceFactory.js +107 -29
  32. package/dist/deviceFactory.js.map +1 -1
  33. package/dist/devices/genericDevice.d.ts +59 -37
  34. package/dist/devices/genericDevice.d.ts.map +1 -1
  35. package/dist/devices/genericDevice.js +376 -78
  36. package/dist/devices/genericDevice.js.map +1 -1
  37. package/dist/errors.d.ts +38 -0
  38. package/dist/errors.d.ts.map +1 -0
  39. package/dist/errors.js +32 -0
  40. package/dist/errors.js.map +1 -0
  41. package/dist/homebridge-ui/device-types.js +246 -0
  42. package/dist/homebridge-ui/device-types.js.map +1 -0
  43. package/dist/homebridge-ui/deviceCommandMapper.js +319 -0
  44. package/dist/homebridge-ui/deviceCommandMapper.js.map +1 -0
  45. package/dist/homebridge-ui/endpoints/config.d.ts +3 -0
  46. package/dist/homebridge-ui/endpoints/config.d.ts.map +1 -0
  47. package/dist/homebridge-ui/endpoints/config.js +90 -0
  48. package/dist/homebridge-ui/endpoints/config.js.map +1 -0
  49. package/dist/homebridge-ui/endpoints/devices.d.ts +6 -0
  50. package/dist/homebridge-ui/endpoints/devices.d.ts.map +1 -0
  51. package/dist/homebridge-ui/endpoints/devices.js +144 -0
  52. package/dist/homebridge-ui/endpoints/devices.js.map +1 -0
  53. package/dist/homebridge-ui/endpoints/discovery.d.ts +7 -0
  54. package/dist/homebridge-ui/endpoints/discovery.d.ts.map +1 -0
  55. package/dist/homebridge-ui/endpoints/discovery.js +219 -0
  56. package/dist/homebridge-ui/endpoints/discovery.js.map +1 -0
  57. package/dist/homebridge-ui/errors.js +32 -0
  58. package/dist/homebridge-ui/errors.js.map +1 -0
  59. package/dist/homebridge-ui/homebridge-ui/endpoints/config.js +90 -0
  60. package/dist/homebridge-ui/homebridge-ui/endpoints/config.js.map +1 -0
  61. package/dist/homebridge-ui/homebridge-ui/endpoints/devices.js +144 -0
  62. package/dist/homebridge-ui/homebridge-ui/endpoints/devices.js.map +1 -0
  63. package/dist/homebridge-ui/homebridge-ui/endpoints/discovery.js +219 -0
  64. package/dist/homebridge-ui/homebridge-ui/endpoints/discovery.js.map +1 -0
  65. package/dist/homebridge-ui/homebridge-ui/server.js +11 -0
  66. package/dist/homebridge-ui/homebridge-ui/server.js.map +1 -0
  67. package/dist/homebridge-ui/homebridge-ui/utils/config-parser.js +108 -0
  68. package/dist/homebridge-ui/homebridge-ui/utils/config-parser.js.map +1 -0
  69. package/dist/homebridge-ui/homebridge-ui/utils/device-migration.js +111 -0
  70. package/dist/homebridge-ui/homebridge-ui/utils/device-migration.js.map +1 -0
  71. package/dist/homebridge-ui/homebridge-ui/utils/logger.js +17 -0
  72. package/dist/homebridge-ui/homebridge-ui/utils/logger.js.map +1 -0
  73. package/dist/homebridge-ui/public/css/styles.css +483 -0
  74. package/dist/homebridge-ui/public/index.html +197 -621
  75. package/dist/homebridge-ui/public/js/advanced-settings.d.ts +3 -0
  76. package/dist/homebridge-ui/public/js/advanced-settings.d.ts.map +1 -0
  77. package/dist/homebridge-ui/public/js/advanced-settings.js +95 -0
  78. package/dist/homebridge-ui/public/js/advanced-settings.js.map +1 -0
  79. package/dist/homebridge-ui/public/js/advanced-settings.ts +94 -0
  80. package/dist/homebridge-ui/public/js/api.d.ts +66 -0
  81. package/dist/homebridge-ui/public/js/api.d.ts.map +1 -0
  82. package/dist/homebridge-ui/public/js/api.js +295 -0
  83. package/dist/homebridge-ui/public/js/api.js.map +1 -0
  84. package/dist/homebridge-ui/public/js/api.ts +355 -0
  85. package/dist/homebridge-ui/public/js/app.d.ts +2 -0
  86. package/dist/homebridge-ui/public/js/app.d.ts.map +1 -0
  87. package/dist/homebridge-ui/public/js/app.js +3722 -0
  88. package/dist/homebridge-ui/public/js/app.js.map +7 -0
  89. package/dist/homebridge-ui/public/js/app.ts +22 -0
  90. package/dist/homebridge-ui/public/js/constants.d.ts +2 -0
  91. package/dist/homebridge-ui/public/js/constants.d.ts.map +1 -0
  92. package/dist/homebridge-ui/public/js/constants.js +2 -0
  93. package/dist/homebridge-ui/public/js/constants.js.map +1 -0
  94. package/dist/homebridge-ui/public/js/constants.ts +1 -0
  95. package/dist/homebridge-ui/public/js/credentials.d.ts +3 -0
  96. package/dist/homebridge-ui/public/js/credentials.d.ts.map +1 -0
  97. package/dist/homebridge-ui/public/js/credentials.js +99 -0
  98. package/dist/homebridge-ui/public/js/credentials.js.map +1 -0
  99. package/dist/homebridge-ui/public/js/credentials.ts +105 -0
  100. package/dist/homebridge-ui/public/js/devices-delete.d.ts +3 -0
  101. package/dist/homebridge-ui/public/js/devices-delete.d.ts.map +1 -0
  102. package/dist/homebridge-ui/public/js/devices-delete.js +199 -0
  103. package/dist/homebridge-ui/public/js/devices-delete.js.map +1 -0
  104. package/dist/homebridge-ui/public/js/devices-delete.ts +227 -0
  105. package/dist/homebridge-ui/public/js/devices.d.ts +9 -0
  106. package/dist/homebridge-ui/public/js/devices.d.ts.map +1 -0
  107. package/dist/homebridge-ui/public/js/devices.js +98 -0
  108. package/dist/homebridge-ui/public/js/devices.js.map +1 -0
  109. package/dist/homebridge-ui/public/js/devices.ts +106 -0
  110. package/dist/homebridge-ui/public/js/discovery.d.ts +9 -0
  111. package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -0
  112. package/dist/homebridge-ui/public/js/discovery.js +1201 -0
  113. package/dist/homebridge-ui/public/js/discovery.js.map +1 -0
  114. package/dist/homebridge-ui/public/js/discovery.ts +1335 -0
  115. package/dist/homebridge-ui/public/js/logger.d.ts +7 -0
  116. package/dist/homebridge-ui/public/js/logger.d.ts.map +1 -0
  117. package/dist/homebridge-ui/public/js/logger.js +17 -0
  118. package/dist/homebridge-ui/public/js/logger.js.map +1 -0
  119. package/dist/homebridge-ui/public/js/logger.ts +17 -0
  120. package/dist/homebridge-ui/public/js/modal.d.ts +5 -0
  121. package/dist/homebridge-ui/public/js/modal.d.ts.map +1 -0
  122. package/dist/homebridge-ui/public/js/modal.js +35 -0
  123. package/dist/homebridge-ui/public/js/modal.js.map +1 -0
  124. package/dist/homebridge-ui/public/js/modal.ts +35 -0
  125. package/dist/homebridge-ui/public/js/modals.d.ts +15 -0
  126. package/dist/homebridge-ui/public/js/modals.d.ts.map +1 -0
  127. package/dist/homebridge-ui/public/js/modals.js +675 -0
  128. package/dist/homebridge-ui/public/js/modals.js.map +1 -0
  129. package/dist/homebridge-ui/public/js/modals.ts +765 -0
  130. package/dist/homebridge-ui/public/js/render.d.ts +71 -0
  131. package/dist/homebridge-ui/public/js/render.d.ts.map +1 -0
  132. package/dist/homebridge-ui/public/js/render.js +960 -0
  133. package/dist/homebridge-ui/public/js/render.js.map +1 -0
  134. package/dist/homebridge-ui/public/js/render.ts +1084 -0
  135. package/dist/homebridge-ui/public/js/toast.d.ts +6 -0
  136. package/dist/homebridge-ui/public/js/toast.d.ts.map +1 -0
  137. package/dist/homebridge-ui/public/js/toast.js +38 -0
  138. package/dist/homebridge-ui/public/js/toast.js.map +1 -0
  139. package/dist/homebridge-ui/public/js/toast.ts +44 -0
  140. package/dist/homebridge-ui/public/js/types.d.ts +23 -0
  141. package/dist/homebridge-ui/public/js/types.d.ts.map +1 -0
  142. package/dist/homebridge-ui/public/js/types.js +2 -0
  143. package/dist/homebridge-ui/public/js/types.js.map +1 -0
  144. package/dist/homebridge-ui/public/js/types.ts +26 -0
  145. package/dist/homebridge-ui/server.d.ts +1 -3
  146. package/dist/homebridge-ui/server.d.ts.map +1 -1
  147. package/dist/homebridge-ui/server.js +8 -471
  148. package/dist/homebridge-ui/server.js.map +1 -1
  149. package/dist/homebridge-ui/settings.js +8 -0
  150. package/dist/homebridge-ui/settings.js.map +1 -0
  151. package/dist/homebridge-ui/switchbotClient.js +247 -0
  152. package/dist/homebridge-ui/switchbotClient.js.map +1 -0
  153. package/dist/homebridge-ui/utils/config-parser.d.ts +39 -0
  154. package/dist/homebridge-ui/utils/config-parser.d.ts.map +1 -0
  155. package/dist/homebridge-ui/utils/config-parser.js +108 -0
  156. package/dist/homebridge-ui/utils/config-parser.js.map +1 -0
  157. package/dist/homebridge-ui/utils/device-migration.d.ts +35 -0
  158. package/dist/homebridge-ui/utils/device-migration.d.ts.map +1 -0
  159. package/dist/homebridge-ui/utils/device-migration.js +111 -0
  160. package/dist/homebridge-ui/utils/device-migration.js.map +1 -0
  161. package/dist/homebridge-ui/utils/logger.d.ts +7 -0
  162. package/dist/homebridge-ui/utils/logger.d.ts.map +1 -0
  163. package/dist/homebridge-ui/utils/logger.js +17 -0
  164. package/dist/homebridge-ui/utils/logger.js.map +1 -0
  165. package/dist/index.d.ts +10 -0
  166. package/dist/index.d.ts.map +1 -1
  167. package/dist/index.js +12 -2
  168. package/dist/index.js.map +1 -1
  169. package/dist/settings.d.ts +1 -0
  170. package/dist/settings.d.ts.map +1 -1
  171. package/dist/settings.js +1 -0
  172. package/dist/settings.js.map +1 -1
  173. package/dist/switchbotClient.d.ts +12 -10
  174. package/dist/switchbotClient.d.ts.map +1 -1
  175. package/dist/switchbotClient.js +156 -103
  176. package/dist/switchbotClient.js.map +1 -1
  177. package/dist/utils.d.ts +76 -1
  178. package/dist/utils.d.ts.map +1 -1
  179. package/dist/utils.js +1121 -4
  180. package/dist/utils.js.map +1 -1
  181. package/docs/assets/highlight.css +16 -2
  182. package/docs/assets/main.js +1 -1
  183. package/docs/index.html +82 -5
  184. package/docs/variables/default.html +3 -1
  185. package/eslint.config.js +9 -5
  186. package/nodemon.json +2 -2
  187. package/package.json +34 -21
  188. package/scripts/build-ui.js +37 -0
  189. package/scripts/free-dev-ports.mjs +105 -0
  190. package/scripts/generate-matter-maps.js +34 -17
  191. package/scripts/sync-device-types.mjs +31 -0
  192. package/src/SwitchBotHAPPlatform.ts +558 -0
  193. package/src/SwitchBotMatterPlatform.ts +538 -0
  194. package/src/device-types.js +246 -0
  195. package/src/device-types.js.map +1 -0
  196. package/src/device-types.ts +261 -0
  197. package/src/deviceCommandMapper.js +319 -0
  198. package/src/deviceCommandMapper.js.map +1 -0
  199. package/src/deviceCommandMapper.ts +333 -0
  200. package/src/deviceFactory.ts +125 -45
  201. package/src/devices/genericDevice.ts +411 -69
  202. package/src/errors.js +32 -0
  203. package/src/errors.js.map +1 -0
  204. package/src/errors.ts +35 -0
  205. package/src/homebridge-ui/endpoints/config.ts +110 -0
  206. package/src/homebridge-ui/endpoints/devices.ts +153 -0
  207. package/src/homebridge-ui/endpoints/discovery.ts +240 -0
  208. package/src/homebridge-ui/public/css/styles.css +483 -0
  209. package/src/homebridge-ui/public/index.html +197 -621
  210. package/src/homebridge-ui/public/js/advanced-settings.ts +94 -0
  211. package/src/homebridge-ui/public/js/api.ts +355 -0
  212. package/src/homebridge-ui/public/js/app.ts +22 -0
  213. package/src/homebridge-ui/public/js/constants.ts +1 -0
  214. package/src/homebridge-ui/public/js/credentials.ts +105 -0
  215. package/src/homebridge-ui/public/js/devices-delete.ts +227 -0
  216. package/src/homebridge-ui/public/js/devices.ts +106 -0
  217. package/src/homebridge-ui/public/js/discovery.ts +1335 -0
  218. package/src/homebridge-ui/public/js/logger.ts +17 -0
  219. package/src/homebridge-ui/public/js/modal.ts +35 -0
  220. package/src/homebridge-ui/public/js/modals.ts +765 -0
  221. package/src/homebridge-ui/public/js/render.ts +1084 -0
  222. package/src/homebridge-ui/public/js/toast.ts +44 -0
  223. package/src/homebridge-ui/public/js/types.ts +26 -0
  224. package/src/homebridge-ui/server.ts +9 -554
  225. package/src/homebridge-ui/utils/config-parser.ts +125 -0
  226. package/src/homebridge-ui/utils/device-migration.ts +144 -0
  227. package/src/homebridge-ui/utils/logger.ts +17 -0
  228. package/src/index.ts +12 -2
  229. package/src/settings.js +8 -0
  230. package/src/settings.js.map +1 -0
  231. package/src/settings.ts +2 -0
  232. package/src/switchbotClient.js +247 -0
  233. package/src/switchbotClient.js.map +1 -0
  234. package/src/switchbotClient.ts +177 -114
  235. package/src/utils.ts +1133 -5
  236. package/test/client/switchbot-client-debounce.spec.ts +35 -0
  237. package/test/client/switchbot-client-openapi.spec.ts +19 -0
  238. package/test/client/switchbotClient.spec.ts +64 -0
  239. package/test/device/device-mapping.spec.ts +23 -0
  240. package/test/device/deviceBase.spec.ts +26 -0
  241. package/test/device/deviceFactory-edge.spec.ts +15 -0
  242. package/test/device/deviceFactory.spec.ts +33 -0
  243. package/test/device/fan-swing.spec.ts +34 -0
  244. package/test/device/genericDevice-blepoll.spec.ts +47 -0
  245. package/test/device/irdevice.spec.ts +9 -0
  246. package/test/device/lock-users.spec.ts +35 -0
  247. package/test/device/matter-descriptors.spec.ts +22 -0
  248. package/test/device/matter-device-state.spec.ts +37 -0
  249. package/test/e2e/run-e2e.spec.ts +18 -19
  250. package/test/errors/errors.spec.ts +10 -0
  251. package/test/helpers/matter-harness.ts +20 -9
  252. package/test/homebridge-ui/server.spec.ts +9 -0
  253. package/test/platform/accessory-restore.spec.ts +37 -0
  254. package/test/platform/matter-childbridge.spec.ts +34 -0
  255. package/test/platform/matter-integration.spec.ts +33 -0
  256. package/test/platform/platform-edge.spec.ts +73 -0
  257. package/test/platform/platform.integration.spec.ts +34 -0
  258. package/test/utils/utils-extra.spec.ts +10 -0
  259. package/test/utils/utils.spec.ts +53 -0
  260. package/todo/TODO.md +80 -0
  261. package/tsconfig.ui.json +11 -0
  262. package/.github/npm-version-script-esm.js +0 -97
  263. package/.github/workflows/beta-release.yml +0 -52
  264. package/dist/platform.d.ts +0 -35
  265. package/dist/platform.d.ts.map +0 -1
  266. package/dist/platform.js +0 -945
  267. package/dist/platform.js.map +0 -1
  268. package/src/platform.ts +0 -963
  269. package/test/accessory-restore.spec.ts +0 -73
  270. package/test/device-mapping.spec.ts +0 -37
  271. package/test/deviceFactory.spec.ts +0 -18
  272. package/test/fan-swing.spec.ts +0 -29
  273. package/test/lock-users.spec.ts +0 -44
  274. package/test/matter-childbridge.spec.ts +0 -55
  275. package/test/matter-descriptors.spec.ts +0 -97
  276. package/test/matter-device-state.spec.ts +0 -101
  277. package/test/matter-integration.spec.ts +0 -70
  278. package/test/platform.integration.spec.ts +0 -55
  279. package/test/switchbot-client-debounce.spec.ts +0 -131
  280. package/test/switchbot-client-openapi.spec.ts +0 -56
  281. package/test/switchbotClient.spec.ts +0 -10
  282. package/test/utils.spec.ts +0 -20
package/src/platform.ts DELETED
@@ -1,963 +0,0 @@
1
- import type { SwitchBotPluginConfig } from './settings.js'
2
- import type { API, Logger, PlatformConfig } from 'homebridge'
3
-
4
- import { createDevice } from './deviceFactory.js'
5
- import { PLATFORM_NAME, PLUGIN_NAME } from './settings.js'
6
- import { SwitchBotClient } from './switchbotClient.js'
7
-
8
- // Which device types should prefer Matter if available
9
- // Based on HAP service mappings: device implementations use specific HomeKit services
10
- // that map to corresponding Matter clusters when Matter is enabled
11
- const DEVICE_MATTER_SUPPORTED: Record<string, boolean> = {
12
- // Core devices
13
- 'bot': true, // Switch → OnOff
14
- 'curtain': true, // WindowCovering → WindowCovering
15
- 'fan': true, // Fan → FanControl
16
- 'light': true, // Lightbulb → OnOff + LevelControl
17
- 'lightstrip': true, // Lightbulb (color) → OnOff + LevelControl + ColorControl
18
- 'motion': true, // MotionSensor → OccupancySensing
19
- 'contact': true, // ContactSensor → BooleanState
20
- 'vacuum': true, // Switch → RobotVacuumCleaner
21
- 'lock': true, // LockMechanism → DoorLock
22
- 'humidifier': true, // Fan + Humidity → OnOff + FanControl + RelativeHumidityMeasurement
23
- 'temperature': true, // TemperatureSensor → TemperatureMeasurement
24
-
25
- // Switch devices
26
- 'relay': true, // Switch → OnOff
27
- 'relay switch 1': true, // Switch → OnOff
28
- 'relay switch 1pm': true, // Switch → OnOff
29
- 'plug': true, // Outlet → OnOff
30
- 'plug mini (jp)': true, // Outlet → OnOff
31
- 'plug mini (us)': true, // Outlet → OnOff
32
-
33
- // Window covering variants
34
- 'blindtilt': true, // WindowCovering → WindowCovering
35
- 'blind tilt': true, // WindowCovering → WindowCovering
36
- 'curtain3': true, // WindowCovering → WindowCovering
37
- 'rollershade': true, // WindowCovering → WindowCovering
38
- 'roller shade': true, // WindowCovering → WindowCovering
39
- 'worollershade': true, // WindowCovering → WindowCovering
40
- 'wo rollershade': true, // WindowCovering → WindowCovering
41
-
42
- // Vacuum variants (normalized to 'vacuum' before lookup)
43
- 'wosweeper': true, // VacuumDevice → RobotVacuumCleaner
44
- 'wosweepermini': true, // VacuumDevice → RobotVacuumCleaner
45
- 'wosweeperminipro': true, // VacuumDevice → RobotVacuumCleaner
46
- 'k10+': true, // VacuumDevice → RobotVacuumCleaner
47
- 'k10+ pro': true, // VacuumDevice → RobotVacuumCleaner
48
-
49
- // Sensors
50
- 'meter': true, // TemperatureSensor + HumiditySensor → TemperatureMeasurement + RelativeHumidityMeasurement
51
- 'meterplus': true, // TemperatureSensor + HumiditySensor → TemperatureMeasurement + RelativeHumidityMeasurement
52
- 'meter plus (jp)': true, // TemperatureSensor + HumiditySensor → TemperatureMeasurement + RelativeHumidityMeasurement
53
- 'meterpro': true, // TemperatureSensor + HumiditySensor → TemperatureMeasurement + RelativeHumidityMeasurement
54
- 'meterpro(co2)': true, // TemperatureSensor + HumiditySensor → TemperatureMeasurement + RelativeHumidityMeasurement
55
- 'waterdetector': true, // LeakSensor → BooleanState
56
- 'water detector': true, // LeakSensor → BooleanState
57
-
58
- // Other devices
59
- 'smart fan': true, // Fan → FanControl
60
- 'strip light': true, // Lightbulb (color) → OnOff + LevelControl + ColorControl
61
- 'hub 2': false, // Hub device - not exposed as accessory
62
- 'walletfinder': false, // Button device - Matter support TBD
63
- }
64
-
65
- // Default Matter cluster configurations by device type
66
- // Maps device types to their Matter cluster states (used when device doesn't provide clusters)
67
- // Note: wosweeper/curtain/plug variants are normalized before cluster lookup (see loadDevices)
68
- const DEVICE_MATTER_CLUSTERS: Record<string, any> = {
69
- // Core devices - aligned with HAP service implementations
70
- bot: { onOff: { onOff: false } }, // Switch → OnOff
71
- vacuum: {
72
- rvcRunMode: {
73
- supportedModes: [
74
- { label: 'Idle', mode: 0, modeTags: [{ value: 16384 }] },
75
- { label: 'Cleaning', mode: 1, modeTags: [{ value: 16385 }] },
76
- ],
77
- currentMode: 0,
78
- },
79
- rvcCleanMode: {
80
- supportedModes: [
81
- { label: 'Vacuum', mode: 0, modeTags: [{ value: 16385 }] },
82
- ],
83
- currentMode: 0,
84
- },
85
- rvcOperationalState: {
86
- operationalStateList: [
87
- { operationalStateId: 0 }, // Stopped
88
- { operationalStateId: 1 }, // Running
89
- { operationalStateId: 2 }, // Paused
90
- { operationalStateId: 3 }, // Error (required)
91
- { operationalStateId: 64 }, // Seeking charger
92
- { operationalStateId: 65 }, // Charging
93
- { operationalStateId: 66 }, // Docked
94
- ],
95
- operationalState: 66,
96
- },
97
- }, // Switch in HAP, RobotVacuumCleaner in Matter
98
- curtain: {
99
- windowCovering: {
100
- currentPositionLiftPercent100ths: 0,
101
- targetPositionLiftPercent100ths: 0,
102
- operationalStatus: 0,
103
- },
104
- }, // WindowCovering → WindowCovering (includes curtain3, rollershade variants via normalization)
105
- blindtilt: {
106
- windowCovering: {
107
- currentPositionLiftPercent100ths: 0,
108
- targetPositionLiftPercent100ths: 0,
109
- currentPositionTiltPercent100ths: 0,
110
- targetPositionTiltPercent100ths: 0,
111
- operationalStatus: 0,
112
- },
113
- }, // WindowCovering with tilt → WindowCovering
114
- fan: {
115
- onOff: { onOff: false },
116
- fanControl: {
117
- fanMode: 0,
118
- percentCurrent: 0,
119
- percentSetting: 0,
120
- speedCurrent: 0,
121
- speedMax: 100,
122
- },
123
- }, // Fan → OnOff + FanControl
124
- light: {
125
- onOff: { onOff: false },
126
- levelControl: {
127
- currentLevel: 0,
128
- minLevel: 0,
129
- maxLevel: 254,
130
- },
131
- }, // Lightbulb → OnOff + LevelControl
132
- lightstrip: {
133
- onOff: { onOff: false },
134
- levelControl: {
135
- currentLevel: 0,
136
- minLevel: 0,
137
- maxLevel: 254,
138
- },
139
- colorControl: {
140
- currentHue: 0,
141
- currentSaturation: 0,
142
- colorTemperatureMireds: 400,
143
- colorMode: 0,
144
- },
145
- }, // Lightbulb with color → OnOff + LevelControl + ColorControl
146
- lock: {
147
- doorLock: {
148
- lockState: 0,
149
- lockType: 0,
150
- actuatorEnabled: true,
151
- operatingMode: 0,
152
- },
153
- }, // LockMechanism → DoorLock
154
- motion: {
155
- occupancySensing: {
156
- occupancy: 0,
157
- occupancySensorType: 0,
158
- },
159
- }, // MotionSensor → OccupancySensing
160
- contact: {
161
- booleanState: {
162
- stateValue: false,
163
- },
164
- }, // ContactSensor → BooleanState
165
- humidifier: {
166
- onOff: { onOff: false },
167
- fanControl: {
168
- fanMode: 0,
169
- percentCurrent: 0,
170
- },
171
- relativeHumidityMeasurement: {
172
- measuredValue: 0,
173
- minMeasuredValue: 0,
174
- maxMeasuredValue: 100,
175
- },
176
- }, // HumidifierDehumidifier → OnOff + FanControl + RelativeHumidityMeasurement
177
- temperature: {
178
- temperatureMeasurement: {
179
- measuredValue: 0,
180
- minMeasuredValue: -27315,
181
- maxMeasuredValue: 32767,
182
- },
183
- }, // TemperatureSensor → TemperatureMeasurement
184
-
185
- // Switch/Outlet devices
186
- relay: { onOff: { onOff: false } }, // Switch → OnOff
187
- plug: {
188
- onOff: { onOff: false },
189
- electricalMeasurement: {
190
- activePower: 0,
191
- rmsCurrent: 0,
192
- rmsVoltage: 0,
193
- },
194
- }, // Outlet → OnOff + ElectricalMeasurement (for PM models)
195
-
196
- // Sensors
197
- meter: {
198
- temperatureMeasurement: {
199
- measuredValue: 0,
200
- minMeasuredValue: -27315,
201
- maxMeasuredValue: 32767,
202
- },
203
- relativeHumidityMeasurement: {
204
- measuredValue: 0,
205
- minMeasuredValue: 0,
206
- maxMeasuredValue: 100,
207
- },
208
- }, // TemperatureSensor + HumiditySensor → TemperatureMeasurement + RelativeHumidityMeasurement
209
- waterdetector: {
210
- booleanState: {
211
- stateValue: false,
212
- },
213
- }, // LeakSensor → BooleanState
214
- }
215
-
216
- const DEVICE_MATTER_DEVICE_TYPE_KEYS: Record<string, string> = {
217
- bot: 'OnOffSwitch',
218
- vacuum: 'RoboticVacuumCleaner',
219
- curtain: 'WindowCovering',
220
- blindtilt: 'WindowCovering',
221
- fan: 'Fan',
222
- light: 'DimmableLight',
223
- lightstrip: 'ExtendedColorLight',
224
- lock: 'DoorLock',
225
- motion: 'MotionSensor',
226
- contact: 'ContactSensor',
227
- humidifier: 'Fan',
228
- temperature: 'TemperatureSensor',
229
- relay: 'OnOffSwitch',
230
- plug: 'OnOffOutlet',
231
- meter: 'TemperatureSensor',
232
- waterdetector: 'LeakSensor',
233
- }
234
-
235
- // Default Matter cluster handlers by device type
236
- // Provides handler stubs for devices with interactive clusters
237
- const DEVICE_MATTER_HANDLERS: Record<string, any> = {
238
- vacuum: {
239
- rvcRunMode: {
240
- // Handle changing vacuum run mode (Idle, Cleaning, Mapping)
241
- changeToMode: async (request: any) => {
242
- const modeNames = ['Idle', 'Cleaning', 'Mapping']
243
- const modeName = modeNames[request?.newMode] || `Unknown (${request?.newMode})`
244
- console.log('[SwitchBot] RVC run mode change requested:', modeName)
245
- // TODO: Send API command to SwitchBot device to change run mode
246
- // await this.client.setDeviceState(deviceId, { command: 'setMode', parameter: modeName })
247
- },
248
- },
249
- rvcCleanMode: {
250
- // Handle changing vacuum clean mode (Vacuum, Mop, etc.)
251
- changeToMode: async (request: any) => {
252
- const modeName = request?.newMode !== undefined ? `Mode ${request.newMode}` : 'Unknown'
253
- console.log('[SwitchBot] RVC clean mode change requested:', modeName)
254
- // TODO: Send API command to SwitchBot device to change clean mode
255
- },
256
- },
257
- rvcOperationalState: {
258
- // Handle pause command
259
- pause: async () => {
260
- console.log('[SwitchBot] RVC pause command received')
261
- // TODO: Send pause command to SwitchBot device
262
- },
263
- // Handle resume command
264
- resume: async () => {
265
- console.log('[SwitchBot] RVC resume command received')
266
- // TODO: Send resume command to SwitchBot device
267
- },
268
- // Handle return to dock command
269
- goHome: async () => {
270
- console.log('[SwitchBot] RVC goHome command received')
271
- // TODO: Send return-to-dock command to SwitchBot device
272
- },
273
- },
274
- },
275
- bot: {
276
- onOff: {
277
- // Handle on/off switch commands
278
- on: async () => {
279
- console.log('[SwitchBot] Bot ON command received')
280
- // TODO: Send ON command to SwitchBot device
281
- },
282
- off: async () => {
283
- console.log('[SwitchBot] Bot OFF command received')
284
- // TODO: Send OFF command to SwitchBot device
285
- },
286
- },
287
- },
288
- curtain: {
289
- windowCovering: {
290
- // Handle curtain position changes
291
- goToLiftPercentage: async (request: any) => {
292
- console.log('[SwitchBot] Curtain position change requested:', request?.liftPercent100thsValue)
293
- // TODO: Send position command to SwitchBot device
294
- },
295
- upOrOpen: async () => {
296
- console.log('[SwitchBot] Curtain open command received')
297
- // TODO: Send open command to SwitchBot device
298
- },
299
- downOrClose: async () => {
300
- console.log('[SwitchBot] Curtain close command received')
301
- // TODO: Send close command to SwitchBot device
302
- },
303
- stopMotion: async () => {
304
- console.log('[SwitchBot] Curtain stop command received')
305
- // TODO: Send stop command to SwitchBot device
306
- },
307
- },
308
- },
309
- plug: {
310
- onOff: {
311
- on: async () => {
312
- console.log('[SwitchBot] Plug ON command received')
313
- },
314
- off: async () => {
315
- console.log('[SwitchBot] Plug OFF command received')
316
- },
317
- },
318
- },
319
- lock: {
320
- doorLock: {
321
- setLockState: async (request: any) => {
322
- const state = request?.lockState === 1 ? 'LOCKED' : 'UNLOCKED'
323
- console.log('[SwitchBot] Lock state change requested:', state)
324
- },
325
- },
326
- },
327
- }
328
-
329
- function resolveMatterDeviceType(matterApi: any, type: string, createdDeviceType?: any, clusters?: any): any {
330
- if (createdDeviceType && typeof createdDeviceType === 'object' && typeof createdDeviceType.with === 'function') {
331
- return createdDeviceType
332
- }
333
-
334
- const lowerType = (typeof createdDeviceType === 'string' && createdDeviceType) ? createdDeviceType.toLowerCase() : (type || '').toLowerCase()
335
-
336
- // Cluster-based upgrade for color lights if descriptor omitted device type.
337
- const hasColorControl = !!clusters?.colorControl
338
- const inferredType = hasColorControl && lowerType === 'light'
339
- ? 'lightstrip'
340
- : lowerType
341
-
342
- const mappedKey = DEVICE_MATTER_DEVICE_TYPE_KEYS[inferredType] || 'OnOffSwitch'
343
- return matterApi?.deviceTypes?.[mappedKey] || matterApi?.deviceTypes?.OnOffSwitch
344
- }
345
-
346
- export class SwitchBotHAPPlatform {
347
- api: API | undefined
348
- log: Logger
349
- config: SwitchBotPluginConfig
350
- devices: any[] = []
351
- // cached accessories restored by Homebridge
352
- accessories: Map<string, any>
353
- // Track last loaded config to detect changes
354
- private lastConfigHash: string = ''
355
- private configReloadInterval: NodeJS.Timeout | null = null
356
-
357
- constructor(log: Logger, config: PlatformConfig, api?: API) {
358
- this.log = log
359
- this.config = (config as any) ?? {}
360
- this.api = api
361
- this.accessories = new Map()
362
- this.log.info('SwitchBot HAP platform initialized')
363
-
364
- // Create/shared SwitchBot client and attach to config so child devices reuse it.
365
- try {
366
- const client = new SwitchBotClient(this.config)
367
- void client.init()
368
- ;(this.config as any)._client = client
369
- } catch (e) {
370
- this.log.debug('Failed to create shared SwitchBot client', e)
371
- }
372
-
373
- // Create/shared SwitchBot client and attach to config so child devices reuse it.
374
- try {
375
- const client = new SwitchBotClient(this.config)
376
- void client.init()
377
- ;(this.config as any)._client = client
378
- } catch (e) {
379
- this.log.debug('Failed to create shared SwitchBot client', e)
380
- }
381
-
382
- // Wait for Homebridge to finish launching to create/register accessories
383
- if (this.api && typeof (this.api as any).on === 'function') {
384
- ;(this.api as any).on('didFinishLaunching', () => {
385
- void this.loadDevices()
386
- // Start periodic config reload to pick up UI changes
387
- this.configReloadInterval = setInterval(() => {
388
- void this.checkAndReloadDevices()
389
- }, 10000) // Check every 10 seconds
390
- })
391
- } else {
392
- void this.loadDevices()
393
- // Start periodic config reload to pick up UI changes
394
- this.configReloadInterval = setInterval(() => {
395
- void this.checkAndReloadDevices()
396
- }, 10000) // Check every 10 seconds
397
- }
398
- }
399
-
400
- private getConfigHash(): string {
401
- // Create a simple hash of current device config to detect changes
402
- const devices = (this.config as any)?.devices ?? []
403
- return JSON.stringify(devices.map((d: any) => ({
404
- id: d.deviceId ?? d.id,
405
- type: d.configDeviceType ?? d.type,
406
- name: d.configDeviceName ?? d.name,
407
- })))
408
- }
409
-
410
- private async checkAndReloadDevices() {
411
- const currentHash = this.getConfigHash()
412
- if (currentHash !== this.lastConfigHash) {
413
- this.log.info('[SwitchBot] Detected config changes, reloading devices...')
414
- // Clear existing devices
415
- this.devices = []
416
- await this.loadDevices()
417
- }
418
- }
419
-
420
- async loadDevices() {
421
- const devices = (this.config as any)?.devices ?? []
422
- for (const raw of devices) {
423
- // Normalize config keys from UI schema to internal shape
424
- const d: any = {
425
- id: raw.deviceId ?? raw.id,
426
- name: raw.configDeviceName ?? raw.name,
427
- type: raw.configDeviceType ?? raw.type ?? raw.deviceType ?? 'unknown',
428
- _raw: raw,
429
- }
430
-
431
- let type: string = d.type
432
- // Normalize device type variants for consistent Matter cluster lookup
433
- // Vacuum variants
434
- if (['wosweeper', 'wosweepermini', 'wosweeperminipro', 'k10+', 'k10+ pro'].includes((type || '').toLowerCase())) {
435
- type = 'vacuum'
436
- }
437
- // Window covering variants
438
- if (['blindtilt', 'blind tilt', 'curtain3', 'rollershade', 'roller shade', 'worollershade', 'wo rollershade'].includes((type || '').toLowerCase())) {
439
- type = 'curtain'
440
- }
441
- // Plug variants
442
- if (['plug mini (jp)', 'plug mini (us)'].includes((type || '').toLowerCase())) {
443
- type = 'plug'
444
- }
445
- // Meter variants
446
- if (['meterplus', 'meter plus (jp)', 'meterpro', 'meterpro(co2)'].includes((type || '').toLowerCase())) {
447
- type = 'meter'
448
- }
449
- // Relay switch variants
450
- if (['relay switch 1', 'relay switch 1pm'].includes((type || '').toLowerCase())) {
451
- type = 'relay'
452
- }
453
- // Water detector variants
454
- if (['water detector'].includes((type || '').toLowerCase())) {
455
- type = 'waterdetector'
456
- }
457
- // Fan variants
458
- if (['smart fan'].includes((type || '').toLowerCase())) {
459
- type = 'fan'
460
- }
461
- // Light variants
462
- if (['strip light'].includes((type || '').toLowerCase())) {
463
- type = 'lightstrip'
464
- }
465
-
466
- const matterSupported = !!DEVICE_MATTER_SUPPORTED[(type || '').toLowerCase()]
467
- // Auto-detect Matter from Homebridge API, allow manual override via config
468
- const matterAvailable = !!(this.api?.isMatterAvailable?.() && this.api?.isMatterEnabled?.())
469
- const matterEnabled = matterAvailable || !!this.config.enableMatter
470
- const useMatter = !!(matterEnabled && matterSupported && (!!this.config.preferMatter || matterAvailable))
471
-
472
- try {
473
- const created = await createDevice({ id: d.id, type, name: d.name }, this.config, useMatter)
474
- this.devices.push(created)
475
- // Prefer Matter: try registering to the Matter child bridge first.
476
- let matterRegistered = false
477
- if (useMatter) {
478
- this.log.info(`Attempting Matter registration for ${d.id} (${type})`)
479
- // If Homebridge Matter APIs are available, register the accessory
480
- try {
481
- const matterApi = (this.api as any)?.matter
482
- if (this.api?.isMatterAvailable?.() && this.api?.isMatterEnabled?.() && matterApi && typeof matterApi.registerPlatformAccessories === 'function') {
483
- const createdDesc = created.createAccessory?.(this.api) || { id: d.id, name: d.name || type }
484
- const uuid = matterApi.uuid.generate(`${d.id}`)
485
- const defaultClusters = DEVICE_MATTER_CLUSTERS[type.toLowerCase()] || { onOff: { onOff: false } }
486
- const clusters = createdDesc.clusters || defaultClusters
487
- const deviceType = resolveMatterDeviceType(matterApi, type, createdDesc.deviceType, clusters)
488
- const accessory: any = {
489
- UUID: uuid,
490
- displayName: createdDesc.name || d.name || type,
491
- deviceType,
492
- manufacturer: createdDesc.manufacturer || 'SwitchBot',
493
- model: createdDesc.model || type,
494
- serialNumber: createdDesc.serialNumber || d.id,
495
- reachable: createdDesc.reachable !== false,
496
- firmwareRevision: createdDesc.firmwareRevision || '1.0.0',
497
- hardwareRevision: createdDesc.hardwareRevision || '',
498
- clusters,
499
- context: { deviceId: d.id, type, _created: true },
500
- }
501
- try {
502
- await matterApi.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory])
503
- this.accessories.set(uuid, accessory)
504
- matterRegistered = true
505
- this.log.info(`Registered Matter accessory ${d.id} (${type}) with uuid=${uuid}`)
506
- } catch (e) {
507
- this.log.warn('Failed to register Matter accessory with Homebridge matter API', e)
508
- }
509
- } else {
510
- this.log.info('Homebridge Matter API not available or not enabled; will fallback to HAP')
511
- }
512
- } catch (e) {
513
- this.log.warn('Matter registration attempt failed', e)
514
- }
515
- }
516
-
517
- // If Matter wasn't registered (either not supported, API missing, or registration failed), fall back to HAP registration.
518
- if (!matterRegistered && this.api && (this.api as any).hap) {
519
- // Basic HAP accessory creation using homebridge API when available
520
- try {
521
- const hap = (this.api as any).hap
522
- const uuid = hap.uuid.generate(`${d.id}`)
523
- // Reuse cached accessory if available by uuid
524
- let accessory: any = this.accessories.get(uuid)
525
- // If not found by uuid, attempt to find by stored deviceId in accessory.context
526
- if (!accessory) {
527
- for (const [, a] of Array.from(this.accessories.entries())) {
528
- try {
529
- if (a && a.context && a.context.deviceId === d.id) {
530
- accessory = a
531
- break
532
- }
533
- } catch (e) {
534
- // ignore
535
- }
536
- }
537
- }
538
-
539
- if (!accessory) {
540
- accessory = new (this.api as any).platformAccessory(d.name || type, uuid)
541
- // Store device metadata on accessory.context for persistence across restarts
542
- try {
543
- accessory.context = accessory.context || {}
544
- accessory.context.deviceId = d.id
545
- accessory.context.type = type
546
- } catch (e) {
547
- // ignore context failures
548
- }
549
- // Register new accessory with Homebridge so it's cached
550
- try {
551
- ;(this.api as any).registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory])
552
- } catch (e) {
553
- // older API variations may require different registration; ignore if unavailable
554
- }
555
- this.accessories.set(uuid, accessory)
556
- } else {
557
- // ensure context includes deviceId (in case restored accessory lacked it)
558
- try {
559
- accessory.context = accessory.context || {}
560
- accessory.context.deviceId = accessory.context.deviceId || d.id
561
- accessory.context.type = accessory.context.type || type
562
- } catch (e) {
563
- // ignore
564
- }
565
- }
566
- // Add basic service descriptor from device
567
- const accDesc = created.createAccessory?.(this.api)
568
- if (accDesc && accDesc.services) {
569
- for (const s of accDesc.services) {
570
- const Service = hap.Service[s.type] || hap.Service[s.type]
571
- if (!Service) {
572
- continue
573
- }
574
- const service = accessory.getService(Service) || accessory.addService(Service)
575
- for (const [charName, getterSetterRaw] of Object.entries(s.characteristics || {})) {
576
- const getterSetter: any = getterSetterRaw
577
- const Characteristic = (hap.Characteristic as any)[charName]
578
- if (!Characteristic) {
579
- continue
580
- }
581
- // Apply characteristic props if provided (min/max/step)
582
- if (getterSetter && getterSetter.props) {
583
- try {
584
- service.getCharacteristic(Characteristic).setProps(getterSetter.props)
585
- } catch (e) {
586
- // ignore setProps failures on older HAP implementations
587
- }
588
- }
589
-
590
- // Wire simple get/set handlers if provided
591
- if (getterSetter && typeof getterSetter.get === 'function') {
592
- service.getCharacteristic(Characteristic).onGet(getterSetter.get)
593
- }
594
- if (getterSetter && typeof getterSetter.set === 'function') {
595
- service.getCharacteristic(Characteristic).onSet(getterSetter.set)
596
- }
597
- }
598
- }
599
- }
600
- this.log.info(`Created/updated HAP accessory ${d.id} (${type})`)
601
- } catch (e) {
602
- this.log.warn('HAP accessory creation failed', e)
603
- }
604
- } else if (!matterRegistered) {
605
- this.log.info(`Created HAP descriptor for ${d.id} (${type}) (API not available to register)`)
606
- }
607
- } catch (e) {
608
- this.log.error(`Failed to create device ${d.id}:`, e as any)
609
- }
610
- }
611
- // Update hash after successfully loading devices
612
- this.lastConfigHash = this.getConfigHash()
613
- }
614
-
615
- // Example lifecycle method called by Homebridge
616
- async configureAccessory(accessory: any) {
617
- // Homebridge calls this for restored cached accessories — keep a reference.
618
- try {
619
- const uuid = accessory.UUID || accessory.UUID
620
- this.accessories.set(uuid, accessory)
621
- this.log.info(`Restored cached accessory ${accessory.displayName || uuid}`)
622
- } catch (e) {
623
- this.log.warn('configureAccessory failed to restore accessory', e)
624
- }
625
- }
626
-
627
- // Called by Homebridge when a cached Matter accessory is restored
628
- configureMatterAccessory?(accessory: any) {
629
- try {
630
- const uuid = accessory.uuid || accessory.UUID || accessory.uuid
631
- this.accessories.set(uuid, accessory)
632
- this.log.info(`Restored cached Matter accessory ${accessory.displayName || uuid}`)
633
- } catch (e) {
634
- this.log.warn('configureMatterAccessory failed to restore accessory', e)
635
- }
636
- }
637
- }
638
-
639
- // Matter platform implementation (placeholder)
640
- export class SwitchBotMatterPlatform {
641
- api: API | undefined
642
- log: Logger
643
- config: SwitchBotPluginConfig
644
- devices: any[] = []
645
- accessories: Map<string, any>
646
- // Track last loaded config to detect changes
647
- private lastConfigHash: string = ''
648
- private configReloadInterval: NodeJS.Timeout | null = null
649
-
650
- constructor(log: Logger, config: PlatformConfig, api?: API) {
651
- this.log = log
652
- this.config = (config as any) ?? {}
653
- this.api = api
654
- this.accessories = new Map()
655
- this.log.info('SwitchBot Matter platform initialized')
656
-
657
- if (this.api && typeof (this.api as any).on === 'function') {
658
- ;(this.api as any).on('didFinishLaunching', () => {
659
- ;(async () => {
660
- // After launch, perform discovery (if any) and register Matter accessories
661
- try {
662
- await this.loadDevices()
663
- if ((this.api as any).isMatterAvailable?.() && (this.api as any).isMatterEnabled?.() && (this.api as any).matter && typeof (this.api as any).matter.registerPlatformAccessories === 'function') {
664
- try {
665
- await (this as any).registerMatterAccessories?.()
666
- } catch (e) {
667
- this.log.warn('registerMatterAccessories failed', e)
668
- }
669
- }
670
- } catch (e) {
671
- this.log.warn('Error during Matter platform startup', e)
672
- }
673
- })()
674
- // Start periodic config reload to pick up UI changes
675
- this.configReloadInterval = setInterval(() => {
676
- void this.checkAndReloadDevices()
677
- }, 10000) // Check every 10 seconds
678
- })
679
- } else {
680
- void this.loadDevices()
681
- // Start periodic config reload to pick up UI changes
682
- this.configReloadInterval = setInterval(() => {
683
- void this.checkAndReloadDevices()
684
- }, 10000) // Check every 10 seconds
685
- }
686
- // Create/shared SwitchBot client and attach to config so child devices reuse it.
687
- try {
688
- const client = new SwitchBotClient(this.config)
689
- void client.init()
690
- ;(this.config as any)._client = client
691
- } catch (e) {
692
- this.log.debug('Failed to create shared SwitchBot client', e)
693
- }
694
- }
695
-
696
- async loadDevices() {
697
- const devices = (this.config as any)?.devices ?? []
698
- for (const raw of devices) {
699
- // Normalize config keys produced by the UI schema
700
- const d: any = {
701
- id: raw.deviceId ?? raw.id,
702
- name: raw.configDeviceName ?? raw.name,
703
- type: raw.configDeviceType ?? raw.type ?? raw.deviceType ?? 'unknown',
704
- _raw: raw,
705
- }
706
-
707
- let type: string = d.type
708
- // Normalize device type variants for consistent Matter cluster lookup
709
- // Vacuum variants
710
- if (['wosweeper', 'wosweepermini', 'wosweeperminipro', 'k10+', 'k10+ pro'].includes((type || '').toLowerCase())) {
711
- type = 'vacuum'
712
- }
713
- // Window covering variants
714
- if (['blindtilt', 'blind tilt', 'curtain3', 'rollershade', 'roller shade', 'worollershade', 'wo rollershade'].includes((type || '').toLowerCase())) {
715
- type = 'curtain'
716
- }
717
- // Plug variants
718
- if (['plug mini (jp)', 'plug mini (us)'].includes((type || '').toLowerCase())) {
719
- type = 'plug'
720
- }
721
- // Meter variants
722
- if (['meterplus', 'meter plus (jp)', 'meterpro', 'meterpro(co2)'].includes((type || '').toLowerCase())) {
723
- type = 'meter'
724
- }
725
- // Relay switch variants
726
- if (['relay switch 1', 'relay switch 1pm'].includes((type || '').toLowerCase())) {
727
- type = 'relay'
728
- }
729
- // Water detector variants
730
- if (['water detector'].includes((type || '').toLowerCase())) {
731
- type = 'waterdetector'
732
- }
733
- // Fan variants
734
- if (['smart fan'].includes((type || '').toLowerCase())) {
735
- type = 'fan'
736
- }
737
- // Light variants
738
- if (['strip light'].includes((type || '').toLowerCase())) {
739
- type = 'lightstrip'
740
- }
741
-
742
- const matterSupported = !!DEVICE_MATTER_SUPPORTED[(type || '').toLowerCase()]
743
- // Auto-detect Matter from Homebridge API, allow manual override via config
744
- const matterAvailable = this.api?.isMatterAvailable?.() && this.api?.isMatterEnabled?.()
745
- const matterEnabled = matterAvailable || !!this.config.enableMatter
746
- const useMatter = matterEnabled && matterSupported
747
- try {
748
- const created = await createDevice({ id: d.id, type, name: d.name }, this.config, useMatter)
749
- this.devices.push(created)
750
- if (useMatter) {
751
- this.log.info(`Prepared Matter accessory for ${d.id} (${type})${matterAvailable ? ' (auto-detected)' : ' (manually enabled)'}`)
752
- } else {
753
- if (!matterEnabled) {
754
- this.log.info(`Skipping Matter for ${d.id} (${type}) - Matter not available on this bridge`)
755
- } else if (!matterSupported) {
756
- this.log.info(`Skipping Matter for ${d.id} (${type}) - device type not supported`)
757
- } else {
758
- this.log.info(`Skipping Matter for ${d.id} (${type}) - not supported`)
759
- }
760
- }
761
- } catch (e) {
762
- this.log.error(`Failed to create Matter device ${d.id}:`, e as any)
763
- }
764
- }
765
- // Update hash after successfully loading devices
766
- this.lastConfigHash = this.getConfigHash()
767
- }
768
-
769
- private getConfigHash(): string {
770
- // Create a simple hash of current device config to detect changes
771
- const devices = (this.config as any)?.devices ?? []
772
- return JSON.stringify(devices.map((d: any) => ({
773
- id: d.deviceId ?? d.id,
774
- type: d.configDeviceType ?? d.type,
775
- name: d.configDeviceName ?? d.name,
776
- })))
777
- }
778
-
779
- private async checkAndReloadDevices() {
780
- const currentHash = this.getConfigHash()
781
- if (currentHash !== this.lastConfigHash) {
782
- this.log.info('[SwitchBot] Detected config changes, reloading devices...')
783
- // Clear existing devices
784
- this.devices = []
785
- await this.loadDevices()
786
- }
787
- }
788
-
789
- async configureAccessory(accessory: any) {
790
- try {
791
- const uuid = accessory.UUID || accessory.UUID
792
- this.accessories.set(uuid, accessory)
793
- this.log.info(`Restored cached Matter accessory ${accessory.displayName || uuid}`)
794
- } catch (e) {
795
- this.log.warn('configureAccessory failed to restore Matter accessory', e)
796
- }
797
- }
798
-
799
- // Homebridge calls this when restoring cached Matter accessories
800
- configureMatterAccessory(accessory: any) {
801
- try {
802
- const uuid = accessory.uuid || accessory.UUID || accessory.uuid
803
- this.accessories.set(uuid, accessory)
804
- this.log.info(`Restored cached Matter accessory ${accessory.displayName || uuid}`)
805
- } catch (e) {
806
- this.log.warn('configureMatterAccessory failed to restore Matter accessory', e)
807
- }
808
- }
809
-
810
- // Register serialized Matter accessories via Homebridge Matter API
811
- async registerMatterAccessories() {
812
- if (!this.api) {
813
- return
814
- }
815
- const matterApi = (this.api as any).matter
816
- if (!matterApi || typeof matterApi.registerPlatformAccessories !== 'function') {
817
- this.log.info('Homebridge Matter API not available; skipping Matter accessory registration')
818
- return
819
- }
820
-
821
- const devices = (this.config as any)?.devices ?? []
822
- const accessoriesToRegister: any[] = []
823
-
824
- // Auto-detect Matter from Homebridge API
825
- const matterAvailable = this.api?.isMatterAvailable?.() && this.api?.isMatterEnabled?.()
826
- const matterEnabled = matterAvailable || !!this.config.enableMatter
827
-
828
- for (const raw of devices) {
829
- const d: any = {
830
- id: raw.deviceId ?? raw.id,
831
- name: raw.configDeviceName ?? raw.name,
832
- type: raw.configDeviceType ?? raw.type ?? raw.deviceType ?? 'unknown',
833
- }
834
-
835
- if (!d.id) {
836
- continue
837
- }
838
-
839
- let type: string = d.type
840
- // Normalize device type variants for consistent Matter cluster lookup
841
- // Vacuum variants
842
- if (['wosweeper', 'wosweepermini', 'wosweeperminipro', 'k10+', 'k10+ pro'].includes((type || '').toLowerCase())) {
843
- type = 'vacuum'
844
- }
845
- // Window covering variants
846
- if (['blindtilt', 'blind tilt', 'curtain3', 'rollershade', 'roller shade', 'worollershade', 'wo rollershade'].includes((type || '').toLowerCase())) {
847
- type = 'curtain'
848
- }
849
- // Plug variants
850
- if (['plug mini (jp)', 'plug mini (us)'].includes((type || '').toLowerCase())) {
851
- type = 'plug'
852
- }
853
- // Meter variants
854
- if (['meterplus', 'meter plus (jp)', 'meterpro', 'meterpro(co2)'].includes((type || '').toLowerCase())) {
855
- type = 'meter'
856
- }
857
- // Relay switch variants
858
- if (['relay switch 1', 'relay switch 1pm'].includes((type || '').toLowerCase())) {
859
- type = 'relay'
860
- }
861
- // Water detector variants
862
- if (['water detector'].includes((type || '').toLowerCase())) {
863
- type = 'waterdetector'
864
- }
865
- // Fan variants
866
- if (['smart fan'].includes((type || '').toLowerCase())) {
867
- type = 'fan'
868
- }
869
- // Light variants
870
- if (['strip light'].includes((type || '').toLowerCase())) {
871
- type = 'lightstrip'
872
- }
873
-
874
- const matterSupported = !!DEVICE_MATTER_SUPPORTED[(type || '').toLowerCase()]
875
- const useMatter = matterEnabled && matterSupported
876
- if (!useMatter) {
877
- continue
878
- }
879
-
880
- try {
881
- const created = await createDevice({ id: d.id, type, name: d.name }, this.config, true)
882
- const createdDesc = created.createAccessory?.(this.api) || { id: d.id, name: d.name || type }
883
- const uuid = matterApi.uuid.generate(`${d.id}`)
884
- // Try to find existing restored accessory by deviceId
885
- let existing: any | undefined
886
- for (const [, a] of Array.from(this.accessories.entries())) {
887
- try {
888
- if (a && a.context && a.context.deviceId === d.id) {
889
- existing = a
890
- break
891
- }
892
- } catch (e) {
893
- // ignore
894
- }
895
- }
896
-
897
- if (existing) {
898
- // Ensure context and displayName are up to date
899
- // Prioritize device-specific Matter clusters (e.g., RVC for vacuum) over generic HAP-derived clusters
900
- let clusters = DEVICE_MATTER_CLUSTERS[type.toLowerCase()]
901
- if (!clusters) {
902
- clusters = existing.clusters || createdDesc.clusters || { onOff: { onOff: false } }
903
- }
904
- const deviceType = resolveMatterDeviceType(matterApi, type, existing.deviceType || createdDesc.deviceType, clusters)
905
- existing.context = existing.context || {}
906
- existing.context.deviceId = existing.context.deviceId || d.id
907
- existing.context.type = existing.context.type || type
908
- existing.deviceType = deviceType
909
- existing.manufacturer = existing.manufacturer || createdDesc.manufacturer || 'SwitchBot'
910
- existing.model = existing.model || createdDesc.model || type
911
- existing.serialNumber = existing.serialNumber || createdDesc.serialNumber || d.id
912
- existing.reachable = existing.reachable !== false
913
- existing.firmwareRevision = existing.firmwareRevision || createdDesc.firmwareRevision || '1.0.0'
914
- existing.hardwareRevision = existing.hardwareRevision || createdDesc.hardwareRevision || ''
915
- existing.clusters = clusters
916
- existing.handlers = createdDesc.handlers || DEVICE_MATTER_HANDLERS[type.toLowerCase()] || undefined
917
- existing.displayName = createdDesc.name || d.name || type
918
- existing.UUID = existing.UUID || existing.uuid || uuid
919
- accessoriesToRegister.push(existing)
920
- this.accessories.set(existing.UUID || uuid, existing)
921
- } else {
922
- // Prioritize device-specific Matter clusters (e.g., RVC for vacuum) over generic HAP-derived clusters
923
- let clusters = DEVICE_MATTER_CLUSTERS[type.toLowerCase()]
924
- if (!clusters) {
925
- clusters = createdDesc.clusters || { onOff: { onOff: false } }
926
- }
927
- const deviceType = resolveMatterDeviceType(matterApi, type, createdDesc.deviceType, clusters)
928
- const serialized: any = {
929
- UUID: uuid,
930
- displayName: createdDesc.name || d.name || type,
931
- deviceType,
932
- manufacturer: createdDesc.manufacturer || 'SwitchBot',
933
- model: createdDesc.model || type,
934
- serialNumber: createdDesc.serialNumber || d.id,
935
- reachable: createdDesc.reachable !== false,
936
- firmwareRevision: createdDesc.firmwareRevision || '1.0.0',
937
- hardwareRevision: createdDesc.hardwareRevision || '',
938
- clusters,
939
- handlers: createdDesc.handlers || DEVICE_MATTER_HANDLERS[type.toLowerCase()] || undefined,
940
- context: { deviceId: d.id, type, created: true },
941
- }
942
- accessoriesToRegister.push(serialized)
943
- this.accessories.set(uuid, serialized)
944
- }
945
- } catch (e) {
946
- this.log.warn(`Failed to prepare Matter accessory for ${d.id} (${type})`, e)
947
- }
948
- }
949
-
950
- if (accessoriesToRegister.length > 0) {
951
- try {
952
- await matterApi.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, accessoriesToRegister)
953
- this.log.info(`Registered ${accessoriesToRegister.length} Matter accessory(ies) with Homebridge`)
954
- } catch (e) {
955
- this.log.warn('Failed to register Matter accessories', e)
956
- }
957
- } else {
958
- this.log.info('No Matter accessories to register')
959
- }
960
- }
961
- }
962
-
963
- export default SwitchBotHAPPlatform