Skip to content

Add QR code login for the cloud interface#2089

Open
alexissan wants to merge 2 commits into
rytilahti:masterfrom
alexissan:qr-code-cloud-login
Open

Add QR code login for the cloud interface#2089
alexissan wants to merge 2 commits into
rytilahti:masterfrom
alexissan:qr-code-cloud-login

Conversation

@alexissan

Copy link
Copy Markdown

Fixes #2038.

Problem

Xiaomi serves a captcha on third-party password logins for many accounts. micloud cannot answer it, so miiocli cloud fails with MiCloudAccessDenied even when the credentials are correct. #2038 has been open since August 2025 and people were still hitting it in April 2026.

micloud has had no release since 2022, so waiting for a fix there doesn't look promising. From the thread:

If someone knows another library that supports the modern login workflow, we could swap this project to use the new library. Alternatively, it would be great if micloud could be improved to fix the situation :-)

Xiaomi's QR code flow is unaffected by the captcha, so rather than a new dependency this adds a small built-in backend for it.

What this does

$ miiocli cloud --qr list --locale de
Scan this QR code with the Xiaomi Home app: /tmp/miio-cloud-login-qr.png
Or open this URL and sign in: https://eu.account.xiaomi.com/longPolling/login?...

== Robot Vacuum (Device online) ==
	Model: xiaomi.vacuum.b108gl
	Token: ...

Request a login code, show it, long poll until the user approves it in the Xiaomi Home app, then call the signed device list endpoint.

Also available programmatically:

ci = CloudInterface(use_qr=True)
devs = ci.get_devices(locale="de")

on_qr=callable(png_bytes, login_url) overrides how the code is presented, for anyone embedding this.

Design

QrCodeLogin exposes only login() and get_devices(country=...) — the two methods CloudInterface used from micloud — and calls the same /home/device_list endpoint, so it returns identical records and CloudDeviceInfo parses them unchanged. I verified that against a live account rather than assuming it.

  • Password login is untouched. QR is opt-in, and CloudInterface keeps its existing signature.
  • Password failures now point at --qr in the error message, since that's the actionable next step.
  • _micloud became _backend, since it is no longer necessarily micloud.
  • It never handles the account password at all.

Dependencies

  • requests is now declared explicitly. It was already installed as a micloud dependency, so this adds nothing to an existing install — but declaring it means dropping micloud later won't break this path.
  • RC4 is implemented inline (~15 lines) rather than imported. cryptography moved ARC4 to hazmat.decrepit in 43.0, so there is no import path valid across the whole cryptography>=35 range you support, and pycryptodome isn't a dependency. This is Xiaomi's transport obfuscation, not a security boundary, and there's a test asserting the 1024-byte keystream discard against a textbook RC4.

The signing scheme follows Xiaomi-cloud-tokens-extractor (MIT), credited in the module docstring.

Testing

  • 18 new unit tests in miio/tests/test_cloud_qr.py covering RC4, the signing scheme, response decoding, locale URLs, and the CloudInterface wiring. All offline.
  • Full suite: 1520 passed. The 5 failures in chunmi/cooker_multi are pre-existing — I confirmed they fail identically on an unmodified checkout.
  • ruff and ruff format clean, with no noqa suppressions added.
  • mypy reports no new errors; cloud_qr.py is clean, and the 4 in cloud.py are byte-for-byte the same on an unmodified checkout.
  • Verified end to end against a real Xiaomi account and device: QR login, the signed device list call, and CloudDeviceInfo parsing a live record.

Happy to adjust the shape of this — put it behind a different flag, split the backend elsewhere, or drop the requests declaration and rely on the transitive dependency, whatever fits best.

Xiaomi serves a captcha on third-party password logins for many accounts,
which micloud cannot answer, so `miiocli cloud` fails with
MiCloudAccessDenied even when credentials are correct (rytilahti#2038). micloud has
had no release since 2022, so this cannot be fixed downstream.

Xiaomi's QR code flow is unaffected by the captcha. This adds a small
built-in backend implementing it: request a code, show it, long poll until
the user approves it in the Xiaomi Home app, then call the signed device
list endpoint.

The backend deliberately exposes only login() and get_devices(country=...)
and calls the same /home/device_list endpoint as micloud, so it returns
identical records and CloudDeviceInfo parses them unchanged. Password
login is untouched; QR is opt-in via `miiocli cloud --qr` or
CloudInterface(use_qr=True), and password failures now point at it.

It also never handles the account password at all.

Notes on dependencies:

- requests is now declared explicitly. It was already installed as a
  micloud dependency, so this adds nothing to an existing install, and
  declaring it means dropping micloud later does not break this path.
- RC4 is implemented inline rather than imported. cryptography moved ARC4
  to hazmat.decrepit in 43.0, so there is no import path valid across
  cryptography>=35, and pycryptodome is not a dependency. It is ~15 lines
  and is Xiaomi's transport obfuscation, not a security boundary.

The request signing scheme follows Xiaomi-cloud-tokens-extractor (MIT).

Tested with 18 new unit tests, and end to end against a real account and
device: QR login, the signed device list call, and CloudDeviceInfo parsing
of a live record.
@codecov

codecov Bot commented Jul 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 84.50%. Comparing base (9a00e08) to head (e32a540).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2089      +/-   ##
==========================================
+ Coverage   84.31%   84.50%   +0.18%     
==========================================
  Files         201      202       +1     
  Lines       20656    20813     +157     
  Branches     1121     1134      +13     
==========================================
+ Hits        17417    17587     +170     
+ Misses       3047     3031      -16     
- Partials      192      195       +3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

codecov flagged the patch at 52%. The pure functions were tested but the
network paths were not, which is where the real risk is.

Adds 23 tests with a mocked session covering the full login walk, the
long poll (including that a requests.Timeout is normal and must not
abort it), service token retrieval, and the signed POST -- asserting the
request is actually signed and encrypted rather than only that it
returns. Also covers the untouched-but-now-reachable password path, the
new CLI flag, and that --qr never prompts for credentials it will not use.

cloud_qr.py goes from 48% to 100%.

Drops the try/except around importing requests. It became dead code once
requests was declared a dependency, so it is now a plain module-level
import and the per-function imports are gone with it.
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.

"miiocli cloud" Login failed with verified correct password

1 participant