Skip to content

fix(cloud): use the same login method as the code request in cloud im… - #115

Merged
Lash-L merged 1 commit into
Python-roborock:mainfrom
Jamie932:fix/cloud-import-login-method-mismatch
Sep 28, 2026
Merged

Lash-L merged 1 commit into
Python-roborock:mainfrom
Jamie932:fix/cloud-import-login-method-mismatch

Conversation

@Jamie932

@Jamie932 Jamie932 commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

First thanks a lot for this project - happy to have now be running fully local! Hit a snag whilst implementing it myself though, hence this PR.

The Issue

Cloud import on the admin page ("Send Code" → "Fetch Data") wouldn't accept any code I gave it, just Invalid code - check your code and try again. every time. I'm in the UK on the EU cloud. The response after sending the code also showed "country": "None", as a string, which was the first clue.

Why

The code gets sent one way and the login tries another:

  • The admin page doesn't send a region, so the library looks the account up starting at usiot. For my account usiot comes back with countrycode=44 but no country, and because the lookup stops at the first answer with either, euiot never gets asked.
  • With no country, request_code_v4() quietly falls back to the old v1 endpoint to send the code.
  • request_code() then saved the country as str(None), so the string "None".
  • submit_code() makes a new client pinned to euiot and calls code_login_v4 with country=session_data.country or None. Because the saved value is the text "None" rather than an actual None, the or None never kicks in and the login goes v4. Even with a proper None it'd still end up v4, because euiot does return a country (GB) when you ask it directly.
  • The code was sent with v1 and the login uses v4, so Roborock rejects it with 2018.

HA's own Roborock setup doesn't hit this because it uses the same client for both steps, so it ends up v1/v1. It only breaks here because a second client works the country out again.

The fix

  • Keep country as a proper None instead of the string "None".
  • Remember which method actually sent the code (v1 or v4), using the same rule the library uses.
  • In submit_code(), log in the same way: code_login() for v1, or code_login_v4() with the stored country/country_code for v4. Nothing gets looked up again.

The v4 path works the same as before, apart from using the stored values.

Testing

  • Added tests to tests/test_cloud.py with a faked client, so no network:
    • No country on discovery: records v1, country is None, the login uses code_login, it doesn't look the country up again, and the device identifier carries over.
    • Normal case: v4 both ends with the stored values.
    • Unknown and expired sessions still raise ValueError.
    • Three of these fail on the old cloud.py, so they do catch it.
  • uv run pytest -q passes, all 244.
  • Tested it for real on my account with no region set. It sent the code via v1, logged in via v1 on euiot, and the import worked and found my vacuum. Before this, every code got rejected.
  • I haven't been able to test the v4 path live, since my account doesn't take it. It's covered by the tests and only changes to use the stored values.

…port

RoborockApiClient.request_code_v4() silently falls back to the v1
sendEmailCode endpoint when getUrlByEmail resolves a country_code but
no country (observed on usiot for at least one UK/EU account). cloud.py
stored that fallback as country="None" (a string) and then built a new
RoborockApiClient for submit_code(), which re-derived country from
euiot and always attempted the v4 login. Sending a v1 code and logging
in with v4 makes Roborock reject every code with response 2018
(RoborockInvalidCode).

Record which method request_code() actually used (mirroring the
library's own fallback rule) and store country as str | None instead
of stringifying it. submit_code() now replays that exact method
(code_login for v1, code_login_v4 with the stored country/country_code
for v4) instead of re-deriving anything.

Adds regression coverage in tests/test_cloud.py for both the v1
fallback path and the normal v4 path.
@Lash-L

Lash-L commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

TY! Believe it or not, I have made this mistake historically when it comes to working with regions... Appreciate the PR!

@Lash-L
Lash-L merged commit 8c6d2a5 into Python-roborock:main Sep 28, 2026
3 checks passed
@Lash-L Lash-L mentioned this pull request Oct 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants