@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.
- package/CHANGELOG.md +19 -2
- package/README.md +41 -4
- package/config.schema.json +8 -26
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js.map +1 -1
- package/dist/devices/BaseMatterAccessory.d.ts +74 -0
- package/dist/devices/BaseMatterAccessory.d.ts.map +1 -0
- package/dist/devices/BaseMatterAccessory.js +210 -0
- package/dist/devices/BaseMatterAccessory.js.map +1 -0
- package/dist/devices/RoboticVacuumAccessory.d.ts +85 -0
- package/dist/devices/RoboticVacuumAccessory.d.ts.map +1 -0
- package/dist/devices/RoboticVacuumAccessory.js +944 -0
- package/dist/devices/RoboticVacuumAccessory.js.map +1 -0
- package/dist/devices/index.d.ts +9 -0
- package/dist/devices/index.d.ts.map +1 -0
- package/dist/devices/index.js +8 -0
- package/dist/devices/index.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/login.js +1 -1
- package/dist/login.js.map +1 -1
- package/dist/platform.d.ts +39 -16
- package/dist/platform.d.ts.map +1 -1
- package/dist/platform.js +196 -104
- package/dist/platform.js.map +1 -1
- package/dist/sharkiq-js/ayla_api.js.map +1 -1
- package/dist/sharkiq-js/room_encoding.d.ts +8 -0
- package/dist/sharkiq-js/room_encoding.d.ts.map +1 -0
- package/dist/sharkiq-js/room_encoding.js +35 -0
- package/dist/sharkiq-js/room_encoding.js.map +1 -0
- package/dist/sharkiq-js/sharkiq.d.ts.map +1 -1
- package/dist/sharkiq-js/sharkiq.js +3 -20
- package/dist/sharkiq-js/sharkiq.js.map +1 -1
- package/docs/assets/highlight.css +9 -2
- package/docs/index.html +35 -3
- package/docs/variables/default.html +2 -1
- package/package.json +15 -15
- package/dist/errorHandling.d.ts +0 -19
- package/dist/errorHandling.d.ts.map +0 -1
- package/dist/errorHandling.js +0 -27
- package/dist/errorHandling.js.map +0 -1
- package/dist/platformAccessory.d.ts +0 -33
- package/dist/platformAccessory.d.ts.map +0 -1
- package/dist/platformAccessory.js +0 -384
- 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
|