From e8c2fb6ae8e8964ef6eb7f8b0d1f3064ef681881 Mon Sep 17 00:00:00 2001 From: Luke Date: Sun, 21 Sep 2025 14:33:31 -0400 Subject: [PATCH 1/6] fix: cycle through iot urls --- roborock/exceptions.py | 4 +++ roborock/web_api.py | 71 ++++++++++++++++++++++++++---------------- 2 files changed, 48 insertions(+), 27 deletions(-) diff --git a/roborock/exceptions.py b/roborock/exceptions.py index b3e4bd41..0861fb85 100644 --- a/roborock/exceptions.py +++ b/roborock/exceptions.py @@ -77,3 +77,7 @@ class RoborockTooManyRequest(RoborockException): class RoborockRateLimit(RoborockException): """Class for our rate limits exceptions.""" + + +class RoborockNoResponseFromBaseURL(RoborockException): + """We could not find an url that had a record of the given account.""" diff --git a/roborock/web_api.py b/roborock/web_api.py index 0bd231b9..3f6ef009 100644 --- a/roborock/web_api.py +++ b/roborock/web_api.py @@ -22,11 +22,10 @@ RoborockInvalidEmail, RoborockInvalidUserAgreement, RoborockMissingParameters, + RoborockNoResponseFromBaseURL, RoborockNoUserAgreement, RoborockRateLimit, RoborockTooFrequentCodeRequests, - RoborockTooManyRequest, - RoborockUrlException, ) _LOGGER = logging.getLogger(__name__) @@ -52,37 +51,49 @@ class RoborockApiClient: def __init__(self, username: str, base_url=None, session: aiohttp.ClientSession | None = None) -> None: """Sample API Client.""" self._username = username - self._default_url = "https://euiot.roborock.com" self.base_url = base_url self._device_identifier = secrets.token_urlsafe(16) self.session = session + self._country = None + self._country_code = None async def _get_base_url(self) -> str: if not self.base_url: - url_request = PreparedRequest(self._default_url, self.session) - response = await url_request.request( - "post", - "/api/v1/getUrlByEmail", - params={"email": self._username, "needtwostepauth": "false"}, + for iot_url in [ + "https://usiot.roborock.com", + "https://euiot.roborock.com", + "https://cniot.roborock.com", + "https://ruiot.roborock.com", + ]: + url_request = PreparedRequest(iot_url, self.session) + response = await url_request.request( + "post", + "/api/v1/getUrlByEmail", + params={"email": self._username, "needtwostepauth": "false"}, + ) + if response is None: + continue + response_code = response.get("code") + if response_code != 200: + if response_code == 2003: + raise RoborockInvalidEmail("Your email was incorrectly formatted.") + elif response_code == 1001: + raise RoborockMissingParameters( + "You are missing parameters for this request, are you sure you entered your username?" + ) + else: + _LOGGER.warning( + "Failed to get base url for %s with the following context: %s", self._username, response + ) + if response["data"]["countrycode"] is not None: + self._country_code = response["data"]["countrycode"] + self._country = response["data"]["country"] + self.base_url = response["data"]["url"] + return self.base_url + raise RoborockNoResponseFromBaseURL( + "No account was found for any base url we tried. Either your email is incorrect or we do not have a" + " record of the roborock server your device is on." ) - if response is None: - raise RoborockUrlException("get url by email returned None") - response_code = response.get("code") - if response_code != 200: - _LOGGER.info("Get base url failed for %s with the following context: %s", self._username, response) - if response_code == 2003: - raise RoborockInvalidEmail("Your email was incorrectly formatted.") - elif response_code == 1001: - raise RoborockMissingParameters( - "You are missing parameters for this request, are you sure you entered your username?" - ) - elif response_code == 9002: - raise RoborockTooManyRequest("Please temporarily disable making requests and try again later.") - raise RoborockUrlException(f"error code: {response_code} msg: {response.get('error')}") - response_data = response.get("data") - if response_data is None: - raise RoborockUrlException("response does not have 'data'") - self.base_url = response_data.get("url") return self.base_url def _get_header_client_id(self): @@ -249,7 +260,9 @@ async def _sign_key_v3(self, s: str) -> str: return code_response["data"]["k"] - async def code_login_v4(self, code: int | str, country: str, country_code: int) -> UserData: + async def code_login_v4( + self, code: int | str, country: str | None = None, country_code: int | None = None + ) -> UserData: """ Login via code authentication. :param code: The code from the email. @@ -257,6 +270,10 @@ async def code_login_v4(self, code: int | str, country: str, country_code: int) :param country_code: the country phone number code i.e. 1 for US. """ base_url = await self._get_base_url() + if country is None: + country = self._country + if country_code is None: + country_code = self._country_code header_clientid = self._get_header_client_id() x_mercy_ks = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(16)) x_mercy_k = await self._sign_key_v3(x_mercy_ks) From 9c1ad7cb0ae686e1a3d1484a5ba8862e2c022632 Mon Sep 17 00:00:00 2001 From: Luke Date: Sun, 21 Sep 2025 14:46:32 -0400 Subject: [PATCH 2/6] fix: tests --- tests/conftest.py | 2 +- tests/mock_data.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index a38d429f..6ca1e775 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -173,7 +173,7 @@ def mock_rest() -> aioresponses: with aioresponses() as mocked: # Match the base URL and allow any query params mocked.post( - re.compile(r"https://euiot\.roborock\.com/api/v1/getUrlByEmail.*"), + re.compile(r"https://.*iot\.roborock\.com/api/v1/getUrlByEmail.*"), status=200, payload={ "code": 200, diff --git a/tests/mock_data.py b/tests/mock_data.py index 98cd816e..e779d780 100644 --- a/tests/mock_data.py +++ b/tests/mock_data.py @@ -766,7 +766,7 @@ BASE_URL_REQUEST = { "code": 200, "msg": "success", - "data": {"url": "https://sample.com"}, + "data": {"url": "https://sample.com", "countrycode": 1, "country": "US"}, } GET_CODE_RESPONSE = {"code": 200, "msg": "success", "data": None} From a444eae568e3351cd9eab55d3572c2a7de72154b Mon Sep 17 00:00:00 2001 From: Luke Date: Sun, 21 Sep 2025 15:24:21 -0400 Subject: [PATCH 3/6] chore: convert to store all iot login info together --- roborock/web_api.py | 92 ++++++++++++++++++++++++++++++--------------- 1 file changed, 62 insertions(+), 30 deletions(-) diff --git a/roborock/web_api.py b/roborock/web_api.py index 3f6ef009..b45526f0 100644 --- a/roborock/web_api.py +++ b/roborock/web_api.py @@ -8,6 +8,7 @@ import secrets import string import time +from dataclasses import dataclass import aiohttp from aiohttp import ContentTypeError, FormData @@ -31,6 +32,15 @@ _LOGGER = logging.getLogger(__name__) +@dataclass +class IotLoginInfo: + """Information about the login to the iot server.""" + + base_url: str + country_code: str + country: str + + class RoborockApiClient: _LOGIN_RATES = [ Rate(1, Duration.SECOND), @@ -48,23 +58,29 @@ class RoborockApiClient: _login_limiter = Limiter(_LOGIN_RATES) _home_data_limiter = Limiter(_HOME_DATA_RATES) - def __init__(self, username: str, base_url=None, session: aiohttp.ClientSession | None = None) -> None: + def __init__( + self, username: str, base_url: str | None = None, session: aiohttp.ClientSession | None = None + ) -> None: """Sample API Client.""" self._username = username - self.base_url = base_url + self._base_url = base_url self._device_identifier = secrets.token_urlsafe(16) self.session = session - self._country = None - self._country_code = None - - async def _get_base_url(self) -> str: - if not self.base_url: - for iot_url in [ - "https://usiot.roborock.com", - "https://euiot.roborock.com", - "https://cniot.roborock.com", - "https://ruiot.roborock.com", - ]: + self._iot_login_info: IotLoginInfo | None = None + + async def _get_iot_login_info(self) -> IotLoginInfo: + if self._iot_login_info is None: + valid_urls = ( + [ + "https://usiot.roborock.com", + "https://euiot.roborock.com", + "https://cniot.roborock.com", + "https://ruiot.roborock.com", + ] + if self._base_url is None + else [self._base_url] + ) + for iot_url in valid_urls: url_request = PreparedRequest(iot_url, self.session) response = await url_request.request( "post", @@ -86,15 +102,31 @@ async def _get_base_url(self) -> str: "Failed to get base url for %s with the following context: %s", self._username, response ) if response["data"]["countrycode"] is not None: - self._country_code = response["data"]["countrycode"] - self._country = response["data"]["country"] - self.base_url = response["data"]["url"] - return self.base_url + self._iot_login_info = IotLoginInfo( + base_url=response["data"]["url"], + country=response["data"]["country"], + country_code=response["data"]["countrycode"], + ) + return self._iot_login_info raise RoborockNoResponseFromBaseURL( "No account was found for any base url we tried. Either your email is incorrect or we do not have a" " record of the roborock server your device is on." ) - return self.base_url + return self._iot_login_info + + @property + async def base_url(self): + if self._base_url is not None: + return self._base_url + return (await self._get_iot_login_info()).base_url + + @property + async def country(self): + return (await self._get_iot_login_info()).country + + @property + async def country_code(self): + return (await self._get_iot_login_info()).country_code def _get_header_client_id(self): md5 = hashlib.md5() @@ -178,7 +210,7 @@ async def request_code(self) -> None: except BucketFullException as ex: _LOGGER.info(ex.meta_info) raise RoborockRateLimit("Reached maximum requests for login. Please try again later.") from ex - base_url = await self._get_base_url() + base_url = await self.base_url header_clientid = self._get_header_client_id() code_request = PreparedRequest(base_url, self.session, {"header_clientid": header_clientid}) @@ -209,7 +241,7 @@ async def request_code_v4(self) -> None: except BucketFullException as ex: _LOGGER.info(ex.meta_info) raise RoborockRateLimit("Reached maximum requests for login. Please try again later.") from ex - base_url = await self._get_base_url() + base_url = await self.base_url header_clientid = self._get_header_client_id() code_request = PreparedRequest( base_url, @@ -240,7 +272,7 @@ async def request_code_v4(self) -> None: async def _sign_key_v3(self, s: str) -> str: """Sign a randomly generated string.""" - base_url = await self._get_base_url() + base_url = await self.base_url header_clientid = self._get_header_client_id() code_request = PreparedRequest(base_url, self.session, {"header_clientid": header_clientid}) @@ -269,11 +301,11 @@ async def code_login_v4( :param country: The two-character representation of the country, i.e. "US" :param country_code: the country phone number code i.e. 1 for US. """ - base_url = await self._get_base_url() + base_url = await self.base_url if country is None: - country = self._country + country = await self.country if country_code is None: - country_code = self._country_code + country_code = await self.country_code header_clientid = self._get_header_client_id() x_mercy_ks = "".join(secrets.choice(string.ascii_letters + string.digits) for _ in range(16)) x_mercy_k = await self._sign_key_v3(x_mercy_ks) @@ -321,7 +353,7 @@ async def pass_login(self, password: str) -> UserData: except BucketFullException as ex: _LOGGER.info(ex.meta_info) raise RoborockRateLimit("Reached maximum requests for login. Please try again later.") from ex - base_url = await self._get_base_url() + base_url = await self.base_url header_clientid = self._get_header_client_id() login_request = PreparedRequest(base_url, self.session, {"header_clientid": header_clientid}) @@ -360,7 +392,7 @@ async def pass_login_v3(self, password: str) -> UserData: raise NotImplementedError("Pass_login_v3 has not yet been implemented") async def code_login(self, code: int | str) -> UserData: - base_url = await self._get_base_url() + base_url = await self.base_url header_clientid = self._get_header_client_id() login_request = PreparedRequest(base_url, self.session, {"header_clientid": header_clientid}) @@ -393,7 +425,7 @@ async def code_login(self, code: int | str) -> UserData: return UserData.from_dict(user_data) async def _get_home_id(self, user_data: UserData): - base_url = await self._get_base_url() + base_url = await self.base_url header_clientid = self._get_header_client_id() home_id_request = PreparedRequest(base_url, self.session, {"header_clientid": header_clientid}) home_id_response = await home_id_request.request( @@ -564,7 +596,7 @@ async def execute_scene(self, user_data: UserData, scene_id: int) -> None: async def get_products(self, user_data: UserData) -> ProductResponse: """Gets all products and their schemas, good for determining status codes and model numbers.""" - base_url = await self._get_base_url() + base_url = await self.base_url header_clientid = self._get_header_client_id() product_request = PreparedRequest(base_url, self.session, {"header_clientid": header_clientid}) product_response = await product_request.request( @@ -582,7 +614,7 @@ async def get_products(self, user_data: UserData) -> ProductResponse: raise RoborockException("product result was an unexpected type") async def download_code(self, user_data: UserData, product_id: int): - base_url = await self._get_base_url() + base_url = await self.base_url header_clientid = self._get_header_client_id() product_request = PreparedRequest(base_url, self.session, {"header_clientid": header_clientid}) request = {"apilevel": 99999, "productids": [product_id], "type": 2} @@ -595,7 +627,7 @@ async def download_code(self, user_data: UserData, product_id: int): return response["data"][0]["url"] async def download_category_code(self, user_data: UserData): - base_url = await self._get_base_url() + base_url = await self.base_url header_clientid = self._get_header_client_id() product_request = PreparedRequest(base_url, self.session, {"header_clientid": header_clientid}) response = await product_request.request( From e12088daff3487ae5249aa432a956f9e573e943e Mon Sep 17 00:00:00 2001 From: Luke Date: Sun, 21 Sep 2025 15:29:45 -0400 Subject: [PATCH 4/6] fix: tests --- tests/test_api.py | 49 ++++++++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index a4771a02..3d8ea47a 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -49,24 +49,28 @@ async def test_get_base_url_no_url(): rc = RoborockApiClient("sample@gmail.com") with patch("roborock.web_api.PreparedRequest.request") as mock_request: mock_request.return_value = BASE_URL_REQUEST - await rc._get_base_url() - assert rc.base_url == "https://sample.com" + await rc._get_iot_login_info() + assert await rc.base_url == "https://sample.com" async def test_request_code(): rc = RoborockApiClient("sample@gmail.com") - with patch("roborock.web_api.RoborockApiClient._get_base_url"), patch( - "roborock.web_api.RoborockApiClient._get_header_client_id" - ), patch("roborock.web_api.PreparedRequest.request") as mock_request: + with ( + patch("roborock.web_api.RoborockApiClient._get_iot_login_info"), + patch("roborock.web_api.RoborockApiClient._get_header_client_id"), + patch("roborock.web_api.PreparedRequest.request") as mock_request, + ): mock_request.return_value = GET_CODE_RESPONSE await rc.request_code() async def test_get_home_data(): rc = RoborockApiClient("sample@gmail.com") - with patch("roborock.web_api.RoborockApiClient._get_base_url"), patch( - "roborock.web_api.RoborockApiClient._get_header_client_id" - ), patch("roborock.web_api.PreparedRequest.request") as mock_prepared_request: + with ( + patch("roborock.web_api.RoborockApiClient._get_iot_login_info"), + patch("roborock.web_api.RoborockApiClient._get_header_client_id"), + patch("roborock.web_api.PreparedRequest.request") as mock_prepared_request, + ): mock_prepared_request.side_effect = [ {"code": 200, "msg": "success", "data": {"rrHomeId": 1}}, {"code": 200, "success": True, "result": HOME_DATA_RAW}, @@ -117,10 +121,11 @@ async def test_get_prop(): home_data = HomeData.from_dict(HOME_DATA_RAW) device_info = DeviceData(device=home_data.devices[0], model=home_data.products[0].model) rmc = RoborockMqttClientV1(UserData.from_dict(USER_DATA), device_info) - with patch("roborock.version_1_apis.roborock_mqtt_client_v1.RoborockMqttClientV1.get_status") as get_status, patch( - "roborock.version_1_apis.roborock_client_v1.RoborockClientV1.send_command" - ), patch("roborock.version_1_apis.roborock_client_v1.AttributeCache.async_value"), patch( - "roborock.version_1_apis.roborock_mqtt_client_v1.RoborockMqttClientV1.get_dust_collection_mode" + with ( + patch("roborock.version_1_apis.roborock_mqtt_client_v1.RoborockMqttClientV1.get_status") as get_status, + patch("roborock.version_1_apis.roborock_client_v1.RoborockClientV1.send_command"), + patch("roborock.version_1_apis.roborock_client_v1.AttributeCache.async_value"), + patch("roborock.version_1_apis.roborock_mqtt_client_v1.RoborockMqttClientV1.get_dust_collection_mode"), ): status = S7MaxVStatus.from_dict(STATUS) status.dock_type = RoborockDockTypeCode.auto_empty_dock_pure @@ -194,8 +199,9 @@ async def test_disconnect_failure(connected_mqtt_client: RoborockMqttClientV1) - assert connected_mqtt_client.is_connected() # Make the MQTT client returns with an error when disconnecting - with patch("roborock.cloud_api.mqtt.Client.disconnect", return_value=mqtt.MQTT_ERR_PROTOCOL), pytest.raises( - RoborockException, match="Failed to disconnect" + with ( + patch("roborock.cloud_api.mqtt.Client.disconnect", return_value=mqtt.MQTT_ERR_PROTOCOL), + pytest.raises(RoborockException, match="Failed to disconnect"), ): await connected_mqtt_client.async_disconnect() @@ -231,8 +237,9 @@ async def test_subscribe_failure( response_queue.put(mqtt_packet.gen_connack(rc=0, flags=2)) - with patch("roborock.cloud_api.mqtt.Client.subscribe", return_value=(mqtt.MQTT_ERR_NO_CONN, None)), pytest.raises( - RoborockException, match="Failed to subscribe" + with ( + patch("roborock.cloud_api.mqtt.Client.subscribe", return_value=(mqtt.MQTT_ERR_NO_CONN, None)), + pytest.raises(RoborockException, match="Failed to subscribe"), ): await mqtt_client.async_connect() @@ -298,8 +305,9 @@ async def test_publish_failure( msg = mqtt.MQTTMessageInfo(0) msg.rc = mqtt.MQTT_ERR_PROTOCOL - with patch("roborock.cloud_api.mqtt.Client.publish", return_value=msg), pytest.raises( - RoborockException, match="Failed to publish" + with ( + patch("roborock.cloud_api.mqtt.Client.publish", return_value=msg), + pytest.raises(RoborockException, match="Failed to publish"), ): await connected_mqtt_client.get_room_mapping() @@ -308,7 +316,8 @@ async def test_future_timeout( connected_mqtt_client: RoborockMqttClientV1, ) -> None: """Test a timeout raised while waiting for an RPC response.""" - with patch("roborock.roborock_future.async_timeout.timeout", side_effect=asyncio.TimeoutError), pytest.raises( - RoborockTimeout, match="Timeout after" + with ( + patch("roborock.roborock_future.async_timeout.timeout", side_effect=asyncio.TimeoutError), + pytest.raises(RoborockTimeout, match="Timeout after"), ): await connected_mqtt_client.get_room_mapping() From d85b4e1fbd73945b904f71781256a6f215cd93e0 Mon Sep 17 00:00:00 2001 From: Luke Date: Wed, 1 Oct 2025 09:56:10 -0400 Subject: [PATCH 5/6] chore: add tests --- roborock/web_api.py | 21 ++++----- tests/test_web_api.py | 101 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 108 insertions(+), 14 deletions(-) diff --git a/roborock/web_api.py b/roborock/web_api.py index b45526f0..f5dbd52e 100644 --- a/roborock/web_api.py +++ b/roborock/web_api.py @@ -30,6 +30,12 @@ ) _LOGGER = logging.getLogger(__name__) +BASE_URLS = [ + "https://usiot.roborock.com", + "https://euiot.roborock.com", + "https://cniot.roborock.com", + "https://ruiot.roborock.com", +] @dataclass @@ -70,16 +76,7 @@ def __init__( async def _get_iot_login_info(self) -> IotLoginInfo: if self._iot_login_info is None: - valid_urls = ( - [ - "https://usiot.roborock.com", - "https://euiot.roborock.com", - "https://cniot.roborock.com", - "https://ruiot.roborock.com", - ] - if self._base_url is None - else [self._base_url] - ) + valid_urls = BASE_URLS if self._base_url is None else [self._base_url] for iot_url in valid_urls: url_request = PreparedRequest(iot_url, self.session) response = await url_request.request( @@ -98,9 +95,7 @@ async def _get_iot_login_info(self) -> IotLoginInfo: "You are missing parameters for this request, are you sure you entered your username?" ) else: - _LOGGER.warning( - "Failed to get base url for %s with the following context: %s", self._username, response - ) + raise RoborockException(f"{response.get('msg')} - response code: {response_code}") if response["data"]["countrycode"] is not None: self._iot_login_info = IotLoginInfo( base_url=response["data"]["url"], diff --git a/tests/test_web_api.py b/tests/test_web_api.py index 1a11f200..a58804c0 100644 --- a/tests/test_web_api.py +++ b/tests/test_web_api.py @@ -1,7 +1,10 @@ +import re + import aiohttp +from aioresponses.compat import normalize_url from roborock import HomeData, HomeDataScene, UserData -from roborock.web_api import RoborockApiClient +from roborock.web_api import IotLoginInfo, RoborockApiClient from tests.mock_data import HOME_DATA_RAW, USER_DATA @@ -71,3 +74,99 @@ async def test_code_login_v4_flow(mock_rest) -> None: await api.request_code_v4() ud = await api.code_login_v4(4123, "US", 1) assert ud == UserData.from_dict(USER_DATA) + + +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 + + mock_rest.post( + re.compile("https://usiot.roborock.com/api/v1/getUrlByEmail.*"), + status=200, + payload={ + "code": 200, + "data": {"url": "https://usiot.roborock.com", "country": None, "countrycode": None}, + "msg": "Success", + }, + ) + + # 2. Mock EU URL to return a server-side error code + mock_rest.post( + re.compile("https://euiot.roborock.com/api/v1/getUrlByEmail.*"), + status=200, + payload={ + "code": 200, + "data": {"url": "https://euiot.roborock.com", "country": None, "countrycode": None}, + "msg": "Success", + }, + ) + + # 3. Mock CN URL to return the correct, valid data + mock_rest.post( + re.compile("https://cniot.roborock.com/api/v1/getUrlByEmail.*"), + status=200, + payload={ + "code": 200, + "data": {"url": "https://cniot.roborock.com", "country": "CN", "countrycode": "86"}, + "msg": "Success", + }, + ) + + # The RU URL should not be called, but we can mock it just in case + # to catch unexpected behavior. + mock_rest.post(re.compile("https://ruiot.roborock.com/api/v1/getUrlByEmail.*"), status=500) + + client = RoborockApiClient("test@example.com") + result = await client._get_iot_login_info() + + assert result is not None + assert isinstance(result, IotLoginInfo) + assert result.base_url == "https://cniot.roborock.com" + assert result.country == "CN" + assert result.country_code == "86" + + assert client._iot_login_info == result + # Check that all three urls were called. We have to do this kind of weirdly as aioresponses seems to have a bug. + assert ( + len( + mock_rest.requests[ + ( + "post", + normalize_url( + "https://usiot.roborock.com/api/v1/getUrlByEmail?email=test%2540example.com&needtwostepauth=false" + ), + ) + ] + ) + == 1 + ) + assert ( + len( + mock_rest.requests[ + ( + "post", + normalize_url( + "https://euiot.roborock.com/api/v1/getUrlByEmail?email=test%2540example.com&needtwostepauth=false" + ), + ) + ] + ) + == 1 + ) + assert ( + len( + mock_rest.requests[ + ( + "post", + normalize_url( + "https://cniot.roborock.com/api/v1/getUrlByEmail?email=test%2540example.com&needtwostepauth=false" + ), + ) + ] + ) + == 1 + ) + # Make sure we just have the three we tested for above. + assert len(mock_rest.requests) == 3 From 94ea367a319c2d0760880e8fda6eea7bc23c5f47 Mon Sep 17 00:00:00 2001 From: Luke Date: Fri, 3 Oct 2025 09:47:30 -0400 Subject: [PATCH 6/6] fix: commenting --- tests/test_web_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_web_api.py b/tests/test_web_api.py index a58804c0..d71a585e 100644 --- a/tests/test_web_api.py +++ b/tests/test_web_api.py @@ -92,7 +92,7 @@ async def test_url_cycling(mock_rest) -> None: }, ) - # 2. Mock EU URL to return a server-side error code + # 2. Mock EU URL to return valid status but None for countrycode mock_rest.post( re.compile("https://euiot.roborock.com/api/v1/getUrlByEmail.*"), status=200,