@switchbot/homebridge-switchbot 5.0.0-beta.52 → 5.0.0-beta.53
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.
- package/dist/devices-matter/RoboticVacuumAccessory.d.ts +28 -51
- package/dist/devices-matter/RoboticVacuumAccessory.d.ts.map +1 -1
- package/dist/devices-matter/RoboticVacuumAccessory.js +256 -258
- package/dist/devices-matter/RoboticVacuumAccessory.js.map +1 -1
- package/dist/test/matter/devices-matter/roboticVacuumAccessory.test.d.ts +2 -0
- package/dist/test/matter/devices-matter/roboticVacuumAccessory.test.d.ts.map +1 -0
- package/dist/test/matter/devices-matter/roboticVacuumAccessory.test.js +366 -0
- package/dist/test/matter/devices-matter/roboticVacuumAccessory.test.js.map +1 -0
- package/docs/variables/default.html +1 -1
- package/package.json +1 -1
- package/src/devices-matter/RoboticVacuumAccessory.ts +308 -309
- package/src/test/matter/devices-matter/roboticVacuumAccessory.test.ts +453 -0
|
@@ -1,97 +1,167 @@
|
|
|
1
|
-
|
|
1
|
+
//
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Robotic Vacuum Cleaner Accessory
|
|
4
|
+
* Robotic Vacuum Cleaner Accessory (SwitchBot-aligned)
|
|
5
5
|
*
|
|
6
|
-
* This is
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
-
*
|
|
20
|
-
* -
|
|
21
|
-
* -
|
|
22
|
-
*
|
|
23
|
-
* -
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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,
|
|
85
138
|
},
|
|
86
139
|
}
|
|
140
|
+
}
|
|
87
141
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
+
}
|
|
94
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)
|
|
95
165
|
super(api, log, {
|
|
96
166
|
uuid: opts?.uuid ?? api.matter.uuid.generate(serialNumber),
|
|
97
167
|
displayName: opts?.displayName ?? 'Robot Vacuum',
|
|
@@ -101,224 +171,199 @@ export class RoboticVacuumAccessory extends BaseMatterAccessory {
|
|
|
101
171
|
model: opts?.model ?? 'HB-MATTER-VACUUM-ROBOTIC',
|
|
102
172
|
firmwareRevision: opts?.firmwareRevision ?? '2.0.0',
|
|
103
173
|
hardwareRevision: opts?.hardwareRevision ?? '1.0.0',
|
|
104
|
-
clusters
|
|
105
|
-
|
|
106
|
-
|
|
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
|
+
if (capabilities.cleanAction === 'vacuum-only') {
|
|
184
|
+
return {
|
|
185
|
+
supportedModes: [
|
|
186
|
+
{ label: 'Quiet', mode: 0, modeTags: [{ value: 2 }] },
|
|
187
|
+
{ label: 'Standard', mode: 1, modeTags: [{ value: 16384 }] },
|
|
188
|
+
{ label: 'Strong', mode: 2, modeTags: [{ value: 7 }] },
|
|
189
|
+
{ label: 'MAX', mode: 3, modeTags: [{ value: 8 }] },
|
|
190
|
+
],
|
|
191
|
+
currentMode: 1,
|
|
192
|
+
}
|
|
193
|
+
} else if (capabilities.cleanAction === 'vacuum-or-mop') {
|
|
194
|
+
return {
|
|
195
|
+
supportedModes: [
|
|
196
|
+
{ label: 'Vacuum', mode: 0, modeTags: [{ value: 16385 }] },
|
|
197
|
+
{ label: 'Mop', mode: 1, modeTags: [{ value: 16386 }] },
|
|
198
|
+
],
|
|
199
|
+
currentMode: 0,
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
return {
|
|
203
|
+
supportedModes: [
|
|
204
|
+
{ label: 'Vacuum', mode: 0, modeTags: [{ value: 16385 }] },
|
|
205
|
+
{ label: 'Vacuum & Mop', mode: 1, modeTags: [{ value: 16385 }, { value: 16386 }] },
|
|
206
|
+
],
|
|
207
|
+
currentMode: 0,
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
})(),
|
|
211
|
+
rvcOperationalState: {
|
|
212
|
+
operationalStateList: [
|
|
213
|
+
{ operationalStateId: 0 }, // Stopped
|
|
214
|
+
{ operationalStateId: 1 }, // Running
|
|
215
|
+
{ operationalStateId: 2 }, // Paused
|
|
216
|
+
{ operationalStateId: 64 }, // Seeking Charger
|
|
217
|
+
{ operationalStateId: 65 }, // Charging
|
|
218
|
+
{ operationalStateId: 66 }, // Docked
|
|
219
|
+
],
|
|
220
|
+
operationalState: 66,
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
handlers: (() => {
|
|
224
|
+
const opHandlers: Record<string, any> = {
|
|
225
|
+
stop: async () => this.handleStop(),
|
|
226
|
+
start: async () => this.handleStart(),
|
|
227
|
+
goHome: async () => this.handleGoHome(),
|
|
228
|
+
}
|
|
229
|
+
if (capabilities.pause) {
|
|
230
|
+
opHandlers.pause = async () => this.handlePause()
|
|
231
|
+
}
|
|
232
|
+
if (capabilities.resume) {
|
|
233
|
+
opHandlers.resume = async () => this.handleResume()
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
rvcRunMode: { changeToMode: async (request: MatterRequests.ChangeToMode) => this.handleChangeRunMode(request) },
|
|
237
|
+
rvcCleanMode: { changeToMode: async (request: MatterRequests.ChangeToMode) => this.handleChangeCleanMode(request) },
|
|
238
|
+
rvcOperationalState: opHandlers as any,
|
|
239
|
+
}
|
|
240
|
+
})(),
|
|
241
|
+
context: { deviceId: opts?.deviceId, ...(opts?.context ?? {}), deviceType: (opts?.context as any)?.deviceType },
|
|
107
242
|
})
|
|
108
243
|
|
|
244
|
+
this.capabilities = capabilities
|
|
109
245
|
this.logInfo('initialized and ready.')
|
|
110
246
|
}
|
|
111
247
|
|
|
112
248
|
private async handleChangeRunMode(request: MatterRequests.ChangeToMode): Promise<void> {
|
|
113
249
|
this.logInfo(`ChangeToMode (run) request received: ${JSON.stringify(request)}`)
|
|
114
250
|
const { newMode } = request
|
|
115
|
-
const modeStr = ['Idle', 'Cleaning'
|
|
251
|
+
const modeStr = ['Idle', 'Cleaning'][newMode] || `Unknown (mode=${newMode})`
|
|
116
252
|
this.logInfo(`changing run mode to: ${modeStr}`)
|
|
117
|
-
// TODO: await myVacuumAPI.setRunMode(newMode)
|
|
118
|
-
|
|
119
|
-
// Clear any existing timers
|
|
120
|
-
this.clearTimers()
|
|
121
|
-
|
|
122
253
|
if (newMode === 1) {
|
|
123
|
-
|
|
124
|
-
this.updateOperationalState(1) // Running
|
|
125
|
-
|
|
126
|
-
// Simulate cleaning completion after 15 seconds
|
|
127
|
-
const completionTimer = setTimeout(() => {
|
|
128
|
-
this.logInfo('cleaning completed, returning to dock.')
|
|
129
|
-
this.updateRunMode(0) // Set to Idle - cleaning session ending
|
|
130
|
-
this.returnToDock()
|
|
131
|
-
}, 15000)
|
|
132
|
-
|
|
133
|
-
this.activeTimers.push(completionTimer)
|
|
254
|
+
await this.handleStart()
|
|
134
255
|
} else if (newMode === 0) {
|
|
135
|
-
|
|
136
|
-
this.returnToDock()
|
|
137
|
-
} else if (newMode === 2) {
|
|
138
|
-
// Switching to Mapping mode - start mapping
|
|
139
|
-
this.updateOperationalState(1) // Running
|
|
140
|
-
|
|
141
|
-
// Simulate mapping completion after 20 seconds
|
|
142
|
-
const completionTimer = setTimeout(() => {
|
|
143
|
-
this.logInfo('mapping completed, returning to dock.')
|
|
144
|
-
this.returnToDock()
|
|
145
|
-
}, 20000)
|
|
146
|
-
|
|
147
|
-
this.activeTimers.push(completionTimer)
|
|
256
|
+
await this.handleGoHome()
|
|
148
257
|
}
|
|
149
258
|
}
|
|
150
259
|
|
|
151
260
|
private async handleChangeCleanMode(request: MatterRequests.ChangeToMode): Promise<void> {
|
|
152
261
|
this.logInfo(`ChangeToMode (clean) request received: ${JSON.stringify(request)}`)
|
|
153
262
|
const { newMode } = request
|
|
154
|
-
|
|
155
|
-
'
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
'
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
'
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
263
|
+
if (this.capabilities.cleanAction === 'vacuum-only') {
|
|
264
|
+
const mapPow = ['Quiet', 'Standard', 'Strong', 'MAX'] as const
|
|
265
|
+
const label = mapPow[newMode] ?? `Unknown (${newMode})`
|
|
266
|
+
this.logInfo(`changing suction level to: ${label}`)
|
|
267
|
+
if (this.capabilities.suctionKind === 'powLevel-0-3') {
|
|
268
|
+
try {
|
|
269
|
+
await this.sendOpenAPICommand('PowLevel', String(newMode))
|
|
270
|
+
} catch (e: any) {
|
|
271
|
+
this.logWarn(`OpenAPI PowLevel failed: ${String(e?.message ?? e)}`)
|
|
272
|
+
}
|
|
273
|
+
} else if (this.capabilities.suctionKind === 'fanLevel-1-4') {
|
|
274
|
+
this.currentFanLevel = (Math.min(3, Math.max(0, Number(newMode))) + 1) as 1 | 2 | 3 | 4
|
|
275
|
+
try {
|
|
276
|
+
await this.sendOpenAPICommand('changeParam', JSON.stringify({ fanLevel: this.currentFanLevel }))
|
|
277
|
+
} catch (e: any) {
|
|
278
|
+
this.logWarn(`OpenAPI changeParam(fanLevel) failed: ${String(e?.message ?? e)}`)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
this.updateCleanMode(newMode)
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (this.capabilities.cleanAction === 'vacuum-or-mop') {
|
|
286
|
+
const label = newMode === 0 ? 'Vacuum' : 'Mop'
|
|
287
|
+
this.currentCleanAction = newMode === 0 ? 'vacuum' : 'mop'
|
|
288
|
+
this.logInfo(`changing clean action to: ${label}`)
|
|
289
|
+
this.updateCleanMode(newMode)
|
|
290
|
+
return
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const label = newMode === 0 ? 'Vacuum' : 'Vacuum & Mop'
|
|
294
|
+
this.currentCleanAction = newMode === 0 ? 'vacuum' : 'vacuum_mop'
|
|
295
|
+
this.logInfo(`changing clean action to: ${label}`)
|
|
296
|
+
this.updateCleanMode(newMode)
|
|
174
297
|
}
|
|
175
298
|
|
|
176
299
|
private async handlePause(): Promise<void> {
|
|
177
300
|
this.logInfo('pausing.')
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
301
|
+
try {
|
|
302
|
+
await this.sendOpenAPICommand('pause')
|
|
303
|
+
} catch (e: any) {
|
|
304
|
+
this.logWarn(`OpenAPI pause failed: ${String(e?.message ?? e)}`)
|
|
305
|
+
}
|
|
306
|
+
this.updateOperationalState(2)
|
|
181
307
|
}
|
|
182
308
|
|
|
183
309
|
private async handleStop(): Promise<void> {
|
|
184
310
|
this.logInfo('stopping.')
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
311
|
+
try {
|
|
312
|
+
if (this.capabilities.cleanAction === 'vacuum-only' && this.capabilities.suctionKind === 'powLevel-0-3') {
|
|
313
|
+
await this.sendOpenAPICommand('stop')
|
|
314
|
+
} else {
|
|
315
|
+
await this.sendOpenAPICommand('pause')
|
|
316
|
+
}
|
|
317
|
+
} catch (e: any) {
|
|
318
|
+
this.logWarn(`OpenAPI stop/pause failed: ${String(e?.message ?? e)}`)
|
|
319
|
+
}
|
|
320
|
+
this.updateRunMode(0)
|
|
321
|
+
this.updateOperationalState(0)
|
|
189
322
|
}
|
|
190
323
|
|
|
191
324
|
private async handleStart(): Promise<void> {
|
|
192
|
-
this.logInfo('starting
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
this.
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
this.
|
|
325
|
+
this.logInfo('starting clean.')
|
|
326
|
+
try {
|
|
327
|
+
if (this.capabilities.cleanAction === 'vacuum-only' && this.capabilities.suctionKind === 'powLevel-0-3') {
|
|
328
|
+
await this.sendOpenAPICommand('start')
|
|
329
|
+
} else {
|
|
330
|
+
const action = this.currentCleanAction === 'vacuum_mop' ? 'sweep_mop' : this.currentCleanAction
|
|
331
|
+
const param: any = { action, param: { fanLevel: this.currentFanLevel } }
|
|
332
|
+
if (this.capabilities.waterLevel) {
|
|
333
|
+
param.param.waterLevel = 1
|
|
334
|
+
}
|
|
335
|
+
await this.sendOpenAPICommand('startClean', JSON.stringify(param))
|
|
336
|
+
}
|
|
337
|
+
} catch (e: any) {
|
|
338
|
+
this.logWarn(`OpenAPI start/startClean failed: ${String(e?.message ?? e)}`)
|
|
339
|
+
}
|
|
340
|
+
this.updateRunMode(1)
|
|
341
|
+
this.updateOperationalState(1)
|
|
209
342
|
}
|
|
210
343
|
|
|
211
344
|
private async handleResume(): Promise<void> {
|
|
212
|
-
this.logInfo('
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
// Clear any existing timers
|
|
216
|
-
this.clearTimers()
|
|
217
|
-
|
|
218
|
-
this.updateRunMode(1) // Set to Cleaning mode
|
|
219
|
-
this.updateOperationalState(1) // Running
|
|
220
|
-
|
|
221
|
-
// Simulate cleaning completion after 10 seconds (shorter since resuming)
|
|
222
|
-
const completionTimer = setTimeout(() => {
|
|
223
|
-
this.logInfo('cleaning completed, returning to dock.')
|
|
224
|
-
this.updateRunMode(0) // Set to Idle - cleaning session ending
|
|
225
|
-
this.returnToDock()
|
|
226
|
-
}, 10000)
|
|
227
|
-
|
|
228
|
-
this.activeTimers.push(completionTimer)
|
|
345
|
+
this.logInfo('resume requested.')
|
|
346
|
+
await this.handleStart()
|
|
229
347
|
}
|
|
230
348
|
|
|
231
349
|
private async handleGoHome(): Promise<void> {
|
|
232
|
-
this.logInfo('
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
// Set to Idle mode since we're ending the cleaning session
|
|
241
|
-
this.updateRunMode(0)
|
|
242
|
-
|
|
243
|
-
// Initiate return to dock sequence
|
|
244
|
-
this.returnToDock()
|
|
245
|
-
})
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
private async handleSelectAreas(request: MatterRequests.SelectAreas): Promise<void> {
|
|
249
|
-
this.logInfo(`SelectAreas request received: ${JSON.stringify(request)}`)
|
|
250
|
-
const { newAreas } = request
|
|
251
|
-
const areaNames = newAreas.map((id: number) =>
|
|
252
|
-
['Living Room', 'Kitchen', 'Bedroom', 'Bathroom'][id] || `Area ${id}`,
|
|
253
|
-
)
|
|
254
|
-
this.logInfo(`selecting areas: ${areaNames.join(', ')}`)
|
|
255
|
-
// TODO: await myVacuumAPI.selectAreas(newAreas)
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
private async handleSkipArea(request: MatterRequests.SkipArea): Promise<void> {
|
|
259
|
-
this.logInfo(`SkipArea request received: ${JSON.stringify(request)}`)
|
|
260
|
-
const { skippedArea } = request
|
|
261
|
-
const areaName = ['Living Room', 'Kitchen', 'Bedroom', 'Bathroom'][skippedArea] || `Area ${skippedArea}`
|
|
262
|
-
this.logInfo(`skipping area: ${areaName}`)
|
|
263
|
-
// TODO: await myVacuumAPI.skipArea(skippedArea)
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Helper method to clear all active timers
|
|
268
|
-
*/
|
|
269
|
-
private clearTimers(): void {
|
|
270
|
-
this.activeTimers.forEach(timer => clearTimeout(timer))
|
|
271
|
-
this.activeTimers = []
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* Helper method to initiate return to dock sequence
|
|
276
|
-
* Can be called synchronously from other handlers
|
|
277
|
-
*/
|
|
278
|
-
private returnToDock(): void {
|
|
279
|
-
this.logInfo('initiating return to dock sequence.')
|
|
280
|
-
|
|
281
|
-
// Defer ALL state updates to ensure handler completes first
|
|
282
|
-
setImmediate(() => {
|
|
283
|
-
// Start seeking charger directly (skip intermediate Stopped state)
|
|
284
|
-
this.updateOperationalState(64) // Seeking Charger
|
|
285
|
-
|
|
286
|
-
// After 5 seconds, start charging
|
|
287
|
-
const chargingTimer = setTimeout(() => {
|
|
288
|
-
this.logInfo('reached dock, now charging.')
|
|
289
|
-
this.updateOperationalState(65) // Charging
|
|
290
|
-
|
|
291
|
-
// After 3 more seconds, fully docked
|
|
292
|
-
const dockedTimer = setTimeout(() => {
|
|
293
|
-
this.logInfo('fully charged and docked.')
|
|
294
|
-
this.updateOperationalState(66) // Docked
|
|
295
|
-
}, 3000)
|
|
296
|
-
|
|
297
|
-
this.activeTimers.push(dockedTimer)
|
|
298
|
-
}, 5000)
|
|
299
|
-
|
|
300
|
-
this.activeTimers.push(chargingTimer)
|
|
301
|
-
})
|
|
350
|
+
this.logInfo('returning to dock.')
|
|
351
|
+
try {
|
|
352
|
+
await this.sendOpenAPICommand('dock')
|
|
353
|
+
} catch (e: any) {
|
|
354
|
+
this.logWarn(`OpenAPI dock failed: ${String(e?.message ?? e)}`)
|
|
355
|
+
}
|
|
356
|
+
this.updateRunMode(0)
|
|
357
|
+
this.updateOperationalState(64)
|
|
302
358
|
}
|
|
303
359
|
|
|
304
|
-
/**
|
|
305
|
-
* Update Methods - Use these to update the vacuum state from your API/device
|
|
306
|
-
*
|
|
307
|
-
* Since this is a platform accessory, state updates work immediately after registration.
|
|
308
|
-
*/
|
|
309
|
-
|
|
310
360
|
public updateOperationalState(state: number): void {
|
|
311
361
|
this.updateState('rvcOperationalState', { operationalState: state })
|
|
312
362
|
const states = [
|
|
313
363
|
'Stopped',
|
|
314
364
|
'Running',
|
|
315
365
|
'Paused',
|
|
316
|
-
|
|
317
|
-
null,
|
|
318
|
-
null,
|
|
319
|
-
null,
|
|
320
|
-
null,
|
|
321
|
-
...Array.from({ length: 56 }).fill(null),
|
|
366
|
+
...Array.from({ length: 61 }).fill(null),
|
|
322
367
|
'Seeking Charger',
|
|
323
368
|
'Charging',
|
|
324
369
|
'Docked',
|
|
@@ -328,76 +373,32 @@ export class RoboticVacuumAccessory extends BaseMatterAccessory {
|
|
|
328
373
|
|
|
329
374
|
public updateRunMode(mode: number): void {
|
|
330
375
|
this.updateState('rvcRunMode', { currentMode: mode })
|
|
331
|
-
const modes = ['Idle', 'Cleaning'
|
|
376
|
+
const modes = ['Idle', 'Cleaning']
|
|
332
377
|
this.logInfo(`run mode updated to: ${modes[mode] || `Unknown (${mode})`}`)
|
|
333
378
|
}
|
|
334
379
|
|
|
335
380
|
public updateCleanMode(mode: number): void {
|
|
336
381
|
this.updateState('rvcCleanMode', { currentMode: mode })
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
'
|
|
340
|
-
|
|
341
|
-
'
|
|
342
|
-
'Deep Vacuum',
|
|
343
|
-
'Deep Mop',
|
|
344
|
-
'Quick Clean',
|
|
345
|
-
'Max Clean',
|
|
346
|
-
'Min Clean',
|
|
347
|
-
'Quiet Vacuum',
|
|
348
|
-
'Quiet Mop',
|
|
349
|
-
'Night Mode',
|
|
350
|
-
'Eco Vacuum',
|
|
351
|
-
'Eco Mop',
|
|
352
|
-
'Auto',
|
|
353
|
-
]
|
|
354
|
-
this.logInfo(`clean mode updated to: ${modes[mode] || `Unknown (${mode})`}`)
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
public updateSelectedAreas(areaIds: number[]): void {
|
|
358
|
-
this.updateState('serviceArea', { selectedAreas: areaIds })
|
|
359
|
-
const areaNames = areaIds.map(id =>
|
|
360
|
-
['Living Room', 'Kitchen', 'Bedroom', 'Bathroom'][id] || `Area ${id}`,
|
|
361
|
-
)
|
|
362
|
-
this.logInfo(`selected areas updated to: ${areaNames.join(', ') || 'All Areas'}`)
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
public updateCurrentArea(areaId: number | null): void {
|
|
366
|
-
this.updateState('serviceArea', { currentArea: areaId })
|
|
367
|
-
if (areaId !== null) {
|
|
368
|
-
const areaName = ['Living Room', 'Kitchen', 'Bedroom', 'Bathroom'][areaId] || `Area ${areaId}`
|
|
369
|
-
this.logInfo(`current area updated to: ${areaName}`)
|
|
382
|
+
let label = `Unknown (${mode})`
|
|
383
|
+
if (this.capabilities.cleanAction === 'vacuum-only') {
|
|
384
|
+
label = ['Quiet', 'Standard', 'Strong', 'MAX'][mode] || label
|
|
385
|
+
} else if (this.capabilities.cleanAction === 'vacuum-or-mop') {
|
|
386
|
+
label = ['Vacuum', 'Mop'][mode] || label
|
|
370
387
|
} else {
|
|
371
|
-
|
|
388
|
+
label = ['Vacuum', 'Vacuum & Mop'][mode] || label
|
|
372
389
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
public updateProgress(progress: Array<{ areaId: number, status: number }>): void {
|
|
376
|
-
this.updateState('serviceArea', { progress })
|
|
377
|
-
this.logInfo(`progress updated: ${progress.length} areas`)
|
|
390
|
+
this.logInfo(`clean mode updated to: ${label}`)
|
|
378
391
|
}
|
|
379
392
|
|
|
380
393
|
/**
|
|
381
|
-
* Update battery percentage
|
|
382
|
-
*
|
|
383
|
-
* Note: The Matter specification for RoboticVacuumCleaner does not include
|
|
384
|
-
* the PowerSource cluster. Battery information for robotic vacuums should be
|
|
385
|
-
* communicated through device-specific status reporting or via the RVC
|
|
386
|
-
* operational state cluster.
|
|
387
|
-
*
|
|
388
|
-
* This method is retained for API compatibility but does not update any
|
|
389
|
-
* Matter cluster state.
|
|
390
|
-
*
|
|
391
|
-
* @param percentageOrBatPercentRemaining - Battery percentage (0-100) or batPercentRemaining (0-200)
|
|
394
|
+
* Update battery percentage (no PowerSource cluster for this device type)
|
|
392
395
|
*/
|
|
393
396
|
public async updateBatteryPercentage(percentageOrBatPercentRemaining: number): Promise<void> {
|
|
394
|
-
// Accept either 0–100 (percentage) or 0–200 (batPercentRemaining) to be robust
|
|
395
397
|
const isBatPercent = Number(percentageOrBatPercentRemaining) > 100
|
|
396
398
|
const percentage = isBatPercent
|
|
397
399
|
? Math.max(0, Math.min(100, Math.round(Number(percentageOrBatPercentRemaining) / 2)))
|
|
398
400
|
: Math.max(0, Math.min(100, Math.round(Number(percentageOrBatPercentRemaining))))
|
|
399
401
|
|
|
400
|
-
// Determine charge level based on percentage
|
|
401
402
|
let chargeLevel = 0 // Ok
|
|
402
403
|
if (percentage < 20) {
|
|
403
404
|
chargeLevel = 2 // Critical
|
|
@@ -405,10 +406,8 @@ export class RoboticVacuumAccessory extends BaseMatterAccessory {
|
|
|
405
406
|
chargeLevel = 1 // Warning
|
|
406
407
|
}
|
|
407
408
|
|
|
408
|
-
// Log battery status only - PowerSource cluster is not supported for RoboticVacuumCleaner
|
|
409
409
|
this.logInfo(`battery status: ${percentage}% (${chargeLevel === 0 ? 'Ok' : chargeLevel === 1 ? 'Warning' : 'Critical'})`)
|
|
410
410
|
|
|
411
|
-
// Store battery info in context for reference
|
|
412
411
|
if (this.context) {
|
|
413
412
|
(this.context as any).batteryPercentage = percentage
|
|
414
413
|
;(this.context as any).batteryChargeLevel = chargeLevel
|