|
6 | 6 | import logging |
7 | 7 | import math |
8 | 8 | import secrets |
| 9 | +import string |
9 | 10 | import time |
10 | 11 |
|
11 | 12 | import aiohttp |
@@ -190,6 +191,113 @@ async def request_code(self) -> None: |
190 | 191 | else: |
191 | 192 | raise RoborockException(f"{code_response.get('msg')} - response code: {code_response.get('code')}") |
192 | 193 |
|
| 194 | + async def request_code_v4(self) -> None: |
| 195 | + """Request a code using the v4 endpoint.""" |
| 196 | + try: |
| 197 | + self._login_limiter.try_acquire("login") |
| 198 | + except BucketFullException as ex: |
| 199 | + _LOGGER.info(ex.meta_info) |
| 200 | + raise RoborockRateLimit("Reached maximum requests for login. Please try again later.") from ex |
| 201 | + base_url = await self._get_base_url() |
| 202 | + header_clientid = self._get_header_client_id() |
| 203 | + code_request = PreparedRequest( |
| 204 | + base_url, |
| 205 | + self.session, |
| 206 | + { |
| 207 | + "header_clientid": header_clientid, |
| 208 | + "Content-Type": "application/x-www-form-urlencoded", |
| 209 | + "header_clientlang": "en", |
| 210 | + }, |
| 211 | + ) |
| 212 | + |
| 213 | + code_response = await code_request.request( |
| 214 | + "post", |
| 215 | + "/api/v4/email/code/send", |
| 216 | + params={"email": self._username, "type": "login", "platform": ""}, |
| 217 | + ) |
| 218 | + if code_response is None: |
| 219 | + raise RoborockException("Failed to get a response from send email code") |
| 220 | + response_code = code_response.get("code") |
| 221 | + if response_code != 200: |
| 222 | + _LOGGER.info("Request code failed for %s with the following context: %s", self._username, code_response) |
| 223 | + if response_code == 2008: |
| 224 | + raise RoborockAccountDoesNotExist("Account does not exist - check your login and try again.") |
| 225 | + elif response_code == 9002: |
| 226 | + raise RoborockTooFrequentCodeRequests("You have attempted to request too many codes. Try again later") |
| 227 | + else: |
| 228 | + raise RoborockException(f"{code_response.get('msg')} - response code: {code_response.get('code')}") |
| 229 | + |
| 230 | + async def sign_key_v3(self, s: str) -> str: |
| 231 | + """Sign a randomly generated string.""" |
| 232 | + base_url = await self._get_base_url() |
| 233 | + header_clientid = self._get_header_client_id() |
| 234 | + code_request = PreparedRequest(base_url, self.session, {"header_clientid": header_clientid}) |
| 235 | + |
| 236 | + code_response = await code_request.request( |
| 237 | + "post", |
| 238 | + "/api/v3/key/sign", |
| 239 | + params={"s": s}, |
| 240 | + ) |
| 241 | + |
| 242 | + if not code_response or "data" not in code_response or "k" not in code_response["data"]: |
| 243 | + raise RoborockException("Failed to get a response from sign key") |
| 244 | + response_code = code_response.get("code") |
| 245 | + |
| 246 | + if response_code != 200: |
| 247 | + _LOGGER.info("Request code failed for %s with the following context: %s", self._username, code_response) |
| 248 | + raise RoborockException(f"{code_response.get('msg')} - response code: {code_response.get('code')}") |
| 249 | + |
| 250 | + return code_response["data"]["k"] |
| 251 | + |
| 252 | + async def code_login_v4(self, code: int | str, country: str, country_code: int) -> UserData: |
| 253 | + """ |
| 254 | + Login via code authentication. |
| 255 | + :param code: The code from the email. |
| 256 | + :param country: The two-character representation of the country, i.e. "US" |
| 257 | + :param country_code: the country phone number code i.e. 1 for US. |
| 258 | + """ |
| 259 | + base_url = await self._get_base_url() |
| 260 | + header_clientid = self._get_header_client_id() |
| 261 | + x_mercy_ks = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(16)) |
| 262 | + x_mercy_k = await self.sign_key_v3(x_mercy_ks) |
| 263 | + login_request = PreparedRequest( |
| 264 | + base_url, |
| 265 | + self.session, |
| 266 | + {"header_clientid": header_clientid, "x-mercy-ks": x_mercy_ks, "x-mercy-k": x_mercy_k}, |
| 267 | + ) |
| 268 | + login_response = await login_request.request( |
| 269 | + "post", |
| 270 | + "/api/v4/auth/email/login/code", |
| 271 | + params={ |
| 272 | + "country": country, |
| 273 | + "countryCode": country_code, |
| 274 | + "email": self._username, |
| 275 | + "code": code, |
| 276 | + # Major and minor version are the user agreement version, we will need to see if this needs to be |
| 277 | + # dynamic https://usiot.roborock.com/api/v3/app/agreement/latest?country=US |
| 278 | + "majorVersion": 14, |
| 279 | + "minorVersion": 0, |
| 280 | + }, |
| 281 | + ) |
| 282 | + if login_response is None: |
| 283 | + raise RoborockException("Login request response is None") |
| 284 | + response_code = login_response.get("code") |
| 285 | + if response_code != 200: |
| 286 | + _LOGGER.info("Login failed for %s with the following context: %s", self._username, login_response) |
| 287 | + if response_code == 2018: |
| 288 | + raise RoborockInvalidCode("Invalid code - check your code and try again.") |
| 289 | + if response_code == 3009: |
| 290 | + raise RoborockNoUserAgreement("You must accept the user agreement in the Roborock app to continue.") |
| 291 | + if response_code == 3006: |
| 292 | + raise RoborockInvalidUserAgreement( |
| 293 | + "User agreement must be accepted again - or you are attempting to use the Mi Home app account." |
| 294 | + ) |
| 295 | + raise RoborockException(f"{login_response.get('msg')} - response code: {response_code}") |
| 296 | + user_data = login_response.get("data") |
| 297 | + if not isinstance(user_data, dict): |
| 298 | + raise RoborockException("Got unexpected data type for user_data") |
| 299 | + return UserData.from_dict(user_data) |
| 300 | + |
193 | 301 | async def pass_login(self, password: str) -> UserData: |
194 | 302 | try: |
195 | 303 | self._login_limiter.try_acquire("login") |
|
0 commit comments