homebridge-roborock-matter 2.1.0
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/.gitattributes +13 -0
- package/CHANGELOG.md +469 -0
- package/LICENSE +23 -0
- package/README.md +45 -0
- package/__tests__/testdata/b01_q7_get_prop_response.json +1 -0
- package/__tests__/testdata/b01_q7_map_fixture.json +16 -0
- package/config.schema.json +118 -0
- package/dist/crypto.js +65 -0
- package/dist/crypto.js.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/live_message.js +25 -0
- package/dist/live_message.js.map +1 -0
- package/dist/logger.js +37 -0
- package/dist/logger.js.map +1 -0
- package/dist/matter_vacuum_accessory.js +1877 -0
- package/dist/matter_vacuum_accessory.js.map +1 -0
- package/dist/platform.js +441 -0
- package/dist/platform.js.map +1 -0
- package/dist/settings.js +8 -0
- package/dist/settings.js.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/index.js +741 -0
- package/dist/ui/index.js.map +1 -0
- package/docs/matter-rvc-updating-homebridge-report.md +120 -0
- package/homebridge-roborock-vacuum.code-workspace +7 -0
- package/homebridge-ui/public/index.html +257 -0
- package/homebridge-ui/public/index.js +1276 -0
- package/homebridge-ui/public/styles.css +611 -0
- package/homebridge-ui/server.js +3 -0
- package/package.json +91 -0
- package/roborockLib/i18n/de/translations.json +188 -0
- package/roborockLib/i18n/en/translations.json +208 -0
- package/roborockLib/i18n/es/translations.json +188 -0
- package/roborockLib/i18n/fr/translations.json +188 -0
- package/roborockLib/i18n/it/translations.json +188 -0
- package/roborockLib/i18n/nl/translations.json +188 -0
- package/roborockLib/i18n/pl/translations.json +188 -0
- package/roborockLib/i18n/pt/translations.json +188 -0
- package/roborockLib/i18n/ru/translations.json +188 -0
- package/roborockLib/i18n/uk/translations.json +188 -0
- package/roborockLib/i18n/zh-cn/translations.json +188 -0
- package/roborockLib/lib/RRMapParser.js +459 -0
- package/roborockLib/lib/b01Q7Adapter.js +468 -0
- package/roborockLib/lib/deviceFeatures.js +1349 -0
- package/roborockLib/lib/localConnector.js +629 -0
- package/roborockLib/lib/map/map.html +137 -0
- package/roborockLib/lib/map/zones.js +851 -0
- package/roborockLib/lib/message.js +402 -0
- package/roborockLib/lib/messageQueueHandler.js +411 -0
- package/roborockLib/lib/roborockAuth.js +178 -0
- package/roborockLib/lib/roborockCrypto.js +55 -0
- package/roborockLib/lib/roborockPackageHelper.js +142 -0
- package/roborockLib/lib/roborock_mqtt_connector.js +567 -0
- package/roborockLib/lib/sniffing/mitmproxy_roborock.py +300 -0
- package/roborockLib/lib/vacuum.js +992 -0
- package/roborockLib/roborockAPI.js +3635 -0
- package/tsconfig.roborockLib.json +17 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from mitmproxy.utils import strutils
|
|
5
|
+
from mitmproxy import ctx
|
|
6
|
+
from mitmproxy import tcp
|
|
7
|
+
from websocket import create_connection
|
|
8
|
+
import json
|
|
9
|
+
|
|
10
|
+
import struct
|
|
11
|
+
import binascii
|
|
12
|
+
from struct import unpack
|
|
13
|
+
|
|
14
|
+
ws = create_connection("ws://localhost:7906")
|
|
15
|
+
|
|
16
|
+
class MQTTControlPacket:
|
|
17
|
+
# Packet types
|
|
18
|
+
(
|
|
19
|
+
CONNECT,
|
|
20
|
+
CONNACK,
|
|
21
|
+
PUBLISH,
|
|
22
|
+
PUBACK,
|
|
23
|
+
PUBREC,
|
|
24
|
+
PUBREL,
|
|
25
|
+
PUBCOMP,
|
|
26
|
+
SUBSCRIBE,
|
|
27
|
+
SUBACK,
|
|
28
|
+
UNSUBSCRIBE,
|
|
29
|
+
UNSUBACK,
|
|
30
|
+
PINGREQ,
|
|
31
|
+
PINGRESP,
|
|
32
|
+
DISCONNECT,
|
|
33
|
+
) = range(1, 15)
|
|
34
|
+
|
|
35
|
+
# http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Table_2.1_-
|
|
36
|
+
Names = [
|
|
37
|
+
"reserved",
|
|
38
|
+
"CONNECT",
|
|
39
|
+
"CONNACK",
|
|
40
|
+
"PUBLISH",
|
|
41
|
+
"PUBACK",
|
|
42
|
+
"PUBREC",
|
|
43
|
+
"PUBREL",
|
|
44
|
+
"PUBCOMP",
|
|
45
|
+
"SUBSCRIBE",
|
|
46
|
+
"SUBACK",
|
|
47
|
+
"UNSUBSCRIBE",
|
|
48
|
+
"UNSUBACK",
|
|
49
|
+
"PINGREQ",
|
|
50
|
+
"PINGRESP",
|
|
51
|
+
"DISCONNECT",
|
|
52
|
+
"reserved",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
PACKETS_WITH_IDENTIFIER = [
|
|
56
|
+
PUBACK,
|
|
57
|
+
PUBREC,
|
|
58
|
+
PUBREL,
|
|
59
|
+
PUBCOMP,
|
|
60
|
+
SUBSCRIBE,
|
|
61
|
+
SUBACK,
|
|
62
|
+
UNSUBSCRIBE,
|
|
63
|
+
UNSUBACK,
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
def __init__(self, packet):
|
|
67
|
+
self._packet = packet
|
|
68
|
+
|
|
69
|
+
print(sys.executable)
|
|
70
|
+
# Fixed header
|
|
71
|
+
# http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718020
|
|
72
|
+
self.packet_type = self._parse_packet_type()
|
|
73
|
+
self.packet_type_human = self.Names[self.packet_type]
|
|
74
|
+
self.dup, self.qos, self.retain = self._parse_flags()
|
|
75
|
+
self.remaining_length = self._parse_remaining_length()
|
|
76
|
+
# Variable header & Payload
|
|
77
|
+
# http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718024
|
|
78
|
+
# http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718026
|
|
79
|
+
if self.packet_type == self.CONNECT:
|
|
80
|
+
self._parse_connect_variable_headers()
|
|
81
|
+
self._parse_connect_payload()
|
|
82
|
+
elif self.packet_type == self.PUBLISH:
|
|
83
|
+
self._parse_publish_variable_headers()
|
|
84
|
+
self._parse_publish_payload()
|
|
85
|
+
elif self.packet_type == self.SUBSCRIBE:
|
|
86
|
+
self._parse_subscribe_variable_headers()
|
|
87
|
+
self._parse_subscribe_payload()
|
|
88
|
+
elif self.packet_type == self.SUBACK:
|
|
89
|
+
pass
|
|
90
|
+
elif self.packet_type == self.UNSUBSCRIBE:
|
|
91
|
+
pass
|
|
92
|
+
else:
|
|
93
|
+
self.payload = None
|
|
94
|
+
|
|
95
|
+
def pprint(self):
|
|
96
|
+
s = f"[{self.Names[self.packet_type]}]"
|
|
97
|
+
|
|
98
|
+
if self.packet_type == self.CONNECT:
|
|
99
|
+
s += f"""
|
|
100
|
+
|
|
101
|
+
Client Id: {self.payload['ClientId']}
|
|
102
|
+
Will Topic: {self.payload.get('WillTopic')}
|
|
103
|
+
Will Message: {strutils.bytes_to_escaped_str(self.payload.get('WillMessage', b'None'))}
|
|
104
|
+
User Name TEST: {self.payload.get('UserName')}
|
|
105
|
+
Password: {strutils.bytes_to_escaped_str(self.payload.get('Password', b'None'))}
|
|
106
|
+
"""
|
|
107
|
+
elif self.packet_type == self.SUBSCRIBE:
|
|
108
|
+
s += " sent topic filters: "
|
|
109
|
+
s += ", ".join([f"'{tf}'" for tf in self.topic_filters])
|
|
110
|
+
elif self.packet_type == self.PUBLISH:
|
|
111
|
+
topic_name = strutils.bytes_to_escaped_str(self.topic_name)
|
|
112
|
+
payload_hex = binascii.hexlify(self.payload).decode()
|
|
113
|
+
|
|
114
|
+
protocol = unpack('>H', self.payload[15:17])[0]
|
|
115
|
+
|
|
116
|
+
if (protocol == 101):
|
|
117
|
+
s += f" '{payload_hex}' to topic '{topic_name}'"
|
|
118
|
+
elif self.packet_type in [self.PINGREQ, self.PINGRESP]:
|
|
119
|
+
pass
|
|
120
|
+
else:
|
|
121
|
+
s = f"Packet type {self.Names[self.packet_type]} is not supported yet!"
|
|
122
|
+
|
|
123
|
+
return s
|
|
124
|
+
|
|
125
|
+
def _parse_length_prefixed_bytes(self, offset):
|
|
126
|
+
field_length_bytes = self._packet[offset : offset + 2]
|
|
127
|
+
field_length = struct.unpack("!H", field_length_bytes)[0]
|
|
128
|
+
|
|
129
|
+
field_content_bytes = self._packet[offset + 2 : offset + 2 + field_length]
|
|
130
|
+
|
|
131
|
+
return field_length + 2, field_content_bytes
|
|
132
|
+
|
|
133
|
+
def _parse_publish_variable_headers(self):
|
|
134
|
+
offset = len(self._packet) - self.remaining_length
|
|
135
|
+
|
|
136
|
+
field_length, field_content_bytes = self._parse_length_prefixed_bytes(offset)
|
|
137
|
+
self.topic_name = field_content_bytes
|
|
138
|
+
|
|
139
|
+
if self.qos in [0x01, 0x02]:
|
|
140
|
+
offset += field_length
|
|
141
|
+
self.packet_identifier = self._packet[offset : offset + 2]
|
|
142
|
+
|
|
143
|
+
def _parse_publish_payload(self):
|
|
144
|
+
fixed_header_length = len(self._packet) - self.remaining_length
|
|
145
|
+
variable_header_length = 2 + len(self.topic_name)
|
|
146
|
+
|
|
147
|
+
if self.qos in [0x01, 0x02]:
|
|
148
|
+
variable_header_length += 2
|
|
149
|
+
|
|
150
|
+
offset = fixed_header_length + variable_header_length
|
|
151
|
+
|
|
152
|
+
self.payload = self._packet[offset:]
|
|
153
|
+
|
|
154
|
+
def _parse_subscribe_variable_headers(self):
|
|
155
|
+
self._parse_packet_identifier()
|
|
156
|
+
|
|
157
|
+
def _parse_subscribe_payload(self):
|
|
158
|
+
offset = len(self._packet) - self.remaining_length + 2
|
|
159
|
+
|
|
160
|
+
self.topic_filters = {}
|
|
161
|
+
|
|
162
|
+
while len(self._packet) - offset > 0:
|
|
163
|
+
field_length, topic_filter_bytes = self._parse_length_prefixed_bytes(offset)
|
|
164
|
+
offset += field_length
|
|
165
|
+
|
|
166
|
+
qos = self._packet[offset : offset + 1]
|
|
167
|
+
offset += 1
|
|
168
|
+
|
|
169
|
+
topic_filter = topic_filter_bytes.decode("utf-8")
|
|
170
|
+
self.topic_filters[topic_filter] = {"qos": qos}
|
|
171
|
+
|
|
172
|
+
# http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718030
|
|
173
|
+
def _parse_connect_variable_headers(self):
|
|
174
|
+
offset = len(self._packet) - self.remaining_length
|
|
175
|
+
|
|
176
|
+
self.variable_headers = {}
|
|
177
|
+
self.connect_flags = {}
|
|
178
|
+
|
|
179
|
+
self.variable_headers["ProtocolName"] = self._packet[offset : offset + 6]
|
|
180
|
+
self.variable_headers["ProtocolLevel"] = self._packet[offset + 6 : offset + 7]
|
|
181
|
+
self.variable_headers["ConnectFlags"] = self._packet[offset + 7 : offset + 8]
|
|
182
|
+
self.variable_headers["KeepAlive"] = self._packet[offset + 8 : offset + 10]
|
|
183
|
+
# http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc385349229
|
|
184
|
+
self.connect_flags["CleanSession"] = bool(
|
|
185
|
+
self.variable_headers["ConnectFlags"][0] & 0x02
|
|
186
|
+
)
|
|
187
|
+
self.connect_flags["Will"] = bool(
|
|
188
|
+
self.variable_headers["ConnectFlags"][0] & 0x04
|
|
189
|
+
)
|
|
190
|
+
self.will_qos = (self.variable_headers["ConnectFlags"][0] >> 3) & 0x03
|
|
191
|
+
self.connect_flags["WillRetain"] = bool(
|
|
192
|
+
self.variable_headers["ConnectFlags"][0] & 0x20
|
|
193
|
+
)
|
|
194
|
+
self.connect_flags["Password"] = bool(
|
|
195
|
+
self.variable_headers["ConnectFlags"][0] & 0x40
|
|
196
|
+
)
|
|
197
|
+
self.connect_flags["UserName"] = bool(
|
|
198
|
+
self.variable_headers["ConnectFlags"][0] & 0x80
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718031
|
|
202
|
+
def _parse_connect_payload(self):
|
|
203
|
+
fields = []
|
|
204
|
+
offset = len(self._packet) - self.remaining_length + 10
|
|
205
|
+
|
|
206
|
+
while len(self._packet) - offset > 0:
|
|
207
|
+
field_length, field_content = self._parse_length_prefixed_bytes(offset)
|
|
208
|
+
fields.append(field_content)
|
|
209
|
+
offset += field_length
|
|
210
|
+
|
|
211
|
+
self.payload = {}
|
|
212
|
+
|
|
213
|
+
for f in fields:
|
|
214
|
+
# http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc385349242
|
|
215
|
+
if "ClientId" not in self.payload:
|
|
216
|
+
self.payload["ClientId"] = f.decode("utf-8")
|
|
217
|
+
# http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc385349243
|
|
218
|
+
elif self.connect_flags["Will"] and "WillTopic" not in self.payload:
|
|
219
|
+
self.payload["WillTopic"] = f.decode("utf-8")
|
|
220
|
+
elif self.connect_flags["Will"] and "WillMessage" not in self.payload:
|
|
221
|
+
self.payload["WillMessage"] = f
|
|
222
|
+
elif self.connect_flags["UserName"] and "UserName" not in self.payload:
|
|
223
|
+
self.payload["UserName"] = f.decode("utf-8")
|
|
224
|
+
elif self.connect_flags["Password"] and "Password" not in self.payload:
|
|
225
|
+
self.payload["Password"] = f
|
|
226
|
+
else:
|
|
227
|
+
raise Exception("")
|
|
228
|
+
|
|
229
|
+
def _parse_packet_type(self):
|
|
230
|
+
return self._packet[0] >> 4
|
|
231
|
+
|
|
232
|
+
# http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718022
|
|
233
|
+
def _parse_flags(self):
|
|
234
|
+
dup = None
|
|
235
|
+
qos = None
|
|
236
|
+
retain = None
|
|
237
|
+
|
|
238
|
+
if self.packet_type == self.PUBLISH:
|
|
239
|
+
dup = (self._packet[0] >> 3) & 0x01
|
|
240
|
+
qos = (self._packet[0] >> 1) & 0x03
|
|
241
|
+
retain = self._packet[0] & 0x01
|
|
242
|
+
|
|
243
|
+
return dup, qos, retain
|
|
244
|
+
|
|
245
|
+
# http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Table_2.4_Size
|
|
246
|
+
def _parse_remaining_length(self):
|
|
247
|
+
multiplier = 1
|
|
248
|
+
value = 0
|
|
249
|
+
i = 1
|
|
250
|
+
|
|
251
|
+
while True:
|
|
252
|
+
encodedByte = self._packet[i]
|
|
253
|
+
value += (encodedByte & 127) * multiplier
|
|
254
|
+
multiplier *= 128
|
|
255
|
+
|
|
256
|
+
if multiplier > 128 * 128 * 128:
|
|
257
|
+
raise Exception("Malformed Remaining Length")
|
|
258
|
+
|
|
259
|
+
if encodedByte & 128 == 0:
|
|
260
|
+
break
|
|
261
|
+
|
|
262
|
+
i += 1
|
|
263
|
+
|
|
264
|
+
return value
|
|
265
|
+
|
|
266
|
+
# http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Table_2.5_-
|
|
267
|
+
def _parse_packet_identifier(self):
|
|
268
|
+
offset = len(self._packet) - self.remaining_length
|
|
269
|
+
self.packet_identifier = self._packet[offset : offset + 2]
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def send_to_websocket(command, message):
|
|
275
|
+
if ws.connected:
|
|
276
|
+
data = {
|
|
277
|
+
"command": command,
|
|
278
|
+
"message": message
|
|
279
|
+
}
|
|
280
|
+
ws.send(json.dumps(data))
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def tcp_message(flow: tcp.TCPFlow):
|
|
284
|
+
message = flow.messages[-1]
|
|
285
|
+
|
|
286
|
+
mqtt_packet = MQTTControlPacket(message.content)
|
|
287
|
+
|
|
288
|
+
log_message = mqtt_packet.pprint()
|
|
289
|
+
ctx.log.info(log_message)
|
|
290
|
+
|
|
291
|
+
if log_message.startswith("[PUBLISH]") and len(log_message) > len("[PUBLISH] "):
|
|
292
|
+
send_to_websocket("sniffing_decrypt", log_message)
|
|
293
|
+
|
|
294
|
+
# This way we can save topics
|
|
295
|
+
# if mqtt_packet.packet_type == mqtt_packet.PUBLISH:
|
|
296
|
+
# with open("topics.txt", "a") as f:
|
|
297
|
+
# f.write(f"{mqtt_packet.topic_name}\n")
|
|
298
|
+
# elif mqtt_packet.packet_type == mqtt_packet.SUBSCRIBE:
|
|
299
|
+
# with open("topics.txt", "a") as f:
|
|
300
|
+
# f.write(f"{mqtt_packet.topic_filters}\n")
|