@homebridge-plugins/homebridge-sharkiq 2.0.0-beta.2 → 2.0.0-beta.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +19 -2
  2. package/README.md +41 -4
  3. package/config.schema.json +8 -26
  4. package/dist/constants.d.ts.map +1 -1
  5. package/dist/constants.js.map +1 -1
  6. package/dist/devices/BaseMatterAccessory.d.ts +67 -0
  7. package/dist/devices/BaseMatterAccessory.d.ts.map +1 -0
  8. package/dist/devices/BaseMatterAccessory.js +153 -0
  9. package/dist/devices/BaseMatterAccessory.js.map +1 -0
  10. package/dist/devices/RoboticVacuumAccessory.d.ts +67 -0
  11. package/dist/devices/RoboticVacuumAccessory.d.ts.map +1 -0
  12. package/dist/devices/RoboticVacuumAccessory.js +645 -0
  13. package/dist/devices/RoboticVacuumAccessory.js.map +1 -0
  14. package/dist/devices/index.d.ts +9 -0
  15. package/dist/devices/index.d.ts.map +1 -0
  16. package/dist/devices/index.js +8 -0
  17. package/dist/devices/index.js.map +1 -0
  18. package/dist/index.d.ts +3 -0
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +3 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/platform.d.ts +39 -16
  23. package/dist/platform.d.ts.map +1 -1
  24. package/dist/platform.js +195 -92
  25. package/dist/platform.js.map +1 -1
  26. package/dist/sharkiq-js/room_encoding.d.ts +8 -0
  27. package/dist/sharkiq-js/room_encoding.d.ts.map +1 -0
  28. package/dist/sharkiq-js/room_encoding.js +35 -0
  29. package/dist/sharkiq-js/room_encoding.js.map +1 -0
  30. package/dist/sharkiq-js/sharkiq.d.ts.map +1 -1
  31. package/dist/sharkiq-js/sharkiq.js +2 -20
  32. package/dist/sharkiq-js/sharkiq.js.map +1 -1
  33. package/docs/assets/highlight.css +9 -2
  34. package/docs/index.html +35 -3
  35. package/docs/variables/default.html +2 -1
  36. package/package.json +3 -3
  37. package/test/context.spec.ts +33 -0
  38. package/test/encodeRoomList.spec.ts +25 -0
  39. package/test/polling.spec.ts +74 -0
  40. package/dist/errorHandling.d.ts +0 -19
  41. package/dist/errorHandling.d.ts.map +0 -1
  42. package/dist/errorHandling.js +0 -27
  43. package/dist/errorHandling.js.map +0 -1
  44. package/dist/platformAccessory.d.ts +0 -33
  45. package/dist/platformAccessory.d.ts.map +0 -1
  46. package/dist/platformAccessory.js +0 -384
  47. package/dist/platformAccessory.js.map +0 -1
