@switchbot/homebridge-switchbot 5.0.0-beta.7 → 5.0.0-beta.70

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 (271) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +45 -3
  3. package/config.schema.json +871 -13754
  4. package/dist/devices-hap/airpurifier.d.ts.map +1 -1
  5. package/dist/devices-hap/airpurifier.js +12 -6
  6. package/dist/devices-hap/airpurifier.js.map +1 -1
  7. package/dist/devices-hap/blindtilt.js +3 -3
  8. package/dist/devices-hap/bot.d.ts.map +1 -1
  9. package/dist/devices-hap/bot.js +16 -5
  10. package/dist/devices-hap/bot.js.map +1 -1
  11. package/dist/devices-hap/ceilinglight.d.ts.map +1 -1
  12. package/dist/devices-hap/ceilinglight.js +13 -7
  13. package/dist/devices-hap/ceilinglight.js.map +1 -1
  14. package/dist/devices-hap/colorbulb.d.ts.map +1 -1
  15. package/dist/devices-hap/colorbulb.js +49 -9
  16. package/dist/devices-hap/colorbulb.js.map +1 -1
  17. package/dist/devices-hap/contact.js +3 -3
  18. package/dist/devices-hap/curtain.js +2 -2
  19. package/dist/devices-hap/curtain.js.map +1 -1
  20. package/dist/devices-hap/device.d.ts +18 -8
  21. package/dist/devices-hap/device.d.ts.map +1 -1
  22. package/dist/devices-hap/device.js +141 -69
  23. package/dist/devices-hap/device.js.map +1 -1
  24. package/dist/devices-hap/fan.d.ts.map +1 -1
  25. package/dist/devices-hap/fan.js +12 -6
  26. package/dist/devices-hap/fan.js.map +1 -1
  27. package/dist/devices-hap/hub.d.ts.map +1 -1
  28. package/dist/devices-hap/hub.js +6 -5
  29. package/dist/devices-hap/hub.js.map +1 -1
  30. package/dist/devices-hap/humidifier.d.ts +5 -0
  31. package/dist/devices-hap/humidifier.d.ts.map +1 -1
  32. package/dist/devices-hap/humidifier.js +92 -4
  33. package/dist/devices-hap/humidifier.js.map +1 -1
  34. package/dist/devices-hap/iosensor.d.ts.map +1 -1
  35. package/dist/devices-hap/iosensor.js +36 -21
  36. package/dist/devices-hap/iosensor.js.map +1 -1
  37. package/dist/devices-hap/lightstrip.d.ts.map +1 -1
  38. package/dist/devices-hap/lightstrip.js +38 -8
  39. package/dist/devices-hap/lightstrip.js.map +1 -1
  40. package/dist/devices-hap/lock.d.ts.map +1 -1
  41. package/dist/devices-hap/lock.js +14 -6
  42. package/dist/devices-hap/lock.js.map +1 -1
  43. package/dist/devices-hap/meter.d.ts.map +1 -1
  44. package/dist/devices-hap/meter.js +6 -5
  45. package/dist/devices-hap/meter.js.map +1 -1
  46. package/dist/devices-hap/meterplus.d.ts.map +1 -1
  47. package/dist/devices-hap/meterplus.js +6 -5
  48. package/dist/devices-hap/meterplus.js.map +1 -1
  49. package/dist/devices-hap/meterpro.d.ts.map +1 -1
  50. package/dist/devices-hap/meterpro.js +7 -6
  51. package/dist/devices-hap/meterpro.js.map +1 -1
  52. package/dist/devices-hap/motion.js +3 -3
  53. package/dist/devices-hap/plug.d.ts.map +1 -1
  54. package/dist/devices-hap/plug.js +11 -6
  55. package/dist/devices-hap/plug.js.map +1 -1
  56. package/dist/devices-hap/relayswitch.js +3 -3
  57. package/dist/devices-hap/robotvacuumcleaner.d.ts.map +1 -1
  58. package/dist/devices-hap/robotvacuumcleaner.js +13 -6
  59. package/dist/devices-hap/robotvacuumcleaner.js.map +1 -1
  60. package/dist/devices-hap/waterdetector.js +3 -3
  61. package/dist/devices-matter/BaseMatterAccessory.d.ts +27 -0
  62. package/dist/devices-matter/BaseMatterAccessory.d.ts.map +1 -1
  63. package/dist/devices-matter/BaseMatterAccessory.js +169 -5
  64. package/dist/devices-matter/BaseMatterAccessory.js.map +1 -1
  65. package/dist/devices-matter/ColorLightAccessory.d.ts.map +1 -1
  66. package/dist/devices-matter/ColorLightAccessory.js +12 -12
  67. package/dist/devices-matter/ColorLightAccessory.js.map +1 -1
  68. package/dist/devices-matter/ColorTemperatureLightAccessory.d.ts.map +1 -1
  69. package/dist/devices-matter/ColorTemperatureLightAccessory.js +5 -7
  70. package/dist/devices-matter/ColorTemperatureLightAccessory.js.map +1 -1
  71. package/dist/devices-matter/DimmableLightAccessory.js +9 -9
  72. package/dist/devices-matter/DimmableLightAccessory.js.map +1 -1
  73. package/dist/devices-matter/ExtendedColorLightAccessory.d.ts.map +1 -1
  74. package/dist/devices-matter/ExtendedColorLightAccessory.js +14 -15
  75. package/dist/devices-matter/ExtendedColorLightAccessory.js.map +1 -1
  76. package/dist/devices-matter/OnOffLightAccessory.d.ts.map +1 -1
  77. package/dist/devices-matter/OnOffLightAccessory.js +8 -16
  78. package/dist/devices-matter/OnOffLightAccessory.js.map +1 -1
  79. package/dist/devices-matter/OnOffOutletAccessory.d.ts +2 -0
  80. package/dist/devices-matter/OnOffOutletAccessory.d.ts.map +1 -1
  81. package/dist/devices-matter/OnOffOutletAccessory.js +10 -7
  82. package/dist/devices-matter/OnOffOutletAccessory.js.map +1 -1
  83. package/dist/devices-matter/OnOffSwitchAccessory.js +2 -2
  84. package/dist/devices-matter/OnOffSwitchAccessory.js.map +1 -1
  85. package/dist/devices-matter/RoboticVacuumAccessory.d.ts +36 -43
  86. package/dist/devices-matter/RoboticVacuumAccessory.d.ts.map +1 -1
  87. package/dist/devices-matter/RoboticVacuumAccessory.js +478 -268
  88. package/dist/devices-matter/RoboticVacuumAccessory.js.map +1 -1
  89. package/dist/devices-matter/VenetianBlindAccessory.d.ts +6 -6
  90. package/dist/devices-matter/VenetianBlindAccessory.d.ts.map +1 -1
  91. package/dist/devices-matter/VenetianBlindAccessory.js.map +1 -1
  92. package/dist/devices-matter/WindowBlindAccessory.d.ts +5 -5
  93. package/dist/devices-matter/WindowBlindAccessory.d.ts.map +1 -1
  94. package/dist/devices-matter/WindowBlindAccessory.js +57 -6
  95. package/dist/devices-matter/WindowBlindAccessory.js.map +1 -1
  96. package/dist/homebridge-ui/public/index.html +219 -19
  97. package/dist/homebridge-ui/server.js +0 -31
  98. package/dist/homebridge-ui/server.js.map +1 -1
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +4 -9
  101. package/dist/index.js.map +1 -1
  102. package/dist/irdevice/irdevice.d.ts +11 -10
  103. package/dist/irdevice/irdevice.d.ts.map +1 -1
  104. package/dist/irdevice/irdevice.js +76 -35
  105. package/dist/irdevice/irdevice.js.map +1 -1
  106. package/dist/platform-hap.d.ts +26 -15
  107. package/dist/platform-hap.d.ts.map +1 -1
  108. package/dist/platform-hap.js +333 -153
  109. package/dist/platform-hap.js.map +1 -1
  110. package/dist/platform-matter.d.ts +93 -6
  111. package/dist/platform-matter.d.ts.map +1 -1
  112. package/dist/platform-matter.js +1878 -229
  113. package/dist/platform-matter.js.map +1 -1
  114. package/dist/settings.d.ts +75 -7
  115. package/dist/settings.d.ts.map +1 -1
  116. package/dist/settings.js.map +1 -1
  117. package/dist/test/apiRequestTracker.test.d.ts +2 -0
  118. package/dist/test/apiRequestTracker.test.d.ts.map +1 -0
  119. package/dist/test/apiRequestTracker.test.js +392 -0
  120. package/dist/test/apiRequestTracker.test.js.map +1 -0
  121. package/dist/test/hap/device-webhook-context.test.d.ts +2 -0
  122. package/dist/test/hap/device-webhook-context.test.d.ts.map +1 -0
  123. package/dist/test/hap/device-webhook-context.test.js +128 -0
  124. package/dist/test/hap/device-webhook-context.test.js.map +1 -0
  125. package/dist/test/hap/platform-hap.logging.test.d.ts +2 -0
  126. package/dist/test/hap/platform-hap.logging.test.d.ts.map +1 -0
  127. package/dist/test/hap/platform-hap.logging.test.js +33 -0
  128. package/dist/test/hap/platform-hap.logging.test.js.map +1 -0
  129. package/dist/test/hap/platform-hap.test.d.ts +2 -0
  130. package/dist/test/hap/platform-hap.test.d.ts.map +1 -0
  131. package/dist/test/hap/platform-hap.test.js +62 -0
  132. package/dist/test/hap/platform-hap.test.js.map +1 -0
  133. package/dist/test/helpers/platform-fixtures.d.ts +9 -0
  134. package/dist/test/helpers/platform-fixtures.d.ts.map +1 -0
  135. package/dist/test/helpers/platform-fixtures.js +30 -0
  136. package/dist/test/helpers/platform-fixtures.js.map +1 -0
  137. package/dist/test/homebridge-ui/server.test.d.ts +2 -0
  138. package/dist/test/homebridge-ui/server.test.d.ts.map +1 -0
  139. package/dist/test/homebridge-ui/server.test.js +445 -0
  140. package/dist/test/homebridge-ui/server.test.js.map +1 -0
  141. package/dist/{index.test.d.ts.map → test/index.test.d.ts.map} +1 -1
  142. package/dist/test/index.test.js +19 -0
  143. package/dist/test/index.test.js.map +1 -0
  144. package/dist/test/matter/devices-matter/baseMatterAccessory.test.d.ts +2 -0
  145. package/dist/test/matter/devices-matter/baseMatterAccessory.test.d.ts.map +1 -0
  146. package/dist/test/matter/devices-matter/baseMatterAccessory.test.js +71 -0
  147. package/dist/test/matter/devices-matter/baseMatterAccessory.test.js.map +1 -0
  148. package/dist/test/matter/platform-matter.additional.test.d.ts +2 -0
  149. package/dist/test/matter/platform-matter.additional.test.d.ts.map +1 -0
  150. package/dist/test/matter/platform-matter.additional.test.js +35 -0
  151. package/dist/test/matter/platform-matter.additional.test.js.map +1 -0
  152. package/dist/test/matter/platform-matter.bleparse.test.d.ts +2 -0
  153. package/dist/test/matter/platform-matter.bleparse.test.d.ts.map +1 -0
  154. package/dist/test/matter/platform-matter.bleparse.test.js +43 -0
  155. package/dist/test/matter/platform-matter.bleparse.test.js.map +1 -0
  156. package/dist/test/matter/platform-matter.cleanup.test.d.ts +2 -0
  157. package/dist/test/matter/platform-matter.cleanup.test.d.ts.map +1 -0
  158. package/dist/test/matter/platform-matter.cleanup.test.js +70 -0
  159. package/dist/test/matter/platform-matter.cleanup.test.js.map +1 -0
  160. package/dist/test/matter/platform-matter.keepstale.test.d.ts +2 -0
  161. package/dist/test/matter/platform-matter.keepstale.test.d.ts.map +1 -0
  162. package/dist/test/matter/platform-matter.keepstale.test.js +27 -0
  163. package/dist/test/matter/platform-matter.keepstale.test.js.map +1 -0
  164. package/dist/test/matter/platform-matter.logging.test.d.ts +2 -0
  165. package/dist/test/matter/platform-matter.logging.test.d.ts.map +1 -0
  166. package/dist/test/matter/platform-matter.logging.test.js +29 -0
  167. package/dist/test/matter/platform-matter.logging.test.js.map +1 -0
  168. package/dist/test/matter/platform-matter.mapping.test.d.ts +2 -0
  169. package/dist/test/matter/platform-matter.mapping.test.d.ts.map +1 -0
  170. package/dist/test/matter/platform-matter.mapping.test.js +43 -0
  171. package/dist/test/matter/platform-matter.mapping.test.js.map +1 -0
  172. package/dist/test/matter/platform-matter.openapi-mapping.test.d.ts +2 -0
  173. package/dist/test/matter/platform-matter.openapi-mapping.test.d.ts.map +1 -0
  174. package/dist/test/matter/platform-matter.openapi-mapping.test.js +84 -0
  175. package/dist/test/matter/platform-matter.openapi-mapping.test.js.map +1 -0
  176. package/dist/test/matter/platform-matter.test.d.ts +2 -0
  177. package/dist/test/matter/platform-matter.test.d.ts.map +1 -0
  178. package/dist/test/matter/platform-matter.test.js +117 -0
  179. package/dist/test/matter/platform-matter.test.js.map +1 -0
  180. package/dist/test/matter/platform-matter.unregister.test.d.ts +2 -0
  181. package/dist/test/matter/platform-matter.unregister.test.d.ts.map +1 -0
  182. package/dist/test/matter/platform-matter.unregister.test.js +30 -0
  183. package/dist/test/matter/platform-matter.unregister.test.js.map +1 -0
  184. package/dist/test/matter/platform-matter.webhook.test.d.ts +2 -0
  185. package/dist/test/matter/platform-matter.webhook.test.d.ts.map +1 -0
  186. package/dist/test/matter/platform-matter.webhook.test.js +46 -0
  187. package/dist/test/matter/platform-matter.webhook.test.js.map +1 -0
  188. package/dist/test/utils.test.d.ts +2 -0
  189. package/dist/test/utils.test.d.ts.map +1 -0
  190. package/dist/test/utils.test.js +95 -0
  191. package/dist/test/utils.test.js.map +1 -0
  192. package/dist/test/verifyconfig.test.d.ts.map +1 -0
  193. package/dist/{verifyconfig.test.js → test/verifyconfig.test.js} +2 -2
  194. package/dist/test/verifyconfig.test.js.map +1 -0
  195. package/dist/utils.d.ts +204 -3
  196. package/dist/utils.d.ts.map +1 -1
  197. package/dist/utils.js +713 -33
  198. package/dist/utils.js.map +1 -1
  199. package/docs/assets/highlight.css +14 -0
  200. package/docs/assets/main.js +2 -2
  201. package/docs/index.html +31 -2
  202. package/docs/variables/default.html +1 -1
  203. package/package.json +15 -15
  204. package/src/devices-hap/airpurifier.ts +11 -6
  205. package/src/devices-hap/blindtilt.ts +3 -3
  206. package/src/devices-hap/bot.ts +15 -5
  207. package/src/devices-hap/ceilinglight.ts +12 -7
  208. package/src/devices-hap/colorbulb.ts +46 -10
  209. package/src/devices-hap/contact.ts +3 -3
  210. package/src/devices-hap/curtain.ts +2 -2
  211. package/src/devices-hap/device.ts +149 -70
  212. package/src/devices-hap/fan.ts +11 -6
  213. package/src/devices-hap/hub.ts +6 -5
  214. package/src/devices-hap/humidifier.ts +97 -4
  215. package/src/devices-hap/iosensor.ts +36 -21
  216. package/src/devices-hap/lightstrip.ts +35 -8
  217. package/src/devices-hap/lock.ts +13 -6
  218. package/src/devices-hap/meter.ts +6 -5
  219. package/src/devices-hap/meterplus.ts +6 -5
  220. package/src/devices-hap/meterpro.ts +7 -6
  221. package/src/devices-hap/motion.ts +3 -3
  222. package/src/devices-hap/plug.ts +10 -6
  223. package/src/devices-hap/relayswitch.ts +3 -3
  224. package/src/devices-hap/robotvacuumcleaner.ts +12 -6
  225. package/src/devices-hap/waterdetector.ts +3 -3
  226. package/src/devices-matter/BaseMatterAccessory.ts +176 -5
  227. package/src/devices-matter/ColorLightAccessory.ts +12 -12
  228. package/src/devices-matter/ColorTemperatureLightAccessory.ts +5 -7
  229. package/src/devices-matter/DimmableLightAccessory.ts +9 -9
  230. package/src/devices-matter/ExtendedColorLightAccessory.ts +14 -15
  231. package/src/devices-matter/OnOffLightAccessory.ts +8 -16
  232. package/src/devices-matter/OnOffOutletAccessory.ts +12 -7
  233. package/src/devices-matter/OnOffSwitchAccessory.ts +2 -2
  234. package/src/devices-matter/RoboticVacuumAccessory.ts +529 -315
  235. package/src/devices-matter/VenetianBlindAccessory.ts +5 -5
  236. package/src/devices-matter/WindowBlindAccessory.ts +53 -10
  237. package/src/homebridge-ui/public/index.html +219 -19
  238. package/src/homebridge-ui/server.ts +0 -34
  239. package/src/index.ts +4 -10
  240. package/src/irdevice/irdevice.ts +74 -35
  241. package/src/platform-hap.ts +365 -169
  242. package/src/platform-matter.ts +1923 -230
  243. package/src/settings.ts +78 -3
  244. package/src/test/apiRequestTracker.test.ts +417 -0
  245. package/src/test/hap/device-webhook-context.test.ts +136 -0
  246. package/src/test/hap/platform-hap.logging.test.ts +36 -0
  247. package/src/test/hap/platform-hap.test.ts +70 -0
  248. package/src/test/helpers/platform-fixtures.ts +33 -0
  249. package/src/test/homebridge-ui/server.test.ts +486 -0
  250. package/src/test/index.test.ts +24 -0
  251. package/src/test/matter/devices-matter/baseMatterAccessory.test.ts +88 -0
  252. package/src/test/matter/platform-matter.additional.test.ts +44 -0
  253. package/src/test/matter/platform-matter.bleparse.test.ts +47 -0
  254. package/src/test/matter/platform-matter.cleanup.test.ts +86 -0
  255. package/src/test/matter/platform-matter.keepstale.test.ts +37 -0
  256. package/src/test/matter/platform-matter.logging.test.ts +33 -0
  257. package/src/test/matter/platform-matter.mapping.test.ts +57 -0
  258. package/src/test/matter/platform-matter.openapi-mapping.test.ts +109 -0
  259. package/src/test/matter/platform-matter.test.ts +144 -0
  260. package/src/test/matter/platform-matter.unregister.test.ts +39 -0
  261. package/src/test/matter/platform-matter.webhook.test.ts +54 -0
  262. package/src/test/utils.test.ts +96 -0
  263. package/src/{verifyconfig.test.ts → test/verifyconfig.test.ts} +12 -11
  264. package/src/utils.ts +777 -36
  265. package/dist/index.test.js +0 -14
  266. package/dist/index.test.js.map +0 -1
  267. package/dist/verifyconfig.test.d.ts.map +0 -1
  268. package/dist/verifyconfig.test.js.map +0 -1
  269. package/src/index.test.ts +0 -19
  270. /package/dist/{index.test.d.ts → test/index.test.d.ts} +0 -0
  271. /package/dist/{verifyconfig.test.d.ts → test/verifyconfig.test.d.ts} +0 -0
