Skip to content

Answer the official app's firmware calls with the cloud's real update information - #254

Draft
virtitnerd wants to merge 10 commits into
Python-roborock:mainfrom
virtitnerd:pr/firmware-check
Draft

virtitnerd wants to merge 10 commits into
Python-roborock:mainfrom
virtitnerd:pr/firmware-check

Conversation

@virtitnerd

@virtitnerd virtitnerd commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Depends on: #253

Depends on the plugin catalog refresh PR for the cloud client helpers, and through it on #224. The top six
commits are the new ones.

Why

The server answered the official app's firmware calls with a generic success, so the app could never see that a robot
had an update. The routes the app really uses turned out to be GET /ota/firmware/<duid>/updatev2,
GET /ota/firmware/updatev2?duids=... and GET /ota/firmware/upgrade/status?duids=....

What

The server reads the update information from Roborock's cloud with the stored login and returns it, so the app shows
the real version and release notes. These routes require the app's signature, and a read is never refused as a
replayed signature (the app repeats them). The upgrade call is still refused with an error instead of pretending to
start. GET /admin/api/cloud/firmware lists every robot's status on the account for scripts.

Testing

tests/test_ota_firmware.py covers both URL styles, the signature requirement and the replay case, with a fake
cloud. Full suite: 480 passed. Checked against the iOS app, which now shows the real version for a Qrevo Pro and a
Qrevo Curv 2 Flow.

Notes

Needs the cloud login the server stored at sign-in. Without it the check is reported as unavailable.

AlexanderBabel and others added 10 commits October 30, 2026 21:17
The cloud lists newer models (e.g. roborock.vacuum.a226) under a string
product id, so the import never fetched their app plugin and /api/v5/product
answered with a hashed id that matches no plugin. The app then fails with
"device initialization failed".

The import now fetches the cloud's /api/v5/product to map models to their
numeric id, and fetches /api/v1/appplugin for both type 2 (Android) and
type 1 (iOS), which get different zips. The appplugin route answers iOS
requests from the iOS catalog, falling back to the existing one.

Fixes Python-roborock#221
The snapshot keeps the cloud login and the plugin catalogs fetched at sign-in, and the
plugin routes serve those. Newer robots can need newer plugins than the snapshot holds, so
CloudImportManager.refresh_plugin_catalogs asks the cloud again with the stored login and
replaces only the catalogs. The sign-in now also stores the device identifier the cloud
ties the login to. A rejected login or an empty answer leaves the snapshot as it was.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
POST /admin/api/cloud/refresh-plugins re-reads them with the stored login, taking an
optional platform type. GET /admin/api/cloud/plugins shows what is stored without a cloud
request. Both return each plugin's version, API level and download URL.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The refresh calls the same _fetch_plugin_catalogs as the sign-in import, so it gets
whatever that fetches, instead of carrying its own platform type.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The firmware check is a Hawk-signed IoT call, so it uses the rriot credentials the sign-in
stored. The refresh and the check now share one helper for loading that login.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
GET /ota/firmware/<duid>/updatev2 returns what the cloud says, so the official app and
LocalRock can show an available update and its notes. POST /ota/device/<duid>/upgrade used to
answer success and do nothing; it is now refused with a clear error, since installing a
firmware needs the command the cloud pushes to the robot, which is not known yet.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ture

The app checks with GET /ota/firmware/updatev2?duids=...&lang=..., reads progress from
/ota/firmware/upgrade/status?duids=..., and installs with POST /ota/device/<duid>/upgrade (or
/upgrade/latest-stable). The check and the progress are now read from the cloud, both upgrade
forms are refused, and the older path-style check still works.

These routes answer from the stored cloud login, so they now need the same Hawk signature as
the /user/ routes instead of being open.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The app sends the firmware check and the progress read within milliseconds of each other and
gives them the same timestamp and nonce. The replay check keys on timestamp and nonce, not the
path, so the second request was refused and the Firmware Update screen showed a network error
until the app retried. The signature and its age are still verified for these read-only GETs;
only the repeated-nonce refusal is skipped. Everything else, including the upgrade POST, keeps it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
GET /admin/api/cloud/firmware asks the cloud about each robot in the home data the sign-in
stored, including robots that are not paired to this server, so an available update can be
seen before pairing. A failure for one robot is reported on its row without hiding the rest.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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