@switchbot/homebridge-switchbot 5.0.0-beta.52 → 5.0.0-beta.54
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 +274 -258
- package/dist/devices-matter/RoboticVacuumAccessory.js.map +1 -1
- package/dist/homebridge-ui/public/index.html +32 -0
- 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 +326 -309
- package/src/homebridge-ui/public/index.html +32 -0
- 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,217 @@ 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
|
+
this.logInfo(`[OpenAPI] Sending PowLevel: ${newMode}`)
|
|
270
|
+
await this.sendOpenAPICommand('PowLevel', String(newMode))
|
|
271
|
+
this.logInfo(`[OpenAPI] PowLevel command sent: ${newMode}`)
|
|
272
|
+
} catch (e: any) {
|
|
273
|
+
this.logWarn(`OpenAPI PowLevel failed: ${String(e?.message ?? e)}`)
|
|
274
|
+
}
|
|
275
|
+
} else if (this.capabilities.suctionKind === 'fanLevel-1-4') {
|
|
276
|
+
this.currentFanLevel = (Math.min(3, Math.max(0, Number(newMode))) + 1) as 1 | 2 | 3 | 4
|
|
277
|
+
try {
|
|
278
|
+
this.logInfo(`[OpenAPI] Sending changeParam fanLevel: ${this.currentFanLevel}`)
|
|
279
|
+
await this.sendOpenAPICommand('changeParam', JSON.stringify({ fanLevel: this.currentFanLevel }))
|
|
280
|
+
this.logInfo(`[OpenAPI] changeParam command sent: fanLevel=${this.currentFanLevel}`)
|
|
281
|
+
} catch (e: any) {
|
|
282
|
+
this.logWarn(`OpenAPI changeParam(fanLevel) failed: ${String(e?.message ?? e)}`)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
this.updateCleanMode(newMode)
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (this.capabilities.cleanAction === 'vacuum-or-mop') {
|
|
290
|
+
const label = newMode === 0 ? 'Vacuum' : 'Mop'
|
|
291
|
+
this.currentCleanAction = newMode === 0 ? 'vacuum' : 'mop'
|
|
292
|
+
this.logInfo(`changing clean action to: ${label}`)
|
|
293
|
+
this.logInfo(`[OpenAPI] (no command sent for cleanAction change, just updating state)`)
|
|
294
|
+
this.updateCleanMode(newMode)
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const label = newMode === 0 ? 'Vacuum' : 'Vacuum & Mop'
|
|
299
|
+
this.currentCleanAction = newMode === 0 ? 'vacuum' : 'vacuum_mop'
|
|
300
|
+
this.logInfo(`changing clean action to: ${label}`)
|
|
301
|
+
this.logInfo(`[OpenAPI] (no command sent for cleanAction change, just updating state)`)
|
|
302
|
+
this.updateCleanMode(newMode)
|
|
174
303
|
}
|
|
175
304
|
|
|
176
305
|
private async handlePause(): Promise<void> {
|
|
177
306
|
this.logInfo('pausing.')
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
307
|
+
try {
|
|
308
|
+
this.logInfo(`[OpenAPI] Sending pause command`)
|
|
309
|
+
await this.sendOpenAPICommand('pause')
|
|
310
|
+
this.logInfo(`[OpenAPI] pause command sent`)
|
|
311
|
+
} catch (e: any) {
|
|
312
|
+
this.logWarn(`OpenAPI pause failed: ${String(e?.message ?? e)}`)
|
|
313
|
+
}
|
|
314
|
+
this.updateOperationalState(2)
|
|
181
315
|
}
|
|
182
316
|
|
|
183
317
|
private async handleStop(): Promise<void> {
|
|
184
318
|
this.logInfo('stopping.')
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
319
|
+
try {
|
|
320
|
+
if (this.capabilities.cleanAction === 'vacuum-only' && this.capabilities.suctionKind === 'powLevel-0-3') {
|
|
321
|
+
this.logInfo(`[OpenAPI] Sending stop command`)
|
|
322
|
+
await this.sendOpenAPICommand('stop')
|
|
323
|
+
this.logInfo(`[OpenAPI] stop command sent`)
|
|
324
|
+
} else {
|
|
325
|
+
this.logInfo(`[OpenAPI] Sending pause command (for stop)`)
|
|
326
|
+
await this.sendOpenAPICommand('pause')
|
|
327
|
+
this.logInfo(`[OpenAPI] pause command sent (for stop)`)
|
|
328
|
+
}
|
|
329
|
+
} catch (e: any) {
|
|
330
|
+
this.logWarn(`OpenAPI stop/pause failed: ${String(e?.message ?? e)}`)
|
|
331
|
+
}
|
|
332
|
+
this.updateRunMode(0)
|
|
333
|
+
this.updateOperationalState(0)
|
|
189
334
|
}
|
|
190
335
|
|
|
191
336
|
private async handleStart(): Promise<void> {
|
|
192
|
-
this.logInfo('starting
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
337
|
+
this.logInfo('starting clean.')
|
|
338
|
+
try {
|
|
339
|
+
if (this.capabilities.cleanAction === 'vacuum-only' && this.capabilities.suctionKind === 'powLevel-0-3') {
|
|
340
|
+
this.logInfo(`[OpenAPI] Sending start command`)
|
|
341
|
+
await this.sendOpenAPICommand('start')
|
|
342
|
+
this.logInfo(`[OpenAPI] start command sent`)
|
|
343
|
+
} else {
|
|
344
|
+
const action = this.currentCleanAction === 'vacuum_mop' ? 'sweep_mop' : this.currentCleanAction
|
|
345
|
+
const param: any = { action, param: { fanLevel: this.currentFanLevel } }
|
|
346
|
+
if (this.capabilities.waterLevel) {
|
|
347
|
+
param.param.waterLevel = 1
|
|
348
|
+
}
|
|
349
|
+
this.logInfo(`[OpenAPI] Sending startClean: ${JSON.stringify(param)}`)
|
|
350
|
+
await this.sendOpenAPICommand('startClean', JSON.stringify(param))
|
|
351
|
+
this.logInfo(`[OpenAPI] startClean command sent: ${JSON.stringify(param)}`)
|
|
352
|
+
}
|
|
353
|
+
} catch (e: any) {
|
|
354
|
+
this.logWarn(`OpenAPI start/startClean failed: ${String(e?.message ?? e)}`)
|
|
355
|
+
}
|
|
356
|
+
this.updateRunMode(1)
|
|
357
|
+
this.updateOperationalState(1)
|
|
209
358
|
}
|
|
210
359
|
|
|
211
360
|
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)
|
|
361
|
+
this.logInfo('resume requested.')
|
|
362
|
+
await this.handleStart()
|
|
229
363
|
}
|
|
230
364
|
|
|
231
365
|
private async handleGoHome(): Promise<void> {
|
|
232
|
-
this.logInfo('
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
})
|
|
366
|
+
this.logInfo('returning to dock.')
|
|
367
|
+
try {
|
|
368
|
+
this.logInfo(`[OpenAPI] Sending dock command`)
|
|
369
|
+
await this.sendOpenAPICommand('dock')
|
|
370
|
+
this.logInfo(`[OpenAPI] dock command sent`)
|
|
371
|
+
} catch (e: any) {
|
|
372
|
+
this.logWarn(`OpenAPI dock failed: ${String(e?.message ?? e)}`)
|
|
373
|
+
}
|
|
374
|
+
this.updateRunMode(0)
|
|
375
|
+
this.updateOperationalState(64)
|
|
302
376
|
}
|
|
303
377
|
|
|
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
378
|
public updateOperationalState(state: number): void {
|
|
311
379
|
this.updateState('rvcOperationalState', { operationalState: state })
|
|
312
380
|
const states = [
|
|
313
381
|
'Stopped',
|
|
314
382
|
'Running',
|
|
315
383
|
'Paused',
|
|
316
|
-
|
|
317
|
-
null,
|
|
318
|
-
null,
|
|
319
|
-
null,
|
|
320
|
-
null,
|
|
321
|
-
...Array.from({ length: 56 }).fill(null),
|
|
384
|
+
...Array.from({ length: 61 }).fill(null),
|
|
322
385
|
'Seeking Charger',
|
|
323
386
|
'Charging',
|
|
324
387
|
'Docked',
|
|
@@ -328,76 +391,32 @@ export class RoboticVacuumAccessory extends BaseMatterAccessory {
|
|
|
328
391
|
|
|
329
392
|
public updateRunMode(mode: number): void {
|
|
330
393
|
this.updateState('rvcRunMode', { currentMode: mode })
|
|
331
|
-
const modes = ['Idle', 'Cleaning'
|
|
394
|
+
const modes = ['Idle', 'Cleaning']
|
|
332
395
|
this.logInfo(`run mode updated to: ${modes[mode] || `Unknown (${mode})`}`)
|
|
333
396
|
}
|
|
334
397
|
|
|
335
398
|
public updateCleanMode(mode: number): void {
|
|
336
399
|
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}`)
|
|
400
|
+
let label = `Unknown (${mode})`
|
|
401
|
+
if (this.capabilities.cleanAction === 'vacuum-only') {
|
|
402
|
+
label = ['Quiet', 'Standard', 'Strong', 'MAX'][mode] || label
|
|
403
|
+
} else if (this.capabilities.cleanAction === 'vacuum-or-mop') {
|
|
404
|
+
label = ['Vacuum', 'Mop'][mode] || label
|
|
370
405
|
} else {
|
|
371
|
-
|
|
406
|
+
label = ['Vacuum', 'Vacuum & Mop'][mode] || label
|
|
372
407
|
}
|
|
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`)
|
|
408
|
+
this.logInfo(`clean mode updated to: ${label}`)
|
|
378
409
|
}
|
|
379
410
|
|
|
380
411
|
/**
|
|
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)
|
|
412
|
+
* Update battery percentage (no PowerSource cluster for this device type)
|
|
392
413
|
*/
|
|
393
414
|
public async updateBatteryPercentage(percentageOrBatPercentRemaining: number): Promise<void> {
|
|
394
|
-
// Accept either 0–100 (percentage) or 0–200 (batPercentRemaining) to be robust
|
|
395
415
|
const isBatPercent = Number(percentageOrBatPercentRemaining) > 100
|
|
396
416
|
const percentage = isBatPercent
|
|
397
417
|
? Math.max(0, Math.min(100, Math.round(Number(percentageOrBatPercentRemaining) / 2)))
|
|
398
418
|
: Math.max(0, Math.min(100, Math.round(Number(percentageOrBatPercentRemaining))))
|
|
399
419
|
|
|
400
|
-
// Determine charge level based on percentage
|
|
401
420
|
let chargeLevel = 0 // Ok
|
|
402
421
|
if (percentage < 20) {
|
|
403
422
|
chargeLevel = 2 // Critical
|
|
@@ -405,10 +424,8 @@ export class RoboticVacuumAccessory extends BaseMatterAccessory {
|
|
|
405
424
|
chargeLevel = 1 // Warning
|
|
406
425
|
}
|
|
407
426
|
|
|
408
|
-
// Log battery status only - PowerSource cluster is not supported for RoboticVacuumCleaner
|
|
409
427
|
this.logInfo(`battery status: ${percentage}% (${chargeLevel === 0 ? 'Ok' : chargeLevel === 1 ? 'Warning' : 'Critical'})`)
|
|
410
428
|
|
|
411
|
-
// Store battery info in context for reference
|
|
412
429
|
if (this.context) {
|
|
413
430
|
(this.context as any).batteryPercentage = percentage
|
|
414
431
|
;(this.context as any).batteryChargeLevel = chargeLevel
|