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.
Files changed (59) hide show
  1. package/.gitattributes +13 -0
  2. package/CHANGELOG.md +469 -0
  3. package/LICENSE +23 -0
  4. package/README.md +45 -0
  5. package/__tests__/testdata/b01_q7_get_prop_response.json +1 -0
  6. package/__tests__/testdata/b01_q7_map_fixture.json +16 -0
  7. package/config.schema.json +118 -0
  8. package/dist/crypto.js +65 -0
  9. package/dist/crypto.js.map +1 -0
  10. package/dist/index.js +10 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/live_message.js +25 -0
  13. package/dist/live_message.js.map +1 -0
  14. package/dist/logger.js +37 -0
  15. package/dist/logger.js.map +1 -0
  16. package/dist/matter_vacuum_accessory.js +1877 -0
  17. package/dist/matter_vacuum_accessory.js.map +1 -0
  18. package/dist/platform.js +441 -0
  19. package/dist/platform.js.map +1 -0
  20. package/dist/settings.js +8 -0
  21. package/dist/settings.js.map +1 -0
  22. package/dist/types.js +3 -0
  23. package/dist/types.js.map +1 -0
  24. package/dist/ui/index.js +741 -0
  25. package/dist/ui/index.js.map +1 -0
  26. package/docs/matter-rvc-updating-homebridge-report.md +120 -0
  27. package/homebridge-roborock-vacuum.code-workspace +7 -0
  28. package/homebridge-ui/public/index.html +257 -0
  29. package/homebridge-ui/public/index.js +1276 -0
  30. package/homebridge-ui/public/styles.css +611 -0
  31. package/homebridge-ui/server.js +3 -0
  32. package/package.json +91 -0
  33. package/roborockLib/i18n/de/translations.json +188 -0
  34. package/roborockLib/i18n/en/translations.json +208 -0
  35. package/roborockLib/i18n/es/translations.json +188 -0
  36. package/roborockLib/i18n/fr/translations.json +188 -0
  37. package/roborockLib/i18n/it/translations.json +188 -0
  38. package/roborockLib/i18n/nl/translations.json +188 -0
  39. package/roborockLib/i18n/pl/translations.json +188 -0
  40. package/roborockLib/i18n/pt/translations.json +188 -0
  41. package/roborockLib/i18n/ru/translations.json +188 -0
  42. package/roborockLib/i18n/uk/translations.json +188 -0
  43. package/roborockLib/i18n/zh-cn/translations.json +188 -0
  44. package/roborockLib/lib/RRMapParser.js +459 -0
  45. package/roborockLib/lib/b01Q7Adapter.js +468 -0
  46. package/roborockLib/lib/deviceFeatures.js +1349 -0
  47. package/roborockLib/lib/localConnector.js +629 -0
  48. package/roborockLib/lib/map/map.html +137 -0
  49. package/roborockLib/lib/map/zones.js +851 -0
  50. package/roborockLib/lib/message.js +402 -0
  51. package/roborockLib/lib/messageQueueHandler.js +411 -0
  52. package/roborockLib/lib/roborockAuth.js +178 -0
  53. package/roborockLib/lib/roborockCrypto.js +55 -0
  54. package/roborockLib/lib/roborockPackageHelper.js +142 -0
  55. package/roborockLib/lib/roborock_mqtt_connector.js +567 -0
  56. package/roborockLib/lib/sniffing/mitmproxy_roborock.py +300 -0
  57. package/roborockLib/lib/vacuum.js +992 -0
  58. package/roborockLib/roborockAPI.js +3635 -0
  59. 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")