Repository navigation
Install firmware from the official app's Update button (opt-in) - #255
Draft
virtitnerd wants to merge 21 commits into
Draft
virtitnerd wants to merge 21 commits into
virtitnerd wants to merge 21 commits into
Conversation
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 last status, battery and error a robot sent, its last position and the map it was on, and when it last spoke or went offline, written under device_snapshots/ so it survives a restart. A robot that dies or loses Wi-Fi stops answering; this is what is left to say where it was. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Robot-originated frames update the store: status pushes (dps 120/121/122), get_status answers, and the map reply, which is decrypted with the nonce of whichever client's request asked for it. A closing robot connection marks the device offline at once. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Returns status, position, last_seen_at and offline_at; ?map=1 adds the last map the robot sent so a client can draw it while the robot cannot be asked. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A robot that went quiet before the store existed still has its last status, battery and map in the decoded MQTT log. On startup, in a background thread, devices the store has no snapshot for are filled in from that log: last time heard, last status, and the last map decoded with the nonce of the request that asked for it. Devices that already have a snapshot are left alone. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…h it The snapshot had the last map but not which segment is which room, so an offline map could only show generic labels. The proxy now records each get_room_mapping answer, the startup seed reads it from the MQTT log, and /last_known adds each room's name from the home data. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
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>
# Conflicts: # tests/test_admin_api.py
With [ota] enable_install = true (or ROBOROCK_SERVER_OTA_ENABLE_INSTALL=true), the Update button's POST /ota/device/<duid>/upgrade downloads the package the cloud offers, checks it against the MD5 in its address, serves it to the robot from this server and sends the robot's own OTA command. The robot has to be docked, idle and at 80% battery or more. While it runs, the app's status polls are answered from the robot's real state, since the cloud would answer them with an error that the app reads as a failed update. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The cloud only learns a robot's version from the robot's own connection to it, so after a local update it keeps offering the update and showing the old version. The robot reports its version (protocol 203) each time it connects; the server now keeps it and uses it for the update check and for the device list. 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
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.
Depends on: #251, #254
Depends on:
The branch merges both, so until they land the diff also shows their commits. The three new commits are
Install firmware from the official app's Update button,Show the firmware version a robot reports itselfandDocument firmware updates and the [ota] setting.Why
A robot paired to the local server never talks to Roborock's cloud, so it never learns about new firmware and the
official app cannot install it. Keeping a robot out of the cloud should not mean never updating it. People in #158,
#150 and #36 describe updating before moving to the local server for this reason.
What
Off by default:
[ota] enable_install = trueorROBOROCK_SERVER_OTA_ENABLE_INSTALL=true(the variable wins).With it on, the Update button (
POST /ota/device/<duid>/upgrade):/user/devices/<duid>/fw?version=...; the filename carries the MD5);
or more, with no other install running;
miIO.ota, download and install) and follows it through the reboot.Two things that were not obvious and are handled:
Content-Length). Starlette lowercases them, sopackages are served with the names kept as written.
from that poll as a failed update, and polls it whenever the robot is silent for 90 seconds, which the reboot
causes. While an install runs the server answers that poll itself from the robot's real state (and
IDLE, whichthe app reads as success, only after the robot is back).
The robot also reports its version (protocol 203) on every connect. The server keeps it and uses it for the update
check and the device list, because the cloud never learns the new version and would keep offering the same update.
Testing
Unit tests for each part, 550 passed in total. On hardware, a Qrevo Curv 2 Flow went from 02.33.48 to 02.35.52 through
the iOS app's Update button: 283 MB downloaded in 1m24s, installed in 2m15s, offline for 52s, and the robot reported
02.35.52 when it reconnected. The app showed success. An earlier install of a Qrevo Pro's current package confirmed
the delivery path.
Not included
The development tools used to find this (an admin dry run of the robot's OTA command and a cloud path explorer) are
not part of this PR. They are described in
docs/firmware_updates.md.Security
Installs are opt-in. Only the version the cloud offers can be installed, and the package is checked against the MD5
in the address the cloud returns. The robot verifies the firmware's signature itself. The
latest-stablecall staysrefused.
Related
#158, #150, #36 (context). There is no open issue for firmware updates; happy to open one if you prefer.