Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions roborock/web_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class RoborockApiClient:
Rate(40, Duration.DAY),
]

_login_limiter = Limiter(_LOGIN_RATES)
_login_limiter = Limiter(_LOGIN_RATES, max_delay=1000)
_home_data_limiter = Limiter(_HOME_DATA_RATES)

def __init__(
Expand All @@ -74,11 +74,11 @@ def __init__(
self._device_identifier = secrets.token_urlsafe(16)
self.session = session
self._iot_login_info: IotLoginInfo | None = None
self._base_urls = BASE_URLS if base_url is None else [base_url]

async def _get_iot_login_info(self) -> IotLoginInfo:
if self._iot_login_info is None:
valid_urls = BASE_URLS if self._base_url is None else [self._base_url]
for iot_url in valid_urls:
for iot_url in self._base_urls:
url_request = PreparedRequest(iot_url, self.session)
response = await url_request.request(
"post",
Expand Down Expand Up @@ -205,7 +205,7 @@ async def add_device(self, user_data: UserData, s: str, t: str) -> dict:

async def request_code(self) -> None:
try:
self._login_limiter.try_acquire("login")
await self._login_limiter.try_acquire_async("login")
except BucketFullException as ex:
_LOGGER.info(ex.meta_info)
raise RoborockRateLimit("Reached maximum requests for login. Please try again later.") from ex
Expand Down Expand Up @@ -239,7 +239,7 @@ async def request_code_v4(self) -> None:
_LOGGER.info("No country code or country found, trying old version of request code.")
return await self.request_code()
try:
self._login_limiter.try_acquire("login")
await self._login_limiter.try_acquire_async("login")
except BucketFullException as ex:
_LOGGER.info(ex.meta_info)
raise RoborockRateLimit("Reached maximum requests for login. Please try again later.") from ex
Expand Down Expand Up @@ -269,6 +269,10 @@ async def request_code_v4(self) -> None:
raise RoborockAccountDoesNotExist("Account does not exist - check your login and try again.")
elif response_code == 9002:
raise RoborockTooFrequentCodeRequests("You have attempted to request too many codes. Try again later")
elif response_code == 3030 and len(self._base_urls) > 1:
self._base_urls = self._base_urls[1:]
self._iot_login_info = None
return await self.request_code_v4()
else:
raise RoborockException(f"{code_response.get('msg')} - response code: {code_response.get('code')}")

Expand Down Expand Up @@ -363,7 +367,7 @@ async def code_login_v4(

async def pass_login(self, password: str) -> UserData:
try:
self._login_limiter.try_acquire("login")
await self._login_limiter.try_acquire_async("login")
except BucketFullException as ex:
_LOGGER.info(ex.meta_info)
raise RoborockRateLimit("Reached maximum requests for login. Please try again later.") from ex
Expand Down
76 changes: 75 additions & 1 deletion tests/test_web_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ async def test_url_cycling(mock_rest) -> None:
"""Test that we cycle through the URLs correctly."""
# Clear mock rest so that we can override the patches.
mock_rest.clear()
# 1. Mock US URL to return valid status but None for countrycode

# 1. Mock US URL to return valid status but None for countrycode
mock_rest.post(
re.compile("https://usiot.roborock.com/api/v1/getUrlByEmail.*"),
status=200,
Expand Down Expand Up @@ -184,6 +184,80 @@ async def test_url_cycling(mock_rest) -> None:
assert len(mock_rest.requests) == 3


async def test_thirty_thirty_cycling(mock_rest) -> None:
"""Test that we cycle through the URLs correctly when users have deleted accounts in higher prio regions."""
# Clear mock rest so that we can override the patches.
mock_rest.clear()

mock_rest.post(
re.compile("https://usiot.roborock.com/api/v1/getUrlByEmail.*"),
status=200,
payload={
"code": 200,
"data": {"url": "https://usiot.roborock.com", "country": "US", "countrycode": 1},
"msg": "Account in deletion",
},
)

mock_rest.post(
re.compile("https://euiot.roborock.com/api/v1/getUrlByEmail.*"),
status=200,
payload={
"code": 200,
"data": {"url": "https://euiot.roborock.com", "country": "EU", "countrycode": 49},
"msg": "Success",
},
)

mock_rest.post(
re.compile("https://usiot.roborock.com/api/v4/email/code/send.*"),
status=200,
payload={
"code": 3030,
},
)
mock_rest.post(
re.compile("https://euiot.roborock.com/api/v4/email/code/send.*"),
status=200,
payload={
"code": 200,
},
)

mock_rest.post(re.compile("https://ruiot.roborock.com/api/v1/getUrlByEmail.*"), status=500)
mock_rest.post(re.compile("https://cniot.roborock.com/api/v1/getUrlByEmail.*"), status=500)

client = RoborockApiClient("test@example.com")
await client.request_code_v4()

print(mock_rest.requests)
Copy link

Copilot AI Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug print statement should be removed before merging. This appears to be leftover from development/debugging.

Suggested change
print(mock_rest.requests)

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep please remove this print!

assert (
len(
mock_rest.requests[
(
"post",
normalize_url("https://euiot.roborock.com/api/v4/email/code/send"),
)
]
)
== 1
)
assert (
len(
mock_rest.requests[
(
"post",
normalize_url("https://usiot.roborock.com/api/v4/email/code/send"),
)
]
)
== 1
)
# Assert that we didn't try on the Russian or Chinese regions
assert "https://ruiot.roborock.com/api/v4/email/code/send" not in mock_rest.requests
assert "https://cniot.roborock.com/api/v4/email/code/send" not in mock_rest.requests


async def test_missing_country_login(mock_rest) -> None:
"""Test that we cycle through the URLs correctly."""
mock_rest.clear()
Expand Down
Loading