@@ -0,0 +1,645 @@
1
+ /* global NodeJS */
2
+ import { ERROR_MESSAGES, OperatingModes, PowerModes, Properties } from '../sharkiq-js/properties.js';
3
+ import { BaseMatterAccessory } from './BaseMatterAccessory.js';
4
+ export class RoboticVacuumAccessory extends BaseMatterAccessory {
5
+ activeTimers = [];
6
+ sharkDevice;
7
+ pollInterval;
8
+ constructor(api, log, sharkDevice, pollIntervalMs) {
9
+ const serialNumber = (sharkDevice && (sharkDevice.serial_number || sharkDevice._dsn)) || 'VACUUM-001';
10
+ const displayName = (sharkDevice && (sharkDevice.name || sharkDevice._name)) || 'Robot Vacuum';
11
+ const manufacturer = sharkDevice ? 'Shark' : 'Homebridge Matter';
12
+ const model = (sharkDevice && (sharkDevice.vac_model_number || sharkDevice._vac_model_number)) || 'HB-MATTER-VACUUM-ROBOTIC';
13
+ const firmwareRevision = (sharkDevice && (sharkDevice._firmware_version || 'unknown')) || '2.0.0';
14
+ // Build serviceArea cluster from device rooms (local helper so we don't use `this` before super)
15
+ const serviceAreaCluster = (() => {
16
+ if (!sharkDevice || typeof sharkDevice.get_room_list !== 'function') {
17
+ return {
18
+ supportedMaps: [],
19
+ supportedAreas: [],
20
+ selectedAreas: [],
21
+ };
22
+ }
23
+ try {
24
+ const rooms = sharkDevice.get_room_list() || [];
25
+ const supportedAreas = rooms.map((roomName, idx) => ({
26
+ areaId: idx,
27
+ mapId: null,
28
+ areaInfo: {
29
+ locationInfo: {
30
+ locationName: roomName,
31
+ floorNumber: 0,
32
+ areaType: 0,
33
+ },
34
+ landmarkInfo: null,
35
+ },
36
+ }));
37
+ return {
38
+ supportedMaps: [],
39
+ supportedAreas,
40
+ selectedAreas: supportedAreas.map(a => a.areaId),
41
+ };
42
+ }
43
+ catch {
44
+ return {
45
+ supportedMaps: [],
46
+ supportedAreas: [],
47
+ selectedAreas: [],
48
+ };
49
+ }
50
+ })();
51
+ super(api, log, {
52
+ uuid: api.matter.uuid.generate(serialNumber),
53
+ displayName,
54
+ deviceType: api.matter.deviceTypes.RoboticVacuumCleaner,
55
+ serialNumber,
56
+ manufacturer,
57
+ model,
58
+ firmwareRevision,
59
+ hardwareRevision: '1.0.0',
60
+ // Persist useful metadata in accessory.context so cached accessories retain DSN/model/etc.
61
+ context: {
62
+ serialNumber,
63
+ name: displayName,
64
+ vacModel: model,
65
+ vacSerial: (sharkDevice && (sharkDevice.vac_serial_number || sharkDevice._vac_serial_number)) || null,
66
+ oemModel: (sharkDevice && (sharkDevice.oem_model_number || sharkDevice._oem_model_number)) || null,
67
+ pollIntervalMs: typeof pollIntervalMs === 'number' ? pollIntervalMs : null,
68
+ },
69
+ clusters: {
70
+ // Run Mode: Controls what the vacuum is doing (Idle, Cleaning, Mapping)
71
+ rvcRunMode: {
72
+ supportedModes: [
73
+ { label: 'Idle', mode: 0, modeTags: [{ value: 16384 }] }, // RvcRunMode.ModeTag.Idle
74
+ { label: 'Cleaning', mode: 1, modeTags: [{ value: 16385 }] }, // RvcRunMode.ModeTag.Cleaning
75
+ { label: 'Mapping', mode: 2, modeTags: [{ value: 16386 }] }, // RvcRunMode.ModeTag.Mapping
76
+ ],
77
+ currentMode: 0,
78
+ },
79
+ // Clean Mode: Controls HOW the vacuum cleans
80
+ // You can combine semantic tags (0-9) with functional tags (16384-16386)
81
+ // Available semantic tags: Auto, Quick, Quiet, LowNoise, LowEnergy, Vacation, Min, Max, Night, Day
82
+ // Available functional tags: DeepClean, Vacuum, Mop
83
+ rvcCleanMode: {
84
+ supportedModes: [
85
+ // Basic functional modes
86
+ { label: 'Vacuum', mode: 0, modeTags: [{ value: 16385 }] }, // Vacuum
87
+ { label: 'Mop', mode: 1, modeTags: [{ value: 16386 }] }, // Mop
88
+ { label: 'Vacuum & Mop', mode: 2, modeTags: [{ value: 16385 }, { value: 16386 }] }, // Both
89
+ // Deep clean modes
90
+ { label: 'Deep Clean', mode: 3, modeTags: [{ value: 16384 }] }, // DeepClean
91
+ { label: 'Deep Vacuum', mode: 4, modeTags: [{ value: 16384 }, { value: 16385 }] }, // DeepClean + Vacuum
92
+ { label: 'Deep Mop', mode: 5, modeTags: [{ value: 16384 }, { value: 16386 }] }, // DeepClean + Mop
93
+ // Intensity modes
94
+ { label: 'Quick Clean', mode: 6, modeTags: [{ value: 1 }, { value: 16385 }] }, // Quick + Vacuum
95
+ { label: 'Max Clean', mode: 7, modeTags: [{ value: 7 }, { value: 16385 }] }, // Max + Vacuum
96
+ { label: 'Min Clean', mode: 8, modeTags: [{ value: 6 }, { value: 16385 }] }, // Min + Vacuum
97
+ // Quiet modes
98
+ { label: 'Quiet Vacuum', mode: 9, modeTags: [{ value: 2 }, { value: 16385 }] }, // Quiet + Vacuum
99
+ { label: 'Quiet Mop', mode: 10, modeTags: [{ value: 2 }, { value: 16386 }] }, // Quiet + Mop
100
+ { label: 'Night Mode', mode: 11, modeTags: [{ value: 8 }, { value: 16385 }] }, // Night + Vacuum
101
+ // Energy efficient
102
+ { label: 'Eco Vacuum', mode: 12, modeTags: [{ value: 4 }, { value: 16385 }] }, // LowEnergy + Vacuum
103
+ { label: 'Eco Mop', mode: 13, modeTags: [{ value: 4 }, { value: 16386 }] }, // LowEnergy + Mop
104
+ // Auto mode
105
+ { label: 'Auto', mode: 14, modeTags: [{ value: 0 }, { value: 16385 }] }, // Auto + Vacuum
106
+ ],
107
+ currentMode: 0, // start with basic Vacuum
108
+ },
109
+ // Operational State: Current state (Stopped, Running, Paused, Error, etc.)
110
+ rvcOperationalState: {
111
+ operationalStateList: [
112
+ { operationalStateId: 0 }, // stopped (standard label from Matter spec)
113
+ { operationalStateId: 1 }, // running
114
+ { operationalStateId: 2 }, // paused
115
+ { operationalStateId: 3 }, // error
116
+ { operationalStateId: 64 }, // seeking charger
117
+ { operationalStateId: 65 }, // charging
118
+ { operationalStateId: 66 }, // docked
119
+ ],
120
+ operationalState: 66, // start docked
121
+ },
122
+ // Service Area: Room/zone selection for targeted cleaning
123
+ serviceArea: serviceAreaCluster,
124
+ },
125
+ handlers: {
126
+ rvcRunMode: {
127
+ changeToMode: async (request) => this.handleChangeRunMode(request),
128
+ },
129
+ rvcCleanMode: {
130
+ changeToMode: async (request) => this.handleChangeCleanMode(request),
131
+ },
132
+ rvcOperationalState: {
133
+ pause: async () => this.handlePause(),
134
+ stop: async () => this.handleStop(),
135
+ start: async () => this.handleStart(),
136
+ resume: async () => this.handleResume(),
137
+ goHome: async () => this.handleGoHome(),
138
+ },
139
+ serviceArea: {
140
+ selectAreas: async (request) => this.handleSelectAreas(request),
141
+ skipArea: async (request) => this.handleSkipArea(request),
142
+ },
143
+ // Note: battery/diagnostics updates are sent at runtime from polling via
144
+ // updateState(). The previous implementation placed static objects under
145
+ // `handlers` using non-standard cluster names ('power' / 'diagnostics'),
146
+ // which can cause errors when those clusters are not supported by the
147
+ // Matter server. We avoid registering invalid handlers here; runtime
148
+ // updates will be guarded by BaseMatterAccessory.updateState().
149
+ },
150
+ });
151
+ this.sharkDevice = sharkDevice;
152
+ // Start polling device state if we have a Shark device and polling is enabled
153
+ if (this.sharkDevice && typeof pollIntervalMs === 'number' && pollIntervalMs > 0) {
154
+ this.startPolling(pollIntervalMs);
155
+ }
156
+ else if (this.sharkDevice && (typeof pollIntervalMs !== 'number' || pollIntervalMs <= 0)) {
157
+ // If pollIntervalMs is omitted, default to 30s
158
+ if (typeof pollIntervalMs !== 'number') {
159
+ this.startPolling();
160
+ }
161
+ }
162
+ this.logInfo('initialized and ready.');
163
+ }
164
+ startPolling(intervalMs = 30000) {
165
+ if (this.pollInterval) {
166
+ return;
167
+ }
168
+ this.pollInterval = setInterval(async () => {
169
+ try {
170
+ // Request a full update
171
+ await this.sharkDevice.update([]);
172
+ // Update run/operating mode
173
+ const opMode = this.sharkDevice.operating_mode?.() ?? null;
174
+ const powerMode = this.sharkDevice.power_mode?.() ?? null;
175
+ const docked = this.sharkDevice.docked_status?.() ?? null;
176
+ // Map operating mode to Matter run/operational state
177
+ if (docked !== null && docked === 1) {
178
+ // Docked
179
+ this.updateOperationalState(66);
180
+ this.updateRunMode(0);
181
+ }
182
+ else if (opMode !== null) {
183
+ if (opMode === OperatingModes.START) {
184
+ this.updateRunMode(1);
185
+ this.updateOperationalState(1);
186
+ }
187
+ else if (opMode === OperatingModes.PAUSE) {
188
+ this.updateOperationalState(2);
189
+ this.updateRunMode(1);
190
+ }
191
+ else if (opMode === OperatingModes.RETURN) {
192
+ this.updateOperationalState(64);
193
+ this.updateRunMode(0);
194
+ }
195
+ else if (opMode === OperatingModes.STOP) {
196
+ this.updateOperationalState(0);
197
+ this.updateRunMode(0);
198
+ }
199
+ }
200
+ // Map power mode to clean mode (best-effort mapping)
201
+ if (powerMode !== null) {
202
+ let cleanMode = 0; // Vacuum
203
+ if (powerMode === PowerModes.ECO) {
204
+ cleanMode = 12; // Eco Vacuum
205
+ }
206
+ else if (powerMode === PowerModes.MAX) {
207
+ cleanMode = 7; // Max Clean
208
+ }
209
+ else {
210
+ cleanMode = 0;
211
+ }
212
+ this.updateCleanMode(cleanMode);
213
+ }
214
+ // Update service areas selection from device room list
215
+ if (typeof this.sharkDevice.get_room_list === 'function') {
216
+ const rooms = this.sharkDevice.get_room_list() || [];
217
+ const ids = rooms.map((_, idx) => idx);
218
+ this.updateSelectedAreas(ids);
219
+ }
220
+ // Update battery level, charging and diagnostics using upstream helpers when available
221
+ try {
222
+ // Prefer convenience properties if present on the wrapper
223
+ let batteryVal = null;
224
+ let chargingVal = null;
225
+ let errVal = null;
226
+ let errText = null;
227
+ if (this.sharkDevice.battery_capacity && typeof this.sharkDevice.battery_capacity === 'function') {
228
+ batteryVal = this.sharkDevice.battery_capacity();
229
+ }
230
+ else if (typeof this.sharkDevice.get_property_value === 'function') {
231
+ batteryVal = this.sharkDevice.get_property_value(Properties.BATTERY_CAPACITY);
232
+ }
233
+ if (this.sharkDevice.charging_status && typeof this.sharkDevice.charging_status === 'function') {
234
+ chargingVal = this.sharkDevice.charging_status();
235
+ }
236
+ else if (typeof this.sharkDevice.get_property_value === 'function') {
237
+ chargingVal = this.sharkDevice.get_property_value(Properties.CHARGING_STATUS);
238
+ }
239
+ // Error code / text
240
+ if (this.sharkDevice.error_code !== undefined && typeof this.sharkDevice.error_code === 'function') {
241
+ errVal = this.sharkDevice.error_code();
242
+ if (this.sharkDevice.error_text && typeof this.sharkDevice.error_text === 'function') {
243
+ errText = this.sharkDevice.error_text();
244
+ }
245
+ }
246
+ else if (typeof this.sharkDevice.get_property_value === 'function') {
247
+ errVal = this.sharkDevice.get_property_value(Properties.ERROR_CODE);
248
+ }
249
+ if (batteryVal !== null && typeof batteryVal !== 'undefined') {
250
+ const pct = Number(batteryVal) || 0;
251
+ // chargingVal may be boolean-like or numeric
252
+ const isCharging = chargingVal === 1 || chargingVal === true;
253
+ await this.updateState('power', { batteryLevel: Math.max(0, Math.min(100, pct)), charging: isCharging });
254
+ this.logInfo(`battery updated: ${pct}% (charging: ${isCharging})`);
255
+ }
256
+ // RSSI (signal strength) mapping if available
257
+ try {
258
+ const rssi = this.sharkDevice.get_property_value
259
+ ? this.sharkDevice.get_property_value(Properties.RSSI)
260
+ : null;
261
+ if (rssi !== null && typeof rssi !== 'undefined') {
262
+ await this.updateState('diagnostics', { ...null, rssi: Number(rssi) });
263
+ this.logInfo(`rssi updated: ${rssi}`);
264
+ }
265
+ }
266
+ catch (e) {
267
+ // ignore
268
+ }
269
+ if (errVal && Number(errVal) > 0) {
270
+ const errCode = Number(errVal);
271
+ // Prefer human friendly text from device wrapper if provided
272
+ const text = errText || ERROR_MESSAGES[errCode] || `Error ${errCode}`;
273
+ // Map severity: treat these as actionable errors (set operational state to Error)
274
+ await this.updateState('diagnostics', { errorCode: errCode, errorText: text, severity: 'critical' });
275
+ this.updateOperationalState(3); // Error
276
+ this.logWarn(`device error: ${errCode} - ${text}`);
277
+ }
278
+ else if (errVal === 0 || errVal === null || typeof errVal === 'undefined') {
279
+ await this.updateState('diagnostics', { errorCode: 0, errorText: '', severity: 'ok' });
280
+ }
281
+ // Clean complete mapping - if device reports a clean complete flag, reflect it
282
+ try {
283
+ const cleanComplete = this.sharkDevice.get_property_value
284
+ ? this.sharkDevice.get_property_value(Properties.CLEAN_COMPLETE)
285
+ : null;
286
+ if (cleanComplete === 1 || cleanComplete === true) {
287
+ // When cleaning completes, set run mode to Idle and operational to Stopped
288
+ this.updateRunMode(0);
289
+ this.updateOperationalState(0);
290
+ this.logInfo('clean complete reported by device - updating state to Idle/Stopped');
291
+ }
292
+ }
293
+ catch (e) {
294
+ // ignore
295
+ }
296
+ }
297
+ catch (e) {
298
+ this.logDebug('Error updating battery/diagnostics:', e);
299
+ }
300
+ }
301
+ catch (e) {
302
+ this.logError('Polling error for Shark device:', e);
303
+ }
304
+ }, intervalMs);
305
+ }
306
+ stopPolling() {
307
+ if (this.pollInterval) {
308
+ clearInterval(this.pollInterval);
309
+ this.pollInterval = undefined;
310
+ }
311
+ }
312
+ // Helper to build serviceArea cluster from Shark device rooms
313
+ buildServiceAreaCluster(sharkDevice) {
314
+ if (!sharkDevice || typeof sharkDevice.get_room_list !== 'function') {
315
+ return {
316
+ supportedMaps: [],
317
+ supportedAreas: [],
318
+ selectedAreas: [],
319
+ };
320
+ }
321
+ try {
322
+ const rooms = sharkDevice.get_room_list() || [];
323
+ const supportedAreas = rooms.map((roomName, idx) => ({
324
+ areaId: idx,
325
+ mapId: null,
326
+ areaInfo: {
327
+ locationInfo: {
328
+ locationName: roomName,
329
+ floorNumber: 0,
330
+ areaType: 0,
331
+ },
332
+ landmarkInfo: null,
333
+ },
334
+ }));
335
+ return {
336
+ supportedMaps: [],
337
+ supportedAreas,
338
+ selectedAreas: supportedAreas.map(a => a.areaId),
339
+ };
340
+ }
341
+ catch (e) {
342
+ return {
343
+ supportedMaps: [],
344
+ supportedAreas: [],
345
+ selectedAreas: [],
346
+ };
347
+ }
348
+ }
349
+ async handleChangeRunMode(request) {
350
+ this.logInfo(`ChangeToMode (run) request received: ${JSON.stringify(request)}`);
351
+ const { newMode } = request;
352
+ const modeStr = ['Idle', 'Cleaning', 'Mapping'][newMode] || `Unknown (mode=${newMode})`;
353
+ this.logInfo(`changing run mode to: ${modeStr}`);
354
+ if (this.sharkDevice) {
355
+ try {
356
+ if (newMode === 1) {
357
+ await this.sharkDevice.set_operating_mode(OperatingModes.START);
358
+ }
359
+ else if (newMode === 0) {
360
+ await this.sharkDevice.set_operating_mode(OperatingModes.STOP);
361
+ }
362
+ else if (newMode === 2) {
363
+ // Mapping - treat as start for now
364
+ await this.sharkDevice.set_operating_mode(OperatingModes.START);
365
+ }
366
+ }
367
+ catch (e) {
368
+ this.logError('Error setting run mode on Shark device:', e);
369
+ }
370
+ }
371
+ // Clear any existing timers
372
+ this.clearTimers();
373
+ if (newMode === 1) {
374
+ // Switching to Cleaning mode - start the vacuum
375
+ this.updateOperationalState(1); // Running
376
+ // Simulate cleaning completion after 15 seconds
377
+ const completionTimer = setTimeout(() => {
378
+ this.logInfo('cleaning completed, returning to dock.');
379
+ this.updateRunMode(0); // Set to Idle - cleaning session ending
380
+ this.returnToDock();
381
+ }, 15000);
382
+ this.activeTimers.push(completionTimer);
383
+ }
384
+ else if (newMode === 0) {
385
+ // Switching to Idle mode - return to dock
386
+ this.returnToDock();
387
+ }
388
+ else if (newMode === 2) {
389
+ // Switching to Mapping mode - start mapping
390
+ this.updateOperationalState(1); // Running
391
+ // Simulate mapping completion after 20 seconds
392
+ const completionTimer = setTimeout(() => {
393
+ this.logInfo('mapping completed, returning to dock.');
394
+ this.returnToDock();
395
+ }, 20000);
396
+ this.activeTimers.push(completionTimer);
397
+ }
398
+ }
399
+ async handleChangeCleanMode(request) {
400
+ this.logInfo(`ChangeToMode (clean) request received: ${JSON.stringify(request)}`);
401
+ const { newMode } = request;
402
+ const modes = [
403
+ 'Vacuum',
404
+ 'Mop',
405
+ 'Vacuum & Mop',
406
+ 'Deep Clean',
407
+ 'Deep Vacuum',
408
+ 'Deep Mop',
409
+ 'Quick Clean',
410
+ 'Max Clean',
411
+ 'Min Clean',
412
+ 'Quiet Vacuum',
413
+ 'Quiet Mop',
414
+ 'Night Mode',
415
+ 'Eco Vacuum',
416
+ 'Eco Mop',
417
+ 'Auto',
418
+ ];
419
+ const modeStr = modes[newMode] || `Unknown (mode=${newMode})`;
420
+ this.logInfo(`changing clean mode to: ${modeStr}`);
421
+ if (this.sharkDevice) {
422
+ // Shark API doesn't expose a direct clean-mode setter here; keep local state and log.
423
+ this.logInfo('Clean mode change requested for Shark device (not implemented in API wrapper).');
424
+ }
425
+ }
426
+ async handlePause() {
427
+ this.logInfo('pausing.');
428
+ if (this.sharkDevice) {
429
+ try {
430
+ await this.sharkDevice.set_operating_mode(OperatingModes.PAUSE);
431
+ }
432
+ catch (e) {
433
+ this.logError('Error pausing Shark device:', e);
434
+ }
435
+ }
436
+ this.clearTimers(); // Clear cleaning completion timer
437
+ this.updateOperationalState(2); // Paused
438
+ }
439
+ async handleStop() {
440
+ this.logInfo('stopping.');
441
+ if (this.sharkDevice) {
442
+ try {
443
+ await this.sharkDevice.set_operating_mode(OperatingModes.STOP);
444
+ }
445
+ catch (e) {
446
+ this.logError('Error stopping Shark device:', e);
447
+ }
448
+ }
449
+ this.clearTimers();
450
+ this.updateRunMode(0); // Reset to Idle
451
+ this.updateOperationalState(0); // Stopped
452
+ }
453
+ async handleStart() {
454
+ this.logInfo('starting (via start command).');
455
+ if (this.sharkDevice) {
456
+ try {
457
+ await this.sharkDevice.set_operating_mode(OperatingModes.START);
458
+ }
459
+ catch (e) {
460
+ this.logError('Error starting Shark device:', e);
461
+ }
462
+ }
463
+ // Clear any existing timers
464
+ this.clearTimers();
465
+ this.updateRunMode(1); // Set to Cleaning mode - this will trigger the run mode handler logic
466
+ this.updateOperationalState(1); // Running
467
+ // Simulate cleaning completion after 15 seconds
468
+ const completionTimer = setTimeout(() => {
469
+ this.logInfo('cleaning completed, returning to dock.');
470
+ this.updateRunMode(0); // Set to Idle - cleaning session ending
471
+ this.returnToDock();
472
+ }, 15000);
473
+ this.activeTimers.push(completionTimer);
474
+ }
475
+ async handleResume() {
476
+ this.logInfo('resuming.');
477
+ if (this.sharkDevice) {
478
+ try {
479
+ await this.sharkDevice.set_operating_mode(OperatingModes.START);
480
+ }
481
+ catch (e) {
482
+ this.logError('Error resuming Shark device:', e);
483
+ }
484
+ }
485
+ // Clear any existing timers
486
+ this.clearTimers();
487
+ this.updateRunMode(1); // Set to Cleaning mode
488
+ this.updateOperationalState(1); // Running
489
+ // Simulate cleaning completion after 10 seconds (shorter since resuming)
490
+ const completionTimer = setTimeout(() => {
491
+ this.logInfo('cleaning completed, returning to dock.');
492
+ this.updateRunMode(0); // Set to Idle - cleaning session ending
493
+ this.returnToDock();
494
+ }, 10000);
495
+ this.activeTimers.push(completionTimer);
496
+ }
497
+ async handleGoHome() {
498
+ this.logInfo('goHome command received.');
499
+ if (this.sharkDevice) {
500
+ try {
501
+ await this.sharkDevice.set_operating_mode(OperatingModes.RETURN);
502
+ }
503
+ catch (e) {
504
+ this.logError('Error sending Shark device home:', e);
505
+ }
506
+ }
507
+ // Clear any existing timers
508
+ this.clearTimers();
509
+ // Defer state updates to ensure handler completes first
510
+ setImmediate(() => {
511
+ // Set to Idle mode since we're ending the cleaning session
512
+ this.updateRunMode(0);
513
+ // Initiate return to dock sequence
514
+ this.returnToDock();
515
+ });
516
+ }
517
+ async handleSelectAreas(request) {
518
+ this.logInfo(`SelectAreas request received: ${JSON.stringify(request)}`);
519
+ const { newAreas } = request;
520
+ if (this.sharkDevice && typeof this.sharkDevice.get_room_list === 'function') {
521
+ try {
522
+ const rooms = this.sharkDevice.get_room_list() || [];
523
+ const selected = newAreas.map((id) => rooms[id]).filter(Boolean);
524
+ this.logInfo(`selecting areas: ${selected.join(', ')}`);
525
+ if (selected.length > 0) {
526
+ await this.sharkDevice.clean_rooms(selected);
527
+ }
528
+ }
529
+ catch (e) {
530
+ this.logError('Error selecting areas on Shark device:', e);
531
+ }
532
+ }
533
+ else {
534
+ const areaNames = newAreas.map((id) => ['Living Room', 'Kitchen', 'Bedroom', 'Bathroom'][id] || `Area ${id}`);
535
+ this.logInfo(`selecting areas: ${areaNames.join(', ')}`);
536
+ }
537
+ }
538
+ async handleSkipArea(request) {
539
+ this.logInfo(`SkipArea request received: ${JSON.stringify(request)}`);
540
+ const { skippedArea } = request;
541
+ const areaName = ['Living Room', 'Kitchen', 'Bedroom', 'Bathroom'][skippedArea] || `Area ${skippedArea}`;
542
+ this.logInfo(`skipping area: ${areaName}`);
543
+ // Shark API does not provide skipArea; best-effort: log and leave running session intact.
544
+ }
545
+ /**
546
+ * Helper method to clear all active timers
547
+ */
548
+ clearTimers() {
549
+ this.activeTimers.forEach(timer => clearTimeout(timer));
550
+ this.activeTimers = [];
551
+ }
552
+ /**
553
+ * Helper method to initiate return to dock sequence
554
+ * Can be called synchronously from other handlers
555
+ */
556
+ returnToDock() {
557
+ this.logInfo('initiating return to dock sequence.');
558
+ // Defer ALL state updates to ensure handler completes first
559
+ setImmediate(() => {
560
+ // Start seeking charger directly (skip intermediate Stopped state)
561
+ this.updateOperationalState(64); // Seeking Charger
562
+ // After 5 seconds, start charging
563
+ const chargingTimer = setTimeout(() => {
564
+ this.logInfo('reached dock, now charging.');
565
+ this.updateOperationalState(65); // Charging
566
+ // After 3 more seconds, fully docked
567
+ const dockedTimer = setTimeout(() => {
568
+ this.logInfo('fully charged and docked.');
569
+ this.updateOperationalState(66); // Docked
570
+ }, 3000);
571
+ this.activeTimers.push(dockedTimer);
572
+ }, 5000);
573
+ this.activeTimers.push(chargingTimer);
574
+ });
575
+ }
576
+ /**
577
+ * Update Methods - Use these to update the vacuum state from your API/device
578
+ *
579
+ * Since this is a platform accessory, state updates work immediately after registration.
580
+ */
581
+ updateOperationalState(state) {
582
+ this.updateState('rvcOperationalState', { operationalState: state });
583
+ const states = [
584
+ 'Stopped',
585
+ 'Running',
586
+ 'Paused',
587
+ 'Error',
588
+ null,
589
+ null,
590
+ null,
591
+ null,
592
+ ...Array.from({ length: 56 }).fill(null),
593
+ 'Seeking Charger',
594
+ 'Charging',
595
+ 'Docked',
596
+ ];
597
+ this.logInfo(`operational state updated to: ${states[state] || `Unknown (${state})`}`);
598
+ }
599
+ updateRunMode(mode) {
600
+ this.updateState('rvcRunMode', { currentMode: mode });
601
+ const modes = ['Idle', 'Cleaning', 'Mapping'];
602
+ this.logInfo(`run mode updated to: ${modes[mode] || `Unknown (${mode})`}`);
603
+ }
604
+ updateCleanMode(mode) {
605
+ this.updateState('rvcCleanMode', { currentMode: mode });
606
+ const modes = [
607
+ 'Vacuum',
608
+ 'Mop',
609
+ 'Vacuum & Mop',
610
+ 'Deep Clean',
611
+ 'Deep Vacuum',
612
+ 'Deep Mop',
613
+ 'Quick Clean',
614
+ 'Max Clean',
615
+ 'Min Clean',
616
+ 'Quiet Vacuum',
617
+ 'Quiet Mop',
618
+ 'Night Mode',
619
+ 'Eco Vacuum',
620
+ 'Eco Mop',
621
+ 'Auto',
622
+ ];
623
+ this.logInfo(`clean mode updated to: ${modes[mode] || `Unknown (${mode})`}`);
624
+ }
625
+ updateSelectedAreas(areaIds) {
626
+ this.updateState('serviceArea', { selectedAreas: areaIds });
627
+ const areaNames = areaIds.map(id => ['Living Room', 'Kitchen', 'Bedroom', 'Bathroom'][id] || `Area ${id}`);
628
+ this.logInfo(`selected areas updated to: ${areaNames.join(', ') || 'All Areas'}`);
629
+ }
630
+ updateCurrentArea(areaId) {
631
+ this.updateState('serviceArea', { currentArea: areaId });
632
+ if (areaId !== null) {
633
+ const areaName = ['Living Room', 'Kitchen', 'Bedroom', 'Bathroom'][areaId] || `Area ${areaId}`;
634
+ this.logInfo(`current area updated to: ${areaName}`);
635
+ }
636
+ else {
637
+ this.logInfo('current area cleared');
638
+ }
639
+ }
640
+ updateProgress(progress) {
641
+ this.updateState('serviceArea', { progress });
642
+ this.logInfo(`progress updated: ${progress.length} areas`);
643
+ }
644
+ }
645
+ //# sourceMappingURL=RoboticVacuumAccessory.js.map