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