Skip to content

Commit 22680bf

Browse files
feat: adding offline.py for others to test local api
1 parent 6310714 commit 22680bf

File tree

1 file changed

+132
-0
lines changed

1 file changed

+132
-0
lines changed

offline.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import binascii
2+
import json
3+
import math
4+
import socket
5+
import struct
6+
import time
7+
8+
from Crypto.Cipher import AES
9+
from Crypto.Util.Padding import unpad, pad
10+
11+
from roborock import RoborockException
12+
from roborock.api import md5bin, encode_timestamp
13+
from roborock.typing import RoborockCommand
14+
15+
local_ip = "<device_ip>"
16+
local_key = "<device_localkey>"
17+
18+
salt = "TXdfu$jyZ#TZHsg4"
19+
seq = 1
20+
random = 4711
21+
request_id = 10000
22+
counter = 1
23+
24+
get_prefix = 119
25+
app_prefix = 135
26+
set_prefix = 151
27+
28+
# example messages out of wireshark
29+
msgs = [
30+
'00000087312e300000d1d5000198da6424b65900040070772b8ddc5645f5751d58d4a9805c37cc1c607e68d2a1e823c2f228841aed5489ccb0627ae677cea01203261df679d30d987a611e1460668f5aceacd2a4f9a94f95afad0f408768f28cff656e8a37657f831b25e579cea7bdc9b326e91c149012406eb417551668d82f57af0ee6ca113cb4369293',
31+
'00000087312e300000d1d5000198da6424b65900040070772b8ddc5645f5751d58d4a9805c37cc1c607e68d2a1e823c2f228841aed5489ccb0627ae677cea01203261df679d30d987a611e1460668f5aceacd2a4f9a94f95afad0f408768f28cff656e8a37657f831b25e579cea7bdc9b326e91c149012406eb417551668d82f57af0ee6ca113cb4369293',
32+
'00000077312e300000dde70001e4206424b65900040060772b8ddc5645f5751d58d4a9805c37ccb34713eb49547bc1b857a234f517d954a53ea8124688f0e58b8653bcaddc26e47a78f5e6e41e0f6af5dedf97599aba599471cc64c5ad7a95648287484963b87530068800ee12c23ce0aece65acb7047c1dcf29df',
33+
'00000097312e300000bdd0000068736424b65900040080772b8ddc5645f5751d58d4a9805c37cc5913ddddb0a61b3d24be9e857b201181a70bfbeae8768a2b514c9d5c9ff558423c007a32a856e96066c74c5453002cfb7f750c4d9f5c5ad5377bcb95f8298d919aa6968b8c1053a3bc1a2ce7f181d64d988306ad407e0c765a7d19a27ec9255e3fad02f352bab81c012feb8ea0104cd0a6a5218300000087312e300000f1d90001b4746424b65900040070772b8ddc5645f5751d58d4a9805c37ccdf2e18471bebd4bad04eeb563019355614fcc0f7a86a72a302a383c483e13442775c134021aa1f062c690c5e0780edc01e0718befd9e50b030c7cb02459741795a5b74edcf7346d79c045380257c393ecda8d9999868f0c835ad1c3b8730bcdffa0cd9c900000087312e30000187cf00020c2d6424b65900040070772b8ddc5645f5751d58d4a9805c37cc68ba4c061b90da4986b184fc28b8daef01c30401714d39128cf65bf2b74d40469735b3baa56e66c3e9059cb327f1b00c6c8ac4af88cd38b825a6e6c4d91fc6e264f60056fac4a16021160367b8d76981f668b2e2e6d8a60523261bfd046a53c3d58dfead'
34+
]
35+
36+
37+
def send_command(
38+
ip: str, method: RoborockCommand, params: list = None
39+
):
40+
timestamp = math.floor(time.time())
41+
_request_id = request_id + 1
42+
inner = {
43+
"id": _request_id,
44+
"method": method,
45+
"params": params or [],
46+
}
47+
payload = json.dumps(
48+
{
49+
"dps": {"101": json.dumps(inner, separators=(",", ":"))},
50+
"t": timestamp,
51+
},
52+
separators=(",", ":"),
53+
).encode()
54+
print(f"id={_request_id} Requesting method {method} with {params}")
55+
use_prefix = get_prefix
56+
# if method.startswith("app_"):
57+
# use_prefix = app_prefix
58+
# # elif method.startswith("get_"):
59+
# # use_prefix = get_prefix
60+
# elif method.startswith("set_"):
61+
# use_prefix = set_prefix
62+
response1 = _send_msg_raw(ip, timestamp, use_prefix, payload)
63+
return _decode_msg(response1)
64+
65+
66+
def _send_msg_raw(ip: str , timestamp: int, prefix: int, payload: bytes):
67+
protocol = 4
68+
aes_key = md5bin(encode_timestamp(timestamp) + local_key + salt)
69+
cipher = AES.new(aes_key, AES.MODE_ECB)
70+
encrypted = cipher.encrypt(pad(payload, AES.block_size))
71+
encrypted_len = len(encrypted)
72+
msg = struct.pack(
73+
f"!I3sIIIHH{encrypted_len}s",
74+
prefix,
75+
"1.0".encode(),
76+
seq,
77+
random,
78+
timestamp,
79+
protocol,
80+
encrypted_len,
81+
encrypted,
82+
)
83+
crc32 = binascii.crc32(msg[4:])
84+
msg += struct.pack("!I", crc32)
85+
# Send the command to the Roborock device
86+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
87+
s.connect((ip, 58867))
88+
s.send(msg)
89+
_response = s.recv(1024)
90+
91+
return _response
92+
93+
94+
def _decode_msg(msg):
95+
print(msg[:4])
96+
msg = msg[4:]
97+
if msg[0:3] != "1.0".encode():
98+
raise RoborockException("Unknown protocol version")
99+
# crc32 = binascii.crc32(msg[: len(msg) - 4])
100+
if len(msg) == 17:
101+
[version, _seq, _random, timestamp, protocol] = struct.unpack(
102+
"!3sIIIH", msg[0:17]
103+
)
104+
return {
105+
"version": version,
106+
"timestamp": timestamp,
107+
"protocol": protocol,
108+
}
109+
[version, _seq, _random, timestamp, protocol, payload_len] = struct.unpack(
110+
"!3sIIIHH", msg[0:19]
111+
)
112+
[payload, expected_crc32] = struct.unpack_from(f"!{payload_len}sI", msg, 19)
113+
# if crc32 != expected_crc32:
114+
# raise RoborockException(f"Wrong CRC32 {crc32}, expected {expected_crc32}")
115+
116+
aes_key = md5bin(encode_timestamp(timestamp) + local_key + salt)
117+
decipher = AES.new(aes_key, AES.MODE_ECB)
118+
decrypted_payload = unpad(decipher.decrypt(payload), AES.block_size)
119+
return {
120+
"version": version,
121+
"timestamp": timestamp,
122+
"protocol": protocol,
123+
"payload": decrypted_payload,
124+
}
125+
126+
for msg in msgs:
127+
print(_decode_msg(bytes.fromhex(msg)))
128+
129+
# Parse the response to get the current status of the device
130+
response = send_command(local_ip, RoborockCommand.GET_STATUS)
131+
# 4 = "app_get_init_status"
132+
print(response)

0 commit comments

Comments
 (0)