matterbridge-roborock-vacuum-plugin 1.0.5
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/.github/workflows/publish.yml +34 -0
- package/.tarignore +5 -0
- package/LICENSE +202 -0
- package/README.md +34 -0
- package/README_DEV.md +69 -0
- package/bmc-button.svg +22 -0
- package/dist/appliances.js +6 -0
- package/dist/behaviorFactory.js +18 -0
- package/dist/behaviors/BehaviorDeviceGeneric.js +31 -0
- package/dist/behaviors/roborock.vacuum/QREVO_EDGE_5V1/a187.js +94 -0
- package/dist/behaviors/roborock.vacuum/QREVO_EDGE_5V1/initalData.js +81 -0
- package/dist/behaviors/roborock.vacuum/QREVO_EDGE_5V1/runtimes.js +39 -0
- package/dist/behaviors/roborock.vacuum/default/default.js +58 -0
- package/dist/behaviors/roborock.vacuum/default/initalData.js +66 -0
- package/dist/clientManager.js +17 -0
- package/dist/extensions/AxiosStaticExtensions.js +22 -0
- package/dist/extensions/index.js +1 -0
- package/dist/helper.js +16 -0
- package/dist/index.js +4 -0
- package/dist/initialData/getBatteryStatus.js +24 -0
- package/dist/initialData/getOperationalStates.js +22 -0
- package/dist/initialData/getSupportedAreas.js +69 -0
- package/dist/initialData/getSupportedCleanModes.js +11 -0
- package/dist/initialData/getSupportedRunModes.js +18 -0
- package/dist/initialData/index.js +5 -0
- package/dist/model/CloudMessageModel.js +1 -0
- package/dist/model/DockingStationStatus.js +24 -0
- package/dist/model/RoomMap.js +18 -0
- package/dist/notifyMessageTypes.js +9 -0
- package/dist/platform.js +146 -0
- package/dist/platformRunner.js +249 -0
- package/dist/roborockCommunication/RESTAPI/roborockAuthenticateApi.js +73 -0
- package/dist/roborockCommunication/RESTAPI/roborockIoTApi.js +61 -0
- package/dist/roborockCommunication/Zenum/dockType.js +4 -0
- package/dist/roborockCommunication/Zenum/operationStatusCode.js +44 -0
- package/dist/roborockCommunication/Zenum/vacuumAndDockErrorCode.js +68 -0
- package/dist/roborockCommunication/Zmodel/apiResponse.js +1 -0
- package/dist/roborockCommunication/Zmodel/authenticateResponse.js +1 -0
- package/dist/roborockCommunication/Zmodel/baseURL.js +1 -0
- package/dist/roborockCommunication/Zmodel/device.js +1 -0
- package/dist/roborockCommunication/Zmodel/deviceModel.js +28 -0
- package/dist/roborockCommunication/Zmodel/deviceSchema.js +1 -0
- package/dist/roborockCommunication/Zmodel/deviceStatus.js +22 -0
- package/dist/roborockCommunication/Zmodel/dockInfo.js +6 -0
- package/dist/roborockCommunication/Zmodel/home.js +1 -0
- package/dist/roborockCommunication/Zmodel/homeInfo.js +1 -0
- package/dist/roborockCommunication/Zmodel/messageResult.js +7 -0
- package/dist/roborockCommunication/Zmodel/networkInfo.js +1 -0
- package/dist/roborockCommunication/Zmodel/product.js +1 -0
- package/dist/roborockCommunication/Zmodel/room.js +1 -0
- package/dist/roborockCommunication/Zmodel/roomInfo.js +22 -0
- package/dist/roborockCommunication/Zmodel/userData.js +1 -0
- package/dist/roborockCommunication/Zmodel/vacuumError.js +27 -0
- package/dist/roborockCommunication/broadcast/abstractClient.js +41 -0
- package/dist/roborockCommunication/broadcast/client/LocalNetworkClient.js +148 -0
- package/dist/roborockCommunication/broadcast/client/MQTTClient.js +101 -0
- package/dist/roborockCommunication/broadcast/client.js +1 -0
- package/dist/roborockCommunication/broadcast/clientRouter.js +79 -0
- package/dist/roborockCommunication/broadcast/listener/abstractConnectionListener.js +1 -0
- package/dist/roborockCommunication/broadcast/listener/abstractMessageHandler.js +1 -0
- package/dist/roborockCommunication/broadcast/listener/abstractMessageListener.js +1 -0
- package/dist/roborockCommunication/broadcast/listener/implementation/chainedConnectionListener.js +21 -0
- package/dist/roborockCommunication/broadcast/listener/implementation/chainedMessageListener.js +11 -0
- package/dist/roborockCommunication/broadcast/listener/implementation/simpleMessageListener.js +27 -0
- package/dist/roborockCommunication/broadcast/listener/implementation/syncMessageListener.js +35 -0
- package/dist/roborockCommunication/broadcast/listener/index.js +1 -0
- package/dist/roborockCommunication/broadcast/messageProcessor.js +79 -0
- package/dist/roborockCommunication/broadcast/model/dps.js +1 -0
- package/dist/roborockCommunication/broadcast/model/messageContext.js +26 -0
- package/dist/roborockCommunication/broadcast/model/protocol.js +19 -0
- package/dist/roborockCommunication/broadcast/model/requestMessage.js +33 -0
- package/dist/roborockCommunication/broadcast/model/responseMessage.js +14 -0
- package/dist/roborockCommunication/helper/chunkBuffer.js +17 -0
- package/dist/roborockCommunication/helper/cryptoHelper.js +27 -0
- package/dist/roborockCommunication/helper/messageDeserializer.js +74 -0
- package/dist/roborockCommunication/helper/messageSerializer.js +70 -0
- package/dist/roborockCommunication/helper/nameDecoder.js +66 -0
- package/dist/roborockCommunication/helper/sequence.js +16 -0
- package/dist/roborockCommunication/index.js +9 -0
- package/dist/roborockService.js +300 -0
- package/dist/rvc.js +39 -0
- package/dist/settings.js +1 -0
- package/dist/share/function.js +96 -0
- package/dist/share/runtimeHelper.js +29 -0
- package/eslint.config.js +76 -0
- package/matterbridge-roborock-vacuum-plugin.config.json +11 -0
- package/matterbridge-roborock-vacuum-plugin.schema.json +57 -0
- package/package.json +43 -0
- package/prettier.config.js +49 -0
- package/src/appliances.ts +15 -0
- package/src/behaviorFactory.ts +24 -0
- package/src/behaviors/BehaviorDeviceGeneric.ts +39 -0
- package/src/behaviors/roborock.vacuum/QREVO_EDGE_5V1/a187.ts +117 -0
- package/src/behaviors/roborock.vacuum/QREVO_EDGE_5V1/initalData.ts +85 -0
- package/src/behaviors/roborock.vacuum/QREVO_EDGE_5V1/runtimes.ts +29 -0
- package/src/behaviors/roborock.vacuum/default/default.ts +78 -0
- package/src/behaviors/roborock.vacuum/default/initalData.ts +69 -0
- package/src/clientManager.ts +23 -0
- package/src/extensions/AxiosStaticExtensions.ts +35 -0
- package/src/extensions/index.ts +1 -0
- package/src/helper.ts +22 -0
- package/src/index.ts +16 -0
- package/src/initialData/getBatteryStatus.ts +26 -0
- package/src/initialData/getOperationalStates.ts +24 -0
- package/src/initialData/getSupportedAreas.ts +95 -0
- package/src/initialData/getSupportedCleanModes.ts +13 -0
- package/src/initialData/getSupportedRunModes.ts +21 -0
- package/src/initialData/index.ts +5 -0
- package/src/model/CloudMessageModel.ts +13 -0
- package/src/model/DockingStationStatus.ts +37 -0
- package/src/model/RoomMap.ts +44 -0
- package/src/notifyMessageTypes.ts +8 -0
- package/src/platform.ts +192 -0
- package/src/platformRunner.ts +292 -0
- package/src/roborockCommunication/RESTAPI/roborockAuthenticateApi.ts +98 -0
- package/src/roborockCommunication/RESTAPI/roborockIoTApi.ts +70 -0
- package/src/roborockCommunication/Zenum/dockType.ts +3 -0
- package/src/roborockCommunication/Zenum/operationStatusCode.ts +43 -0
- package/src/roborockCommunication/Zenum/vacuumAndDockErrorCode.ts +68 -0
- package/src/roborockCommunication/Zmodel/apiResponse.ts +3 -0
- package/src/roborockCommunication/Zmodel/authenticateResponse.ts +5 -0
- package/src/roborockCommunication/Zmodel/baseURL.ts +5 -0
- package/src/roborockCommunication/Zmodel/device.ts +39 -0
- package/src/roborockCommunication/Zmodel/deviceModel.ts +27 -0
- package/src/roborockCommunication/Zmodel/deviceSchema.ts +8 -0
- package/src/roborockCommunication/Zmodel/deviceStatus.ts +30 -0
- package/src/roborockCommunication/Zmodel/dockInfo.ts +9 -0
- package/src/roborockCommunication/Zmodel/home.ts +13 -0
- package/src/roborockCommunication/Zmodel/homeInfo.ts +5 -0
- package/src/roborockCommunication/Zmodel/messageResult.ts +72 -0
- package/src/roborockCommunication/Zmodel/networkInfo.ts +7 -0
- package/src/roborockCommunication/Zmodel/product.ts +9 -0
- package/src/roborockCommunication/Zmodel/room.ts +4 -0
- package/src/roborockCommunication/Zmodel/roomInfo.ts +25 -0
- package/src/roborockCommunication/Zmodel/userData.ts +23 -0
- package/src/roborockCommunication/Zmodel/vacuumError.ts +35 -0
- package/src/roborockCommunication/broadcast/abstractClient.ts +61 -0
- package/src/roborockCommunication/broadcast/client/LocalNetworkClient.ts +177 -0
- package/src/roborockCommunication/broadcast/client/MQTTClient.ts +129 -0
- package/src/roborockCommunication/broadcast/client.ts +19 -0
- package/src/roborockCommunication/broadcast/clientRouter.ts +100 -0
- package/src/roborockCommunication/broadcast/listener/abstractConnectionListener.ts +5 -0
- package/src/roborockCommunication/broadcast/listener/abstractMessageHandler.ts +11 -0
- package/src/roborockCommunication/broadcast/listener/abstractMessageListener.ts +5 -0
- package/src/roborockCommunication/broadcast/listener/implementation/chainedConnectionListener.ts +26 -0
- package/src/roborockCommunication/broadcast/listener/implementation/chainedMessageListener.ts +16 -0
- package/src/roborockCommunication/broadcast/listener/implementation/simpleMessageListener.ts +37 -0
- package/src/roborockCommunication/broadcast/listener/implementation/syncMessageListener.ts +48 -0
- package/src/roborockCommunication/broadcast/listener/index.ts +3 -0
- package/src/roborockCommunication/broadcast/messageProcessor.ts +110 -0
- package/src/roborockCommunication/broadcast/model/dps.ts +17 -0
- package/src/roborockCommunication/broadcast/model/messageContext.ts +34 -0
- package/src/roborockCommunication/broadcast/model/protocol.ts +19 -0
- package/src/roborockCommunication/broadcast/model/requestMessage.ts +44 -0
- package/src/roborockCommunication/broadcast/model/responseMessage.ts +19 -0
- package/src/roborockCommunication/helper/chunkBuffer.ts +18 -0
- package/src/roborockCommunication/helper/cryptoHelper.ts +34 -0
- package/src/roborockCommunication/helper/messageDeserializer.ts +99 -0
- package/src/roborockCommunication/helper/messageSerializer.ts +82 -0
- package/src/roborockCommunication/helper/nameDecoder.ts +78 -0
- package/src/roborockCommunication/helper/sequence.ts +18 -0
- package/src/roborockCommunication/index.ts +15 -0
- package/src/roborockService.ts +379 -0
- package/src/rvc.ts +66 -0
- package/src/settings.ts +1 -0
- package/src/share/function.ts +106 -0
- package/src/share/runtimeHelper.ts +35 -0
- package/tsconfig.json +37 -0
- package/tsconfig.production.json +19 -0
- package/tslint.json +9 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { RvcRunMode, PowerSource, ServiceArea, RvcOperationalState, RvcCleanMode } from 'matterbridge/matter/clusters';
|
|
2
|
+
import { getVacuumProperty } from './helper.js';
|
|
3
|
+
import { getRunningMode } from './initialData/getSupportedRunModes.js';
|
|
4
|
+
import { CloudMessageModel } from './model/CloudMessageModel.js';
|
|
5
|
+
import { RoborockMatterbridgePlatform } from './platform.js';
|
|
6
|
+
import { state_to_matter_operational_status, state_to_matter_state } from './share/function.js';
|
|
7
|
+
import RoomMap from './model/RoomMap.js';
|
|
8
|
+
import { getBatteryState, getBatteryStatus, getOperationalErrorState } from './initialData/index.js';
|
|
9
|
+
import { NotifyMessageTypes } from './notifyMessageTypes.js';
|
|
10
|
+
import { CloudMessageResult } from './roborockCommunication/Zmodel/messageResult.js';
|
|
11
|
+
import { Protocol } from './roborockCommunication/broadcast/model/protocol.js';
|
|
12
|
+
import { DpsPayload } from './roborockCommunication/broadcast/model/dps.js';
|
|
13
|
+
import { RoborockVacuumCleaner } from './rvc.js';
|
|
14
|
+
import { hasDockingStationError, parseDockingStationStatus } from './model/DockingStationStatus.js';
|
|
15
|
+
import { Device, Home } from './roborockCommunication/index.js';
|
|
16
|
+
import { OperationStatusCode } from './roborockCommunication/Zenum/operationStatusCode.js';
|
|
17
|
+
import { getCurrentCleanModeFromFanPowerFunc, getCurrentCleanModeFromWaterBoxModeFunc, getCurrentCleanModeFunc } from './share/runtimeHelper.js';
|
|
18
|
+
|
|
19
|
+
export class PlatformRunner {
|
|
20
|
+
platform: RoborockMatterbridgePlatform;
|
|
21
|
+
constructor(platform: RoborockMatterbridgePlatform) {
|
|
22
|
+
this.platform = platform;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async updateRobot(messageSource: NotifyMessageTypes, homeData: any): Promise<void> {
|
|
26
|
+
if (messageSource === NotifyMessageTypes.HomeData) {
|
|
27
|
+
this.updateFromHomeData(homeData);
|
|
28
|
+
} else {
|
|
29
|
+
await this.updateFromMQTTMessage(messageSource, homeData);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async requestHomeData(): Promise<void> {
|
|
34
|
+
const platform = this.platform;
|
|
35
|
+
if (!platform.robot || !platform.robot.rrHomeId) return;
|
|
36
|
+
if (platform.roborockService === undefined) return;
|
|
37
|
+
|
|
38
|
+
const homeData = await platform.roborockService.getHomeDataForUpdating(platform.robot.rrHomeId);
|
|
39
|
+
await platform.platformRunner?.updateRobot(NotifyMessageTypes.HomeData, homeData);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public async getRoomMapFromDevice(device: Device): Promise<RoomMap> {
|
|
43
|
+
const platform = this.platform;
|
|
44
|
+
const rooms = device?.rooms ?? [];
|
|
45
|
+
|
|
46
|
+
if (device && platform.roborockService) {
|
|
47
|
+
const roomData = await platform.roborockService.getRoomMappings(device.duid);
|
|
48
|
+
return new RoomMap(roomData ?? [], rooms);
|
|
49
|
+
}
|
|
50
|
+
return new RoomMap([], rooms);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async getRoomMap(): Promise<RoomMap | undefined> {
|
|
54
|
+
const platform = this.platform;
|
|
55
|
+
const rooms = platform.robot?.device.rooms ?? [];
|
|
56
|
+
if (platform.robot?.device === undefined || platform.roborockService === undefined) return undefined;
|
|
57
|
+
if (platform.robot.roomInfo === undefined) {
|
|
58
|
+
const roomData = await platform.roborockService.getRoomMappings(platform.robot.device.duid);
|
|
59
|
+
platform.robot.roomInfo = new RoomMap(roomData ?? [], rooms);
|
|
60
|
+
return platform.robot.roomInfo;
|
|
61
|
+
}
|
|
62
|
+
return platform.robot.roomInfo;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private async updateFromMQTTMessage(messageSource: NotifyMessageTypes, messageData: any, tracked = false): Promise<void> {
|
|
66
|
+
const platform = this.platform;
|
|
67
|
+
const deviceData = platform.robot?.device.data;
|
|
68
|
+
if (deviceData === undefined) {
|
|
69
|
+
platform.log.error('Device data is undefined');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!tracked) {
|
|
74
|
+
platform.log.debug(`${messageSource} updateFromMQTTMessage: ${JSON.stringify(messageData)}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const duid = messageData.duid;
|
|
78
|
+
if (platform.robot === undefined) return;
|
|
79
|
+
if (!platform.robot.serialNumber) {
|
|
80
|
+
platform.log.error('Robot serial number is undefined');
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
//duid is set as device serial number
|
|
85
|
+
if (platform.robot.serialNumber !== duid) {
|
|
86
|
+
platform.log.notice(`DUID mismatch: ${duid}, device serial number: ${platform.robot.serialNumber}`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
switch (messageSource) {
|
|
91
|
+
case NotifyMessageTypes.ErrorOccurred: {
|
|
92
|
+
const errorCode = messageData.errorCode as number;
|
|
93
|
+
const operationalStateId = getOperationalErrorState(errorCode);
|
|
94
|
+
if (operationalStateId) {
|
|
95
|
+
platform.log.error(`Error occurred: ${errorCode}`);
|
|
96
|
+
platform.robot.updateAttribute(RvcOperationalState.Cluster.id, 'operationalState', operationalStateId, platform.log);
|
|
97
|
+
}
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
case NotifyMessageTypes.BatteryUpdate: {
|
|
101
|
+
const batteryLevel = messageData.percentage as number;
|
|
102
|
+
if ((messageData.percentage as number) && batteryLevel) {
|
|
103
|
+
platform.robot.updateAttribute(PowerSource.Cluster.id, 'batPercentRemaining', batteryLevel * 2, platform.log);
|
|
104
|
+
platform.robot.updateAttribute(PowerSource.Cluster.id, 'batChargeLevel', getBatteryStatus(batteryLevel), platform.log);
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
case NotifyMessageTypes.LocalMessage: {
|
|
110
|
+
const data = messageData.statusType as CloudMessageResult;
|
|
111
|
+
if (data) {
|
|
112
|
+
const state = state_to_matter_state(data.state);
|
|
113
|
+
if (state) {
|
|
114
|
+
platform.robot.updateAttribute(RvcRunMode.Cluster.id, 'currentMode', getRunningMode(deviceData.model, state), platform.log);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const currentRoom = data.cleaning_info?.segment_id ?? -1;
|
|
118
|
+
const currentMappedAreas = this.platform.roborockService?.getSupportedAreas(duid);
|
|
119
|
+
const isMappedArea = currentMappedAreas?.some((x) => x.areaId == currentRoom);
|
|
120
|
+
|
|
121
|
+
if (currentRoom !== -1 && isMappedArea) {
|
|
122
|
+
const roomMap = await this.getRoomMap();
|
|
123
|
+
this.platform.log.debug('RoomMap:', JSON.stringify(roomMap));
|
|
124
|
+
this.platform.log.debug('CurrentRoom:', currentRoom);
|
|
125
|
+
platform.robot.updateAttribute(ServiceArea.Cluster.id, 'currentArea', currentRoom, platform.log);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (data.battery) {
|
|
129
|
+
const batteryLevel = data.battery as number;
|
|
130
|
+
platform.robot.updateAttribute(PowerSource.Cluster.id, 'batPercentRemaining', batteryLevel * 2, platform.log);
|
|
131
|
+
platform.robot.updateAttribute(PowerSource.Cluster.id, 'batChargeState', getBatteryState(data.state, data.battery), platform.log);
|
|
132
|
+
platform.robot.updateAttribute(PowerSource.Cluster.id, 'batChargeLevel', getBatteryStatus(batteryLevel), platform.log);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const currentCleanMode = getCurrentCleanModeFunc(deviceData.model)(data.fan_power, data.water_box_mode);
|
|
136
|
+
if (currentCleanMode) {
|
|
137
|
+
platform.robot.updateAttribute(RvcCleanMode.Cluster.id, 'currentMode', currentCleanMode, platform.log);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
this.processAdditionalProps(platform.robot, data);
|
|
141
|
+
}
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
case NotifyMessageTypes.CloudMessage: {
|
|
146
|
+
var data = messageData.data as CloudMessageModel;
|
|
147
|
+
if (!data) {
|
|
148
|
+
data = messageData as CloudMessageModel;
|
|
149
|
+
}
|
|
150
|
+
if (!data) return;
|
|
151
|
+
this.handlerCloudMessage(data, duid, deviceData.model);
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
default:
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private handlerCloudMessage(data: CloudMessageModel, duid: string, model: string): void {
|
|
161
|
+
const platform = this.platform;
|
|
162
|
+
const messageTypes = Object.keys(data.dps).map(Number);
|
|
163
|
+
const self = this;
|
|
164
|
+
|
|
165
|
+
//Known: 122, 121, 102,
|
|
166
|
+
//Unknown: 128, 139
|
|
167
|
+
messageTypes.forEach(async (messageType) => {
|
|
168
|
+
switch (messageType) {
|
|
169
|
+
case Protocol.status_update: {
|
|
170
|
+
const status = Number(data.dps[messageType]);
|
|
171
|
+
const matterState = state_to_matter_state(status);
|
|
172
|
+
if (matterState) {
|
|
173
|
+
platform.robot!.updateAttribute(RvcRunMode.Cluster.id, 'currentMode', getRunningMode(model, matterState), platform.log);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const operationalStateId = state_to_matter_operational_status(status);
|
|
177
|
+
if (operationalStateId) {
|
|
178
|
+
platform.robot!.updateAttribute(RvcOperationalState.Cluster.id, 'operationalState', operationalStateId, platform.log);
|
|
179
|
+
}
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
case Protocol.rpc_response: {
|
|
183
|
+
const response = data.dps[messageType] as DpsPayload;
|
|
184
|
+
//ignore network info
|
|
185
|
+
if (!self.isStatusUpdate(response.result)) {
|
|
186
|
+
platform.log.notice('Ignore message:', JSON.stringify(data));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const roboStatus = response.result[0] as CloudMessageResult;
|
|
191
|
+
if (roboStatus) {
|
|
192
|
+
const message = { duid: duid, statusType: { ...roboStatus } };
|
|
193
|
+
platform.log.debug('rpc_response:', JSON.stringify(message));
|
|
194
|
+
await self.updateFromMQTTMessage(NotifyMessageTypes.LocalMessage, message, true);
|
|
195
|
+
}
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
case Protocol.suction_power: {
|
|
199
|
+
const fanPower = data.dps[messageType] as number;
|
|
200
|
+
const currentCleanMode = getCurrentCleanModeFromFanPowerFunc(model)(fanPower);
|
|
201
|
+
if (currentCleanMode) {
|
|
202
|
+
platform.robot!.updateAttribute(RvcCleanMode.Cluster.id, 'currentMode', currentCleanMode, platform.log);
|
|
203
|
+
}
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
case Protocol.water_box_mode: {
|
|
208
|
+
const water_box_mode = data.dps[messageType] as number;
|
|
209
|
+
const currentCleanMode = getCurrentCleanModeFromWaterBoxModeFunc(model)(water_box_mode);
|
|
210
|
+
if (currentCleanMode) {
|
|
211
|
+
platform.robot!.updateAttribute(RvcCleanMode.Cluster.id, 'currentMode', currentCleanMode, platform.log);
|
|
212
|
+
}
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
case Protocol.additional_props:
|
|
216
|
+
case Protocol.back_type: {
|
|
217
|
+
//TODO: check if this is needed
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
default: {
|
|
221
|
+
platform.log.notice(`Unknown message type: ${Protocol[messageType] ?? messageType} ,`, JSON.stringify(data));
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private async processAdditionalProps(robot: RoborockVacuumCleaner, message: CloudMessageResult): Promise<void> {
|
|
229
|
+
//dss -> DockingStationStatus
|
|
230
|
+
const platform = this.platform;
|
|
231
|
+
if (platform.config.enableExperimentalFeature && message.dss !== undefined) {
|
|
232
|
+
const dss = parseDockingStationStatus(message.dss);
|
|
233
|
+
this.platform.log.debug('DockingStationStatus:', JSON.stringify(dss));
|
|
234
|
+
|
|
235
|
+
//Experimental feature
|
|
236
|
+
if (dss && hasDockingStationError(dss)) {
|
|
237
|
+
platform.robot!.updateAttribute(RvcOperationalState.Cluster.id, 'operationalState', RvcOperationalState.OperationalState.Error, platform.log);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (message.fan_power !== undefined) {
|
|
242
|
+
const fanPower = message.fan_power as number;
|
|
243
|
+
//robot.updateAttribute(RvcRunMode.Cluster.id, 'fanPower', fanPower, this.platform.log);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private isStatusUpdate(result: any): boolean {
|
|
248
|
+
return result && Array.isArray(result) && result.length > 0 && (result[0] as CloudMessageResult).msg_ver !== undefined && (result[0] as CloudMessageResult).msg_ver !== null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private updateFromHomeData(homeData: Home): void {
|
|
252
|
+
const platform = this.platform;
|
|
253
|
+
if (platform.robot === undefined) return;
|
|
254
|
+
const device = homeData.devices.find((d: Device) => d.duid === platform.robot?.serialNumber);
|
|
255
|
+
const deviceData = platform.robot?.device.data;
|
|
256
|
+
if (!device || deviceData === undefined) {
|
|
257
|
+
platform.log.error('Device not found in home data');
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
device.schema = homeData.products.find((prd) => prd.id == device.productId || prd.model == device.data.model)?.schema ?? [];
|
|
262
|
+
this.platform.log.debug('updateFromHomeData-homeData:', JSON.stringify(homeData));
|
|
263
|
+
this.platform.log.debug('updateFromHomeData-device:', JSON.stringify(device));
|
|
264
|
+
|
|
265
|
+
const batteryLevel = getVacuumProperty(device, 'battery');
|
|
266
|
+
if (batteryLevel) {
|
|
267
|
+
platform.robot.updateAttribute(PowerSource.Cluster.id, 'batPercentRemaining', batteryLevel ? batteryLevel * 2 : 200, platform.log);
|
|
268
|
+
platform.robot.updateAttribute(PowerSource.Cluster.id, 'batChargeLevel', getBatteryStatus(batteryLevel), platform.log);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const state = getVacuumProperty(device, 'state');
|
|
272
|
+
const matterState = state_to_matter_state(state);
|
|
273
|
+
if (!state || !matterState) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
this.platform.log.debug(`updateFromHomeData-RvcRunMode code: ${state} name: ${OperationStatusCode[state]}, matterState: ${RvcRunMode.ModeTag[matterState]}`);
|
|
277
|
+
|
|
278
|
+
if (matterState) {
|
|
279
|
+
platform.robot.updateAttribute(RvcRunMode.Cluster.id, 'currentMode', getRunningMode(deviceData.model, matterState), platform.log);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const operationalStateId = state_to_matter_operational_status(state);
|
|
283
|
+
if (operationalStateId) {
|
|
284
|
+
this.platform.log.debug(`updateFromHomeData-OperationalState: ${RvcOperationalState.OperationalState[operationalStateId]}`);
|
|
285
|
+
platform.robot!.updateAttribute(RvcOperationalState.Cluster.id, 'operationalState', operationalStateId, platform.log);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (batteryLevel) {
|
|
289
|
+
platform.robot.updateAttribute(PowerSource.Cluster.id, 'batChargeState', getBatteryState(state, batteryLevel), platform.log);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import axios, { AxiosInstance, AxiosStatic } from 'axios';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
import { AnsiLogger } from 'matterbridge/logger';
|
|
4
|
+
import { URLSearchParams } from 'node:url';
|
|
5
|
+
import { AuthenticateResponse } from '../Zmodel/authenticateResponse.js';
|
|
6
|
+
import { BaseUrl } from '../Zmodel/baseURL.js';
|
|
7
|
+
import { HomeInfo } from '../Zmodel/homeInfo.js';
|
|
8
|
+
import { UserData } from '../Zmodel/userData.js';
|
|
9
|
+
|
|
10
|
+
export class RoborockAuthenticateApi {
|
|
11
|
+
private readonly logger: AnsiLogger;
|
|
12
|
+
private axiosFactory: AxiosStatic;
|
|
13
|
+
private deviceId: string;
|
|
14
|
+
private username?: string;
|
|
15
|
+
private authToken?: string;
|
|
16
|
+
|
|
17
|
+
constructor(logger: AnsiLogger, axiosFactory: AxiosStatic = axios) {
|
|
18
|
+
this.deviceId = crypto.randomUUID();
|
|
19
|
+
this.axiosFactory = axiosFactory;
|
|
20
|
+
this.logger = logger;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async loginWithPassword(username: string, password: string): Promise<UserData> {
|
|
24
|
+
const api = await this.getAPIFor(username);
|
|
25
|
+
const response = await api.post(
|
|
26
|
+
'api/v1/login',
|
|
27
|
+
new URLSearchParams({
|
|
28
|
+
username: username,
|
|
29
|
+
password: password,
|
|
30
|
+
needtwostepauth: 'false',
|
|
31
|
+
}).toString(),
|
|
32
|
+
);
|
|
33
|
+
return this.auth(username, response.data);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async getHomeDetails(): Promise<HomeInfo | undefined> {
|
|
37
|
+
if (!this.username || !this.authToken) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const api = await this.getAPIFor(this.username);
|
|
42
|
+
const response = await api.get('api/v1/getHomeDetail');
|
|
43
|
+
|
|
44
|
+
const apiResponse: AuthenticateResponse<HomeInfo> = response.data;
|
|
45
|
+
if (!apiResponse.data) {
|
|
46
|
+
throw new Error('Failed to retrieve the home details');
|
|
47
|
+
}
|
|
48
|
+
return apiResponse.data;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private async getAPIFor(username: string): Promise<AxiosInstance> {
|
|
52
|
+
const baseUrl = await this.getBaseUrl(username);
|
|
53
|
+
return this.apiForUser(username, baseUrl);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private async getBaseUrl(username: string): Promise<string> {
|
|
57
|
+
const api = await this.apiForUser(username);
|
|
58
|
+
const response = await api.post(
|
|
59
|
+
'api/v1/getUrlByEmail',
|
|
60
|
+
new URLSearchParams({
|
|
61
|
+
email: username,
|
|
62
|
+
needtwostepauth: 'false',
|
|
63
|
+
}).toString(),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const apiResponse: AuthenticateResponse<BaseUrl> = response.data;
|
|
67
|
+
if (!apiResponse.data) {
|
|
68
|
+
throw new Error('Failed to retrieve base URL: ' + apiResponse.msg);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return apiResponse.data.url;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private async apiForUser(username: string, baseUrl: string = 'https://usiot.roborock.com'): Promise<AxiosInstance> {
|
|
75
|
+
return this.axiosFactory.create({
|
|
76
|
+
baseURL: baseUrl,
|
|
77
|
+
headers: {
|
|
78
|
+
header_clientid: crypto.createHash('md5').update(username).update(this.deviceId).digest('base64'),
|
|
79
|
+
Authorization: this.authToken,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private auth(username: string, response: AuthenticateResponse<UserData>): UserData {
|
|
85
|
+
const userdata = response.data;
|
|
86
|
+
if (!userdata || !userdata.token) {
|
|
87
|
+
throw new Error('Authentication failed: ' + response.msg + ' code: ' + response.code);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this.loginWithAuthToken(username, userdata.token);
|
|
91
|
+
return userdata;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private loginWithAuthToken(username: string, token: string): void {
|
|
95
|
+
this.username = username;
|
|
96
|
+
this.authToken = token;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import axios, { AxiosInstance } from 'axios';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
import { AnsiLogger } from 'matterbridge/logger';
|
|
4
|
+
import { ApiResponse } from '../Zmodel/apiResponse.js';
|
|
5
|
+
import { Home } from '../Zmodel/home.js';
|
|
6
|
+
import { UserData } from '../Zmodel/userData.js';
|
|
7
|
+
|
|
8
|
+
export class RoborockIoTApi {
|
|
9
|
+
logger: AnsiLogger;
|
|
10
|
+
private readonly api: AxiosInstance;
|
|
11
|
+
|
|
12
|
+
constructor(userdata: UserData, logger: AnsiLogger) {
|
|
13
|
+
this.logger = logger;
|
|
14
|
+
|
|
15
|
+
this.api = axios.create({ baseURL: userdata.rriot.r.a });
|
|
16
|
+
this.api.interceptors.request.use((config) => {
|
|
17
|
+
try {
|
|
18
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
19
|
+
const nonce = crypto
|
|
20
|
+
.randomBytes(6)
|
|
21
|
+
.toString('base64')
|
|
22
|
+
.substring(0, 6)
|
|
23
|
+
.replace(/[+/]/g, (m) => (m === '+' ? 'X' : 'Y'));
|
|
24
|
+
const url = this.api ? new URL(this.api.getUri(config)).pathname : '';
|
|
25
|
+
const data = [userdata.rriot.u, userdata.rriot.s, nonce, timestamp, crypto.createHash('md5').update(url).digest('hex'), '', ''].join(':');
|
|
26
|
+
const hmac = crypto.createHmac('sha256', userdata.rriot.h).update(data).digest('base64');
|
|
27
|
+
config.headers['Authorization'] = `Hawk id="${userdata.rriot.u}", s="${userdata.rriot.s}", ts="${timestamp}", nonce="${nonce}", mac="${hmac}"`;
|
|
28
|
+
} catch (error) {
|
|
29
|
+
this.logger.error('Failed to initialize RESTAPI' + JSON.stringify(error));
|
|
30
|
+
}
|
|
31
|
+
return config;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async getHome(homeId: number): Promise<Home | undefined> {
|
|
36
|
+
const result = await this.api.get(`user/homes/${homeId}`);
|
|
37
|
+
|
|
38
|
+
const apiResponse: ApiResponse<Home> = result.data;
|
|
39
|
+
if (apiResponse.result) {
|
|
40
|
+
return apiResponse.result;
|
|
41
|
+
} else {
|
|
42
|
+
this.logger.error('Failed to retrieve the home data');
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async getHomev2(homeId: number): Promise<Home | undefined> {
|
|
48
|
+
const result = await this.api.get('v2/user/homes/' + homeId);
|
|
49
|
+
|
|
50
|
+
const apiResponse: ApiResponse<Home> = result.data;
|
|
51
|
+
if (apiResponse.result) {
|
|
52
|
+
return apiResponse.result;
|
|
53
|
+
} else {
|
|
54
|
+
this.logger.error('Failed to retrieve the home data');
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async getHomev3(homeId: number): Promise<Home | undefined> {
|
|
60
|
+
const result = await this.api.get('v3/user/homes/' + homeId);
|
|
61
|
+
|
|
62
|
+
const apiResponse: ApiResponse<Home> = result.data;
|
|
63
|
+
if (apiResponse.result) {
|
|
64
|
+
return apiResponse.result;
|
|
65
|
+
} else {
|
|
66
|
+
this.logger.error('Failed to retrieve the home data');
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export enum OperationStatusCode {
|
|
2
|
+
Unknown = 0,
|
|
3
|
+
Initiating = 1,
|
|
4
|
+
Sleeping = 2, //or ChargerDisconnected
|
|
5
|
+
Idle = 3,
|
|
6
|
+
RemoteControl = 4,
|
|
7
|
+
Cleaning = 5,
|
|
8
|
+
ReturningDock = 6,
|
|
9
|
+
ManualMode = 7,
|
|
10
|
+
Charging = 8,
|
|
11
|
+
ChargingError = 9,
|
|
12
|
+
Paused = 10,
|
|
13
|
+
SpotCleaning = 11,
|
|
14
|
+
InError = 12,
|
|
15
|
+
ShuttingDown = 13,
|
|
16
|
+
Updating = 14,
|
|
17
|
+
Docking = 15,
|
|
18
|
+
GoTo = 16,
|
|
19
|
+
ZoneClean = 17,
|
|
20
|
+
RoomClean = 18,
|
|
21
|
+
EmptyingDustContainer = 22,
|
|
22
|
+
WashingTheMop = 23,
|
|
23
|
+
WashingTheMop2 = 25,
|
|
24
|
+
GoingToWashTheMop = 26,
|
|
25
|
+
InCall = 28,
|
|
26
|
+
Mapping = 29,
|
|
27
|
+
SOMETHING_NEED_TO_FIGUREOUT = 30,
|
|
28
|
+
Patrol = 32,
|
|
29
|
+
FullyCharged = 100,
|
|
30
|
+
DeviceOffline = 101,
|
|
31
|
+
Locked = 103,
|
|
32
|
+
AirDryingStopping = 202,
|
|
33
|
+
RobotStatusMopping = 6301,
|
|
34
|
+
CleanMopCleaning = 6302,
|
|
35
|
+
CleanMopMopping = 6303,
|
|
36
|
+
RoomMopping = 6304,
|
|
37
|
+
RoomCleanMopCleaning = 6305,
|
|
38
|
+
RoomCleanMopMopping = 6306,
|
|
39
|
+
ZoneMopping = 6307,
|
|
40
|
+
ZoneCleanMopCleaning = 6308,
|
|
41
|
+
ZoneCleanMopMopping = 6309,
|
|
42
|
+
BackToDockWashingDuster = 6310,
|
|
43
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export enum VacuumErrorCode {
|
|
2
|
+
None = 0,
|
|
3
|
+
LidarBlocked = 1,
|
|
4
|
+
BumperStuck = 2,
|
|
5
|
+
WheelsSuspended = 3,
|
|
6
|
+
CliffSensorError = 4,
|
|
7
|
+
MainBrushJammed = 5,
|
|
8
|
+
SideBrushJammed = 6,
|
|
9
|
+
WheelsJammed = 7,
|
|
10
|
+
RobotTrapped = 8,
|
|
11
|
+
NoDustbin = 9,
|
|
12
|
+
StrainerError = 10,
|
|
13
|
+
CompassError = 11,
|
|
14
|
+
LowBattery = 12,
|
|
15
|
+
ChargingError = 13,
|
|
16
|
+
BatteryError = 14,
|
|
17
|
+
WallSensorDirty = 15,
|
|
18
|
+
RobotTilted = 16,
|
|
19
|
+
SideBrushError = 17,
|
|
20
|
+
FanError = 18,
|
|
21
|
+
DockNotConnectedToPower = 19,
|
|
22
|
+
OpticalFlowSensorDirt = 20,
|
|
23
|
+
VerticalBumperPressed = 21,
|
|
24
|
+
DockLocatorError = 22,
|
|
25
|
+
ReturnToDockFail = 23,
|
|
26
|
+
NogoZoneDetected = 24,
|
|
27
|
+
CameraError = 25,
|
|
28
|
+
WallSensorError = 26,
|
|
29
|
+
VibrariseJammed = 27,
|
|
30
|
+
RobotOnCarpet = 28,
|
|
31
|
+
FilterBlocked = 29,
|
|
32
|
+
InvisibleWallDetected = 30,
|
|
33
|
+
CannotCrossCarpet = 31,
|
|
34
|
+
InternalError = 32,
|
|
35
|
+
CleanAutoEmptyDock = 34,
|
|
36
|
+
AutoEmptyDockVoltage = 35,
|
|
37
|
+
MoppingRollerJammed = 36, // Wash roller may be jammed
|
|
38
|
+
MoppingRollerNotLowered = 37, // wash roller not lowered properly
|
|
39
|
+
ClearWaterBoxHoare = 38,
|
|
40
|
+
DirtyWaterBoxHoare = 39,
|
|
41
|
+
SinkStrainerHoare = 40,
|
|
42
|
+
ClearWaterTankEmpty = 41,
|
|
43
|
+
ClearBrushInstalledProperly = 42, // Check that the water filter has been correctly installed
|
|
44
|
+
ClearBrushPositioningError = 43,
|
|
45
|
+
FilterScreenException = 44, // Clean the dock water filter
|
|
46
|
+
MoppingRollerJammed2 = 45, // Wash roller may be jammed
|
|
47
|
+
UpWaterException = 48,
|
|
48
|
+
DrainWaterException = 49,
|
|
49
|
+
TemperatureProtection = 51,
|
|
50
|
+
CleanCarouselException = 52,
|
|
51
|
+
CleanCarouselWaterFull = 53,
|
|
52
|
+
WaterCarriageDrop = 54,
|
|
53
|
+
CheckCleanCarouse = 55,
|
|
54
|
+
AudioError = 56,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export enum DockErrorCode {
|
|
58
|
+
None = 0,
|
|
59
|
+
DuctBlockage = 34, // Duct blockage detected
|
|
60
|
+
WaterEmpty = 38, // Clean water tank empty
|
|
61
|
+
WasteWaterTankFull = 39, // Waste water tank full
|
|
62
|
+
MaintenanceBrushJammed = 42, // Maintenance brush jammed
|
|
63
|
+
DirtyTankLatchOpen = 44, // Dirty tank latch open
|
|
64
|
+
NoDustbin = 46, // No dustbin detected
|
|
65
|
+
CleaningTankFullOrBlocked = 53, // Cleaning tank full or blocked
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const SUPPORTED_ATTACHMENTS = ['WATERTANK', 'MOP'];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { DeviceSchema } from './deviceSchema.js';
|
|
2
|
+
import { Room } from './room.js';
|
|
3
|
+
|
|
4
|
+
export interface Device {
|
|
5
|
+
duid: string;
|
|
6
|
+
name: string;
|
|
7
|
+
sn: string;
|
|
8
|
+
serialNumber: string;
|
|
9
|
+
|
|
10
|
+
activeTime: number;
|
|
11
|
+
createTime: number;
|
|
12
|
+
|
|
13
|
+
localKey: string;
|
|
14
|
+
|
|
15
|
+
/** The protocol version of the robot. */
|
|
16
|
+
pv: string;
|
|
17
|
+
online: boolean;
|
|
18
|
+
|
|
19
|
+
productId: string;
|
|
20
|
+
|
|
21
|
+
rrHomeId: number;
|
|
22
|
+
|
|
23
|
+
/** The firmware version of the robot. */
|
|
24
|
+
fv: string;
|
|
25
|
+
|
|
26
|
+
deviceStatus: Record<string, number>;
|
|
27
|
+
rooms: Room[];
|
|
28
|
+
|
|
29
|
+
schema: DeviceSchema[];
|
|
30
|
+
|
|
31
|
+
data: {
|
|
32
|
+
id: string;
|
|
33
|
+
firmwareVersion: string;
|
|
34
|
+
serialNumber: string;
|
|
35
|
+
model: string;
|
|
36
|
+
category: string;
|
|
37
|
+
batteryLevel: number;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export enum DeviceModel {
|
|
2
|
+
Q5 = 'roborock.vacuum.a34',
|
|
3
|
+
Q5_PRO = 'roborock.vacuum.a72',
|
|
4
|
+
S5 = 'roborock.vacuum.s5',
|
|
5
|
+
S5_MAX = 'roborock.vacuum.s5e',
|
|
6
|
+
S6 = 'roborock.vacuum.s6',
|
|
7
|
+
S6_MAXV = 'roborock.vacuum.a10',
|
|
8
|
+
S6_PURE = 'roborock.vacuum.a08',
|
|
9
|
+
Q7 = 'roborock.vacuum.a40',
|
|
10
|
+
Q7_MAX = 'roborock.vacuum.a38',
|
|
11
|
+
Q7_PLUS = 'roborock.vacuum.a40',
|
|
12
|
+
S7 = 'roborock.vacuum.a15',
|
|
13
|
+
S7_MAXV = 'roborock.vacuum.a27',
|
|
14
|
+
S7_MAXV_ULTRA = 'roborock.vacuum.a65',
|
|
15
|
+
S7_PRO_ULTRA = 'roborock.vacuum.a62',
|
|
16
|
+
Q8_MAX = 'roborock.vacuum.a73',
|
|
17
|
+
S8 = 'roborock.vacuum.a51',
|
|
18
|
+
S8_PRO_ULTRA = 'roborock.vacuum.a70',
|
|
19
|
+
S8_MAXV_ULTRA = 'roborock.vacuum.a97',
|
|
20
|
+
QREVO_MASTER = 'roborock.vacuum.a117',
|
|
21
|
+
QREVO_CURV = 'roborock.vacuum.a135',
|
|
22
|
+
QREVO_S = 'roborock.vacuum.a104',
|
|
23
|
+
QREVO_PRO = 'roborock.vacuum.a101',
|
|
24
|
+
QREVO_MAXV = 'roborock.vacuum.a87',
|
|
25
|
+
QREVO_EDGE_5V1 = 'roborock.vacuum.a187',
|
|
26
|
+
QREVO_EDGE_5AE = 'roborock.vacuum.xxxx',
|
|
27
|
+
}
|