@diveflo/matterbridge-mova 0.1.18
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/LICENSE +202 -0
- package/README.md +108 -0
- package/dist/constants.d.ts +49 -0
- package/dist/constants.js +303 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/module.d.ts +2 -0
- package/dist/module.js +2 -0
- package/dist/mova.d.ts +13 -0
- package/dist/mova.js +384 -0
- package/dist/movaCloud.d.ts +54 -0
- package/dist/movaCloud.js +1096 -0
- package/dist/platform.d.ts +22 -0
- package/dist/platform.js +246 -0
- package/dist/types.d.ts +247 -0
- package/dist/types.js +180 -0
- package/matterbridge-mova.schema.json +45 -0
- package/package.json +97 -0
package/dist/module.js
ADDED
package/dist/mova.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { RoboticVacuumCleaner } from 'matterbridge/devices';
|
|
2
|
+
import type { MovaPlatform } from './platform.js';
|
|
3
|
+
import type { MovaCloudProtocol } from './movaCloud.js';
|
|
4
|
+
import type { MovaDevice, DeviceStatus, RoomInfo } from './types.js';
|
|
5
|
+
export interface MovaVacuumDevice {
|
|
6
|
+
did: string;
|
|
7
|
+
name: string;
|
|
8
|
+
model: string;
|
|
9
|
+
device: RoboticVacuumCleaner;
|
|
10
|
+
updateStatus: (status: DeviceStatus) => void;
|
|
11
|
+
updateRooms: (rooms: RoomInfo[]) => void;
|
|
12
|
+
}
|
|
13
|
+
export declare function discoverAndRegisterDevices(platform: MovaPlatform, cloud: MovaCloudProtocol, device: MovaDevice, rooms: RoomInfo[], initialStatus: DeviceStatus | null): Promise<MovaVacuumDevice | null>;
|
package/dist/mova.js
ADDED
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import { RoboticVacuumCleaner } from 'matterbridge/devices';
|
|
2
|
+
import { MovaState, MovaStatus, MovaCleaningMode, MovaFanSpeed } from './types.js';
|
|
3
|
+
import { getOperationalStateFromMova, getOperationalErrorFromMova } from './constants.js';
|
|
4
|
+
const BatChargeState = {
|
|
5
|
+
Unknown: 0,
|
|
6
|
+
IsCharging: 1,
|
|
7
|
+
IsAtFullCharge: 2,
|
|
8
|
+
IsNotCharging: 3,
|
|
9
|
+
};
|
|
10
|
+
const RvcRunModeTag = {
|
|
11
|
+
Idle: 16384,
|
|
12
|
+
Cleaning: 16385,
|
|
13
|
+
Mapping: 16386,
|
|
14
|
+
};
|
|
15
|
+
const RvcCleanModeTag = {
|
|
16
|
+
Vacuum: 16385,
|
|
17
|
+
Mop: 16386,
|
|
18
|
+
};
|
|
19
|
+
const RvcOperationalStateValue = {
|
|
20
|
+
Stopped: 0x00,
|
|
21
|
+
Running: 0x01,
|
|
22
|
+
Paused: 0x02,
|
|
23
|
+
Error: 0x03,
|
|
24
|
+
SeekingCharger: 0x40,
|
|
25
|
+
Charging: 0x41,
|
|
26
|
+
Docked: 0x42,
|
|
27
|
+
};
|
|
28
|
+
const ServiceAreaType = {
|
|
29
|
+
Room: 0,
|
|
30
|
+
};
|
|
31
|
+
const RVC_RUN_MODES = [
|
|
32
|
+
{ label: 'Idle', mode: 0, modeTags: [{ value: RvcRunModeTag.Idle }] },
|
|
33
|
+
{ label: 'Cleaning', mode: 1, modeTags: [{ value: RvcRunModeTag.Cleaning }] },
|
|
34
|
+
{ label: 'Mapping', mode: 2, modeTags: [{ value: RvcRunModeTag.Mapping }] },
|
|
35
|
+
];
|
|
36
|
+
const RVC_CLEAN_MODES = [
|
|
37
|
+
{ label: 'Vacuum', mode: 0, modeTags: [{ value: RvcCleanModeTag.Vacuum }] },
|
|
38
|
+
{ label: 'Vacuum & Mop', mode: 1, modeTags: [{ value: RvcCleanModeTag.Vacuum }, { value: RvcCleanModeTag.Mop }] },
|
|
39
|
+
{ label: 'Mop Only', mode: 2, modeTags: [{ value: RvcCleanModeTag.Mop }] },
|
|
40
|
+
];
|
|
41
|
+
const RVC_OPERATIONAL_STATES = [
|
|
42
|
+
{ operationalStateId: RvcOperationalStateValue.Stopped },
|
|
43
|
+
{ operationalStateId: RvcOperationalStateValue.Running },
|
|
44
|
+
{ operationalStateId: RvcOperationalStateValue.Paused },
|
|
45
|
+
{ operationalStateId: RvcOperationalStateValue.Error },
|
|
46
|
+
{ operationalStateId: RvcOperationalStateValue.SeekingCharger },
|
|
47
|
+
{ operationalStateId: RvcOperationalStateValue.Charging },
|
|
48
|
+
{ operationalStateId: RvcOperationalStateValue.Docked },
|
|
49
|
+
];
|
|
50
|
+
const CONFIG_SUCTION_LEVELS = {
|
|
51
|
+
quiet: MovaFanSpeed.Quiet,
|
|
52
|
+
standard: MovaFanSpeed.Standard,
|
|
53
|
+
strong: MovaFanSpeed.Strong,
|
|
54
|
+
turbo: MovaFanSpeed.Turbo,
|
|
55
|
+
};
|
|
56
|
+
function configuredSuctionLevel(value) {
|
|
57
|
+
if (typeof value === 'string' && value in CONFIG_SUCTION_LEVELS) {
|
|
58
|
+
return CONFIG_SUCTION_LEVELS[value];
|
|
59
|
+
}
|
|
60
|
+
return MovaFanSpeed.Standard;
|
|
61
|
+
}
|
|
62
|
+
function configuredVacuumAndMopMode(value) {
|
|
63
|
+
return value === 'vac-then-mop' ? 'vac-then-mop' : 'vac-mop';
|
|
64
|
+
}
|
|
65
|
+
function rvcToMovaCleanMode(rvcMode, vacuumAndMopMode) {
|
|
66
|
+
if (rvcMode === 0)
|
|
67
|
+
return MovaCleaningMode.SweepingAndMopping;
|
|
68
|
+
if (rvcMode === 1)
|
|
69
|
+
return vacuumAndMopMode === 'vac-then-mop' ? MovaCleaningMode.MoppingAfterSweeping : MovaCleaningMode.Sweeping;
|
|
70
|
+
if (rvcMode === 2)
|
|
71
|
+
return MovaCleaningMode.Mopping;
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
function movaToRvcCleanMode(cleaningMode) {
|
|
75
|
+
if (cleaningMode === MovaCleaningMode.SweepingAndMopping)
|
|
76
|
+
return 0;
|
|
77
|
+
if (cleaningMode === MovaCleaningMode.Mopping)
|
|
78
|
+
return 2;
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
export async function discoverAndRegisterDevices(platform, cloud, device, rooms, initialStatus) {
|
|
82
|
+
const { log } = platform;
|
|
83
|
+
const suctionLevel = configuredSuctionLevel(platform.config.suctionLevel);
|
|
84
|
+
const vacuumAndMopMode = configuredVacuumAndMopMode(platform.config.vacuumAndMopMode);
|
|
85
|
+
log.info(`Creating Matter RVC device for ${device.name} (${device.model})`);
|
|
86
|
+
const supportedAreas = rooms.length > 0
|
|
87
|
+
? rooms.map((room) => ({
|
|
88
|
+
areaId: room.id,
|
|
89
|
+
mapId: null,
|
|
90
|
+
areaInfo: {
|
|
91
|
+
locationInfo: {
|
|
92
|
+
locationName: room.name,
|
|
93
|
+
floorNumber: room.floorId ?? null,
|
|
94
|
+
areaType: ServiceAreaType.Room,
|
|
95
|
+
},
|
|
96
|
+
landmarkInfo: null,
|
|
97
|
+
},
|
|
98
|
+
}))
|
|
99
|
+
: [
|
|
100
|
+
{
|
|
101
|
+
areaId: 1,
|
|
102
|
+
mapId: null,
|
|
103
|
+
areaInfo: {
|
|
104
|
+
locationInfo: {
|
|
105
|
+
locationName: 'Home',
|
|
106
|
+
floorNumber: null,
|
|
107
|
+
areaType: ServiceAreaType.Room,
|
|
108
|
+
},
|
|
109
|
+
landmarkInfo: null,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
let supportedAreaIds = supportedAreas.map((area) => area.areaId);
|
|
114
|
+
const initialSelectedAreas = [...supportedAreaIds];
|
|
115
|
+
const initialOperationalState = initialStatus ? getOperationalStateFromMova(initialStatus.state, initialStatus.status) : RvcOperationalStateValue.Docked;
|
|
116
|
+
const definitiveDockedStatuses = [MovaStatus.Charging, MovaStatus.ChargingComplete, MovaStatus.Sleeping, MovaStatus.Standby, MovaStatus.Idle];
|
|
117
|
+
let initialRunMode = 0;
|
|
118
|
+
if (initialStatus && !definitiveDockedStatuses.includes(initialStatus.status)) {
|
|
119
|
+
const activeCleaningStates = [MovaState.Cleaning, MovaState.Mopping, MovaState.ManualCleaning, MovaState.ZonedCleaning, MovaState.SpotCleaning, MovaState.CruiseRunning];
|
|
120
|
+
const activeCleaningStatuses = [
|
|
121
|
+
MovaStatus.Sweeping,
|
|
122
|
+
MovaStatus.Mopping,
|
|
123
|
+
MovaStatus.SweepingAndMopping,
|
|
124
|
+
MovaStatus.SegmentCleaning,
|
|
125
|
+
MovaStatus.ZoneCleaning,
|
|
126
|
+
MovaStatus.SpotCleaning,
|
|
127
|
+
];
|
|
128
|
+
if (activeCleaningStates.includes(initialStatus.state) || activeCleaningStatuses.includes(initialStatus.status)) {
|
|
129
|
+
initialRunMode = 1;
|
|
130
|
+
}
|
|
131
|
+
else if (initialStatus.state === MovaState.FastMapping || initialStatus.status === MovaStatus.FastMapping) {
|
|
132
|
+
initialRunMode = 2;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const rvc = new RoboticVacuumCleaner(device.name, device.did, 'server', initialRunMode, RVC_RUN_MODES, 0, RVC_CLEAN_MODES, null, null, initialOperationalState, RVC_OPERATIONAL_STATES, supportedAreas, initialSelectedAreas, null, []);
|
|
136
|
+
rvc.uniqueId = device.did;
|
|
137
|
+
let trackedRunMode = initialRunMode;
|
|
138
|
+
let trackedCleanMode = 0;
|
|
139
|
+
let trackedOperationalState = initialOperationalState;
|
|
140
|
+
let trackedError = undefined;
|
|
141
|
+
let selectedAreas = [...initialSelectedAreas];
|
|
142
|
+
function normalizeSelectedAreas(areas) {
|
|
143
|
+
if (!areas) {
|
|
144
|
+
return [...supportedAreaIds];
|
|
145
|
+
}
|
|
146
|
+
if (areas.length === 0) {
|
|
147
|
+
return [...supportedAreaIds];
|
|
148
|
+
}
|
|
149
|
+
return Array.from(new Set(areas));
|
|
150
|
+
}
|
|
151
|
+
function isWholeHomeSelection(areas) {
|
|
152
|
+
return supportedAreaIds.length > 0 && areas.length === supportedAreaIds.length && supportedAreaIds.every((id) => areas.includes(id));
|
|
153
|
+
}
|
|
154
|
+
rvc.addCommandHandler('identify', async () => {
|
|
155
|
+
log.info(`Identify command for ${device.name}`);
|
|
156
|
+
await cloud.locate(device.did);
|
|
157
|
+
});
|
|
158
|
+
rvc.addCommandHandler('changeToMode', async (data) => {
|
|
159
|
+
const newMode = data.request?.newMode ?? data.newMode;
|
|
160
|
+
const clusterName = data.cluster;
|
|
161
|
+
log.debug(`changeToMode: cluster=${clusterName}, newMode=${newMode}`);
|
|
162
|
+
if (newMode === undefined) {
|
|
163
|
+
log.warn(`changeToMode: newMode is undefined, cannot process command`);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const isRunMode = clusterName === 'rvcRunMode' || clusterName === undefined;
|
|
167
|
+
const isCleanMode = clusterName === 'rvcCleanMode';
|
|
168
|
+
if (isRunMode) {
|
|
169
|
+
log.info(`RvcRunMode.changeToMode for ${device.name}: mode=${newMode}`);
|
|
170
|
+
if (newMode === 0) {
|
|
171
|
+
const success = await cloud.stopCleaning(device.did);
|
|
172
|
+
if (success) {
|
|
173
|
+
trackedRunMode = 0;
|
|
174
|
+
rvc.setAttribute('RvcRunMode', 'currentMode', trackedRunMode, log);
|
|
175
|
+
rvc.setAttribute('ServiceArea', 'currentArea', null, log);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
else if (newMode === 1) {
|
|
179
|
+
const movaCleanMode = rvcToMovaCleanMode(trackedCleanMode, vacuumAndMopMode) ?? MovaCleaningMode.SweepingAndMopping;
|
|
180
|
+
const cleanWholeHome = isWholeHomeSelection(selectedAreas);
|
|
181
|
+
log.info(`Starting ${cleanWholeHome ? 'cleaning' : `rooms ${selectedAreas.join(', ')}`} with mode=${trackedCleanMode}, suction=${suctionLevel}`);
|
|
182
|
+
const success = !cleanWholeHome
|
|
183
|
+
? await cloud.cleanRooms(device.did, selectedAreas, 1, movaCleanMode, suctionLevel)
|
|
184
|
+
: await cloud.startCleaning(device.did, movaCleanMode, suctionLevel);
|
|
185
|
+
if (success) {
|
|
186
|
+
trackedRunMode = 1;
|
|
187
|
+
rvc.setAttribute('RvcRunMode', 'currentMode', trackedRunMode, log);
|
|
188
|
+
trackedOperationalState = RvcOperationalStateValue.Running;
|
|
189
|
+
rvc.setAttribute('RvcOperationalState', 'operationalState', trackedOperationalState, log);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else if (newMode === 2) {
|
|
193
|
+
log.warn(`Mapping mode (2) cannot be started via Matter - use the Mova app to initiate mapping`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
else if (isCleanMode) {
|
|
197
|
+
log.info(`RvcCleanMode.changeToMode for ${device.name}: mode=${newMode}`);
|
|
198
|
+
if (rvcToMovaCleanMode(newMode, vacuumAndMopMode) === undefined) {
|
|
199
|
+
log.warn(`Unsupported clean mode ${newMode}`);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (trackedOperationalState === RvcOperationalStateValue.Running) {
|
|
203
|
+
log.warn(`Cannot change clean mode while actively cleaning - pause or stop first`);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
trackedCleanMode = newMode;
|
|
207
|
+
rvc.setAttribute('RvcCleanMode', 'currentMode', trackedCleanMode, log);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
rvc.addCommandHandler('pause', async () => {
|
|
211
|
+
log.info(`Pause command for ${device.name}`);
|
|
212
|
+
const success = await cloud.pauseCleaning(device.did);
|
|
213
|
+
if (success) {
|
|
214
|
+
trackedRunMode = 0;
|
|
215
|
+
rvc.setAttribute('RvcRunMode', 'currentMode', trackedRunMode, log);
|
|
216
|
+
trackedOperationalState = RvcOperationalStateValue.Paused;
|
|
217
|
+
rvc.setAttribute('RvcOperationalState', 'operationalState', trackedOperationalState, log);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
rvc.addCommandHandler('resume', async () => {
|
|
221
|
+
log.info(`Resume command for ${device.name}`);
|
|
222
|
+
const success = await cloud.resumeCleaning(device.did);
|
|
223
|
+
if (success) {
|
|
224
|
+
trackedRunMode = 1;
|
|
225
|
+
rvc.setAttribute('RvcRunMode', 'currentMode', trackedRunMode, log);
|
|
226
|
+
trackedOperationalState = RvcOperationalStateValue.Running;
|
|
227
|
+
rvc.setAttribute('RvcOperationalState', 'operationalState', trackedOperationalState, log);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
rvc.addCommandHandler('goHome', async () => {
|
|
231
|
+
log.info(`GoHome command for ${device.name} - stopping cleaning and returning to dock`);
|
|
232
|
+
await cloud.stopCleaning(device.did);
|
|
233
|
+
const success = await cloud.goHome(device.did);
|
|
234
|
+
if (success) {
|
|
235
|
+
trackedRunMode = 0;
|
|
236
|
+
rvc.setAttribute('RvcRunMode', 'currentMode', trackedRunMode, log);
|
|
237
|
+
trackedOperationalState = RvcOperationalStateValue.SeekingCharger;
|
|
238
|
+
rvc.setAttribute('RvcOperationalState', 'operationalState', trackedOperationalState, log);
|
|
239
|
+
rvc.setAttribute('ServiceArea', 'currentArea', null, log);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
rvc.addCommandHandler('selectAreas', async (data) => {
|
|
243
|
+
const areas = data.request?.newAreas ?? data.newAreas;
|
|
244
|
+
const nextAreas = normalizeSelectedAreas(areas);
|
|
245
|
+
selectedAreas = nextAreas;
|
|
246
|
+
log.info(`SelectAreas command for ${device.name}: ${isWholeHomeSelection(selectedAreas) ? 'all rooms' : selectedAreas.join(', ')}`);
|
|
247
|
+
rvc.setAttribute('ServiceArea', 'selectedAreas', selectedAreas, log);
|
|
248
|
+
});
|
|
249
|
+
function updateStatus(status) {
|
|
250
|
+
const newOperationalState = getOperationalStateFromMova(status.state, status.status);
|
|
251
|
+
if (newOperationalState !== trackedOperationalState) {
|
|
252
|
+
log.info(`Operational state: ${trackedOperationalState} -> ${newOperationalState} (state=${status.state}, status=${status.status})`);
|
|
253
|
+
trackedOperationalState = newOperationalState;
|
|
254
|
+
rvc.setAttribute('RvcOperationalState', 'operationalState', trackedOperationalState, log);
|
|
255
|
+
}
|
|
256
|
+
let newRunMode = 0;
|
|
257
|
+
const definitiveDockedStatuses = [MovaStatus.Charging, MovaStatus.ChargingComplete, MovaStatus.Sleeping, MovaStatus.Standby, MovaStatus.Idle];
|
|
258
|
+
if (!definitiveDockedStatuses.includes(status.status)) {
|
|
259
|
+
const activeCleaningStates = [MovaState.Cleaning, MovaState.Mopping, MovaState.ManualCleaning, MovaState.ZonedCleaning, MovaState.SpotCleaning, MovaState.CruiseRunning];
|
|
260
|
+
const activeCleaningStatuses = [
|
|
261
|
+
MovaStatus.Sweeping,
|
|
262
|
+
MovaStatus.Mopping,
|
|
263
|
+
MovaStatus.SweepingAndMopping,
|
|
264
|
+
MovaStatus.SegmentCleaning,
|
|
265
|
+
MovaStatus.ZoneCleaning,
|
|
266
|
+
MovaStatus.SpotCleaning,
|
|
267
|
+
];
|
|
268
|
+
if (activeCleaningStates.includes(status.state) || activeCleaningStatuses.includes(status.status)) {
|
|
269
|
+
newRunMode = 1;
|
|
270
|
+
}
|
|
271
|
+
else if (status.state === MovaState.FastMapping || status.status === MovaStatus.FastMapping) {
|
|
272
|
+
newRunMode = 2;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (newRunMode !== trackedRunMode) {
|
|
276
|
+
log.info(`Run mode: ${trackedRunMode} -> ${newRunMode} (state=${status.state}, status=${status.status})`);
|
|
277
|
+
trackedRunMode = newRunMode;
|
|
278
|
+
rvc.setAttribute('RvcRunMode', 'currentMode', trackedRunMode, log);
|
|
279
|
+
}
|
|
280
|
+
const newError = getOperationalErrorFromMova(status.errorCode);
|
|
281
|
+
if (newError !== trackedError) {
|
|
282
|
+
trackedError = newError;
|
|
283
|
+
const errorStateId = trackedError ?? 0;
|
|
284
|
+
rvc.setAttribute('RvcOperationalState', 'operationalError', { errorStateId }, log);
|
|
285
|
+
}
|
|
286
|
+
if (status.battery > 0) {
|
|
287
|
+
try {
|
|
288
|
+
rvc.setAttribute('PowerSource', 'batPercentRemaining', status.battery * 2, log);
|
|
289
|
+
let chargeState = BatChargeState.Unknown;
|
|
290
|
+
const isCharging = status.state === MovaState.Charging || status.status === MovaStatus.Charging;
|
|
291
|
+
const isChargingComplete = status.status === MovaStatus.ChargingComplete;
|
|
292
|
+
if (isCharging || isChargingComplete) {
|
|
293
|
+
chargeState = status.battery >= 100 ? BatChargeState.IsAtFullCharge : BatChargeState.IsCharging;
|
|
294
|
+
}
|
|
295
|
+
else if ((status.state === MovaState.Idle || status.state === MovaState.Dormant || status.state === MovaState.Sleeping) && status.battery >= 100) {
|
|
296
|
+
chargeState = BatChargeState.IsAtFullCharge;
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
chargeState = BatChargeState.IsNotCharging;
|
|
300
|
+
}
|
|
301
|
+
rvc.setAttribute('PowerSource', 'batChargeState', chargeState, log);
|
|
302
|
+
log.debug(`Battery: ${status.battery}%, state: ${status.state}, status: ${status.status}, chargeState: ${chargeState}`);
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (status.cleaningMode !== undefined) {
|
|
308
|
+
const newCleanMode = movaToRvcCleanMode(status.cleaningMode);
|
|
309
|
+
if (newCleanMode !== trackedCleanMode) {
|
|
310
|
+
trackedCleanMode = newCleanMode;
|
|
311
|
+
rvc.setAttribute('RvcCleanMode', 'currentMode', trackedCleanMode, log);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
else if (trackedCleanMode === undefined) {
|
|
315
|
+
trackedCleanMode = 0;
|
|
316
|
+
rvc.setAttribute('RvcCleanMode', 'currentMode', trackedCleanMode, log);
|
|
317
|
+
}
|
|
318
|
+
try {
|
|
319
|
+
rvc.setAttribute('ServiceArea', 'currentArea', status.currentArea ?? null, log);
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function updateRooms(newRooms) {
|
|
325
|
+
if (newRooms.length === 0) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
log.info(`Updating ServiceArea with ${newRooms.length} rooms: ${newRooms.map((r) => r.name).join(', ')}`);
|
|
329
|
+
const newSupportedAreas = newRooms.map((room) => ({
|
|
330
|
+
areaId: room.id,
|
|
331
|
+
mapId: null,
|
|
332
|
+
areaInfo: {
|
|
333
|
+
locationInfo: {
|
|
334
|
+
locationName: room.name,
|
|
335
|
+
floorNumber: room.floorId ?? null,
|
|
336
|
+
areaType: ServiceAreaType.Room,
|
|
337
|
+
},
|
|
338
|
+
landmarkInfo: null,
|
|
339
|
+
},
|
|
340
|
+
}));
|
|
341
|
+
supportedAreaIds = newSupportedAreas.map((area) => area.areaId);
|
|
342
|
+
if (selectedAreas.length > 0) {
|
|
343
|
+
selectedAreas = selectedAreas.filter((areaId) => supportedAreaIds.includes(areaId));
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
selectedAreas = [...supportedAreaIds];
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
rvc.setAttribute('ServiceArea', 'supportedAreas', newSupportedAreas, log);
|
|
350
|
+
rvc.setAttribute('ServiceArea', 'selectedAreas', selectedAreas, log);
|
|
351
|
+
log.info(`ServiceArea updated successfully with ${newRooms.length} rooms`);
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
log.error(`Failed to update ServiceArea: ${error}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
await platform.registerDevice(rvc);
|
|
359
|
+
log.info(`Registered ${device.name} as Matter RVC device with full cluster support`);
|
|
360
|
+
rvc.setAttribute('RvcOperationalState', 'operationalState', initialOperationalState, log);
|
|
361
|
+
log.info(`Set initial operational state to ${initialOperationalState} (0=Stopped, 1=Running, 2=Paused, 3=Error, 64=SeekingCharger, 65=Charging, 66=Docked)`);
|
|
362
|
+
rvc.setAttribute('RvcRunMode', 'currentMode', initialRunMode, log);
|
|
363
|
+
log.info(`Set initial run mode to ${initialRunMode} (0=Idle, 1=Cleaning, 2=Mapping)`);
|
|
364
|
+
rvc.setAttribute('RvcCleanMode', 'currentMode', trackedCleanMode, log);
|
|
365
|
+
log.info(`Set initial clean mode to ${trackedCleanMode} (0=Vacuum, 1=Vacuum&Mop, 2=MopOnly)`);
|
|
366
|
+
const initialError = initialStatus ? getOperationalErrorFromMova(initialStatus.errorCode) : null;
|
|
367
|
+
rvc.setAttribute('RvcOperationalState', 'operationalError', { errorStateId: initialError ?? 0 }, log);
|
|
368
|
+
if (initialStatus) {
|
|
369
|
+
updateStatus(initialStatus);
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
did: device.did,
|
|
373
|
+
name: device.name,
|
|
374
|
+
model: device.model,
|
|
375
|
+
device: rvc,
|
|
376
|
+
updateStatus,
|
|
377
|
+
updateRooms,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
log.error(`Failed to register device ${device.name}: ${error}`);
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { AnsiLogger } from 'matterbridge/logger';
|
|
2
|
+
import { type AuthResult, type MovaDevice, type DeviceStatus, type MovaCountry, type RoomInfo, MovaFanSpeed, MovaCleaningMode } from './types.js';
|
|
3
|
+
export declare class MovaCloudProtocol {
|
|
4
|
+
private log;
|
|
5
|
+
private session;
|
|
6
|
+
private mqttClients;
|
|
7
|
+
private messageId;
|
|
8
|
+
private pendingRequests;
|
|
9
|
+
private deviceStatusCallbacks;
|
|
10
|
+
private deviceOwnerIds;
|
|
11
|
+
private deviceModels;
|
|
12
|
+
private deviceBindDomains;
|
|
13
|
+
private loginCredentials;
|
|
14
|
+
private isRefreshing;
|
|
15
|
+
constructor(log: AnsiLogger);
|
|
16
|
+
private ensureValidToken;
|
|
17
|
+
private refreshToken;
|
|
18
|
+
login(username: string, password: string, country: MovaCountry): Promise<AuthResult>;
|
|
19
|
+
private apiCall;
|
|
20
|
+
getDevices(): Promise<MovaDevice[]>;
|
|
21
|
+
getDeviceProperties(did: string): Promise<DeviceStatus | null>;
|
|
22
|
+
getRoomInfo(did: string): Promise<RoomInfo[]>;
|
|
23
|
+
private decodeMapPayload;
|
|
24
|
+
private decodeRismPayload;
|
|
25
|
+
private parseRoomsFromMapData;
|
|
26
|
+
private static readonly ROOM_TYPE_NAMES;
|
|
27
|
+
private parseSegInf;
|
|
28
|
+
tryFetchMapOnStartup(did: string): Promise<boolean>;
|
|
29
|
+
private fetchMapFromCloudStorage;
|
|
30
|
+
connectMqtt(device: MovaDevice): Promise<boolean>;
|
|
31
|
+
private requestMapDataViaMqtt;
|
|
32
|
+
private handleMqttMessage;
|
|
33
|
+
private roomUpdateCallbacks;
|
|
34
|
+
onRoomUpdate(did: string, callback: (rooms: RoomInfo[]) => void): void;
|
|
35
|
+
private cachedRooms;
|
|
36
|
+
private cachedStatus;
|
|
37
|
+
getCachedRooms(did: string): RoomInfo[];
|
|
38
|
+
private parseStatusFromMqtt;
|
|
39
|
+
sendCommand(did: string, method: string, params?: unknown[]): Promise<unknown>;
|
|
40
|
+
private sendCloudCommand;
|
|
41
|
+
onDeviceStatus(did: string, callback: (status: DeviceStatus) => void): void;
|
|
42
|
+
private confirmCommandSuccess;
|
|
43
|
+
startCleaning(did: string, cleaningMode?: MovaCleaningMode, fanSpeed?: MovaFanSpeed, confirm?: boolean): Promise<boolean>;
|
|
44
|
+
stopCleaning(did: string, confirm?: boolean): Promise<boolean>;
|
|
45
|
+
pauseCleaning(did: string, confirm?: boolean): Promise<boolean>;
|
|
46
|
+
resumeCleaning(did: string, confirm?: boolean): Promise<boolean>;
|
|
47
|
+
goHome(did: string, confirm?: boolean): Promise<boolean>;
|
|
48
|
+
locate(did: string): Promise<boolean>;
|
|
49
|
+
cleanRooms(did: string, roomIds: number[], repeat?: number, cleaningMode?: MovaCleaningMode, fanSpeed?: MovaFanSpeed): Promise<boolean>;
|
|
50
|
+
setCleaningMode(did: string, cleaningMode: MovaCleaningMode): Promise<void>;
|
|
51
|
+
private roomWaterLevelForCleaningMode;
|
|
52
|
+
setSuctionLevel(did: string, fanSpeed: MovaFanSpeed): Promise<void>;
|
|
53
|
+
disconnect(): Promise<void>;
|
|
54
|
+
}
|