@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.
@@ -1,67 +1,93 @@
1
- /* global NodeJS */
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
- activeTimers = [];
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 clusters = opts?.clusters ?? {
8
- rvcRunMode: {
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
- handlers,
76
- context: { deviceId: opts?.deviceId, ...(opts?.context ?? {}) },
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', 'Mapping'][newMode] || `Unknown (mode=${newMode})`;
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
- // Switching to Cleaning mode - start the vacuum
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
- // Switching to Idle mode - return to dock
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
- const modes = [
118
- 'Vacuum',
119
- 'Mop',
120
- 'Vacuum & Mop',
121
- 'Deep Clean',
122
- 'Deep Vacuum',
123
- 'Deep Mop',
124
- 'Quick Clean',
125
- 'Max Clean',
126
- 'Min Clean',
127
- 'Quiet Vacuum',
128
- 'Quiet Mop',
129
- 'Night Mode',
130
- 'Eco Vacuum',
131
- 'Eco Mop',
132
- 'Auto',
133
- ];
134
- const modeStr = modes[newMode] || `Unknown (mode=${newMode})`;
135
- this.logInfo(`changing clean mode to: ${modeStr}`);
136
- // TODO: await myVacuumAPI.setCleanMode(newMode)
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
- // TODO: await myVacuumAPI.pause()
141
- this.clearTimers(); // Clear cleaning completion timer
142
- this.updateOperationalState(2); // Paused
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
- // TODO: await myVacuumAPI.stop()
147
- this.clearTimers();
148
- this.updateRunMode(0); // Reset to Idle
149
- this.updateOperationalState(0); // Stopped
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 (via start command).');
153
- // TODO: await myVacuumAPI.start()
154
- // Clear any existing timers
155
- this.clearTimers();
156
- this.updateRunMode(1); // Set to Cleaning mode - this will trigger the run mode handler logic
157
- this.updateOperationalState(1); // Running
158
- // Simulate cleaning completion after 15 seconds
159
- const completionTimer = setTimeout(() => {
160
- this.logInfo('cleaning completed, returning to dock.');
161
- this.updateRunMode(0); // Set to Idle - cleaning session ending
162
- this.returnToDock();
163
- }, 15000);
164
- this.activeTimers.push(completionTimer);
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('resuming.');
168
- // TODO: await myVacuumAPI.resume()
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('goHome command received.');
183
- // TODO: await myVacuumAPI.goHome()
184
- // Clear any existing timers
185
- this.clearTimers();
186
- // Defer state updates to ensure handler completes first
187
- setImmediate(() => {
188
- // Set to Idle mode since we're ending the cleaning session
189
- this.updateRunMode(0);
190
- // Initiate return to dock sequence
191
- this.returnToDock();
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
- 'Error',
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', 'Mapping'];
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
- const modes = [
270
- 'Vacuum',
271
- 'Mop',
272
- 'Vacuum & Mop',
273
- 'Deep Clean',
274
- 'Deep Vacuum',
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
- this.logInfo('current area cleared');
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;