Add QR code login for the cloud interface#2089
Open
alexissan wants to merge 2 commits into
Open
Conversation
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 Report✅ All modified and coverable lines are covered by tests. 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. |
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #2038.
Problem
Xiaomi serves a captcha on third-party password logins for many accounts.
micloudcannot answer it, somiiocli cloudfails withMiCloudAccessDeniedeven when the credentials are correct. #2038 has been open since August 2025 and people were still hitting it in April 2026.micloudhas had no release since 2022, so waiting for a fix there doesn't look promising. From the thread: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
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:
on_qr=callable(png_bytes, login_url)overrides how the code is presented, for anyone embedding this.Design
QrCodeLoginexposes onlylogin()andget_devices(country=...)— the two methodsCloudInterfaceused frommicloud— and calls the same/home/device_listendpoint, so it returns identical records andCloudDeviceInfoparses them unchanged. I verified that against a live account rather than assuming it.CloudInterfacekeeps its existing signature.--qrin the error message, since that's the actionable next step._micloudbecame_backend, since it is no longer necessarily micloud.Dependencies
requestsis now declared explicitly. It was already installed as amiclouddependency, so this adds nothing to an existing install — but declaring it means droppingmicloudlater won't break this path.cryptographymoved ARC4 tohazmat.decrepitin 43.0, so there is no import path valid across the wholecryptography>=35range you support, andpycryptodomeisn'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
miio/tests/test_cloud_qr.pycovering RC4, the signing scheme, response decoding, locale URLs, and theCloudInterfacewiring. All offline.chunmi/cooker_multiare pre-existing — I confirmed they fail identically on an unmodified checkout.ruffandruff formatclean, with nonoqasuppressions added.mypyreports no new errors;cloud_qr.pyis clean, and the 4 incloud.pyare byte-for-byte the same on an unmodified checkout.CloudDeviceInfoparsing a live record.Happy to adjust the shape of this — put it behind a different flag, split the backend elsewhere, or drop the
requestsdeclaration and rely on the transitive dependency, whatever fits best.