Skip to content

Commit 059e2bf

Browse files
ximexclaude
andcommitted
feat(web_api): add inbox message retrieval
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 9f1a4e0 commit 059e2bf

4 files changed

Lines changed: 101 additions & 1 deletion

File tree

roborock/data/containers.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,23 @@ class HomeDataScene(RoborockBase):
328328
name: str
329329

330330

331+
@dataclass
332+
class InboxMessage(RoborockBase):
333+
"""A single user/device inbox message (`/user/inbox`)."""
334+
335+
id: int | None = None
336+
trigger: str | None = None
337+
"""Message category, e.g. NOTIFICATION, WARNING, SYSTEM, MARKETING, SHARE."""
338+
duid: str | None = None
339+
"""Device this message relates to (None for account-level messages)."""
340+
subject: str | None = None
341+
content: str | None = None
342+
extra: str | None = None
343+
"""Raw JSON string with action/templateId metadata."""
344+
read: bool | None = None
345+
create_time: str | None = None
346+
347+
331348
@dataclass
332349
class HomeDataSchedule(RoborockBase):
333350
id: int

roborock/web_api.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from pyrate_limiter import Duration, Limiter, Rate
1616

1717
from roborock import HomeDataSchedule
18-
from roborock.data import HomeData, HomeDataRoom, HomeDataScene, ProductResponse, RRiot, UserData
18+
from roborock.data import HomeData, HomeDataRoom, HomeDataScene, InboxMessage, ProductResponse, RRiot, UserData
1919
from roborock.exceptions import (
2020
RoborockAccountDoesNotExist,
2121
RoborockException,
@@ -608,6 +608,42 @@ async def get_scenes(self, user_data: UserData, device_id: str) -> list[HomeData
608608
else:
609609
raise RoborockException("scene_response result was an unexpected type")
610610

611+
async def get_inbox_messages(
612+
self,
613+
user_data: UserData,
614+
message_type: str = "NOTIFICATION",
615+
device_id: str | None = None,
616+
offset: int = 0,
617+
limit: int = 20,
618+
) -> list[InboxMessage]:
619+
"""Get inbox messages of a given type, newest first.
620+
621+
``message_type`` is one of NOTIFICATION/WARNING (device-scoped, require
622+
``device_id``) or SYSTEM/MARKETING/SHARE (account-scoped). rriot API host with
623+
Hawk auth.
624+
"""
625+
rriot = user_data.rriot
626+
if rriot is None:
627+
raise RoborockException("rriot is none")
628+
if rriot.r.a is None:
629+
raise RoborockException("Missing field 'a' in rriot reference")
630+
path = "/user/inbox"
631+
params = {"offset": str(offset), "limit": str(limit), "type": message_type}
632+
if device_id is not None:
633+
params["duid"] = device_id
634+
inbox_request = PreparedRequest(
635+
rriot.r.a,
636+
self.session,
637+
{
638+
"Authorization": _get_hawk_authentication(rriot, path, params=params),
639+
},
640+
)
641+
inbox_response = await inbox_request.request("get", path, params=params)
642+
if not inbox_response or not inbox_response.get("success"):
643+
raise RoborockException(inbox_response)
644+
result = inbox_response.get("result") or {}
645+
return [InboxMessage.from_dict(message) for message in result.get("list", [])]
646+
611647
async def execute_scene(self, user_data: UserData, scene_id: int) -> None:
612648
rriot = user_data.rriot
613649
if rriot is None:

tests/fixtures/web_api_fixtures.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,41 @@ def mock_rest_fixture(skip_rate_limit: Any, home_data: dict[str, Any]) -> aiores
137137
status=200,
138138
payload={"api": None, "code": 200, "result": None, "status": "ok", "success": True},
139139
)
140+
mocked.get(
141+
re.compile(r"https://.*roborock\.com/user/inbox(\?.*)?$"),
142+
status=200,
143+
payload={
144+
"api": None,
145+
"result": {
146+
"total": 2,
147+
"list": [
148+
{
149+
"id": 2458218464,
150+
"trigger": "NOTIFICATION",
151+
"duid": "abc123",
152+
"subject": "Roborock Q7",
153+
"content": "Battery low. Please charge.",
154+
"extra": '{"templateId":34}',
155+
"read": False,
156+
"createTime": "2026-05-29T14:03:16.542+00:00",
157+
},
158+
{
159+
"id": 2458212274,
160+
"trigger": "NOTIFICATION",
161+
"duid": "abc123",
162+
"subject": "Roborock Q7",
163+
"content": "Cleaning complete. Returning to dock.",
164+
"extra": '{"templateId":24}',
165+
"read": True,
166+
"createTime": "2026-05-23T06:08:39.687+00:00",
167+
},
168+
],
169+
},
170+
"status": "ok",
171+
"success": True,
172+
},
173+
repeat=True,
174+
)
140175
mocked.post(
141176
re.compile(r"https://.*iot\.roborock\.com/api/v4/email/code/send.*"),
142177
status=200,

tests/test_web_api.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,18 @@ async def test_get_scenes():
8181
]
8282

8383

84+
async def test_get_inbox_messages():
85+
"""Test that we can fetch device inbox messages."""
86+
api = RoborockApiClient(username="test_user@gmail.com")
87+
ud = await api.pass_login("password")
88+
messages = await api.get_inbox_messages(ud, "NOTIFICATION", device_id="abc123")
89+
assert len(messages) == 2
90+
assert messages[0].content == "Battery low. Please charge."
91+
assert messages[0].read is False
92+
assert messages[0].trigger == "NOTIFICATION"
93+
assert messages[1].read is True
94+
95+
8496
async def test_execute_scene(mock_rest):
8597
"""Test that we can execute a scene"""
8698
api = RoborockApiClient(username="test_user@gmail.com")

0 commit comments

Comments
 (0)