88import secrets
99import string
1010import time
11+ from dataclasses import dataclass
1112
1213import aiohttp
1314from aiohttp import ContentTypeError , FormData
2223 RoborockInvalidEmail ,
2324 RoborockInvalidUserAgreement ,
2425 RoborockMissingParameters ,
26+ RoborockNoResponseFromBaseURL ,
2527 RoborockNoUserAgreement ,
2628 RoborockRateLimit ,
2729 RoborockTooFrequentCodeRequests ,
28- RoborockTooManyRequest ,
29- RoborockUrlException ,
3030)
3131
3232_LOGGER = logging .getLogger (__name__ )
33+ BASE_URLS = [
34+ "https://usiot.roborock.com" ,
35+ "https://euiot.roborock.com" ,
36+ "https://cniot.roborock.com" ,
37+ "https://ruiot.roborock.com" ,
38+ ]
39+
40+
41+ @dataclass
42+ class IotLoginInfo :
43+ """Information about the login to the iot server."""
44+
45+ base_url : str
46+ country_code : str
47+ country : str
3348
3449
3550class RoborockApiClient :
@@ -49,41 +64,64 @@ class RoborockApiClient:
4964 _login_limiter = Limiter (_LOGIN_RATES )
5065 _home_data_limiter = Limiter (_HOME_DATA_RATES )
5166
52- def __init__ (self , username : str , base_url = None , session : aiohttp .ClientSession | None = None ) -> None :
67+ def __init__ (
68+ self , username : str , base_url : str | None = None , session : aiohttp .ClientSession | None = None
69+ ) -> None :
5370 """Sample API Client."""
5471 self ._username = username
55- self ._default_url = "https://euiot.roborock.com"
56- self .base_url = base_url
72+ self ._base_url = base_url
5773 self ._device_identifier = secrets .token_urlsafe (16 )
5874 self .session = session
59-
60- async def _get_base_url (self ) -> str :
61- if not self .base_url :
62- url_request = PreparedRequest (self ._default_url , self .session )
63- response = await url_request .request (
64- "post" ,
65- "/api/v1/getUrlByEmail" ,
66- params = {"email" : self ._username , "needtwostepauth" : "false" },
67- )
68- if response is None :
69- raise RoborockUrlException ("get url by email returned None" )
70- response_code = response .get ("code" )
71- if response_code != 200 :
72- _LOGGER .info ("Get base url failed for %s with the following context: %s" , self ._username , response )
73- if response_code == 2003 :
74- raise RoborockInvalidEmail ("Your email was incorrectly formatted." )
75- elif response_code == 1001 :
76- raise RoborockMissingParameters (
77- "You are missing parameters for this request, are you sure you entered your username?"
75+ self ._iot_login_info : IotLoginInfo | None = None
76+
77+ async def _get_iot_login_info (self ) -> IotLoginInfo :
78+ if self ._iot_login_info is None :
79+ valid_urls = BASE_URLS if self ._base_url is None else [self ._base_url ]
80+ for iot_url in valid_urls :
81+ url_request = PreparedRequest (iot_url , self .session )
82+ response = await url_request .request (
83+ "post" ,
84+ "/api/v1/getUrlByEmail" ,
85+ params = {"email" : self ._username , "needtwostepauth" : "false" },
86+ )
87+ if response is None :
88+ continue
89+ response_code = response .get ("code" )
90+ if response_code != 200 :
91+ if response_code == 2003 :
92+ raise RoborockInvalidEmail ("Your email was incorrectly formatted." )
93+ elif response_code == 1001 :
94+ raise RoborockMissingParameters (
95+ "You are missing parameters for this request, are you sure you entered your username?"
96+ )
97+ else :
98+ raise RoborockException (f"{ response .get ('msg' )} - response code: { response_code } " )
99+ if response ["data" ]["countrycode" ] is not None :
100+ self ._iot_login_info = IotLoginInfo (
101+ base_url = response ["data" ]["url" ],
102+ country = response ["data" ]["country" ],
103+ country_code = response ["data" ]["countrycode" ],
78104 )
79- elif response_code == 9002 :
80- raise RoborockTooManyRequest ("Please temporarily disable making requests and try again later." )
81- raise RoborockUrlException (f"error code: { response_code } msg: { response .get ('error' )} " )
82- response_data = response .get ("data" )
83- if response_data is None :
84- raise RoborockUrlException ("response does not have 'data'" )
85- self .base_url = response_data .get ("url" )
86- return self .base_url
105+ return self ._iot_login_info
106+ raise RoborockNoResponseFromBaseURL (
107+ "No account was found for any base url we tried. Either your email is incorrect or we do not have a"
108+ " record of the roborock server your device is on."
109+ )
110+ return self ._iot_login_info
111+
112+ @property
113+ async def base_url (self ):
114+ if self ._base_url is not None :
115+ return self ._base_url
116+ return (await self ._get_iot_login_info ()).base_url
117+
118+ @property
119+ async def country (self ):
120+ return (await self ._get_iot_login_info ()).country
121+
122+ @property
123+ async def country_code (self ):
124+ return (await self ._get_iot_login_info ()).country_code
87125
88126 def _get_header_client_id (self ):
89127 md5 = hashlib .md5 ()
@@ -167,7 +205,7 @@ async def request_code(self) -> None:
167205 except BucketFullException as ex :
168206 _LOGGER .info (ex .meta_info )
169207 raise RoborockRateLimit ("Reached maximum requests for login. Please try again later." ) from ex
170- base_url = await self ._get_base_url ()
208+ base_url = await self .base_url
171209 header_clientid = self ._get_header_client_id ()
172210 code_request = PreparedRequest (base_url , self .session , {"header_clientid" : header_clientid })
173211
@@ -198,7 +236,7 @@ async def request_code_v4(self) -> None:
198236 except BucketFullException as ex :
199237 _LOGGER .info (ex .meta_info )
200238 raise RoborockRateLimit ("Reached maximum requests for login. Please try again later." ) from ex
201- base_url = await self ._get_base_url ()
239+ base_url = await self .base_url
202240 header_clientid = self ._get_header_client_id ()
203241 code_request = PreparedRequest (
204242 base_url ,
@@ -229,7 +267,7 @@ async def request_code_v4(self) -> None:
229267
230268 async def _sign_key_v3 (self , s : str ) -> str :
231269 """Sign a randomly generated string."""
232- base_url = await self ._get_base_url ()
270+ base_url = await self .base_url
233271 header_clientid = self ._get_header_client_id ()
234272 code_request = PreparedRequest (base_url , self .session , {"header_clientid" : header_clientid })
235273
@@ -249,14 +287,20 @@ async def _sign_key_v3(self, s: str) -> str:
249287
250288 return code_response ["data" ]["k" ]
251289
252- async def code_login_v4 (self , code : int | str , country : str , country_code : int ) -> UserData :
290+ async def code_login_v4 (
291+ self , code : int | str , country : str | None = None , country_code : int | None = None
292+ ) -> UserData :
253293 """
254294 Login via code authentication.
255295 :param code: The code from the email.
256296 :param country: The two-character representation of the country, i.e. "US"
257297 :param country_code: the country phone number code i.e. 1 for US.
258298 """
259- base_url = await self ._get_base_url ()
299+ base_url = await self .base_url
300+ if country is None :
301+ country = await self .country
302+ if country_code is None :
303+ country_code = await self .country_code
260304 header_clientid = self ._get_header_client_id ()
261305 x_mercy_ks = "" .join (secrets .choice (string .ascii_letters + string .digits ) for _ in range (16 ))
262306 x_mercy_k = await self ._sign_key_v3 (x_mercy_ks )
@@ -304,7 +348,7 @@ async def pass_login(self, password: str) -> UserData:
304348 except BucketFullException as ex :
305349 _LOGGER .info (ex .meta_info )
306350 raise RoborockRateLimit ("Reached maximum requests for login. Please try again later." ) from ex
307- base_url = await self ._get_base_url ()
351+ base_url = await self .base_url
308352 header_clientid = self ._get_header_client_id ()
309353
310354 login_request = PreparedRequest (base_url , self .session , {"header_clientid" : header_clientid })
@@ -343,7 +387,7 @@ async def pass_login_v3(self, password: str) -> UserData:
343387 raise NotImplementedError ("Pass_login_v3 has not yet been implemented" )
344388
345389 async def code_login (self , code : int | str ) -> UserData :
346- base_url = await self ._get_base_url ()
390+ base_url = await self .base_url
347391 header_clientid = self ._get_header_client_id ()
348392
349393 login_request = PreparedRequest (base_url , self .session , {"header_clientid" : header_clientid })
@@ -376,7 +420,7 @@ async def code_login(self, code: int | str) -> UserData:
376420 return UserData .from_dict (user_data )
377421
378422 async def _get_home_id (self , user_data : UserData ):
379- base_url = await self ._get_base_url ()
423+ base_url = await self .base_url
380424 header_clientid = self ._get_header_client_id ()
381425 home_id_request = PreparedRequest (base_url , self .session , {"header_clientid" : header_clientid })
382426 home_id_response = await home_id_request .request (
@@ -547,7 +591,7 @@ async def execute_scene(self, user_data: UserData, scene_id: int) -> None:
547591
548592 async def get_products (self , user_data : UserData ) -> ProductResponse :
549593 """Gets all products and their schemas, good for determining status codes and model numbers."""
550- base_url = await self ._get_base_url ()
594+ base_url = await self .base_url
551595 header_clientid = self ._get_header_client_id ()
552596 product_request = PreparedRequest (base_url , self .session , {"header_clientid" : header_clientid })
553597 product_response = await product_request .request (
@@ -565,7 +609,7 @@ async def get_products(self, user_data: UserData) -> ProductResponse:
565609 raise RoborockException ("product result was an unexpected type" )
566610
567611 async def download_code (self , user_data : UserData , product_id : int ):
568- base_url = await self ._get_base_url ()
612+ base_url = await self .base_url
569613 header_clientid = self ._get_header_client_id ()
570614 product_request = PreparedRequest (base_url , self .session , {"header_clientid" : header_clientid })
571615 request = {"apilevel" : 99999 , "productids" : [product_id ], "type" : 2 }
@@ -578,7 +622,7 @@ async def download_code(self, user_data: UserData, product_id: int):
578622 return response ["data" ][0 ]["url" ]
579623
580624 async def download_category_code (self , user_data : UserData ):
581- base_url = await self ._get_base_url ()
625+ base_url = await self .base_url
582626 header_clientid = self ._get_header_client_id ()
583627 product_request = PreparedRequest (base_url , self .session , {"header_clientid" : header_clientid })
584628 response = await product_request .request (
0 commit comments