Skip to content

Commit 9515ab5

Browse files
authored
Merge branch 'main' into scene
2 parents 1574803 + 430c248 commit 9515ab5

File tree

3 files changed

+151
-5
lines changed

3 files changed

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

tests/test_web_api.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,13 @@ async def test_get_scenes():
4141
}
4242
)
4343
]
44+
45+
46+
async def test_nc_prepare():
47+
"""Test adding a device and that nothing breaks"""
48+
api = RoborockApiClient(username="[email protected]")
49+
await api.request_code()
50+
ud = await api.code_login(4123)
51+
prepare = await api.nc_prepare(ud, "America/New_York")
52+
new_device = await api.add_device(ud, prepare["s"], prepare["t"])
53+
assert new_device["duid"] == "rand_duid"

0 commit comments

Comments
 (0)