Skip to content

Commit 430c248

Browse files
authored
feat: add commands to add a new device (#307)
* feat: add commands to add a new device * chore: mr comments
1 parent a0b228c commit 430c248

File tree

3 files changed

+152
-5
lines changed

3 files changed

+152
-5
lines changed

roborock/web_api.py

Lines changed: 91 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import time
1010

1111
import aiohttp
12-
from aiohttp import ContentTypeError
12+
from aiohttp import ContentTypeError, FormData
1313

1414
from roborock.containers import HomeData, HomeDataRoom, ProductResponse, RRiot, UserData
1515
from roborock.exceptions import (
@@ -67,22 +67,108 @@ def _get_header_client_id(self):
6767
md5.update(self._device_identifier.encode())
6868
return base64.b64encode(md5.digest()).decode()
6969

70-
def _get_hawk_authentication(self, rriot: RRiot, url: str) -> str:
70+
def _process_extra_hawk_values(self, values: dict | None) -> str:
71+
if values is None:
72+
return ""
73+
else:
74+
sorted_keys = sorted(values.keys())
75+
result = []
76+
for key in sorted_keys:
77+
value = values.get(key)
78+
result.append(f"{key}={value}")
79+
return hashlib.md5("&".join(result).encode()).hexdigest()
80+
81+
def _get_hawk_authentication(
82+
self, rriot: RRiot, url: str, formdata: dict | None = None, params: dict | None = None
83+
) -> str:
7184
timestamp = math.floor(time.time())
7285
nonce = secrets.token_urlsafe(6)
86+
formdata_str = self._process_extra_hawk_values(formdata)
87+
params_str = self._process_extra_hawk_values(params)
88+
7389
prestr = ":".join(
7490
[
7591
rriot.u,
7692
rriot.s,
7793
nonce,
7894
str(timestamp),
7995
hashlib.md5(url.encode()).hexdigest(),
80-
"",
81-
"",
96+
params_str,
97+
formdata_str,
8298
]
8399
)
84100
mac = base64.b64encode(hmac.new(rriot.h.encode(), prestr.encode(), hashlib.sha256).digest()).decode()
85-
return f'Hawk id="{rriot.u}", s="{rriot.s}", ts="{timestamp}", nonce="{nonce}", mac="{mac}"'
101+
return f'Hawk id="{rriot.u}",s="{rriot.s}",ts="{timestamp}",nonce="{nonce}",mac="{mac}"'
102+
103+
async def nc_prepare(self, user_data: UserData, timezone: str) -> dict:
104+
"""This gets a few critical parameters for adding a device to your account."""
105+
if (
106+
user_data.rriot is None
107+
or user_data.rriot.r is None
108+
or user_data.rriot.u is None
109+
or user_data.rriot.r.a is None
110+
):
111+
raise RoborockException("Your userdata is missing critical attributes.")
112+
base_url = user_data.rriot.r.a
113+
prepare_request = PreparedRequest(base_url)
114+
hid = await self._get_home_id(user_data)
115+
116+
data = FormData()
117+
data.add_field("hid", hid)
118+
data.add_field("tzid", timezone)
119+
120+
prepare_response = await prepare_request.request(
121+
"post",
122+
"/nc/prepare",
123+
headers={
124+
"Authorization": self._get_hawk_authentication(
125+
user_data.rriot, "/nc/prepare", {"hid": hid, "tzid": timezone}
126+
),
127+
},
128+
data=data,
129+
)
130+
131+
if prepare_response is None:
132+
raise RoborockException("prepare_response is None")
133+
if not prepare_response.get("success"):
134+
raise RoborockException(f"{prepare_response.get('msg')} - response code: {prepare_response.get('code')}")
135+
136+
return prepare_response["result"]
137+
138+
async def add_device(self, user_data: UserData, s: str, t: str) -> dict:
139+
"""This will add a new device to your account
140+
it is recommended to only use this during a pairing cycle with a device.
141+
Please see here: https://github.com/Python-roborock/Roborockmitmproxy/blob/main/handshake_protocol.md
142+
"""
143+
if (
144+
user_data.rriot is None
145+
or user_data.rriot.r is None
146+
or user_data.rriot.u is None
147+
or user_data.rriot.r.a is None
148+
):
149+
raise RoborockException("Your userdata is missing critical attributes.")
150+
base_url = user_data.rriot.r.a
151+
add_device_request = PreparedRequest(base_url)
152+
153+
add_device_response = await add_device_request.request(
154+
"GET",
155+
"/user/devices/newadd",
156+
headers={
157+
"Authorization": self._get_hawk_authentication(
158+
user_data.rriot, "/user/devices/newadd", params={"s": s, "t": t}
159+
),
160+
},
161+
params={"s": s, "t": t},
162+
)
163+
164+
if add_device_response is None:
165+
raise RoborockException("add_device is None")
166+
if not add_device_response.get("success"):
167+
raise RoborockException(
168+
f"{add_device_response.get('msg')} - response code: {add_device_response.get('code')}"
169+
)
170+
171+
return add_device_response["result"]
86172

87173
async def request_code(self) -> None:
88174
base_url = await self._get_base_url()

tests/conftest.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,57 @@ def mock_rest() -> aioresponses:
200200
status=200,
201201
payload={"api": None, "code": 200, "result": HOME_DATA_RAW, "status": "ok", "success": True},
202202
)
203+
mocked.post(
204+
re.compile(r"https://api-.*\.roborock\.com/nc/prepare"),
205+
status=200,
206+
payload={
207+
"api": None,
208+
"result": {"r": "US", "s": "ffffff", "t": "eOf6d2BBBB"},
209+
"status": "ok",
210+
"success": True,
211+
},
212+
)
213+
214+
mocked.get(
215+
re.compile(r"https://api-.*\.roborock\.com/user/devices/newadd/*"),
216+
status=200,
217+
payload={
218+
"api": "获取新增设备信息",
219+
"result": {
220+
"activeTime": 1737724598,
221+
"attribute": None,
222+
"cid": None,
223+
"createTime": 0,
224+
"deviceStatus": None,
225+
"duid": "rand_duid",
226+
"extra": "{}",
227+
"f": False,
228+
"featureSet": "0",
229+
"fv": "02.16.12",
230+
"iconUrl": "",
231+
"lat": None,
232+
"localKey": "random_lk",
233+
"lon": None,
234+
"name": "S7",
235+
"newFeatureSet": "0000000000002000",
236+
"online": True,
237+
"productId": "rand_prod_id",
238+
"pv": "1.0",
239+
"roomId": None,
240+
"runtimeEnv": None,
241+
"setting": None,
242+
"share": False,
243+
"shareTime": None,
244+
"silentOtaSwitch": False,
245+
"sn": "Rand_sn",
246+
"timeZoneId": "America/New_York",
247+
"tuyaMigrated": False,
248+
"tuyaUuid": None,
249+
},
250+
"status": "ok",
251+
"success": True,
252+
},
253+
)
203254
yield mocked
204255

205256

tests/test_web_api.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,13 @@ async def test_get_home_data_v2():
2626
ud = await api.code_login(4123)
2727
hd = await api.get_home_data_v2(ud)
2828
assert hd == HomeData.from_dict(HOME_DATA_RAW)
29+
30+
31+
async def test_nc_prepare():
32+
"""Test adding a device and that nothing breaks"""
33+
api = RoborockApiClient(username="[email protected]")
34+
await api.request_code()
35+
ud = await api.code_login(4123)
36+
prepare = await api.nc_prepare(ud, "America/New_York")
37+
new_device = await api.add_device(ud, prepare["s"], prepare["t"])
38+
assert new_device["duid"] == "rand_duid"

0 commit comments

Comments
 (0)