From ac8de3a4779852e166b87d6570c5eb4c3f1f62d3 Mon Sep 17 00:00:00 2001 From: Immanuel Date: Sun, 25 Oct 2026 07:58:00 +0100 Subject: [PATCH] fix(api): return DeviceSortContainerV2 from GET /api/v1/home/{id}/devices/order The Android app (IHomeApiV2, GET /api/v1/home/{homeId}/devices/order) expects ApiWrapper: {"type": "HOME", "listOrders": [{"source": "RR", "did": , "order": }]} The server returned a bare list of duids, which fails in the app with "Expected start of the object '{', but had '[' at path: $.data". Co-Authored-By: Claude Opus 5.5 --- .../https_server/routes/api/v1/home.py | 14 +++- tests/test_home_devices_order.py | 75 +++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 tests/test_home_devices_order.py diff --git a/src/roborock_local_server/bundled_backend/https_server/routes/api/v1/home.py b/src/roborock_local_server/bundled_backend/https_server/routes/api/v1/home.py index 1166ffcf..e57f6fed 100644 --- a/src/roborock_local_server/bundled_backend/https_server/routes/api/v1/home.py +++ b/src/roborock_local_server/bundled_backend/https_server/routes/api/v1/home.py @@ -62,4 +62,16 @@ def build_home_devices_order( for device in devices if isinstance(device, dict) and str(get_value(device, "duid", "did", default="")).strip() ] - return ok(device_order) + # The app reads this as ApiWrapper, an object: + # {"type": DeviceSortType, "listOrders": [{"source", "did", "order"}]} + # A bare list of duids fails with "Expected start of the object '{', but + # had '[' at path: $.data". + return ok( + { + "type": "HOME", + "listOrders": [ + {"source": "RR", "did": duid, "order": index} + for index, duid in enumerate(device_order) + ], + } + ) diff --git a/tests/test_home_devices_order.py b/tests/test_home_devices_order.py new file mode 100644 index 00000000..6788889a --- /dev/null +++ b/tests/test_home_devices_order.py @@ -0,0 +1,75 @@ +import json +from pathlib import Path + +from roborock_local_server.backend import ( + ServerContext, + default_endpoint_rules, + resolve_route, +) + + +class _PairedRuntimeCredentials: + """Treat every inventory device as paired so home_payload keeps it.""" + + def resolve_device(self, *, duid: str = "", **_kwargs): + return {"did": f"did-{duid}", "device_mqtt_usr": "mqtt-user"} + + def __getattr__(self, _name): + return lambda *args, **kwargs: None + + +def _make_context(tmp_path: Path) -> ServerContext: + return ServerContext( + api_host="api.example.com", + mqtt_host="mqtt.example.com", + wood_host="wood.example.com", + region="us", + protocol_login_email="user@example.com", + localkey="key123", + duid="duid-1", + mqtt_usr="usr", + mqtt_passwd="pwd", + mqtt_clientid="cid", + https_port=443, + mqtt_tls_port=8883, + http_jsonl=tmp_path / "http.jsonl", + mqtt_jsonl=tmp_path / "mqtt.jsonl", + loggers={}, + runtime_credentials=_PairedRuntimeCredentials(), + ) + + +def test_home_devices_order_returns_device_sort_container(tmp_path: Path) -> None: + (tmp_path / "web_api_inventory.json").write_text( + json.dumps( + { + "home": {"id": 424242, "name": "Test Home"}, + "devices": [ + {"duid": "device-a", "name": "Vacuum A", "model": "roborock.vacuum.a15", "product_id": "product-a"}, + {"duid": "device-b", "name": "Vacuum B", "model": "roborock.vacuum.a15", "product_id": "product-a"}, + ], + } + ) + + "\n", + encoding="utf-8", + ) + + route, payload = resolve_route( + rules=default_endpoint_rules(), + context=_make_context(tmp_path), + clean_path="/api/v1/home/424242/devices/order", + query_params={}, + body_params={}, + method="GET", + ) + + assert route == "home_devices_order" + assert payload["code"] == 200 + # DeviceSortContainerV2 is an object, not a bare list of duids. + assert payload["data"] == { + "type": "HOME", + "listOrders": [ + {"source": "RR", "did": "device-a", "order": 0}, + {"source": "RR", "did": "device-b", "order": 1}, + ], + }