@switchbot/homebridge-switchbot 5.0.0-beta.98 → 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 -450
  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 -526
  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 -850
  267. package/dist/platform.js.map +0 -1
  268. package/src/platform.ts +0 -867
  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,867 +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
- function resolveMatterDeviceType(matterApi: any, type: string, createdDeviceType?: any, clusters?: any): any {
236
- if (createdDeviceType && typeof createdDeviceType === 'object' && typeof createdDeviceType.with === 'function') {
237
- return createdDeviceType
238
- }
239
-
240
- const lowerType = (typeof createdDeviceType === 'string' && createdDeviceType) ? createdDeviceType.toLowerCase() : (type || '').toLowerCase()
241
-
242
- // Cluster-based upgrade for color lights if descriptor omitted device type.
243
- const hasColorControl = !!clusters?.colorControl
244
- const inferredType = hasColorControl && lowerType === 'light'
245
- ? 'lightstrip'
246
- : lowerType
247
-
248
- const mappedKey = DEVICE_MATTER_DEVICE_TYPE_KEYS[inferredType] || 'OnOffSwitch'
249
- return matterApi?.deviceTypes?.[mappedKey] || matterApi?.deviceTypes?.OnOffSwitch
250
- }
251
-
252
- export class SwitchBotHAPPlatform {
253
- api: API | undefined
254
- log: Logger
255
- config: SwitchBotPluginConfig
256
- devices: any[] = []
257
- // cached accessories restored by Homebridge
258
- accessories: Map<string, any>
259
- // Track last loaded config to detect changes
260
- private lastConfigHash: string = ''
261
- private configReloadInterval: NodeJS.Timeout | null = null
262
-
263
- constructor(log: Logger, config: PlatformConfig, api?: API) {
264
- this.log = log
265
- this.config = (config as any) ?? {}
266
- this.api = api
267
- this.accessories = new Map()
268
- this.log.info('SwitchBot HAP platform initialized')
269
-
270
- // Create/shared SwitchBot client and attach to config so child devices reuse it.
271
- try {
272
- const client = new SwitchBotClient(this.config)
273
- void client.init()
274
- ;(this.config as any)._client = client
275
- } catch (e) {
276
- this.log.debug('Failed to create shared SwitchBot client', e)
277
- }
278
-
279
- // Create/shared SwitchBot client and attach to config so child devices reuse it.
280
- try {
281
- const client = new SwitchBotClient(this.config)
282
- void client.init()
283
- ;(this.config as any)._client = client
284
- } catch (e) {
285
- this.log.debug('Failed to create shared SwitchBot client', e)
286
- }
287
-
288
- // Wait for Homebridge to finish launching to create/register accessories
289
- if (this.api && typeof (this.api as any).on === 'function') {
290
- ;(this.api as any).on('didFinishLaunching', () => {
291
- void this.loadDevices()
292
- // Start periodic config reload to pick up UI changes
293
- this.configReloadInterval = setInterval(() => {
294
- void this.checkAndReloadDevices()
295
- }, 10000) // Check every 10 seconds
296
- })
297
- } else {
298
- void this.loadDevices()
299
- // Start periodic config reload to pick up UI changes
300
- this.configReloadInterval = setInterval(() => {
301
- void this.checkAndReloadDevices()
302
- }, 10000) // Check every 10 seconds
303
- }
304
- }
305
-
306
- private getConfigHash(): string {
307
- // Create a simple hash of current device config to detect changes
308
- const devices = (this.config as any)?.devices ?? []
309
- return JSON.stringify(devices.map((d: any) => ({
310
- id: d.deviceId ?? d.id,
311
- type: d.configDeviceType ?? d.type,
312
- name: d.configDeviceName ?? d.name,
313
- })))
314
- }
315
-
316
- private async checkAndReloadDevices() {
317
- const currentHash = this.getConfigHash()
318
- if (currentHash !== this.lastConfigHash) {
319
- this.log.info('[SwitchBot] Detected config changes, reloading devices...')
320
- // Clear existing devices
321
- this.devices = []
322
- await this.loadDevices()
323
- }
324
- }
325
-
326
- async loadDevices() {
327
- const devices = (this.config as any)?.devices ?? []
328
- for (const raw of devices) {
329
- // Normalize config keys from UI schema to internal shape
330
- const d: any = {
331
- id: raw.deviceId ?? raw.id,
332
- name: raw.configDeviceName ?? raw.name,
333
- type: raw.configDeviceType ?? raw.type ?? raw.deviceType ?? 'unknown',
334
- _raw: raw,
335
- }
336
-
337
- let type: string = d.type
338
- // Normalize device type variants for consistent Matter cluster lookup
339
- // Vacuum variants
340
- if (['wosweeper', 'wosweepermini', 'wosweeperminipro', 'k10+', 'k10+ pro'].includes((type || '').toLowerCase())) {
341
- type = 'vacuum'
342
- }
343
- // Window covering variants
344
- if (['blindtilt', 'blind tilt', 'curtain3', 'rollershade', 'roller shade', 'worollershade', 'wo rollershade'].includes((type || '').toLowerCase())) {
345
- type = 'curtain'
346
- }
347
- // Plug variants
348
- if (['plug mini (jp)', 'plug mini (us)'].includes((type || '').toLowerCase())) {
349
- type = 'plug'
350
- }
351
- // Meter variants
352
- if (['meterplus', 'meter plus (jp)', 'meterpro', 'meterpro(co2)'].includes((type || '').toLowerCase())) {
353
- type = 'meter'
354
- }
355
- // Relay switch variants
356
- if (['relay switch 1', 'relay switch 1pm'].includes((type || '').toLowerCase())) {
357
- type = 'relay'
358
- }
359
- // Water detector variants
360
- if (['water detector'].includes((type || '').toLowerCase())) {
361
- type = 'waterdetector'
362
- }
363
- // Fan variants
364
- if (['smart fan'].includes((type || '').toLowerCase())) {
365
- type = 'fan'
366
- }
367
- // Light variants
368
- if (['strip light'].includes((type || '').toLowerCase())) {
369
- type = 'lightstrip'
370
- }
371
-
372
- const matterSupported = !!DEVICE_MATTER_SUPPORTED[(type || '').toLowerCase()]
373
- // Auto-detect Matter from Homebridge API, allow manual override via config
374
- const matterAvailable = !!(this.api?.isMatterAvailable?.() && this.api?.isMatterEnabled?.())
375
- const matterEnabled = matterAvailable || !!this.config.enableMatter
376
- const useMatter = !!(matterEnabled && matterSupported && (!!this.config.preferMatter || matterAvailable))
377
-
378
- try {
379
- const created = await createDevice({ id: d.id, type, name: d.name }, this.config, useMatter)
380
- this.devices.push(created)
381
- // Prefer Matter: try registering to the Matter child bridge first.
382
- let matterRegistered = false
383
- if (useMatter) {
384
- this.log.info(`Attempting Matter registration for ${d.id} (${type})`)
385
- // If Homebridge Matter APIs are available, register the accessory
386
- try {
387
- const matterApi = (this.api as any)?.matter
388
- if (this.api?.isMatterAvailable?.() && this.api?.isMatterEnabled?.() && matterApi && typeof matterApi.registerPlatformAccessories === 'function') {
389
- const createdDesc = created.createAccessory?.(this.api) || { id: d.id, name: d.name || type }
390
- const uuid = matterApi.uuid.generate(`${d.id}`)
391
- const defaultClusters = DEVICE_MATTER_CLUSTERS[type.toLowerCase()] || { onOff: { onOff: false } }
392
- const clusters = createdDesc.clusters || defaultClusters
393
- const deviceType = resolveMatterDeviceType(matterApi, type, createdDesc.deviceType, clusters)
394
- const accessory: any = {
395
- UUID: uuid,
396
- displayName: createdDesc.name || d.name || type,
397
- deviceType,
398
- manufacturer: createdDesc.manufacturer || 'SwitchBot',
399
- model: createdDesc.model || type,
400
- serialNumber: createdDesc.serialNumber || d.id,
401
- reachable: createdDesc.reachable !== false,
402
- firmwareRevision: createdDesc.firmwareRevision || '1.0.0',
403
- hardwareRevision: createdDesc.hardwareRevision || '',
404
- clusters,
405
- context: { deviceId: d.id, type, _created: true },
406
- }
407
- try {
408
- await matterApi.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory])
409
- this.accessories.set(uuid, accessory)
410
- matterRegistered = true
411
- this.log.info(`Registered Matter accessory ${d.id} (${type}) with uuid=${uuid}`)
412
- } catch (e) {
413
- this.log.warn('Failed to register Matter accessory with Homebridge matter API', e)
414
- }
415
- } else {
416
- this.log.info('Homebridge Matter API not available or not enabled; will fallback to HAP')
417
- }
418
- } catch (e) {
419
- this.log.warn('Matter registration attempt failed', e)
420
- }
421
- }
422
-
423
- // If Matter wasn't registered (either not supported, API missing, or registration failed), fall back to HAP registration.
424
- if (!matterRegistered && this.api && (this.api as any).hap) {
425
- // Basic HAP accessory creation using homebridge API when available
426
- try {
427
- const hap = (this.api as any).hap
428
- const uuid = hap.uuid.generate(`${d.id}`)
429
- // Reuse cached accessory if available by uuid
430
- let accessory: any = this.accessories.get(uuid)
431
- // If not found by uuid, attempt to find by stored deviceId in accessory.context
432
- if (!accessory) {
433
- for (const [, a] of Array.from(this.accessories.entries())) {
434
- try {
435
- if (a && a.context && a.context.deviceId === d.id) {
436
- accessory = a
437
- break
438
- }
439
- } catch (e) {
440
- // ignore
441
- }
442
- }
443
- }
444
-
445
- if (!accessory) {
446
- accessory = new (this.api as any).platformAccessory(d.name || type, uuid)
447
- // Store device metadata on accessory.context for persistence across restarts
448
- try {
449
- accessory.context = accessory.context || {}
450
- accessory.context.deviceId = d.id
451
- accessory.context.type = type
452
- } catch (e) {
453
- // ignore context failures
454
- }
455
- // Register new accessory with Homebridge so it's cached
456
- try {
457
- ;(this.api as any).registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory])
458
- } catch (e) {
459
- // older API variations may require different registration; ignore if unavailable
460
- }
461
- this.accessories.set(uuid, accessory)
462
- } else {
463
- // ensure context includes deviceId (in case restored accessory lacked it)
464
- try {
465
- accessory.context = accessory.context || {}
466
- accessory.context.deviceId = accessory.context.deviceId || d.id
467
- accessory.context.type = accessory.context.type || type
468
- } catch (e) {
469
- // ignore
470
- }
471
- }
472
- // Add basic service descriptor from device
473
- const accDesc = created.createAccessory?.(this.api)
474
- if (accDesc && accDesc.services) {
475
- for (const s of accDesc.services) {
476
- const Service = hap.Service[s.type] || hap.Service[s.type]
477
- if (!Service) {
478
- continue
479
- }
480
- const service = accessory.getService(Service) || accessory.addService(Service)
481
- for (const [charName, getterSetterRaw] of Object.entries(s.characteristics || {})) {
482
- const getterSetter: any = getterSetterRaw
483
- const Characteristic = (hap.Characteristic as any)[charName]
484
- if (!Characteristic) {
485
- continue
486
- }
487
- // Apply characteristic props if provided (min/max/step)
488
- if (getterSetter && getterSetter.props) {
489
- try {
490
- service.getCharacteristic(Characteristic).setProps(getterSetter.props)
491
- } catch (e) {
492
- // ignore setProps failures on older HAP implementations
493
- }
494
- }
495
-
496
- // Wire simple get/set handlers if provided
497
- if (getterSetter && typeof getterSetter.get === 'function') {
498
- service.getCharacteristic(Characteristic).onGet(getterSetter.get)
499
- }
500
- if (getterSetter && typeof getterSetter.set === 'function') {
501
- service.getCharacteristic(Characteristic).onSet(getterSetter.set)
502
- }
503
- }
504
- }
505
- }
506
- this.log.info(`Created/updated HAP accessory ${d.id} (${type})`)
507
- } catch (e) {
508
- this.log.warn('HAP accessory creation failed', e)
509
- }
510
- } else if (!matterRegistered) {
511
- this.log.info(`Created HAP descriptor for ${d.id} (${type}) (API not available to register)`)
512
- }
513
- } catch (e) {
514
- this.log.error(`Failed to create device ${d.id}:`, e as any)
515
- }
516
- }
517
- // Update hash after successfully loading devices
518
- this.lastConfigHash = this.getConfigHash()
519
- }
520
-
521
- // Example lifecycle method called by Homebridge
522
- async configureAccessory(accessory: any) {
523
- // Homebridge calls this for restored cached accessories — keep a reference.
524
- try {
525
- const uuid = accessory.UUID || accessory.UUID
526
- this.accessories.set(uuid, accessory)
527
- this.log.info(`Restored cached accessory ${accessory.displayName || uuid}`)
528
- } catch (e) {
529
- this.log.warn('configureAccessory failed to restore accessory', e)
530
- }
531
- }
532
-
533
- // Called by Homebridge when a cached Matter accessory is restored
534
- configureMatterAccessory?(accessory: any) {
535
- try {
536
- const uuid = accessory.uuid || accessory.UUID || accessory.uuid
537
- this.accessories.set(uuid, accessory)
538
- this.log.info(`Restored cached Matter accessory ${accessory.displayName || uuid}`)
539
- } catch (e) {
540
- this.log.warn('configureMatterAccessory failed to restore accessory', e)
541
- }
542
- }
543
- }
544
-
545
- // Matter platform implementation (placeholder)
546
- export class SwitchBotMatterPlatform {
547
- api: API | undefined
548
- log: Logger
549
- config: SwitchBotPluginConfig
550
- devices: any[] = []
551
- accessories: Map<string, any>
552
- // Track last loaded config to detect changes
553
- private lastConfigHash: string = ''
554
- private configReloadInterval: NodeJS.Timeout | null = null
555
-
556
- constructor(log: Logger, config: PlatformConfig, api?: API) {
557
- this.log = log
558
- this.config = (config as any) ?? {}
559
- this.api = api
560
- this.accessories = new Map()
561
- this.log.info('SwitchBot Matter platform initialized')
562
-
563
- if (this.api && typeof (this.api as any).on === 'function') {
564
- ;(this.api as any).on('didFinishLaunching', () => {
565
- ;(async () => {
566
- // After launch, perform discovery (if any) and register Matter accessories
567
- try {
568
- await this.loadDevices()
569
- if ((this.api as any).isMatterAvailable?.() && (this.api as any).isMatterEnabled?.() && (this.api as any).matter && typeof (this.api as any).matter.registerPlatformAccessories === 'function') {
570
- try {
571
- await (this as any).registerMatterAccessories?.()
572
- } catch (e) {
573
- this.log.warn('registerMatterAccessories failed', e)
574
- }
575
- }
576
- } catch (e) {
577
- this.log.warn('Error during Matter platform startup', e)
578
- }
579
- })()
580
- // Start periodic config reload to pick up UI changes
581
- this.configReloadInterval = setInterval(() => {
582
- void this.checkAndReloadDevices()
583
- }, 10000) // Check every 10 seconds
584
- })
585
- } else {
586
- void this.loadDevices()
587
- // Start periodic config reload to pick up UI changes
588
- this.configReloadInterval = setInterval(() => {
589
- void this.checkAndReloadDevices()
590
- }, 10000) // Check every 10 seconds
591
- }
592
- // Create/shared SwitchBot client and attach to config so child devices reuse it.
593
- try {
594
- const client = new SwitchBotClient(this.config)
595
- void client.init()
596
- ;(this.config as any)._client = client
597
- } catch (e) {
598
- this.log.debug('Failed to create shared SwitchBot client', e)
599
- }
600
- }
601
-
602
- async loadDevices() {
603
- const devices = (this.config as any)?.devices ?? []
604
- for (const raw of devices) {
605
- // Normalize config keys produced by the UI schema
606
- const d: any = {
607
- id: raw.deviceId ?? raw.id,
608
- name: raw.configDeviceName ?? raw.name,
609
- type: raw.configDeviceType ?? raw.type ?? raw.deviceType ?? 'unknown',
610
- _raw: raw,
611
- }
612
-
613
- let type: string = d.type
614
- // Normalize device type variants for consistent Matter cluster lookup
615
- // Vacuum variants
616
- if (['wosweeper', 'wosweepermini', 'wosweeperminipro', 'k10+', 'k10+ pro'].includes((type || '').toLowerCase())) {
617
- type = 'vacuum'
618
- }
619
- // Window covering variants
620
- if (['blindtilt', 'blind tilt', 'curtain3', 'rollershade', 'roller shade', 'worollershade', 'wo rollershade'].includes((type || '').toLowerCase())) {
621
- type = 'curtain'
622
- }
623
- // Plug variants
624
- if (['plug mini (jp)', 'plug mini (us)'].includes((type || '').toLowerCase())) {
625
- type = 'plug'
626
- }
627
- // Meter variants
628
- if (['meterplus', 'meter plus (jp)', 'meterpro', 'meterpro(co2)'].includes((type || '').toLowerCase())) {
629
- type = 'meter'
630
- }
631
- // Relay switch variants
632
- if (['relay switch 1', 'relay switch 1pm'].includes((type || '').toLowerCase())) {
633
- type = 'relay'
634
- }
635
- // Water detector variants
636
- if (['water detector'].includes((type || '').toLowerCase())) {
637
- type = 'waterdetector'
638
- }
639
- // Fan variants
640
- if (['smart fan'].includes((type || '').toLowerCase())) {
641
- type = 'fan'
642
- }
643
- // Light variants
644
- if (['strip light'].includes((type || '').toLowerCase())) {
645
- type = 'lightstrip'
646
- }
647
-
648
- const matterSupported = !!DEVICE_MATTER_SUPPORTED[(type || '').toLowerCase()]
649
- // Auto-detect Matter from Homebridge API, allow manual override via config
650
- const matterAvailable = this.api?.isMatterAvailable?.() && this.api?.isMatterEnabled?.()
651
- const matterEnabled = matterAvailable || !!this.config.enableMatter
652
- const useMatter = matterEnabled && matterSupported
653
- try {
654
- const created = await createDevice({ id: d.id, type, name: d.name }, this.config, useMatter)
655
- this.devices.push(created)
656
- if (useMatter) {
657
- this.log.info(`Prepared Matter accessory for ${d.id} (${type})${matterAvailable ? ' (auto-detected)' : ' (manually enabled)'}`)
658
- } else {
659
- if (!matterEnabled) {
660
- this.log.info(`Skipping Matter for ${d.id} (${type}) - Matter not available on this bridge`)
661
- } else if (!matterSupported) {
662
- this.log.info(`Skipping Matter for ${d.id} (${type}) - device type not supported`)
663
- } else {
664
- this.log.info(`Skipping Matter for ${d.id} (${type}) - not supported`)
665
- }
666
- }
667
- } catch (e) {
668
- this.log.error(`Failed to create Matter device ${d.id}:`, e as any)
669
- }
670
- }
671
- // Update hash after successfully loading devices
672
- this.lastConfigHash = this.getConfigHash()
673
- }
674
-
675
- private getConfigHash(): string {
676
- // Create a simple hash of current device config to detect changes
677
- const devices = (this.config as any)?.devices ?? []
678
- return JSON.stringify(devices.map((d: any) => ({
679
- id: d.deviceId ?? d.id,
680
- type: d.configDeviceType ?? d.type,
681
- name: d.configDeviceName ?? d.name,
682
- })))
683
- }
684
-
685
- private async checkAndReloadDevices() {
686
- const currentHash = this.getConfigHash()
687
- if (currentHash !== this.lastConfigHash) {
688
- this.log.info('[SwitchBot] Detected config changes, reloading devices...')
689
- // Clear existing devices
690
- this.devices = []
691
- await this.loadDevices()
692
- }
693
- }
694
-
695
- async configureAccessory(accessory: any) {
696
- try {
697
- const uuid = accessory.UUID || accessory.UUID
698
- this.accessories.set(uuid, accessory)
699
- this.log.info(`Restored cached Matter accessory ${accessory.displayName || uuid}`)
700
- } catch (e) {
701
- this.log.warn('configureAccessory failed to restore Matter accessory', e)
702
- }
703
- }
704
-
705
- // Homebridge calls this when restoring cached Matter accessories
706
- configureMatterAccessory(accessory: any) {
707
- try {
708
- const uuid = accessory.uuid || accessory.UUID || accessory.uuid
709
- this.accessories.set(uuid, accessory)
710
- this.log.info(`Restored cached Matter accessory ${accessory.displayName || uuid}`)
711
- } catch (e) {
712
- this.log.warn('configureMatterAccessory failed to restore Matter accessory', e)
713
- }
714
- }
715
-
716
- // Register serialized Matter accessories via Homebridge Matter API
717
- async registerMatterAccessories() {
718
- if (!this.api) {
719
- return
720
- }
721
- const matterApi = (this.api as any).matter
722
- if (!matterApi || typeof matterApi.registerPlatformAccessories !== 'function') {
723
- this.log.info('Homebridge Matter API not available; skipping Matter accessory registration')
724
- return
725
- }
726
-
727
- const devices = (this.config as any)?.devices ?? []
728
- const accessoriesToRegister: any[] = []
729
-
730
- // Auto-detect Matter from Homebridge API
731
- const matterAvailable = this.api?.isMatterAvailable?.() && this.api?.isMatterEnabled?.()
732
- const matterEnabled = matterAvailable || !!this.config.enableMatter
733
-
734
- for (const raw of devices) {
735
- const d: any = {
736
- id: raw.deviceId ?? raw.id,
737
- name: raw.configDeviceName ?? raw.name,
738
- type: raw.configDeviceType ?? raw.type ?? raw.deviceType ?? 'unknown',
739
- }
740
-
741
- if (!d.id) {
742
- continue
743
- }
744
-
745
- let type: string = d.type
746
- // Normalize device type variants for consistent Matter cluster lookup
747
- // Vacuum variants
748
- if (['wosweeper', 'wosweepermini', 'wosweeperminipro', 'k10+', 'k10+ pro'].includes((type || '').toLowerCase())) {
749
- type = 'vacuum'
750
- }
751
- // Window covering variants
752
- if (['blindtilt', 'blind tilt', 'curtain3', 'rollershade', 'roller shade', 'worollershade', 'wo rollershade'].includes((type || '').toLowerCase())) {
753
- type = 'curtain'
754
- }
755
- // Plug variants
756
- if (['plug mini (jp)', 'plug mini (us)'].includes((type || '').toLowerCase())) {
757
- type = 'plug'
758
- }
759
- // Meter variants
760
- if (['meterplus', 'meter plus (jp)', 'meterpro', 'meterpro(co2)'].includes((type || '').toLowerCase())) {
761
- type = 'meter'
762
- }
763
- // Relay switch variants
764
- if (['relay switch 1', 'relay switch 1pm'].includes((type || '').toLowerCase())) {
765
- type = 'relay'
766
- }
767
- // Water detector variants
768
- if (['water detector'].includes((type || '').toLowerCase())) {
769
- type = 'waterdetector'
770
- }
771
- // Fan variants
772
- if (['smart fan'].includes((type || '').toLowerCase())) {
773
- type = 'fan'
774
- }
775
- // Light variants
776
- if (['strip light'].includes((type || '').toLowerCase())) {
777
- type = 'lightstrip'
778
- }
779
-
780
- const matterSupported = !!DEVICE_MATTER_SUPPORTED[(type || '').toLowerCase()]
781
- const useMatter = matterEnabled && matterSupported
782
- if (!useMatter) {
783
- continue
784
- }
785
-
786
- try {
787
- const created = await createDevice({ id: d.id, type, name: d.name }, this.config, true)
788
- const createdDesc = created.createAccessory?.(this.api) || { id: d.id, name: d.name || type }
789
- const uuid = matterApi.uuid.generate(`${d.id}`)
790
- // Try to find existing restored accessory by deviceId
791
- let existing: any | undefined
792
- for (const [, a] of Array.from(this.accessories.entries())) {
793
- try {
794
- if (a && a.context && a.context.deviceId === d.id) {
795
- existing = a
796
- break
797
- }
798
- } catch (e) {
799
- // ignore
800
- }
801
- }
802
-
803
- if (existing) {
804
- // Ensure context and displayName are up to date
805
- // Prioritize device-specific Matter clusters (e.g., RVC for vacuum) over generic HAP-derived clusters
806
- let clusters = DEVICE_MATTER_CLUSTERS[type.toLowerCase()]
807
- if (!clusters) {
808
- clusters = existing.clusters || createdDesc.clusters || { onOff: { onOff: false } }
809
- }
810
- const deviceType = resolveMatterDeviceType(matterApi, type, existing.deviceType || createdDesc.deviceType, clusters)
811
- existing.context = existing.context || {}
812
- existing.context.deviceId = existing.context.deviceId || d.id
813
- existing.context.type = existing.context.type || type
814
- existing.deviceType = deviceType
815
- existing.manufacturer = existing.manufacturer || createdDesc.manufacturer || 'SwitchBot'
816
- existing.model = existing.model || createdDesc.model || type
817
- existing.serialNumber = existing.serialNumber || createdDesc.serialNumber || d.id
818
- existing.reachable = existing.reachable !== false
819
- existing.firmwareRevision = existing.firmwareRevision || createdDesc.firmwareRevision || '1.0.0'
820
- existing.hardwareRevision = existing.hardwareRevision || createdDesc.hardwareRevision || ''
821
- existing.clusters = clusters
822
- existing.displayName = createdDesc.name || d.name || type
823
- existing.UUID = existing.UUID || existing.uuid || uuid
824
- accessoriesToRegister.push(existing)
825
- this.accessories.set(existing.UUID || uuid, existing)
826
- } else {
827
- // Prioritize device-specific Matter clusters (e.g., RVC for vacuum) over generic HAP-derived clusters
828
- let clusters = DEVICE_MATTER_CLUSTERS[type.toLowerCase()]
829
- if (!clusters) {
830
- clusters = createdDesc.clusters || { onOff: { onOff: false } }
831
- }
832
- const deviceType = resolveMatterDeviceType(matterApi, type, createdDesc.deviceType, clusters)
833
- const serialized: any = {
834
- UUID: uuid,
835
- displayName: createdDesc.name || d.name || type,
836
- deviceType,
837
- manufacturer: createdDesc.manufacturer || 'SwitchBot',
838
- model: createdDesc.model || type,
839
- serialNumber: createdDesc.serialNumber || d.id,
840
- reachable: createdDesc.reachable !== false,
841
- firmwareRevision: createdDesc.firmwareRevision || '1.0.0',
842
- hardwareRevision: createdDesc.hardwareRevision || '',
843
- clusters,
844
- context: { deviceId: d.id, type, created: true },
845
- }
846
- accessoriesToRegister.push(serialized)
847
- this.accessories.set(uuid, serialized)
848
- }
849
- } catch (e) {
850
- this.log.warn(`Failed to prepare Matter accessory for ${d.id} (${type})`, e)
851
- }
852
- }
853
-
854
- if (accessoriesToRegister.length > 0) {
855
- try {
856
- await matterApi.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, accessoriesToRegister)
857
- this.log.info(`Registered ${accessoriesToRegister.length} Matter accessory(ies) with Homebridge`)
858
- } catch (e) {
859
- this.log.warn('Failed to register Matter accessories', e)
860
- }
861
- } else {
862
- this.log.info('No Matter accessories to register')
863
- }
864
- }
865
- }
866
-
867
- export default SwitchBotHAPPlatform