Skip to content

fix(routes): support /devices/{did}/sh endpoint for access renewal on newer firmware - #199

Open
ratel-dev wants to merge 2 commits into
Python-roborock:mainfrom
ratel-dev:patch-1
Open

ratel-dev wants to merge 2 commits into
Python-roborock:mainfrom
ratel-dev:patch-1

Conversation

@ratel-dev

Copy link
Copy Markdown

Problem & Background

On newer Roborock models with v2 firmware (verified on the Saros 10R / roborock.vacuum.a143, firmware 02.50.56), the robot blocks all incoming RPC control commands (get_status, app_start, get_map_v1, etc.) roughly 48 hours after onboarding, returning:

{"code": -10002, "message": "rrcheck access denied"}

While RPC commands are blocked, MQTT telemetry and status events remain connected. Re-onboarding only temporarily resets the 48-hour grace window.

Root Cause

Roborock firmware requires periodic local access license renewal (rrcheck). While older generations (such as the S7 series) renew this via POST /f/c, newer firmware does not hit /f/c. Instead, it queries:

GET /devices/{did}/sh?did={did}&pid={pid}&signature=...

Because routes/fw/check.py previously only matched /f/c variants, this request fell through to catchall ({"ok": true}), which the robot rejects as an invalid grant. After the ~48-hour grace period expires, internal access control locks up.

Changes

  1. Endpoint Matching: Extended match() in routes/fw/check.py to recognize paths starting with /devices/ and ending with /sh alongside existing _MATCH_PATHS.
  2. DID Resolution: If did is not present in query or body parameters, it is extracted directly from the path segments (/devices/<did>/sh).
  3. Response Envelope: Wrapped the encrypted payload {k, d} inside the envelope expected by /sh requests:
    {
        "k": payload["k"],
        "d": payload["d"],
        "success": True,
        "code": 200,
        "msg": "success",
        "data": payload,
        "result": payload,
    }
  4. Backward Compatibility: Legacy POST /f/c requests continue to receive the standard flat {k, d} dict. Devices without a recovered public key continue falling back to catchall.

Verification & Environment

  • Model: Roborock Saros 10R (roborock.vacuum.a143)
  • Firmware: 02.50.56
  • TLS Certificate: ZeroSSL (valid ACME chain)
  • Deployment: Standalone Docker deployment
  • Status: Tested continuously over several weeks. Nightly access renewal requests at 03:16 UTC are successfully accepted with no further -10002 lockups.

@Lash-L Lash-L self-assigned this Oct 24, 2026
if not did and clean_path.startswith("/devices/"):
parts = clean_path.strip("/").split("/")
if len(parts) >= 2:
did = parts[1]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extract_did() falls back to ctx.duid, so the new if not did branch never reads the path when a default device is configured. For /devices/222/sh with default device 111 and no query DID, I confirmed that only device 111 can decrypt the grant. Use extract_explicit_did() before falling back to the path.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice catch, I've updated to use ctx.extract_explicit_did() first.

@Lash-L
Lash-L marked this pull request as draft October 31, 2026 02:45
@Lash-L

Lash-L commented Oct 31, 2026

Copy link
Copy Markdown
Contributor

Please mark as ready when my comment is resolved (so long as you agree with it)

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