@homebridge-plugins/homebridge-sharkiq 2.0.0-beta.3 → 2.0.0-beta.30

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 +74 -0
  7. package/dist/devices/BaseMatterAccessory.d.ts.map +1 -0
  8. package/dist/devices/BaseMatterAccessory.js +210 -0
  9. package/dist/devices/BaseMatterAccessory.js.map +1 -0
  10. package/dist/devices/RoboticVacuumAccessory.d.ts +85 -0
  11. package/dist/devices/RoboticVacuumAccessory.d.ts.map +1 -0
  12. package/dist/devices/RoboticVacuumAccessory.js +944 -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/login.js +1 -1
  23. package/dist/login.js.map +1 -1
  24. package/dist/platform.d.ts +39 -16
  25. package/dist/platform.d.ts.map +1 -1
  26. package/dist/platform.js +196 -104
  27. package/dist/platform.js.map +1 -1
  28. package/dist/sharkiq-js/ayla_api.js.map +1 -1
  29. package/dist/sharkiq-js/room_encoding.d.ts +8 -0
  30. package/dist/sharkiq-js/room_encoding.d.ts.map +1 -0
  31. package/dist/sharkiq-js/room_encoding.js +35 -0
  32. package/dist/sharkiq-js/room_encoding.js.map +1 -0
  33. package/dist/sharkiq-js/sharkiq.d.ts.map +1 -1
  34. package/dist/sharkiq-js/sharkiq.js +3 -20
  35. package/dist/sharkiq-js/sharkiq.js.map +1 -1
  36. package/docs/assets/highlight.css +9 -2
  37. package/docs/index.html +35 -3
  38. package/docs/variables/default.html +2 -1
  39. package/package.json +15 -15
  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,944 @@
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
+ // Cache the last known run mode so we only emit info logs when it changes
7
+ lastRunMode = null;
8
+ // Use protected for battery/charging cache to match base class
9
+ lastBatteryLevel = null;
10
+ lastChargingStatus = null;
11
+ // Cache operational state so we only emit info logs when it changes
12
+ lastOperationalState = null;
13
+ // Cache last seen RSSI so we only log when it changes
14
+ lastRssi = null;
15
+ // Cache last selected areas so we only emit info logs when selection changes
16
+ lastSelectedAreas = null;
17
+ // Cache last clean mode so we only emit info logs when it changes
18
+ lastCleanMode = null;
19
+ sharkDevice;
20
+ pollInterval;
21
+ constructor(api, log, sharkDevice, pollIntervalMs) {
22
+ // If possible, initialize lastRunMode from the device or persisted state
23
+ // This is done after super() returns so api and other properties are available.
24
+ const serialNumber = (sharkDevice && (sharkDevice.serial_number || sharkDevice._dsn)) || 'VACUUM-001';
25
+ const displayName = (sharkDevice && (sharkDevice.name || sharkDevice._name)) || 'Robot Vacuum';
26
+ const manufacturer = sharkDevice ? 'Shark' : 'Homebridge Matter';
27
+ const model = (sharkDevice && (sharkDevice.vac_model_number || sharkDevice._vac_model_number)) || 'HB-MATTER-VACUUM-ROBOTIC';
28
+ const firmwareRevision = (sharkDevice && (sharkDevice._firmware_version || 'unknown')) || '2.0.0';
29
+ // Build serviceArea cluster from device rooms (local helper so we don't use `this` before super)
30
+ const serviceAreaCluster = (() => {
31
+ if (!sharkDevice || typeof sharkDevice.get_room_list !== 'function') {
32
+ return {
33
+ supportedMaps: [],
34
+ supportedAreas: [],
35
+ selectedAreas: [],
36
+ };
37
+ }
38
+ try {
39
+ const rooms = sharkDevice.get_room_list() || [];
40
+ const supportedAreas = rooms.map((roomName, idx) => ({
41
+ areaId: idx,
42
+ mapId: null,
43
+ areaInfo: {
44
+ locationInfo: {
45
+ locationName: roomName,
46
+ floorNumber: 0,
47
+ areaType: 0,
48
+ },
49
+ landmarkInfo: null,
50
+ },
51
+ }));
52
+ return {
53
+ supportedMaps: [],
54
+ supportedAreas,
55
+ selectedAreas: supportedAreas.map(a => a.areaId),
56
+ };
57
+ }
58
+ catch {
59
+ return {
60
+ supportedMaps: [],
61
+ supportedAreas: [],
62
+ selectedAreas: [],
63
+ };
64
+ }
65
+ })();
66
+ super(api, log, {
67
+ uuid: api.matter.uuid.generate(serialNumber),
68
+ displayName,
69
+ deviceType: api.matter.deviceTypes.RoboticVacuumCleaner,
70
+ serialNumber,
71
+ manufacturer,
72
+ model,
73
+ firmwareRevision,
74
+ hardwareRevision: '1.0.0',
75
+ // Persist useful metadata in accessory.context so cached accessories retain DSN/model/etc.
76
+ context: {
77
+ serialNumber,
78
+ name: displayName,
79
+ vacModel: model,
80
+ vacSerial: (sharkDevice && (sharkDevice.vac_serial_number || sharkDevice._vac_serial_number)) || null,
81
+ oemModel: (sharkDevice && (sharkDevice.oem_model_number || sharkDevice._oem_model_number)) || null,
82
+ pollIntervalMs: typeof pollIntervalMs === 'number' ? pollIntervalMs : null,
83
+ // Cached runtime values persisted so they survive restarts
84
+ lastRunMode: null,
85
+ lastBatteryLevel: null,
86
+ lastChargingStatus: null,
87
+ lastOperationalState: null,
88
+ lastRssi: null,
89
+ lastSelectedAreas: null,
90
+ lastCleanMode: null,
91
+ },
92
+ clusters: {
93
+ // Run Mode: Controls what the vacuum is doing (Idle, Cleaning, Mapping)
94
+ rvcRunMode: {
95
+ supportedModes: [
96
+ { label: 'Idle', mode: 0, modeTags: [{ value: 16384 }] }, // RvcRunMode.ModeTag.Idle
97
+ { label: 'Cleaning', mode: 1, modeTags: [{ value: 16385 }] }, // RvcRunMode.ModeTag.Cleaning
98
+ { label: 'Mapping', mode: 2, modeTags: [{ value: 16386 }] }, // RvcRunMode.ModeTag.Mapping
99
+ ],
100
+ currentMode: 0,
101
+ },
102
+ // Clean Mode: Controls HOW the vacuum cleans
103
+ // You can combine semantic tags (0-9) with functional tags (16384-16386)
104
+ // Available semantic tags: Auto, Quick, Quiet, LowNoise, LowEnergy, Vacation, Min, Max, Night, Day
105
+ // Available functional tags: DeepClean, Vacuum, Mop
106
+ rvcCleanMode: {
107
+ supportedModes: [
108
+ // Basic functional modes
109
+ { label: 'Vacuum', mode: 0, modeTags: [{ value: 16385 }] }, // Vacuum
110
+ { label: 'Mop', mode: 1, modeTags: [{ value: 16386 }] }, // Mop
111
+ { label: 'Vacuum & Mop', mode: 2, modeTags: [{ value: 16385 }, { value: 16386 }] }, // Both
112
+ // Deep clean modes
113
+ { label: 'Deep Clean', mode: 3, modeTags: [{ value: 16384 }] }, // DeepClean
114
+ { label: 'Deep Vacuum', mode: 4, modeTags: [{ value: 16384 }, { value: 16385 }] }, // DeepClean + Vacuum
115
+ { label: 'Deep Mop', mode: 5, modeTags: [{ value: 16384 }, { value: 16386 }] }, // DeepClean + Mop
116
+ // Intensity modes
117
+ { label: 'Quick Clean', mode: 6, modeTags: [{ value: 1 }, { value: 16385 }] }, // Quick + Vacuum
118
+ { label: 'Max Clean', mode: 7, modeTags: [{ value: 7 }, { value: 16385 }] }, // Max + Vacuum
119
+ { label: 'Min Clean', mode: 8, modeTags: [{ value: 6 }, { value: 16385 }] }, // Min + Vacuum
120
+ // Quiet modes
121
+ { label: 'Quiet Vacuum', mode: 9, modeTags: [{ value: 2 }, { value: 16385 }] }, // Quiet + Vacuum
122
+ { label: 'Quiet Mop', mode: 10, modeTags: [{ value: 2 }, { value: 16386 }] }, // Quiet + Mop
123
+ { label: 'Night Mode', mode: 11, modeTags: [{ value: 8 }, { value: 16385 }] }, // Night + Vacuum
124
+ // Energy efficient
125
+ { label: 'Eco Vacuum', mode: 12, modeTags: [{ value: 4 }, { value: 16385 }] }, // LowEnergy + Vacuum
126
+ { label: 'Eco Mop', mode: 13, modeTags: [{ value: 4 }, { value: 16386 }] }, // LowEnergy + Mop
127
+ // Auto mode
128
+ { label: 'Auto', mode: 14, modeTags: [{ value: 0 }, { value: 16385 }] }, // Auto + Vacuum
129
+ ],
130
+ currentMode: 0, // start with basic Vacuum
131
+ },
132
+ // Operational State: Current state (Stopped, Running, Paused, Error, etc.)
133
+ rvcOperationalState: {
134
+ operationalStateList: [
135
+ { operationalStateId: 0 }, // stopped (standard label from Matter spec)
136
+ { operationalStateId: 1 }, // running
137
+ { operationalStateId: 2 }, // paused
138
+ { operationalStateId: 3 }, // error
139
+ { operationalStateId: 64 }, // seeking charger
140
+ { operationalStateId: 65 }, // charging
141
+ { operationalStateId: 66 }, // docked
142
+ ],
143
+ operationalState: 66, // start docked
144
+ },
145
+ // Service Area: Room/zone selection for targeted cleaning
146
+ serviceArea: serviceAreaCluster,
147
+ // Legacy informational clusters are intentionally NOT registered here.
148
+ // updateState() will special-case 'power' and 'diagnostics' and
149
+ // translate or skip updates when the accessory does not actually
150
+ // support those clusters. Registering empty legacy clusters can
151
+ // result in Behavior ID errors on Matter servers that don't expose
152
+ // those behaviors, so we avoid adding them.
153
+ },
154
+ // Map legacy cluster names to supported clusters. This allows callers
155
+ // that still use legacy names to be translated to the correct
156
+ // cluster when appropriate. If the mapped target is not present on
157
+ // the accessory, BaseMatterAccessory.updateState() will safely skip
158
+ // or translate attributes instead of forwarding an update that
159
+ // would cause a Behavior ID error on the Matter server.
160
+ clusterNameMap: {
161
+ power: 'rvcOperationalState',
162
+ diagnostics: 'rvcOperationalState',
163
+ },
164
+ handlers: {
165
+ rvcRunMode: {
166
+ changeToMode: async (request) => this.handleChangeRunMode(request),
167
+ },
168
+ rvcCleanMode: {
169
+ changeToMode: async (request) => this.handleChangeCleanMode(request),
170
+ },
171
+ rvcOperationalState: {
172
+ pause: async () => this.handlePause(),
173
+ stop: async () => this.handleStop(),
174
+ start: async () => this.handleStart(),
175
+ resume: async () => this.handleResume(),
176
+ goHome: async () => this.handleGoHome(),
177
+ },
178
+ serviceArea: {
179
+ selectAreas: async (request) => this.handleSelectAreas(request),
180
+ skipArea: async (request) => this.handleSkipArea(request),
181
+ },
182
+ // Note: battery/diagnostics updates are sent at runtime from polling via
183
+ // updateState(). The previous implementation placed static objects under
184
+ // `handlers` using non-standard cluster names ('power' / 'diagnostics'),
185
+ // which can cause errors when those clusters are not supported by the
186
+ // Matter server. We avoid registering invalid handlers here; runtime
187
+ // updates will be guarded by BaseMatterAccessory.updateState().
188
+ },
189
+ });
190
+ this.sharkDevice = sharkDevice;
191
+ // Start polling device state if we have a Shark device and polling is enabled
192
+ if (this.sharkDevice && typeof pollIntervalMs === 'number' && pollIntervalMs > 0) {
193
+ this.startPolling(pollIntervalMs);
194
+ }
195
+ else if (this.sharkDevice && (typeof pollIntervalMs !== 'number' || pollIntervalMs <= 0)) {
196
+ // If pollIntervalMs is omitted, default to 30s
197
+ if (typeof pollIntervalMs !== 'number') {
198
+ this.startPolling();
199
+ }
200
+ }
201
+ this.logInfo('initialized and ready.');
202
+ // Initialize cached runtime values (device wrapper preferred; fallback to persisted accessory state)
203
+ this.initCachedState(sharkDevice);
204
+ }
205
+ /**
206
+ * Initialize cached runtime values from the Shark device wrapper where
207
+ * available, otherwise fall back to persisted accessory state exposed by the API.
208
+ */
209
+ initCachedState(sharkDevice) {
210
+ try {
211
+ // Prefer value from Shark device wrapper if available
212
+ if (sharkDevice && typeof sharkDevice.operating_mode === 'function') {
213
+ const op = sharkDevice.operating_mode();
214
+ if (typeof op === 'number') {
215
+ this.persistCache('lastRunMode', op, 'lastRunMode');
216
+ }
217
+ // battery
218
+ if (sharkDevice.battery_capacity && typeof sharkDevice.battery_capacity === 'function') {
219
+ const b = sharkDevice.battery_capacity();
220
+ if (typeof b === 'number') {
221
+ this.persistCache('lastBatteryLevel', Number(b), 'lastBatteryLevel');
222
+ }
223
+ }
224
+ // charging status
225
+ if (sharkDevice.charging_status && typeof sharkDevice.charging_status === 'function') {
226
+ const c = sharkDevice.charging_status();
227
+ if (typeof c !== 'undefined') {
228
+ this.persistCache('lastChargingStatus', c === 1 || c === true, 'lastChargingStatus');
229
+ }
230
+ }
231
+ // clean mode mapping
232
+ if (sharkDevice.power_mode && typeof sharkDevice.power_mode === 'function') {
233
+ try {
234
+ const pm = sharkDevice.power_mode();
235
+ if (typeof pm === 'number') {
236
+ let cleanMode = 0;
237
+ if (pm === PowerModes.ECO) {
238
+ cleanMode = 12; // Eco Vacuum
239
+ }
240
+ else if (pm === PowerModes.MAX) {
241
+ cleanMode = 7; // Max Clean
242
+ }
243
+ this.persistCache('lastCleanMode', cleanMode, 'lastCleanMode');
244
+ }
245
+ }
246
+ catch {
247
+ // ignore
248
+ }
249
+ }
250
+ // docked/operational
251
+ if (sharkDevice.docked_status && typeof sharkDevice.docked_status === 'function') {
252
+ const d = sharkDevice.docked_status();
253
+ if (typeof d === 'number') {
254
+ this.persistCache('lastOperationalState', d === 1 ? 66 : null, 'lastOperationalState');
255
+ }
256
+ }
257
+ if (this.lastOperationalState === null && typeof op === 'number') {
258
+ if (op === OperatingModes.START) {
259
+ this.persistCache('lastOperationalState', 1, 'lastOperationalState');
260
+ }
261
+ else if (op === OperatingModes.PAUSE) {
262
+ this.persistCache('lastOperationalState', 2, 'lastOperationalState');
263
+ }
264
+ else if (op === OperatingModes.RETURN) {
265
+ this.persistCache('lastOperationalState', 64, 'lastOperationalState');
266
+ }
267
+ else if (op === OperatingModes.STOP) {
268
+ this.persistCache('lastOperationalState', 0, 'lastOperationalState');
269
+ }
270
+ }
271
+ // selected areas from device room list
272
+ if (typeof sharkDevice.get_room_list === 'function') {
273
+ try {
274
+ const rooms = sharkDevice.get_room_list() || [];
275
+ const ids = rooms.map((_, idx) => idx);
276
+ const sanitized = Array.from(new Set(ids.filter(n => Number.isInteger(n) && n >= 0 && n < rooms.length)));
277
+ this.persistCache('lastSelectedAreas', sanitized, 'lastSelectedAreas');
278
+ }
279
+ catch {
280
+ // ignore
281
+ }
282
+ }
283
+ }
284
+ else if (this.api.matter && typeof this.api.matter.getAccessoryState === 'function') {
285
+ try {
286
+ const state = this.api.matter.getAccessoryState(this.uuid, 'rvcRunMode');
287
+ if (state && typeof state.currentMode === 'number') {
288
+ this.persistCache('lastRunMode', state.currentMode, 'lastRunMode');
289
+ }
290
+ // persisted power cluster
291
+ try {
292
+ const p = this.api.matter.getAccessoryState(this.uuid, 'power');
293
+ if (p) {
294
+ if (typeof p.batteryLevel === 'number') {
295
+ this.persistCache('lastBatteryLevel', Number(p.batteryLevel), 'lastBatteryLevel');
296
+ }
297
+ if (typeof p.charging !== 'undefined') {
298
+ this.persistCache('lastChargingStatus', p.charging === 1 || p.charging === true, 'lastChargingStatus');
299
+ }
300
+ }
301
+ }
302
+ catch {
303
+ // ignore
304
+ }
305
+ // persisted clean mode
306
+ try {
307
+ const c = this.api.matter.getAccessoryState(this.uuid, 'rvcCleanMode');
308
+ if (c && typeof c.currentMode === 'number') {
309
+ this.persistCache('lastCleanMode', Number(c.currentMode), 'lastCleanMode');
310
+ }
311
+ }
312
+ catch {
313
+ // ignore
314
+ }
315
+ // persisted diagnostics (RSSI)
316
+ try {
317
+ const d = this.api.matter.getAccessoryState(this.uuid, 'diagnostics');
318
+ if (d && typeof d.rssi === 'number') {
319
+ this.persistCache('lastRssi', Number(d.rssi), 'lastRssi');
320
+ }
321
+ }
322
+ catch {
323
+ // ignore
324
+ }
325
+ // persisted operational state
326
+ try {
327
+ const o = this.api.matter.getAccessoryState(this.uuid, 'rvcOperationalState');
328
+ if (o && typeof o.operationalState === 'number') {
329
+ this.persistCache('lastOperationalState', Number(o.operationalState), 'lastOperationalState');
330
+ }
331
+ }
332
+ catch {
333
+ // ignore
334
+ }
335
+ // persisted selected areas
336
+ try {
337
+ const s = this.api.matter.getAccessoryState(this.uuid, 'serviceArea');
338
+ if (s && Array.isArray(s.selectedAreas)) {
339
+ const raw = Array.isArray(s.selectedAreas) ? s.selectedAreas.map((v) => Number(v)).filter((n) => Number.isInteger(n) && n >= 0) : [];
340
+ const sanitized = raw.length > 0 ? Array.from(new Set(raw)) : null;
341
+ this.persistCache('lastSelectedAreas', sanitized, 'lastSelectedAreas');
342
+ }
343
+ }
344
+ catch {
345
+ // ignore
346
+ }
347
+ }
348
+ catch {
349
+ // ignore failures reading accessory state
350
+ }
351
+ }
352
+ }
353
+ catch (e) {
354
+ // Ignore any initialization errors; keep caches as-is
355
+ }
356
+ }
357
+ /**
358
+ * Persist a named cache value into accessory context and keep in-memory copy.
359
+ * This centralizes writes so callers only need to update the in-memory field
360
+ * and call this to persist to Homebridge accessory.context.
361
+ */
362
+ persistCache(key, value, inMemoryKey) {
363
+ try {
364
+ // Update in-memory cache if requested
365
+ if (typeof inMemoryKey === 'string' && inMemoryKey.length > 0) {
366
+ try {
367
+ ;
368
+ this[inMemoryKey] = value;
369
+ }
370
+ catch (e) {
371
+ // ignore failures to set in-memory field
372
+ }
373
+ }
374
+ // Write value into this.context under the same key name
375
+ ;
376
+ this.context[key] = value;
377
+ }
378
+ catch (e) {
379
+ // Best-effort: ignore failures to persist context
380
+ }
381
+ }
382
+ startPolling(intervalMs = 30000) {
383
+ if (this.pollInterval) {
384
+ return;
385
+ }
386
+ this.pollInterval = setInterval(async () => {
387
+ try {
388
+ // Request a full update
389
+ this.logDebug('Polling: requesting full update from Shark device...');
390
+ await this.sharkDevice.update([]);
391
+ this.logDebug('Polling: Shark device property_values after update:', this.sharkDevice.property_values);
392
+ // Update run/operating mode
393
+ const opMode = this.sharkDevice.operating_mode?.() ?? null;
394
+ const powerMode = this.sharkDevice.power_mode?.() ?? null;
395
+ const docked = this.sharkDevice.docked_status?.() ?? null;
396
+ this.logDebug('Polling: opMode:', opMode, 'powerMode:', powerMode, 'docked:', docked);
397
+ // Map operating mode to Matter run/operational state
398
+ if (docked !== null && docked === 1) {
399
+ // Docked
400
+ this.updateOperationalState(66);
401
+ this.updateRunMode(0);
402
+ }
403
+ else if (opMode !== null) {
404
+ if (opMode === OperatingModes.START) {
405
+ this.updateRunMode(1);
406
+ this.updateOperationalState(1);
407
+ }
408
+ else if (opMode === OperatingModes.PAUSE) {
409
+ this.updateOperationalState(2);
410
+ this.updateRunMode(1);
411
+ }
412
+ else if (opMode === OperatingModes.RETURN) {
413
+ this.updateOperationalState(64);
414
+ this.updateRunMode(0);
415
+ }
416
+ else if (opMode === OperatingModes.STOP) {
417
+ this.updateOperationalState(0);
418
+ this.updateRunMode(0);
419
+ }
420
+ }
421
+ // Map power mode to clean mode (best-effort mapping)
422
+ if (powerMode !== null) {
423
+ let cleanMode = 0; // Vacuum
424
+ if (powerMode === PowerModes.ECO) {
425
+ cleanMode = 12; // Eco Vacuum
426
+ }
427
+ else if (powerMode === PowerModes.MAX) {
428
+ cleanMode = 7; // Max Clean
429
+ }
430
+ else {
431
+ cleanMode = 0;
432
+ }
433
+ this.updateCleanMode(cleanMode);
434
+ }
435
+ // Update service areas selection from device room list
436
+ if (typeof this.sharkDevice.get_room_list === 'function') {
437
+ const rooms = this.sharkDevice.get_room_list() || [];
438
+ const ids = rooms.map((_, idx) => idx);
439
+ this.logDebug('Polling: device rooms:', rooms, 'room ids:', ids);
440
+ this.updateSelectedAreas(ids);
441
+ }
442
+ // Update battery level, charging and diagnostics using upstream helpers when available
443
+ try {
444
+ // Prefer convenience properties if present on the wrapper
445
+ let batteryVal = null;
446
+ let chargingVal = null;
447
+ let errVal = null;
448
+ let errText = null;
449
+ this.logDebug('Polling: property_values before battery/charging/diagnostics extraction:', this.sharkDevice.property_values);
450
+ if (this.sharkDevice.battery_capacity && typeof this.sharkDevice.battery_capacity === 'function') {
451
+ batteryVal = this.sharkDevice.battery_capacity();
452
+ }
453
+ else if (typeof this.sharkDevice.get_property_value === 'function') {
454
+ batteryVal = this.sharkDevice.get_property_value(Properties.BATTERY_CAPACITY);
455
+ }
456
+ if (this.sharkDevice.charging_status && typeof this.sharkDevice.charging_status === 'function') {
457
+ chargingVal = this.sharkDevice.charging_status();
458
+ }
459
+ else if (typeof this.sharkDevice.get_property_value === 'function') {
460
+ chargingVal = this.sharkDevice.get_property_value(Properties.CHARGING_STATUS);
461
+ }
462
+ // Error code / text
463
+ if (this.sharkDevice.error_code !== undefined && typeof this.sharkDevice.error_code === 'function') {
464
+ errVal = this.sharkDevice.error_code();
465
+ if (this.sharkDevice.error_text && typeof this.sharkDevice.error_text === 'function') {
466
+ errText = this.sharkDevice.error_text();
467
+ }
468
+ }
469
+ else if (typeof this.sharkDevice.get_property_value === 'function') {
470
+ errVal = this.sharkDevice.get_property_value(Properties.ERROR_CODE);
471
+ }
472
+ this.logDebug('Polling: extracted batteryVal:', batteryVal, 'chargingVal:', chargingVal, 'errVal:', errVal, 'errText:', errText);
473
+ if (batteryVal !== null && typeof batteryVal !== 'undefined') {
474
+ const pct = Number(batteryVal) || 0;
475
+ // chargingVal may be boolean-like or numeric
476
+ const isCharging = chargingVal === 1 || chargingVal === true;
477
+ // Update in-memory cache in base for log suppression
478
+ this.lastBatteryLevel = pct;
479
+ this.lastChargingStatus = isCharging;
480
+ await this.updateState('power', { batteryLevel: Math.max(0, Math.min(100, pct)), charging: isCharging });
481
+ this.logDebug(`battery updated: ${pct}% (charging: ${isCharging})`);
482
+ // Only emit info logs when battery level or charging status actually changes
483
+ if (this.lastBatteryLevel !== pct || this.lastChargingStatus !== isCharging) {
484
+ this.logInfo(`battery level: ${pct}%, charging: ${isCharging}`);
485
+ // Persist both in-memory and accessory.context via helper
486
+ this.persistCache('lastBatteryLevel', pct, 'lastBatteryLevel');
487
+ this.persistCache('lastChargingStatus', isCharging, 'lastChargingStatus');
488
+ }
489
+ }
490
+ // RSSI (signal strength) mapping if available
491
+ try {
492
+ const rssi = this.sharkDevice.get_property_value
493
+ ? this.sharkDevice.get_property_value(Properties.RSSI)
494
+ : null;
495
+ this.logDebug('Polling: extracted RSSI:', rssi);
496
+ if (rssi !== null && typeof rssi !== 'undefined') {
497
+ const r = Number(rssi);
498
+ await this.updateState('diagnostics', { ...null, rssi: r });
499
+ // Only log when RSSI value actually changes
500
+ if (this.lastRssi !== r) {
501
+ this.logInfo(`rssi updated: ${r}`);
502
+ this.persistCache('lastRssi', r, 'lastRssi');
503
+ }
504
+ else {
505
+ this.logDebug(`rssi unchanged: ${r}`);
506
+ }
507
+ }
508
+ }
509
+ catch (e) {
510
+ // ignore
511
+ }
512
+ if (errVal && Number(errVal) > 0) {
513
+ const errCode = Number(errVal);
514
+ // Prefer human friendly text from device wrapper if provided
515
+ const text = errText || ERROR_MESSAGES[errCode] || `Error ${errCode}`;
516
+ // Map severity: treat these as actionable errors (set operational state to Error)
517
+ await this.updateState('diagnostics', { errorCode: errCode, errorText: text, severity: 'critical' });
518
+ this.updateOperationalState(3); // Error
519
+ this.logWarn(`device error: ${errCode} - ${text}`);
520
+ }
521
+ else if (errVal === 0 || errVal === null || typeof errVal === 'undefined') {
522
+ await this.updateState('diagnostics', { errorCode: 0, errorText: '', severity: 'ok' });
523
+ }
524
+ // Clean complete mapping - if device reports a clean complete flag, reflect it
525
+ try {
526
+ const cleanComplete = this.sharkDevice.get_property_value
527
+ ? this.sharkDevice.get_property_value(Properties.CLEAN_COMPLETE)
528
+ : null;
529
+ this.logDebug('Polling: extracted cleanComplete:', cleanComplete);
530
+ if (cleanComplete === 1 || cleanComplete === true) {
531
+ // When cleaning completes, set run mode to Idle and operational to Stopped
532
+ this.updateRunMode(0);
533
+ this.updateOperationalState(0);
534
+ this.logInfo('clean complete reported by device - updating state to Idle/Stopped');
535
+ }
536
+ }
537
+ catch (e) {
538
+ // ignore
539
+ }
540
+ }
541
+ catch (e) {
542
+ this.logDebug('Error updating battery/diagnostics:', e);
543
+ }
544
+ }
545
+ catch (e) {
546
+ this.logError('Polling error for Shark device:', e);
547
+ }
548
+ }, intervalMs);
549
+ }
550
+ stopPolling() {
551
+ if (this.pollInterval) {
552
+ clearInterval(this.pollInterval);
553
+ this.pollInterval = undefined;
554
+ }
555
+ }
556
+ // Helper to build serviceArea cluster from Shark device rooms
557
+ buildServiceAreaCluster(sharkDevice) {
558
+ if (!sharkDevice || typeof sharkDevice.get_room_list !== 'function') {
559
+ return {
560
+ supportedMaps: [],
561
+ supportedAreas: [],
562
+ selectedAreas: [],
563
+ };
564
+ }
565
+ try {
566
+ const rooms = sharkDevice.get_room_list() || [];
567
+ const supportedAreas = rooms.map((roomName, idx) => ({
568
+ areaId: idx,
569
+ mapId: null,
570
+ areaInfo: {
571
+ locationInfo: {
572
+ locationName: roomName,
573
+ floorNumber: 0,
574
+ areaType: 0,
575
+ },
576
+ landmarkInfo: null,
577
+ },
578
+ }));
579
+ return {
580
+ supportedMaps: [],
581
+ supportedAreas,
582
+ selectedAreas: supportedAreas.map(a => a.areaId),
583
+ };
584
+ }
585
+ catch (e) {
586
+ return {
587
+ supportedMaps: [],
588
+ supportedAreas: [],
589
+ selectedAreas: [],
590
+ };
591
+ }
592
+ }
593
+ async handleChangeRunMode(request) {
594
+ this.logInfo(`ChangeToMode (run) request received: ${JSON.stringify(request)}`);
595
+ const { newMode } = request;
596
+ const modeStr = ['Idle', 'Cleaning', 'Mapping'][newMode] || `Unknown (mode=${newMode})`;
597
+ this.logInfo(`changing run mode to: ${modeStr}`);
598
+ if (this.sharkDevice) {
599
+ try {
600
+ if (newMode === 1) {
601
+ await this.sharkDevice.set_operating_mode(OperatingModes.START);
602
+ }
603
+ else if (newMode === 0) {
604
+ await this.sharkDevice.set_operating_mode(OperatingModes.STOP);
605
+ }
606
+ else if (newMode === 2) {
607
+ // Mapping - treat as start for now
608
+ await this.sharkDevice.set_operating_mode(OperatingModes.START);
609
+ }
610
+ }
611
+ catch (e) {
612
+ this.logError('Error setting run mode on Shark device:', e);
613
+ }
614
+ }
615
+ // Clear any existing timers
616
+ this.clearTimers();
617
+ if (newMode === 1) {
618
+ // Switching to Cleaning mode - start the vacuum
619
+ this.updateOperationalState(1); // Running
620
+ // Simulate cleaning completion after 15 seconds
621
+ const completionTimer = setTimeout(() => {
622
+ this.logInfo('cleaning completed, returning to dock.');
623
+ this.updateRunMode(0); // Set to Idle - cleaning session ending
624
+ this.returnToDock();
625
+ }, 15000);
626
+ this.activeTimers.push(completionTimer);
627
+ }
628
+ else if (newMode === 0) {
629
+ // Switching to Idle mode - return to dock
630
+ this.returnToDock();
631
+ }
632
+ else if (newMode === 2) {
633
+ // Switching to Mapping mode - start mapping
634
+ this.updateOperationalState(1); // Running
635
+ // Simulate mapping completion after 20 seconds
636
+ const completionTimer = setTimeout(() => {
637
+ this.logInfo('mapping completed, returning to dock.');
638
+ this.returnToDock();
639
+ }, 20000);
640
+ this.activeTimers.push(completionTimer);
641
+ }
642
+ }
643
+ async handleChangeCleanMode(request) {
644
+ this.logInfo(`ChangeToMode (clean) request received: ${JSON.stringify(request)}`);
645
+ const { newMode } = request;
646
+ const modes = [
647
+ 'Vacuum',
648
+ 'Mop',
649
+ 'Vacuum & Mop',
650
+ 'Deep Clean',
651
+ 'Deep Vacuum',
652
+ 'Deep Mop',
653
+ 'Quick Clean',
654
+ 'Max Clean',
655
+ 'Min Clean',
656
+ 'Quiet Vacuum',
657
+ 'Quiet Mop',
658
+ 'Night Mode',
659
+ 'Eco Vacuum',
660
+ 'Eco Mop',
661
+ 'Auto',
662
+ ];
663
+ const modeStr = modes[newMode] || `Unknown (mode=${newMode})`;
664
+ this.logInfo(`changing clean mode to: ${modeStr}`);
665
+ if (this.sharkDevice) {
666
+ // Shark API doesn't expose a direct clean-mode setter here; keep local state and log.
667
+ this.logInfo('Clean mode change requested for Shark device (not implemented in API wrapper).');
668
+ }
669
+ }
670
+ async handlePause() {
671
+ this.logInfo('pausing.');
672
+ if (this.sharkDevice) {
673
+ try {
674
+ await this.sharkDevice.set_operating_mode(OperatingModes.PAUSE);
675
+ }
676
+ catch (e) {
677
+ this.logError('Error pausing Shark device:', e);
678
+ }
679
+ }
680
+ this.clearTimers(); // Clear cleaning completion timer
681
+ this.updateOperationalState(2); // Paused
682
+ }
683
+ async handleStop() {
684
+ this.logInfo('stopping.');
685
+ if (this.sharkDevice) {
686
+ try {
687
+ await this.sharkDevice.set_operating_mode(OperatingModes.STOP);
688
+ }
689
+ catch (e) {
690
+ this.logError('Error stopping Shark device:', e);
691
+ }
692
+ }
693
+ this.clearTimers();
694
+ this.updateRunMode(0); // Reset to Idle
695
+ this.updateOperationalState(0); // Stopped
696
+ }
697
+ async handleStart() {
698
+ this.logInfo('starting (via start command).');
699
+ if (this.sharkDevice) {
700
+ try {
701
+ await this.sharkDevice.set_operating_mode(OperatingModes.START);
702
+ }
703
+ catch (e) {
704
+ this.logError('Error starting Shark device:', e);
705
+ }
706
+ }
707
+ // Clear any existing timers
708
+ this.clearTimers();
709
+ this.updateRunMode(1); // Set to Cleaning mode - this will trigger the run mode handler logic
710
+ this.updateOperationalState(1); // Running
711
+ // Simulate cleaning completion after 15 seconds
712
+ const completionTimer = setTimeout(() => {
713
+ this.logInfo('cleaning completed, returning to dock.');
714
+ this.updateRunMode(0); // Set to Idle - cleaning session ending
715
+ this.returnToDock();
716
+ }, 15000);
717
+ this.activeTimers.push(completionTimer);
718
+ }
719
+ async handleResume() {
720
+ this.logInfo('resuming.');
721
+ if (this.sharkDevice) {
722
+ try {
723
+ await this.sharkDevice.set_operating_mode(OperatingModes.START);
724
+ }
725
+ catch (e) {
726
+ this.logError('Error resuming Shark device:', e);
727
+ }
728
+ }
729
+ // Clear any existing timers
730
+ this.clearTimers();
731
+ this.updateRunMode(1); // Set to Cleaning mode
732
+ this.updateOperationalState(1); // Running
733
+ // Simulate cleaning completion after 10 seconds (shorter since resuming)
734
+ const completionTimer = setTimeout(() => {
735
+ this.logInfo('cleaning completed, returning to dock.');
736
+ this.updateRunMode(0); // Set to Idle - cleaning session ending
737
+ this.returnToDock();
738
+ }, 10000);
739
+ this.activeTimers.push(completionTimer);
740
+ }
741
+ async handleGoHome() {
742
+ this.logInfo('goHome command received.');
743
+ if (this.sharkDevice) {
744
+ try {
745
+ await this.sharkDevice.set_operating_mode(OperatingModes.RETURN);
746
+ }
747
+ catch (e) {
748
+ this.logError('Error sending Shark device home:', e);
749
+ }
750
+ }
751
+ // Clear any existing timers
752
+ this.clearTimers();
753
+ // Defer state updates to ensure handler completes first
754
+ setImmediate(() => {
755
+ // Set to Idle mode since we're ending the cleaning session
756
+ this.updateRunMode(0);
757
+ // Initiate return to dock sequence
758
+ this.returnToDock();
759
+ });
760
+ }
761
+ async handleSelectAreas(request) {
762
+ this.logInfo(`SelectAreas request received: ${JSON.stringify(request)}`);
763
+ const { newAreas } = request;
764
+ if (this.sharkDevice && typeof this.sharkDevice.get_room_list === 'function') {
765
+ try {
766
+ const rooms = this.sharkDevice.get_room_list() || [];
767
+ const selected = newAreas.map((id) => rooms[id]).filter(Boolean);
768
+ this.logInfo(`selecting areas: ${selected.join(', ')}`);
769
+ if (selected.length > 0) {
770
+ await this.sharkDevice.clean_rooms(selected);
771
+ }
772
+ }
773
+ catch (e) {
774
+ this.logError('Error selecting areas on Shark device:', e);
775
+ }
776
+ }
777
+ else {
778
+ const areaNames = newAreas.map((id) => ['Living Room', 'Kitchen', 'Bedroom', 'Bathroom'][id] || `Area ${id}`);
779
+ this.logInfo(`selecting areas: ${areaNames.join(', ')}`);
780
+ }
781
+ }
782
+ async handleSkipArea(request) {
783
+ this.logInfo(`SkipArea request received: ${JSON.stringify(request)}`);
784
+ const { skippedArea } = request;
785
+ const areaName = ['Living Room', 'Kitchen', 'Bedroom', 'Bathroom'][skippedArea] || `Area ${skippedArea}`;
786
+ this.logInfo(`skipping area: ${areaName}`);
787
+ // Shark API does not provide skipArea; best-effort: log and leave running session intact.
788
+ }
789
+ /**
790
+ * Helper method to clear all active timers
791
+ */
792
+ clearTimers() {
793
+ this.activeTimers.forEach(timer => clearTimeout(timer));
794
+ this.activeTimers = [];
795
+ }
796
+ /**
797
+ * Helper method to initiate return to dock sequence
798
+ * Can be called synchronously from other handlers
799
+ */
800
+ returnToDock() {
801
+ this.logInfo('initiating return to dock sequence.');
802
+ // Defer ALL state updates to ensure handler completes first
803
+ setImmediate(() => {
804
+ // Start seeking charger directly (skip intermediate Stopped state)
805
+ this.updateOperationalState(64); // Seeking Charger
806
+ // After 5 seconds, start charging
807
+ const chargingTimer = setTimeout(() => {
808
+ this.logInfo('reached dock, now charging.');
809
+ this.updateOperationalState(65); // Charging
810
+ // After 3 more seconds, fully docked
811
+ const dockedTimer = setTimeout(() => {
812
+ this.logInfo('fully charged and docked.');
813
+ this.updateOperationalState(66); // Docked
814
+ }, 3000);
815
+ this.activeTimers.push(dockedTimer);
816
+ }, 5000);
817
+ this.activeTimers.push(chargingTimer);
818
+ });
819
+ }
820
+ /**
821
+ * Update Methods - Use these to update the vacuum state from your API/device
822
+ *
823
+ * Since this is a platform accessory, state updates work immediately after registration.
824
+ */
825
+ updateOperationalState(state) {
826
+ this.updateState('rvcOperationalState', { operationalState: state });
827
+ const states = [
828
+ 'Stopped',
829
+ 'Running',
830
+ 'Paused',
831
+ 'Error',
832
+ null,
833
+ null,
834
+ null,
835
+ null,
836
+ ...Array.from({ length: 56 }).fill(null),
837
+ 'Seeking Charger',
838
+ 'Charging',
839
+ 'Docked',
840
+ ];
841
+ // Only emit info logs when the operational state actually changes
842
+ if (this.lastOperationalState !== state) {
843
+ this.logInfo(`operational state updated to: ${states[state] || `Unknown (${state})`}`);
844
+ this.lastOperationalState = state;
845
+ this.persistCache('lastOperationalState', state);
846
+ }
847
+ else {
848
+ this.logDebug(`operational state unchanged: ${states[state] || `Unknown (${state})`}`);
849
+ }
850
+ }
851
+ updateRunMode(mode) {
852
+ this.updateState('rvcRunMode', { currentMode: mode });
853
+ const modes = ['Idle', 'Cleaning', 'Mapping'];
854
+ this.logDebug(`run mode updated to: ${modes[mode] || `Unknown (${mode})`}`);
855
+ // Only emit informational logs when the run mode actually changes
856
+ if (this.lastRunMode !== mode) {
857
+ if (mode === 0) {
858
+ this.logInfo('vacuum is now idle.');
859
+ }
860
+ else if (mode === 1) {
861
+ this.logInfo('vacuum is now cleaning.');
862
+ }
863
+ else if (mode === 2) {
864
+ this.logInfo('vacuum is now mapping.');
865
+ }
866
+ this.lastRunMode = mode;
867
+ this.persistCache('lastRunMode', mode);
868
+ }
869
+ }
870
+ updateCleanMode(mode) {
871
+ this.updateState('rvcCleanMode', { currentMode: mode });
872
+ const modes = [
873
+ 'Vacuum',
874
+ 'Mop',
875
+ 'Vacuum & Mop',
876
+ 'Deep Clean',
877
+ 'Deep Vacuum',
878
+ 'Deep Mop',
879
+ 'Quick Clean',
880
+ 'Max Clean',
881
+ 'Min Clean',
882
+ 'Quiet Vacuum',
883
+ 'Quiet Mop',
884
+ 'Night Mode',
885
+ 'Eco Vacuum',
886
+ 'Eco Mop',
887
+ 'Auto',
888
+ ];
889
+ // Only emit info logs when the clean mode actually changes
890
+ if (this.lastCleanMode !== mode) {
891
+ this.logInfo(`clean mode updated to: ${modes[mode] || `Unknown (${mode})`}`);
892
+ this.lastCleanMode = mode;
893
+ this.persistCache('lastCleanMode', mode);
894
+ }
895
+ else {
896
+ this.logDebug(`clean mode unchanged: ${modes[mode] || `Unknown (${mode})`}`);
897
+ }
898
+ }
899
+ updateSelectedAreas(areaIds) {
900
+ this.updateState('serviceArea', { selectedAreas: areaIds });
901
+ // Determine if the selection actually changed (order-insensitive)
902
+ const incoming = Array.isArray(areaIds) ? areaIds.slice().sort((a, b) => a - b) : [];
903
+ const previous = Array.isArray(this.lastSelectedAreas) ? this.lastSelectedAreas.slice().sort((a, b) => a - b) : null;
904
+ let changed = false;
905
+ if (previous === null) {
906
+ changed = true;
907
+ }
908
+ else if (previous.length !== incoming.length) {
909
+ changed = true;
910
+ }
911
+ else {
912
+ for (let i = 0; i < incoming.length; i++) {
913
+ if (incoming[i] !== previous[i]) {
914
+ changed = true;
915
+ break;
916
+ }
917
+ }
918
+ }
919
+ const areaNames = areaIds.map(id => ['Living Room', 'Kitchen', 'Bedroom', 'Bathroom'][id] || `Area ${id}`);
920
+ if (changed) {
921
+ this.logInfo(`selected areas updated to: ${areaNames.join(', ') || 'All Areas'}`);
922
+ this.lastSelectedAreas = Array.isArray(areaIds) ? areaIds.slice() : [];
923
+ this.persistCache('lastSelectedAreas', this.lastSelectedAreas);
924
+ }
925
+ else {
926
+ this.logDebug(`selected areas unchanged: ${areaNames.join(', ') || 'All Areas'}`);
927
+ }
928
+ }
929
+ updateCurrentArea(areaId) {
930
+ this.updateState('serviceArea', { currentArea: areaId });
931
+ if (areaId !== null) {
932
+ const areaName = ['Living Room', 'Kitchen', 'Bedroom', 'Bathroom'][areaId] || `Area ${areaId}`;
933
+ this.logInfo(`current area updated to: ${areaName}`);
934
+ }
935
+ else {
936
+ this.logInfo('current area cleared');
937
+ }
938
+ }
939
+ updateProgress(progress) {
940
+ this.updateState('serviceArea', { progress });
941
+ this.logInfo(`progress updated: ${progress.length} areas`);
942
+ }
943
+ }
944
+ //# sourceMappingURL=RoboticVacuumAccessory.js.map