@@ -1,105 +1,167 @@
1
- /* global NodeJS */
1
+ //
2
2
 
3
3
  /**
4
- * Robotic Vacuum Cleaner Accessory Class
4
+ * Robotic Vacuum Cleaner Accessory (SwitchBot-aligned)
5
5
  *
6
- * This is a comprehensive example demonstrating all available features
7
- * of the RoboticVacuumCleaner device type including:
8
- * - Multiple run modes (Idle, Cleaning, Mapping)
9
- * - Multiple clean modes (Vacuum, Mop, Vacuum & Mop, Deep Clean)
10
- * - Operational state management with realistic transitions
11
- * - Service area (room) support
6
+ * This implementation is intentionally limited to SwitchBot OpenAPI-supported
7
+ * behaviors for Robot Vacuum families. We expose only commands and modes that
8
+ * exist in the public API docs:
9
+ * https://github.com/OpenWonderLabs/SwitchBotAPI
12
10
  *
13
- * IMPORTANT: Platform Matter accessories
14
- * =======================================
15
- * This vacuum is registered as a platform accessory using
16
- * api.matter.registerPlatformAccessories(), which works the same as HAP.
17
- * Platform accessories are registered synchronously and are immediately ready for use.
11
+ * Capability matrix (derived from OpenAPI v1.1):
12
+ * - S1 / S1 Plus / K10+ / K10+ Pro
13
+ * Commands: start, stop, dock, PowLevel {0-3}
14
+ * No pause/resume, no mop, no area selection
15
+ * - K10+ Pro Combo
16
+ * • Commands: startClean {action: sweep|mop, param: {fanLevel 1-4}}, pause, dock
17
+ * • changeParam {fanLevel}, setVolume
18
+ * - S10 / S20 / K11+ / K20+ Pro
19
+ * • Commands: startClean {action: sweep|sweep_mop|mop (model-dependent), param: {fanLevel 1-4, waterLevel 1-2?}},
20
+ * pause, dock, changeParam, setVolume, (S10/S20) selfClean/addWaterForHumi
21
+ * • Some models support waterLevel, some do not (e.g. K10+ Pro Combo)
18
22
  *
19
- * Demo behavior:
20
- * - Start/Resume: Sets run mode to "Cleaning", runs for 15/10 seconds, then returns to dock
21
- * - Return to dock: Immediately sets "Idle" mode, then Seeking (5s) → Charging (3s) → Docked
22
- * - Stop: Immediately stops and resets to "Idle" mode
23
- * - Pause: Cancels automatic completion timer, keeps "Cleaning" mode
23
+ * Matter clusters exposed:
24
+ * - rvcRunMode: Idle/Cleaning only (no Mapping mode via OpenAPI)
25
+ * - rvcCleanMode: Interpreted per model:
26
+ * Basic vac (S1/K10 family): suction levels Quiet/Standard/Strong/MAX
27
+ * Mop-capable (K10+ Pro Combo/K11+/K20+ Pro): action Vacuum or Mop
28
+ * • Mop+Vac-capable (S10/S20): action → Vacuum or Vacuum & Mop
29
+ * - rvcOperationalState: start/stop/pause/goHome handlers are attached only when supported
30
+ * - serviceArea: NOT exposed (area/room selection isn’t in public OpenAPI)
24
31
  */
