Skip to content

fix(plugin): resolve string product ids and serve the iOS app plugin - #224

Open
AlexanderBabel wants to merge 1 commit into
Python-roborock:mainfrom
AlexanderBabel:fix/string-product-id-plugins
Open

AlexanderBabel wants to merge 1 commit into
Python-roborock:mainfrom
AlexanderBabel:fix/string-product-id-plugins

Conversation

@AlexanderBabel

Copy link
Copy Markdown

Fixes #221.

Problem

On a Qrevo Curv/Edge 2 Pro (roborock.vacuum.a226), the official iOS app (4.74.02) showed "Device initialization failed" against the local server. The server log showed POST /api/v1/appplugin but no /plugin/proxy/... download. Two gaps caused it:

  1. String product ids. Cloud home data lists this model with a string product id (30uADinyLKa3Qpga60B1Ke), on both the product and the device. _plugin_product_ids() only accepts int(...), so the import never asked for /api/v1/appplugin ("error": "no product ids available"). /api/v5/product then answered with stable_int(...) % 1_000_000 as the numeric id, which matches no plugin. The cloud has a numeric id for the model: its /api/v5/product returns {"id": 199, "model": "roborock.vacuum.a226"}.
  2. One zip for both platforms. The cloud serves a different zip for the same plugin version per request type: 1 for iOS, 2 for Android. The import only fetched type: 2, and the route answered every request with that. The iOS app cannot load the Android zip.

Fix

  • Import (import_web_inventory_from_cloud.py): fetches the cloud's /api/v5/product into plugin_catalogs.product. It maps string product ids to their numeric id by model, then fetches /api/v1/appplugin for type: 2 (appplugin, unchanged) and type: 1 (new appplugin_ios).
  • /api/v5/product (product_service.py): for a model without an entry in MODEL_PRODUCT_ID_OVERRIDES, the numeric id now comes from the cloud catalog in the snapshot, before the hash fallback. Numeric ids and rriotid are unchanged.
  • /api/v1/appplugin: a request with "type": 1 gets appplugin_ios. If that catalog is missing (snapshots imported before this change), it falls back to the current list, so existing setups behave as before. A GET without a body is unchanged.
  • shared/data_helpers.model_product_ids() holds the model → id walk that both sides use.

Nothing is hardcoded for the a226. Every id and URL comes from the cloud import.

Tests

  • tests/test_cloud_plugin_catalogs.py: with home data that uses a string product id, the import requests appplugin with productids: [199] for both type 1 and 2, and stores the product catalog and both plugin catalogs.
  • tests/test_product_response.py: a string product id gets the cloud's numeric id for its model, and rriotid stays the string.
  • tests/test_plugin_routes.py: POST /api/v1/appplugin returns the iOS zip for type: 1 and the Android zip for type: 2. A snapshot without appplugin_ios falls back to the existing catalog.

Full suite: uv run pytest -q passes (459 tests).

Live check (Oct 2026), a226 on firmware 02.43.62, iOS app 4.74.02:

  • I ran _fetch_plugin_catalogs from this branch against the real cloud with my imported session. It resolved 30uADinyLKa3Qpga60B1Ke → 199 and returned plugin 6846 with 69588500….zip for type 2 and 22598cbf….zip for type 1.
  • With the same ids and URLs served by the local server, which I patched locally before writing this PR, the iPhone app opens the device, and remote view and live video work.

Notes

  • endpoint_rules.py still has an older copy of _build_product_response that nothing calls. I left it alone, as fix(api): add rriotid, categoryId, category code and agreements to /api/v5/product #207 did.
  • appfeatureplugin and plugins are still fetched only with type: 2. I didn't check whether the cloud serves different zips for iOS there too.
  • The import makes two more cloud calls: /api/v5/product and the second appplugin. Like the other catalogs, a failure is stored as an error and doesn't stop the import.

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
Copilot AI balanced review requested due to automatic review settings October 30, 2026 20:17

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@AlexanderBabel
AlexanderBabel requested a balanced review from Copilot October 30, 2026 20:20

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

Copilot AI left a comment

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.

🟢 Approval recommended

Regression tests cover product resolution, catalog imports, and platform-specific routing, providing evidence for the implemented behavior.

No open issues 🎉

Review effort: Lite

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.

App: "device initialization failed" for models with a string cloud product id (a226), and iOS gets the Android plugin

2 participants