25
32
 
26
33
  import type { API, Logger, MatterRequests } from 'homebridge'
27
34
 
28
35
  import { BaseMatterAccessory } from './BaseMatterAccessory.js'
29
36
 
30
- export class RoboticVacuumAccessory extends BaseMatterAccessory {
31
- private activeTimers: NodeJS.Timeout[] = []
37
+ type SwitchBotVacuumModel
38
+ = 'S1'
39
+ | 'S1 Plus'
40
+ | 'S1 Pro'
41
+ | 'S1 Mini'
42
+ | 'WoSweeper'
43
+ | 'WoSweeperMini'
44
+ | 'K10+'
45
+ | 'K10+ Pro'
46
+ | 'K10+ Pro Combo'
47
+ | 'S10'
48
+ | 'S20'
49
+ | 'K11+'
50
+ | 'K20+ Pro'
51
+
52
+ interface VacuumCapabilities {
53
+ // Whether pause/resume is available
54
+ pause: boolean
55
+ resume: boolean
56
+ // Clean action type exposed via rvcCleanMode
57
+ cleanAction: 'vacuum-only' | 'vacuum-or-mop' | 'vacuum-or-vacmop'
58
+ // Suction mapping: basic uses PowLevel 0-3; advanced uses fanLevel 1-4
59
+ suctionKind: 'powLevel-0-3' | 'fanLevel-1-4' | 'none'
60
+ // Whether waterLevel parameter is supported on start/changeParam
61
+ waterLevel: boolean
62
+ // Advanced commands available (S10/S20 only)
63
+ advancedCommands?: {
64
+ setVolume?: boolean
65
+ selfClean?: boolean
66
+ addWaterForHumi?: boolean
67
+ }
68
+ }
32
69
 
33
- constructor(api: API, log: Logger, opts?: Partial<import('./BaseMatterAccessory').BaseMatterAccessoryConfig & { deviceId?: string }>) {
34
- const serialNumber = opts?.serialNumber ?? 'VACUUM-001'
35
- const clusters = opts?.clusters ?? {
36
- powerSource: {
37
- status: 0,
38
- order: 0,
39
- description: 'Battery',
40
- batPercentRemaining: 100,
41
- batChargeLevel: 2,
42
- batReplaceability: 1,
43
- },
44
- rvcRunMode: {
45
- supportedModes: [
46
- { label: 'Idle', mode: 0, modeTags: [{ value: 16384 }] },
47
- { label: 'Cleaning', mode: 1, modeTags: [{ value: 16385 }] },
48
- { label: 'Mapping', mode: 2, modeTags: [{ value: 16386 }] },
49
- ],
50
- currentMode: 0,
51
- },
52
- rvcCleanMode: {
53
- supportedModes: [
54
- { label: 'Vacuum', mode: 0, modeTags: [{ value: 16385 }] },
55
- { label: 'Mop', mode: 1, modeTags: [{ value: 16386 }] },
56
- { label: 'Vacuum & Mop', mode: 2, modeTags: [{ value: 16385 }, { value: 16386 }] },
57
- { label: 'Deep Clean', mode: 3, modeTags: [{ value: 16384 }] },
58
- { label: 'Deep Vacuum', mode: 4, modeTags: [{ value: 16384 }, { value: 16385 }] },
59
- { label: 'Deep Mop', mode: 5, modeTags: [{ value: 16384 }, { value: 16386 }] },
60
- { label: 'Quick Clean', mode: 6, modeTags: [{ value: 1 }, { value: 16385 }] },
61
- { label: 'Max Clean', mode: 7, modeTags: [{ value: 7 }, { value: 16385 }] },
62
- { label: 'Min Clean', mode: 8, modeTags: [{ value: 6 }, { value: 16385 }] },
63
- { label: 'Quiet Vacuum', mode: 9, modeTags: [{ value: 2 }, { value: 16385 }] },
64
- { label: 'Quiet Mop', mode: 10, modeTags: [{ value: 2 }, { value: 16386 }] },
65
- { label: 'Night Mode', mode: 11, modeTags: [{ value: 8 }, { value: 16385 }] },
66
- { label: 'Eco Vacuum', mode: 12, modeTags: [{ value: 4 }, { value: 16385 }] },
67
- { label: 'Eco Mop', mode: 13, modeTags: [{ value: 4 }, { value: 16386 }] },
68
- { label: 'Auto', mode: 14, modeTags: [{ value: 0 }, { value: 16385 }] },
69
- ],
70
- currentMode: 0,
71
- },
72
- rvcOperationalState: {
73
- operationalStateList: [
74
- { operationalStateId: 0 },
75
- { operationalStateId: 1 },
76
- { operationalStateId: 2 },
77
- { operationalStateId: 3 },
78
- { operationalStateId: 64 },
79
- { operationalStateId: 65 },
80
- { operationalStateId: 66 },
81
- ],
82
- operationalState: 66,
83
- },
84
- serviceArea: {
85
- supportedMaps: [],
86
- supportedAreas: [
87
- { areaId: 0, mapId: null, areaInfo: { locationInfo: { locationName: 'Living Room', floorNumber: 0, areaType: 7 }, landmarkInfo: null } },
88
- { areaId: 1, mapId: null, areaInfo: { locationInfo: { locationName: 'Kitchen', floorNumber: 0, areaType: 10 }, landmarkInfo: null } },
89
- { areaId: 2, mapId: null, areaInfo: { locationInfo: { locationName: 'Bedroom', floorNumber: 0, areaType: 2 }, landmarkInfo: null } },
90
- { areaId: 3, mapId: null, areaInfo: { locationInfo: { locationName: 'Bathroom', floorNumber: 0, areaType: 6 }, landmarkInfo: null } },
91
- ],
92
- selectedAreas: [0, 1, 2, 3],
70
+ function detectModelFromContext(ctx?: Record<string, unknown>): SwitchBotVacuumModel | undefined {
71
+ const t = (ctx?.deviceType as string | undefined)?.trim()
72
+ switch (t) {
73
+ case 'Robot Vacuum Cleaner S1':
74
+ return 'S1'
75
+ case 'Robot Vacuum Cleaner S1 Plus':
76
+ return 'S1 Plus'
77
+ case 'Robot Vacuum Cleaner S1 Pro':
78
+ return 'S1 Pro'
79
+ case 'Robot Vacuum Cleaner S1 Mini':
80
+ return 'S1 Mini'
81
+ case 'WoSweeper':
82
+ return 'WoSweeper'
83
+ case 'WoSweeperMini':
84
+ return 'WoSweeperMini'
85
+ case 'K10+':
86
+ return 'K10+'
87
+ case 'K10+ Pro':
88
+ return 'K10+ Pro'
89
+ case 'Robot Vacuum Cleaner K10+ Pro Combo':
90
+ case 'K10+ Pro Combo':
91
+ return 'K10+ Pro Combo'
92
+ case 'Robot Vacuum Cleaner S10':
93
+ return 'S10'
94
+ case 'Robot Vacuum Cleaner S20':
95
+ case 'S20':
96
+ return 'S20'
97
+ case 'Robot Vacuum Cleaner K11+':
98
+ case 'K11+':
99
+ return 'K11+'
100
+ case 'Robot Vacuum Cleaner K20 Plus Pro':
101
+ case 'K20+ Pro':
102
+ return 'K20+ Pro'
103
+ default:
104
+ return undefined
105
+ }
106
+ }
107
+
108
+ function capabilitiesFor(model?: SwitchBotVacuumModel): VacuumCapabilities {
109
+ // Defaults: conservative basic vac
110
+ if (!model) {
111
+ return { pause: false, resume: false, cleanAction: 'vacuum-only', suctionKind: 'powLevel-0-3', waterLevel: false }
112
+ }
113
+
114
+ // Basic vacuum models (S1 family, WoSweeper family, K10 family without Combo)
115
+ if (model === 'S1' || model === 'S1 Plus' || model === 'S1 Pro' || model === 'S1 Mini'
116
+ || model === 'WoSweeper' || model === 'WoSweeperMini'
117
+ || model === 'K10+' || model === 'K10+ Pro') {
118
+ return { pause: false, resume: false, cleanAction: 'vacuum-only', suctionKind: 'powLevel-0-3', waterLevel: false }
119
+ }
120
+
121
+ // K10+ Pro Combo: vacuum or mop, fanLevel, no waterLevel
122
+ if (model === 'K10+ Pro Combo') {
123
+ return { pause: true, resume: false, cleanAction: 'vacuum-or-mop', suctionKind: 'fanLevel-1-4', waterLevel: false }
124
+ }
125
+
126
+ // S10/S20: vacuum or vacuum+mop combo, fanLevel, waterLevel, plus advanced commands
127
+ if (model === 'S10' || model === 'S20') {
128
+ return {
129
+ pause: true,
130
+ resume: false,
131
+ cleanAction: 'vacuum-or-vacmop',
132
+ suctionKind: 'fanLevel-1-4',
133
+ waterLevel: true,
134
+ advancedCommands: {
135
+ setVolume: true,
136
+ selfClean: true,
137
+ addWaterForHumi: true,
93
138
  },
94
139
  }
140
+ }
95
141
 
96
- const handlers = opts?.handlers ?? {
97
- rvcRunMode: { changeToMode: async (request: MatterRequests.ChangeToMode) => this.handleChangeRunMode(request) },
98
- rvcCleanMode: { changeToMode: async (request: MatterRequests.ChangeToMode) => this.handleChangeCleanMode(request) },
99
- rvcOperationalState: { pause: async () => this.handlePause(), stop: async () => this.handleStop(), start: async () => this.handleStart(), resume: async () => this.handleResume(), goHome: async () => this.handleGoHome() },
100
- serviceArea: { selectAreas: async (request: MatterRequests.SelectAreas) => this.handleSelectAreas(request), skipArea: async (request: MatterRequests.SkipArea) => this.handleSkipArea(request) },
101
- }
142
+ // K11+/K20+ Pro: vacuum or mop, fanLevel, waterLevel, volume control
143
+ return {
144
+ pause: true,
145
+ resume: false,
146
+ cleanAction: 'vacuum-or-mop',
147
+ suctionKind: 'fanLevel-1-4',
148
+ waterLevel: true,
149
+ advancedCommands: {
150
+ setVolume: true,
151
+ },
152
+ }
153
+ }
102
154
 
155
+ export class RoboticVacuumAccessory extends BaseMatterAccessory {
156
+ // Track current high-level preferences that affect OpenAPI payloads
157
+ private currentCleanAction: 'vacuum' | 'mop' | 'vacuum_mop' = 'vacuum'
158
+ private currentFanLevel: 1 | 2 | 3 | 4 = 2
159
+ private capabilities: VacuumCapabilities
160
+
161
+ constructor(api: API, log: Logger, opts?: Partial<import('./BaseMatterAccessory').BaseMatterAccessoryConfig & { deviceId?: string }>) {
162
+ const serialNumber = opts?.serialNumber ?? 'VACUUM-001'
163
+ const model = detectModelFromContext(opts?.context)
164
+ const capabilities = capabilitiesFor(model)
103
165
  super(api, log, {
104
166
  uuid: opts?.uuid ?? api.matter.uuid.generate(serialNumber),
105
167
  displayName: opts?.displayName ?? 'Robot Vacuum',
@@ -109,224 +171,327 @@ export class RoboticVacuumAccessory extends BaseMatterAccessory {
109
171
  model: opts?.model ?? 'HB-MATTER-VACUUM-ROBOTIC',
110
172
  firmwareRevision: opts?.firmwareRevision ?? '2.0.0',
111
173
  hardwareRevision: opts?.hardwareRevision ?? '1.0.0',
112
- clusters,
113
- handlers,
114
- context: { deviceId: opts?.deviceId, ...(opts?.context ?? {}) },
174
+ clusters: {
175
+ rvcRunMode: {
176
+ supportedModes: [
177
+ { label: 'Idle', mode: 0, modeTags: [{ value: 16384 }] },
178
+ { label: 'Cleaning', mode: 1, modeTags: [{ value: 16385 }] },
179
+ ],
180
+ currentMode: 0,
181
+ },
182
+ rvcCleanMode: (() => {
183
+ switch (model as string) {
184
+ case 'K10':
185
+ return {
186
+ supportedModes: [
187
+ { label: 'Vacuum', mode: 0, modeTags: [{ value: 16385 }] },
188
+ ],
189
+ currentMode: 0,
190
+ }
191
+ case 'Robot Vacuum Cleaner S1':
192
+ case 'Robot Vacuum Cleaner S1 Plus':
193
+ return {
194
+ supportedModes: [
195
+ { label: 'Quiet', mode: 0, modeTags: [{ value: 2 }] },
196
+ { label: 'Standard', mode: 1, modeTags: [{ value: 16384 }] },
197
+ { label: 'Strong', mode: 2, modeTags: [{ value: 7 }] },
198
+ { label: 'MAX', mode: 3, modeTags: [{ value: 8 }] },
199
+ ],
200
+ currentMode: 1,
201
+ }
202
+ case 'Mini Robot Vacuum K10+':
203
+ case 'Mini Robot Vacuum K10+ Pro':
204
+ case 'K10+ Pro Combo':
205
+ return {
206
+ supportedModes: [
207
+ { label: 'Vacuum', mode: 0, modeTags: [{ value: 16385 }] },
208
+ { label: 'Mop', mode: 1, modeTags: [{ value: 16386 }] },
209
+ { label: 'Sweep & Mop', mode: 2, modeTags: [{ value: 16385 }, { value: 16386 }] },
210
+ ],
211
+ currentMode: 0,
212
+ }
213
+ case 'Multitasking Household Robot K20+ Pro':
214
+ return {
215
+ supportedModes: [
216
+ { label: 'Vacuum', mode: 0, modeTags: [{ value: 16385 }] },
217
+ { label: 'Vacuum & Mop', mode: 1, modeTags: [{ value: 16385 }, { value: 16386 }] },
218
+ { label: 'Deep Clean', mode: 2, modeTags: [{ value: 16384 }] },
219
+ ],
220
+ currentMode: 0,
221
+ }
222
+ case 'Floor Cleaning Robot S10':
223
+ case 'Floor Cleaning Robot S20':
224
+ return {
225
+ supportedModes: [
226
+ { label: 'Vacuum', mode: 0, modeTags: [{ value: 16385 }] },
227
+ { label: 'Mop', mode: 1, modeTags: [{ value: 16386 }] },
228
+ ],
229
+ currentMode: 0,
230
+ }
231
+ case 'Robot Vacuum K11+':
232
+ return {
233
+ supportedModes: [
234
+ { label: 'Quiet', mode: 0, modeTags: [{ value: 2 }] },
235
+ { label: 'Standard', mode: 1, modeTags: [{ value: 16384 }] },
236
+ { label: 'Strong', mode: 2, modeTags: [{ value: 7 }] },
237
+ { label: 'MAX', mode: 3, modeTags: [{ value: 8 }] },
238
+ { label: 'Deep Clean', mode: 4, modeTags: [{ value: 16384 }, { value: 16385 }] },
239
+ ],
240
+ currentMode: 1,
241
+ }
242
+ default:
243
+ return {
244
+ supportedModes: [
245
+ { label: 'Vacuum', mode: 0, modeTags: [{ value: 16385 }] },
246
+ { label: 'Vacuum & Mop', mode: 1, modeTags: [{ value: 16385 }, { value: 16386 }] },
247
+ { label: 'Deep Clean', mode: 2, modeTags: [{ value: 16384 }] },
248
+ ],
249
+ currentMode: 0,
250
+ }
251
+ }
252
+ })(),
253
+ rvcOperationalState: {
254
+ operationalStateList: [
255
+ { operationalStateId: 0 }, // Stopped
256
+ { operationalStateId: 1 }, // Running
257
+ { operationalStateId: 2 }, // Paused
258
+ { operationalStateId: 3 }, // Error
259
+ { operationalStateId: 64 }, // Seeking Charger
260
+ { operationalStateId: 65 }, // Charging
261
+ { operationalStateId: 66 }, // Docked
262
+ { operationalStateId: 67 }, // In Remote Control
263
+ { operationalStateId: 68 }, // In Dust Collecting
264
+ ],
265
+ operationalState: 66,
266
+ },
267
+ },
268
+ handlers: (() => {
269
+ const opHandlers: Record<string, any> = {
270
+ stop: async () => this.handleStop(),
271
+ start: async () => this.handleStart(),
272
+ goHome: async () => this.handleGoHome(),
273
+ // Always register resume handler, log warning if not supported
274
+ resume: async () => {
275
+ if (capabilities.resume) {
276
+ await this.handleResume()
277
+ } else {
278
+ this.logWarn(`Resume operation is not supported for model: ${this.model}`)
279
+ }
280
+ },
281
+ }
282
+ if (capabilities.pause) {
283
+ opHandlers.pause = async () => this.handlePause()
284
+ }
285
+ return {
286
+ rvcRunMode: { changeToMode: async (request: MatterRequests.ChangeToMode) => this.handleChangeRunMode(request) },
287
+ rvcCleanMode: { changeToMode: async (request: MatterRequests.ChangeToMode) => this.handleChangeCleanMode(request) },
288
+ rvcOperationalState: opHandlers as any,
289
+ }
290
+ })(),
291
+ context: { deviceId: opts?.deviceId, ...(opts?.context ?? {}), deviceType: (opts?.context as any)?.deviceType },
115
292
  })
116
293
 
294
+ this.capabilities = capabilities
117
295
  this.logInfo('initialized and ready.')
118
296
  }
119
297
 
120
298
  private async handleChangeRunMode(request: MatterRequests.ChangeToMode): Promise<void> {
121
299
  this.logInfo(`ChangeToMode (run) request received: ${JSON.stringify(request)}`)
122
300
  const { newMode } = request
123
- const modeStr = ['Idle', 'Cleaning', 'Mapping'][newMode] || `Unknown (mode=${newMode})`
124
- this.logInfo(`changing run mode to: ${modeStr}`)
125
- // TODO: await myVacuumAPI.setRunMode(newMode)
126
-
127
- // Clear any existing timers
128
- this.clearTimers()
129
-
130
- if (newMode === 1) {
131
- // Switching to Cleaning mode - start the vacuum
132
- this.updateOperationalState(1) // Running
133
-
134
- // Simulate cleaning completion after 15 seconds
135
- const completionTimer = setTimeout(() => {
136
- this.logInfo('cleaning completed, returning to dock.')
137
- this.updateRunMode(0) // Set to Idle - cleaning session ending
138
- this.returnToDock()
139
- }, 15000)
140
-
141
- this.activeTimers.push(completionTimer)
142
- } else if (newMode === 0) {
143
- // Switching to Idle mode - return to dock
144
- this.returnToDock()
145
- } else if (newMode === 2) {
146
- // Switching to Mapping mode - start mapping
147
- this.updateOperationalState(1) // Running
148
-
149
- // Simulate mapping completion after 20 seconds
150
- const completionTimer = setTimeout(() => {
151
- this.logInfo('mapping completed, returning to dock.')
152
- this.returnToDock()
153
- }, 20000)
154
-
155
- this.activeTimers.push(completionTimer)
301
+ const modeStr = ['Idle', 'Cleaning', 'Returning to Dock'][newMode] || `Unknown (mode=${newMode})`
302
+ this.logInfo(`Changing run mode to: ${modeStr}`)
303
+
304
+ switch (this.model) {
305
+ case 'Robot Vacuum Cleaner S1':
306
+ case 'Robot Vacuum Cleaner S1 Plus': {
307
+ if (newMode === 1) {
308
+ await this.handleStart()
309
+ } else if (newMode === 0) {
310
+ await this.handleGoHome()
311
+ }
312
+ break
313
+ }
314
+ case 'Mini Robot Vacuum K10+':
315
+ case 'Mini Robot Vacuum K10+ Pro':
316
+ case 'K10+ Pro Combo': {
317
+ if (newMode === 1) {
318
+ await this.handleStart()
319
+ } else if (newMode === 2) {
320
+ await this.handleDock()
321
+ }
322
+ break
323
+ }
324
+ case 'Multitasking Household Robot K20+ Pro': {
325
+ if (newMode === 1) {
326
+ await this.handleStart()
327
+ } else if (newMode === 0) {
328
+ await this.handleGoHome()
329
+ } else if (newMode === 2) {
330
+ await this.handlePause()
331
+ }
332
+ break
333
+ }
334
+ case 'Floor Cleaning Robot S10':
335
+ case 'Floor Cleaning Robot S20': {
336
+ if (newMode === 1) {
337
+ await this.handleStart()
338
+ } else if (newMode === 0) {
339
+ await this.handleGoHome()
340
+ }
341
+ break
342
+ }
343
+ case 'Robot Vacuum K11+': {
344
+ if (newMode === 1) {
345
+ await this.handleStart()
346
+ } else if (newMode === 0) {
347
+ await this.handleGoHome()
348
+ } else if (newMode === 2) {
349
+ await this.handleDock()
350
+ }
351
+ break
352
+ }
353
+ default:
354
+ this.logWarn(`Run mode change not supported for model: ${this.model}`)
156
355
  }
157
356
  }
158
357
 
159
358
  private async handleChangeCleanMode(request: MatterRequests.ChangeToMode): Promise<void> {
160
359
  this.logInfo(`ChangeToMode (clean) request received: ${JSON.stringify(request)}`)
161
360
  const { newMode } = request
162
- const modes = [
163
- 'Vacuum',
164
- 'Mop',
165
- 'Vacuum & Mop',
166
- 'Deep Clean',
167
- 'Deep Vacuum',
168
- 'Deep Mop',
169
- 'Quick Clean',
170
- 'Max Clean',
171
- 'Min Clean',
172
- 'Quiet Vacuum',
173
- 'Quiet Mop',
174
- 'Night Mode',
175
- 'Eco Vacuum',
176
- 'Eco Mop',
177
- 'Auto',
178
- ]
179
- const modeStr = modes[newMode] || `Unknown (mode=${newMode})`
180
- this.logInfo(`changing clean mode to: ${modeStr}`)
181
- // TODO: await myVacuumAPI.setCleanMode(newMode)
361
+
362
+ switch (this.model) {
363
+ case 'Robot Vacuum Cleaner S1':
364
+ case 'Robot Vacuum Cleaner S1 Plus': {
365
+ const mapPowS1 = ['Quiet', 'Standard', 'Strong', 'MAX'] as const
366
+ const labelS1 = mapPowS1[newMode] ?? `Unknown (${newMode})`
367
+ this.logInfo(`Changing suction level to: ${labelS1}`)
368
+ await this.sendOpenAPICommand('PowLevel', String(newMode))
369
+ break
370
+ }
371
+ case 'Mini Robot Vacuum K10+':
372
+ case 'Mini Robot Vacuum K10+ Pro':
373
+ case 'K10+ Pro Combo': {
374
+ const mapPowK10 = ['Vacuum', 'Mop', 'Sweep & Mop'] as const
375
+ const labelK10 = mapPowK10[newMode] ?? `Unknown (${newMode})`
376
+ this.logInfo(`Changing cleaning mode to: ${labelK10}`)
377
+ await this.sendOpenAPICommand('CleanMode', String(newMode))
378
+ break
379
+ }
380
+ case 'Multitasking Household Robot K20+ Pro': {
381
+ const mapPowK20 = ['Vacuum', 'Vacuum & Mop', 'Deep Clean'] as const
382
+ const labelK20 = mapPowK20[newMode] ?? `Unknown (${newMode})`
383
+ this.logInfo(`Changing cleaning mode to: ${labelK20}`)
384
+ await this.sendOpenAPICommand('CleanMode', String(newMode))
385
+ break
386
+ }
387
+ case 'Floor Cleaning Robot S10':
388
+ case 'Floor Cleaning Robot S20': {
389
+ const mapPowS10 = ['Vacuum', 'Mop'] as const
390
+ const labelS10 = mapPowS10[newMode] ?? `Unknown (${newMode})`
391
+ this.logInfo(`Changing cleaning mode to: ${labelS10}`)
392
+ await this.sendOpenAPICommand('CleanMode', String(newMode))
393
+ break
394
+ }
395
+ case 'Robot Vacuum K11+': {
396
+ const mapPowK11 = ['Quiet', 'Standard', 'Strong', 'MAX', 'Deep Clean'] as const
397
+ const labelK11 = mapPowK11[newMode] ?? `Unknown (${newMode})`
398
+ this.logInfo(`Changing suction level to: ${labelK11}`)
399
+ await this.sendOpenAPICommand('PowLevel', String(newMode))
400
+ break
401
+ }
402
+ default:
403
+ this.logWarn(`Clean mode change not supported for model: ${this.model}`)
404
+ }
182
405
  }
183
406
 
184
407
  private async handlePause(): Promise<void> {
185
408
  this.logInfo('pausing.')
186
- // TODO: await myVacuumAPI.pause()
187
- this.clearTimers() // Clear cleaning completion timer
188
- this.updateOperationalState(2) // Paused
409
+ try {
410
+ this.logInfo(`[OpenAPI] Sending pause command`)
411
+ await this.sendOpenAPICommand('pause')
412
+ this.logInfo(`[OpenAPI] pause command sent`)
413
+ } catch (e: any) {
414
+ this.logWarn(`OpenAPI pause failed: ${String(e?.message ?? e)}`)
415
+ }
416
+ this.updateOperationalState(2)
189
417
  }
190
418
 
191
419
  private async handleStop(): Promise<void> {
192
420
  this.logInfo('stopping.')
193
- // TODO: await myVacuumAPI.stop()
194
- this.clearTimers()
195
- this.updateRunMode(0) // Reset to Idle
196
- this.updateOperationalState(0) // Stopped
421
+ try {
422
+ if (this.capabilities.cleanAction === 'vacuum-only' && this.capabilities.suctionKind === 'powLevel-0-3') {
423
+ this.logInfo(`[OpenAPI] Sending stop command`)
424
+ await this.sendOpenAPICommand('stop')
425
+ this.logInfo(`[OpenAPI] stop command sent`)
426
+ } else {
427
+ this.logInfo(`[OpenAPI] Sending pause command (for stop)`)
428
+ await this.sendOpenAPICommand('pause')
429
+ this.logInfo(`[OpenAPI] pause command sent (for stop)`)
430
+ }
431
+ } catch (e: any) {
432
+ this.logWarn(`OpenAPI stop/pause failed: ${String(e?.message ?? e)}`)
433
+ }
434
+ this.updateRunMode(0)
435
+ this.updateOperationalState(0)
197
436
  }
198
437
 
199
438
  private async handleStart(): Promise<void> {
200
- this.logInfo('starting (via start command).')
201
- // TODO: await myVacuumAPI.start()
202
-
203
- // Clear any existing timers
204
- this.clearTimers()
205
-
206
- this.updateRunMode(1) // Set to Cleaning mode - this will trigger the run mode handler logic
207
- this.updateOperationalState(1) // Running
208
-
209
- // Simulate cleaning completion after 15 seconds
210
- const completionTimer = setTimeout(() => {
211
- this.logInfo('cleaning completed, returning to dock.')
212
- this.updateRunMode(0) // Set to Idle - cleaning session ending
213
- this.returnToDock()
214
- }, 15000)
215
-
216
- this.activeTimers.push(completionTimer)
439
+ this.logInfo('starting clean.')
440
+ try {
441
+ if (this.capabilities.cleanAction === 'vacuum-only' && this.capabilities.suctionKind === 'powLevel-0-3') {
442
+ this.logInfo(`[OpenAPI] Sending start command`)
443
+ await this.sendOpenAPICommand('start')
444
+ this.logInfo(`[OpenAPI] start command sent`)
445
+ } else {
446
+ const action = this.currentCleanAction === 'vacuum_mop' ? 'sweep_mop' : this.currentCleanAction
447
+ const param: any = { action, param: { fanLevel: this.currentFanLevel } }
448
+ if (this.capabilities.waterLevel) {
449
+ param.param.waterLevel = 1
450
+ }
451
+ this.logInfo(`[OpenAPI] Sending startClean: ${JSON.stringify(param)}`)
452
+ await this.sendOpenAPICommand('startClean', JSON.stringify(param))
453
+ this.logInfo(`[OpenAPI] startClean command sent: ${JSON.stringify(param)}`)
454
+ }
455
+ } catch (e: any) {
456
+ this.logWarn(`OpenAPI start/startClean failed: ${String(e?.message ?? e)}`)
457
+ }
458
+ this.updateRunMode(1)
459
+ this.updateOperationalState(1)
217
460
  }
218
461
 
219
462
  private async handleResume(): Promise<void> {
220
- this.logInfo('resuming.')
221
- // TODO: await myVacuumAPI.resume()
222
-
223
- // Clear any existing timers
224
- this.clearTimers()
225
-
226
- this.updateRunMode(1) // Set to Cleaning mode
227
- this.updateOperationalState(1) // Running
228
-
229
- // Simulate cleaning completion after 10 seconds (shorter since resuming)
230
- const completionTimer = setTimeout(() => {
231
- this.logInfo('cleaning completed, returning to dock.')
232
- this.updateRunMode(0) // Set to Idle - cleaning session ending
233
- this.returnToDock()
234
- }, 10000)
235
-
236
- this.activeTimers.push(completionTimer)
463
+ this.logInfo('resume requested.')
464
+ try {
465
+ this.logInfo(`[OpenAPI] Sending resume command`)
466
+ await this.sendOpenAPICommand('resume')
467
+ this.logInfo(`[OpenAPI] resume command sent`)
468
+ } catch (e: any) {
469
+ this.logWarn(`OpenAPI resume failed: ${String(e?.message ?? e)}`)
470
+ }
471
+ this.updateRunMode(1)
472
+ this.updateOperationalState(1)
237
473
  }
238
474
 
239
475
  private async handleGoHome(): Promise<void> {
240
- this.logInfo('goHome command received.')
241
- // TODO: await myVacuumAPI.goHome()
242
-
243
- // Clear any existing timers
244
- this.clearTimers()
245
-
246
- // Defer state updates to ensure handler completes first
247
- setImmediate(() => {
248
- // Set to Idle mode since we're ending the cleaning session
249
- this.updateRunMode(0)
250
-
251
- // Initiate return to dock sequence
252
- this.returnToDock()
253
- })
254
- }
255
-
256
- private async handleSelectAreas(request: MatterRequests.SelectAreas): Promise<void> {
257
- this.logInfo(`SelectAreas request received: ${JSON.stringify(request)}`)
258
- const { newAreas } = request
259
- const areaNames = newAreas.map((id: number) =>
260
- ['Living Room', 'Kitchen', 'Bedroom', 'Bathroom'][id] || `Area ${id}`,
261
- )
262
- this.logInfo(`selecting areas: ${areaNames.join(', ')}`)
263
- // TODO: await myVacuumAPI.selectAreas(newAreas)
264
- }
265
-
266
- private async handleSkipArea(request: MatterRequests.SkipArea): Promise<void> {
267
- this.logInfo(`SkipArea request received: ${JSON.stringify(request)}`)
268
- const { skippedArea } = request
269
- const areaName = ['Living Room', 'Kitchen', 'Bedroom', 'Bathroom'][skippedArea] || `Area ${skippedArea}`
270
- this.logInfo(`skipping area: ${areaName}`)
271
- // TODO: await myVacuumAPI.skipArea(skippedArea)
272
- }
273
-
274
- /**
275
- * Helper method to clear all active timers
276
- */
277
- private clearTimers(): void {
278
- this.activeTimers.forEach(timer => clearTimeout(timer))
279
- this.activeTimers = []
280
- }
281
-
282
- /**
283
- * Helper method to initiate return to dock sequence
284
- * Can be called synchronously from other handlers
285
- */
286
- private returnToDock(): void {
287
- this.logInfo('initiating return to dock sequence.')
288
-
289
- // Defer ALL state updates to ensure handler completes first
290
- setImmediate(() => {
291
- // Start seeking charger directly (skip intermediate Stopped state)
292
- this.updateOperationalState(64) // Seeking Charger
293
-
294
- // After 5 seconds, start charging
295
- const chargingTimer = setTimeout(() => {
296
- this.logInfo('reached dock, now charging.')
297
- this.updateOperationalState(65) // Charging
298
-
299
- // After 3 more seconds, fully docked
300
- const dockedTimer = setTimeout(() => {
301
- this.logInfo('fully charged and docked.')
302
- this.updateOperationalState(66) // Docked
303
- }, 3000)
304
-
305
- this.activeTimers.push(dockedTimer)
306
- }, 5000)
307
-
308
- this.activeTimers.push(chargingTimer)
309
- })
476
+ this.logInfo('returning to dock.')
477
+ try {
478
+ this.logInfo(`[OpenAPI] Sending dock command`)
479
+ await this.sendOpenAPICommand('dock')
480
+ this.logInfo(`[OpenAPI] dock command sent`)
481
+ } catch (e: any) {
482
+ this.logWarn(`OpenAPI dock failed: ${String(e?.message ?? e)}`)
483
+ }
484
+ this.updateRunMode(0)
485
+ this.updateOperationalState(64)
310
486
  }
311
487
 
312
- /**
313
- * Update Methods - Use these to update the vacuum state from your API/device
314
- *
315
- * Since this is a platform accessory, state updates work immediately after registration.
316
- */
317
-
318
488
  public updateOperationalState(state: number): void {
319
489
  this.updateState('rvcOperationalState', { operationalState: state })
320
490
  const states = [
321
491
  'Stopped',
322
492
  'Running',
323
493
  'Paused',
324
- 'Error',
325
- null,
326
- null,
327
- null,
328
- null,
329
- ...Array.from({ length: 56 }).fill(null),
494
+ ...Array.from({ length: 61 }).fill(null),
330
495
  'Seeking Charger',
331
496
  'Charging',
332
497
  'Docked',
@@ -336,72 +501,121 @@ export class RoboticVacuumAccessory extends BaseMatterAccessory {
336
501
 
337
502
  public updateRunMode(mode: number): void {
338
503
  this.updateState('rvcRunMode', { currentMode: mode })
339
- const modes = ['Idle', 'Cleaning', 'Mapping']
504
+ const modes = ['Idle', 'Cleaning']
340
505
  this.logInfo(`run mode updated to: ${modes[mode] || `Unknown (${mode})`}`)
341
506
  }
342
507
 
343
508
  public updateCleanMode(mode: number): void {
344
509
  this.updateState('rvcCleanMode', { currentMode: mode })
345
- const modes = [
346
- 'Vacuum',
347
- 'Mop',
348
- 'Vacuum & Mop',
349
- 'Deep Clean',
350
- 'Deep Vacuum',
351
- 'Deep Mop',
352
- 'Quick Clean',
353
- 'Max Clean',
354
- 'Min Clean',
355
- 'Quiet Vacuum',
356
- 'Quiet Mop',
357
- 'Night Mode',
358
- 'Eco Vacuum',
359
- 'Eco Mop',
360
- 'Auto',
361
- ]
362
- this.logInfo(`clean mode updated to: ${modes[mode] || `Unknown (${mode})`}`)
510
+ const label = mode === 0 ? 'Vacuum' : mode === 1 ? 'Mop' : mode === 2 ? 'Vacuum & Mop' : 'Unknown'
511
+ this.logInfo(`clean mode updated to: ${label}`)
363
512
  }
364
513
 
365
- public updateSelectedAreas(areaIds: number[]): void {
366
- this.updateState('serviceArea', { selectedAreas: areaIds })
367
- const areaNames = areaIds.map(id =>
368
- ['Living Room', 'Kitchen', 'Bedroom', 'Bathroom'][id] || `Area ${id}`,
369
- )
370
- this.logInfo(`selected areas updated to: ${areaNames.join(', ') || 'All Areas'}`)
514
+ /**
515
+ * Update battery percentage (no PowerSource cluster for this device type)
516
+ */
517
+ public async updateBatteryPercentage(percentageOrBatPercentRemaining: number): Promise<void> {
518
+ const isBatPercent = Number(percentageOrBatPercentRemaining) > 100
519
+ const percentage = isBatPercent
520
+ ? Math.max(0, Math.min(100, Math.round(Number(percentageOrBatPercentRemaining) / 2)))
521
+ : Math.max(0, Math.min(100, Math.round(Number(percentageOrBatPercentRemaining))))
522
+
523
+ let chargeLevel = 0 // Ok
524
+ if (percentage < 20) {
525
+ chargeLevel = 2 // Critical
526
+ } else if (percentage < 40) {
527
+ chargeLevel = 1 // Warning
528
+ }
529
+
530
+ this.logInfo(`battery status: ${percentage}% (${chargeLevel === 0 ? 'Ok' : chargeLevel === 1 ? 'Warning' : 'Critical'})`)
531
+
532
+ if (this.context) {
533
+ (this.context as any).batteryPercentage = percentage
534
+ ;(this.context as any).batteryChargeLevel = chargeLevel
535
+ }
371
536
  }
372
537
 
373
- public updateCurrentArea(areaId: number | null): void {
374
- this.updateState('serviceArea', { currentArea: areaId })
375
- if (areaId !== null) {
376
- const areaName = ['Living Room', 'Kitchen', 'Bedroom', 'Bathroom'][areaId] || `Area ${areaId}`
377
- this.logInfo(`current area updated to: ${areaName}`)
378
- } else {
379
- this.logInfo('current area cleared')
538
+ private async handleDock(): Promise<void> {
539
+ this.logInfo('Docking the vacuum.')
540
+ try {
541
+ this.logInfo('[OpenAPI] Sending dock command')
542
+ await this.sendOpenAPICommand('dock')
543
+ this.logInfo('[OpenAPI] Dock command sent successfully')
544
+ this.updateRunMode(0) // Set to Idle after docking
545
+ this.updateOperationalState(64) // Seeking Charger state
546
+ } catch (error: any) {
547
+ this.logWarn(`Docking failed: ${String(error?.message ?? error)}`)
380
548
  }
381
549
  }
382
550
 
383
- public updateProgress(progress: Array<{ areaId: number, status: number }>): void {
384
- this.updateState('serviceArea', { progress })
385
- this.logInfo(`progress updated: ${progress.length} areas`)
551
+ private async handleServiceArea(): Promise<void> {
552
+ this.logWarn(`Service area functionality is not supported for model: ${this.model}`)
553
+ // Placeholder for potential future support.
386
554
  }
387
555
 
388
- /**
389
- * Update battery percentage
390
- * @param percentage - Battery percentage (0-100)
391
- */
392
- public async updateBatteryPercentage(percentage: number): Promise<void> {
393
- // Convert percentage to Matter format (0-200, where 200 = 100%)
394
- const batPercentRemaining = Math.max(0, Math.min(200, Math.round(percentage * 2)))
556
+ private async simulateCleaningSequence(): Promise<void> {
557
+ this.logInfo('Starting cleaning sequence simulation.')
558
+ this.updateOperationalState(1) // Set to Running
559
+
560
+ setTimeout(async () => {
561
+ this.logInfo('Cleaning sequence timer expired. Requesting device status update.')
562
+ try {
563
+ const status = await this.requestDeviceStatus()
564
+ if (status && status.operationalState) {
565
+ this.logInfo(`Device status found: ${status.operationalState}`)
566
+ this.updateOperationalState(status.operationalState)
567
+ } else {
568
+ this.logWarn('Device status not found. Setting operational state to Stopped.')
569
+ this.updateOperationalState(0) // Default to Stopped
570
+ }
571
+ } catch (error: any) {
572
+ this.logWarn(`Failed to fetch device status: ${String(error?.message ?? error)}`)
573
+ this.updateOperationalState(0) // Default to Stopped
574
+ }
575
+ }, 5000) // Simulate a 5-second cleaning sequence
576
+ }
395
577
 
396
- // Determine charge level based on percentage
397
- let batChargeLevel = 0 // Ok
398
- if (percentage < 20) {
399
- batChargeLevel = 2 // Critical
400
- } else if (percentage < 40) {
401
- batChargeLevel = 1 // Warning
578
+ private async simulateDockingSequence(): Promise<void> {
579
+ this.logInfo('Starting docking sequence simulation.')
580
+ this.updateOperationalState(64) // Set to Seeking Charger
581
+
582
+ setTimeout(async () => {
583
+ this.logInfo('Docking sequence timer expired. Requesting device status update.')
584
+ try {
585
+ const status = await this.requestDeviceStatus()
586
+ if (status && status.operationalState) {
587
+ this.logInfo(`Device status found: ${status.operationalState}`)
588
+ this.updateOperationalState(status.operationalState)
589
+ } else {
590
+ this.logWarn('Device status not found. Setting operational state to Docked.')
591
+ this.updateOperationalState(66) // Default to Docked
592
+ }
593
+ } catch (error: any) {
594
+ this.logWarn(`Failed to fetch device status: ${String(error?.message ?? error)}`)
595
+ this.updateOperationalState(66) // Default to Docked
596
+ }
597
+ }, 5000) // Simulate a 5-second docking sequence
598
+ }
599
+
600
+ private async requestDeviceStatus(): Promise<{ operationalState?: number } | null> {
601
+ this.logInfo('Requesting device status from OpenAPI.')
602
+ try {
603
+ const response = await this.sendOpenAPICommand('getDeviceStatus')
604
+ this.logInfo(`Device status response: ${JSON.stringify(response)}`)
605
+ return response
606
+ } catch (error: any) {
607
+ this.logWarn(`Failed to request device status: ${String(error?.message ?? error)}`)
608
+ return null
402
609
  }
610
+ }
611
+
612
+ public async startCleaningSimulation(): Promise<void> {
613
+ this.logInfo('Initiating cleaning simulation.')
614
+ await this.simulateCleaningSequence()
615
+ }
403
616
 
404
- await this.updateState('powerSource', { batPercentRemaining, batChargeLevel })
405
- this.logInfo(`battery updated to: ${percentage}% (${batChargeLevel === 0 ? 'Ok' : batChargeLevel === 1 ? 'Warning' : 'Critical'})`)
617
+ public async startDockingSimulation(): Promise<void> {
618
+ this.logInfo('Initiating docking simulation.')
619
+ await this.simulateDockingSequence()
406
620
  }
407
621
  }