From 02aa8ae85673a166b9f5719658068fd3b254d44f Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 29 Dec 2025 22:48:15 -0800 Subject: [PATCH] chore: Add a01 and b01 q7 byte level tests This exercises the byte level behaviors of the last two protocol types. After adding this, we have at least some basic encoding tests of all devices to avoid accidental regressions. The response builder in the device manager test has been refactored to betters support the v1 , a01, and b01 message building. This updates the two test devices to have local keys that are the same instead of different per device to make the response building simpler in tests. --- tests/devices/traits/b01/q7/test_init.py | 1 + .../__snapshots__/test_device_manager.ambr | 98 ++++++- tests/e2e/test_device_manager.py | 241 +++++++++++++++--- tests/fixtures/logging_fixtures.py | 1 + tests/testdata/home_data_device_q10.json | 2 +- tests/testdata/home_data_device_zeo_one.json | 1 + 6 files changed, 311 insertions(+), 33 deletions(-) diff --git a/tests/devices/traits/b01/q7/test_init.py b/tests/devices/traits/b01/q7/test_init.py index 50096a5a..7036c3b0 100644 --- a/tests/devices/traits/b01/q7/test_init.py +++ b/tests/devices/traits/b01/q7/test_init.py @@ -166,6 +166,7 @@ async def test_q7_response_value_mapping( result = await q7_api.query_values(query) assert result is not None + assert result.status == expected_status async def test_send_decoded_command_non_dict_response(fake_channel: FakeChannel, message_builder: B01MessageBuilder): diff --git a/tests/e2e/__snapshots__/test_device_manager.ambr b/tests/e2e/__snapshots__/test_device_manager.ambr index a0727a01..b7a7a168 100644 --- a/tests/e2e/__snapshots__/test_device_manager.ambr +++ b/tests/e2e/__snapshots__/test_device_manager.ambr @@ -1,4 +1,32 @@ # serializer version: 1 +# name: test_a01_device[home_data0] + [mqtt >] + 00000000 10 29 00 04 4d 51 54 54 05 c2 00 3c 00 00 00 00 |.)..MQTT...<....| + 00000010 08 31 39 36 34 38 66 39 34 00 10 32 33 34 36 37 |.19648f94..23467| + 00000020 38 65 61 38 35 34 66 31 39 39 65 |8ea854f199e| + [mqtt <] + 00000000 20 09 02 00 06 22 00 0a 21 00 14 | ...."..!..| + [mqtt >] + 00000000 82 26 00 01 00 00 20 72 72 2f 6d 2f 6f 2f 75 73 |.&.... rr/m/o/us| + 00000010 65 72 31 32 33 2f 31 39 36 34 38 66 39 34 2f 7a |er123/19648f94/z| + 00000020 65 6f 5f 64 75 69 64 00 |eo_duid.| + [mqtt <] + 00000000 90 04 00 01 00 00 |......| + [mqtt >] + 00000000 30 5a 00 20 72 72 2f 6d 2f 69 2f 75 73 65 72 31 |0Z. rr/m/i/user1| + 00000010 32 33 2f 31 39 36 34 38 66 39 34 2f 7a 65 6f 5f |23/19648f94/zeo_| + 00000020 64 75 69 64 00 41 30 31 00 00 23 82 00 00 23 83 |duid.A01..#...#.| + 00000030 68 a6 a2 24 00 65 00 20 c5 de 2b f6 a9 ba 32 7e |h..$.e. ..+...2~| + 00000040 6b 73 82 bb d8 67 d4 db 7e cd 61 aa 8c 38 56 53 |ks...g..~.a..8VS| + 00000050 ca 4e 15 0d b1 b7 80 a2 0f 16 58 36 |.N........X6| + [mqtt <] + 00000000 30 5e 00 20 72 72 2f 6d 2f 6f 2f 75 73 65 72 31 |0^. rr/m/o/user1| + 00000010 32 33 2f 31 39 36 34 38 66 39 34 2f 7a 65 6f 5f |23/19648f94/zeo_| + 00000020 64 75 69 64 00 00 00 00 37 41 30 31 00 00 00 00 |duid....7A01....| + 00000030 00 00 00 17 68 a6 a2 23 00 66 00 20 c6 d0 06 0c |....h..#.f. ....| + 00000040 04 eb 86 8c 96 8c 51 45 4f 8e 96 93 9e 3d de 35 |......QEO....=.5| + 00000050 bb a3 92 cf 68 49 69 ba 83 25 cc 5d 77 e8 62 8a |....hIi..%.]w.b.| +# --- # name: test_l01_device [mqtt >] 00000000 10 29 00 04 4d 51 54 54 05 c2 00 3c 00 00 00 00 |.)..MQTT...<....| @@ -227,9 +255,73 @@ 00000010 32 33 2f 31 39 36 34 38 66 39 34 2f 64 65 76 69 |23/19648f94/devi| 00000020 63 65 2d 69 64 2d 64 65 66 34 35 36 00 42 30 31 |ce-id-def456.B01| 00000030 00 00 23 82 00 00 23 83 68 a6 a2 23 00 65 00 20 |..#...#.h..#.e. | - 00000040 31 38 71 36 ad 3b 7d 9d 50 0b b6 f0 be 74 5d b9 |18q6.;}.P....t].| - 00000050 7e 75 e3 ca e4 bc 42 34 f6 a5 2e ef c7 de 0c 10 |~u....B4........| - 00000060 62 f0 6c f5 |b.l.| + 00000040 8a bd 8d 51 ad 98 18 0f 13 03 aa 07 25 68 54 bc |...Q........%hT.| + 00000050 dc 66 c3 74 f1 1d ad 3e 5a 5a c3 27 b6 fe b6 cb |.f.t...>ZZ.'....| + 00000060 fe c8 92 09 |....| +# --- +# name: test_q7_device[home_data0] + [mqtt >] + 00000000 10 29 00 04 4d 51 54 54 05 c2 00 3c 00 00 00 00 |.)..MQTT...<....| + 00000010 08 31 39 36 34 38 66 39 34 00 10 32 33 34 36 37 |.19648f94..23467| + 00000020 38 65 61 38 35 34 66 31 39 39 65 |8ea854f199e| + [mqtt <] + 00000000 20 09 02 00 06 22 00 0a 21 00 14 | ...."..!..| + [mqtt >] + 00000000 82 2e 00 01 00 00 28 72 72 2f 6d 2f 6f 2f 75 73 |......(rr/m/o/us| + 00000010 65 72 31 32 33 2f 31 39 36 34 38 66 39 34 2f 64 |er123/19648f94/d| + 00000020 65 76 69 63 65 2d 69 64 2d 64 65 66 34 35 36 00 |evice-id-def456.| + [mqtt <] + 00000000 90 04 00 01 00 00 |......| + [mqtt >] + 00000000 30 b2 01 00 28 72 72 2f 6d 2f 69 2f 75 73 65 72 |0...(rr/m/i/user| + 00000010 31 32 33 2f 31 39 36 34 38 66 39 34 2f 64 65 76 |123/19648f94/dev| + 00000020 69 63 65 2d 69 64 2d 64 65 66 34 35 36 00 42 30 |ice-id-def456.B0| + 00000030 31 00 00 23 83 00 00 23 84 68 a6 a2 25 00 65 00 |1..#...#.h..%.e.| + 00000040 70 9f 24 5b c1 48 2b c9 07 ca c3 e1 c5 01 06 3e |p.$[.H+........>| + 00000050 62 44 d8 8d 7c 45 19 47 5c 53 87 fe 1a a7 a5 0d |bD..|E.G\S......| + 00000060 b4 a8 b5 7e 19 75 8a 4f 0a 37 ca d0 1f d0 a1 5b |...~.u.O.7.....[| + 00000070 e8 ef 45 75 73 aa dd 84 c8 ec d6 c2 e7 64 43 c3 |..Eus........dC.| + 00000080 58 8a 31 7a c0 45 0a 5f 06 b6 4f a3 e1 73 05 58 |X.1z.E._..O..s.X| + 00000090 b4 71 2b c3 cf e5 68 8a db de a2 3f 1a f7 8e 6d |.q+...h....?...m| + 000000a0 ab a4 7f 71 34 c2 93 83 01 7d cd 1e b3 78 c1 d7 |...q4....}...x..| + 000000b0 dc 0c 71 b2 86 |..q..| + [mqtt <] + 00000000 30 96 01 00 28 72 72 2f 6d 2f 6f 2f 75 73 65 72 |0...(rr/m/o/user| + 00000010 31 32 33 2f 31 39 36 34 38 66 39 34 2f 64 65 76 |123/19648f94/dev| + 00000020 69 63 65 2d 69 64 2d 64 65 66 34 35 36 00 00 00 |ice-id-def456...| + 00000030 00 67 42 30 31 00 00 00 01 00 00 00 17 68 a6 a2 |.gB01........h..| + 00000040 23 00 66 00 50 cc c9 4f 81 fd b0 4c 46 6d d7 bb |#.f.P..O...LFm..| + 00000050 aa 87 d8 e5 84 54 b7 5b 58 22 d3 d1 53 d0 1d b8 |.....T.[X"..S...| + 00000060 6f 11 53 4f 77 21 a1 a5 8b 05 7f 9e b7 62 88 df |o.SOw!.......b..| + 00000070 57 1b fe 50 f0 9a 70 bc e1 ad c3 f7 cc f7 3f e4 |W..P..p.......?.| + 00000080 6a dd 1d f5 d2 4a 6d 4d 48 4f b5 75 07 70 7d bf |j....JmMHO.u.p}.| + 00000090 c5 b9 3f e7 73 4b d9 19 cd |..?.sK...| + [mqtt >] + 00000000 30 e2 01 00 28 72 72 2f 6d 2f 69 2f 75 73 65 72 |0...(rr/m/i/user| + 00000010 31 32 33 2f 31 39 36 34 38 66 39 34 2f 64 65 76 |123/19648f94/dev| + 00000020 69 63 65 2d 69 64 2d 64 65 66 34 35 36 00 42 30 |ice-id-def456.B0| + 00000030 31 00 00 23 86 00 00 23 87 68 a6 a2 26 00 65 00 |1..#...#.h..&.e.| + 00000040 a0 4d ae db 7c ad db 6f 8e 4a 1b 01 4c 2b fd fd |.M..|..o.J..L+..| + 00000050 1b 4f df 4c 64 fb 3b ed a6 fc 9f e2 21 e8 95 94 |.O.Ld.;.....!...| + 00000060 49 6c 57 79 9c c5 8e 35 48 fc cc 29 f8 69 9b 54 |IlWy...5H..).i.T| + 00000070 fb 42 33 7e 63 72 a6 17 0f 87 20 31 74 c3 bb 29 |.B3~cr.... 1t..)| + 00000080 5b 6a f3 a7 23 bd 10 42 84 4b 6f 09 a5 6c 0b 3c |[j..#..B.Ko..l.<| + 00000090 d0 0c a4 ba 90 be 70 27 43 73 35 bd 5f 47 bd 1b |......p'Cs5._G..| + 000000a0 b4 e5 0b 98 50 ed 61 80 7d db 40 c1 ad 99 65 e1 |....P.a.}.@...e.| + 000000b0 7e df a6 b8 7d ef 3b 08 92 c3 95 c7 46 a6 f7 32 |~...}.;.....F..2| + 000000c0 b6 6d cb 21 72 b8 a1 ee 85 49 d6 2a 76 33 c0 01 |.m.!r....I.*v3..| + 000000d0 c3 be fa 58 3d fa f2 72 50 84 e4 17 68 e5 21 00 |...X=..rP...h.!.| + 000000e0 98 16 9c 62 43 |...bC| + [mqtt <] + 00000000 30 86 01 00 28 72 72 2f 6d 2f 6f 2f 75 73 65 72 |0...(rr/m/o/user| + 00000010 31 32 33 2f 31 39 36 34 38 66 39 34 2f 64 65 76 |123/19648f94/dev| + 00000020 69 63 65 2d 69 64 2d 64 65 66 34 35 36 00 00 00 |ice-id-def456...| + 00000030 00 57 42 30 31 00 00 00 02 00 00 00 17 68 a6 a2 |.WB01........h..| + 00000040 24 00 66 00 40 cc c9 4f 81 fd b0 4c 46 6d d7 bb |$.f.@..O...LFm..| + 00000050 aa 87 d8 e5 84 54 b7 5b 58 22 d3 d1 53 d0 1d b8 |.....T.[X"..S...| + 00000060 6f 11 53 4f 77 2b ef a8 bf d2 66 05 8e ce 7f 08 |o.SOw+....f.....| + 00000070 76 c0 18 90 ed 04 66 8e 8f 91 30 63 d2 e0 8c 1a |v.....f...0c....| + 00000080 09 5c 7c ea 94 e3 24 15 60 |.\|...$.`| # --- # name: test_v1_device [mqtt >] diff --git a/tests/e2e/test_device_manager.py b/tests/e2e/test_device_manager.py index 21c5eeda..1ae652e9 100644 --- a/tests/e2e/test_device_manager.py +++ b/tests/e2e/test_device_manager.py @@ -15,14 +15,20 @@ import pytest import syrupy +from Crypto.Cipher import AES +from Crypto.Util.Padding import pad +from roborock.data.b01_q7 import WorkStatusMapping from roborock.data.b01_q10.b01_q10_code_mappings import B01_Q10_DP from roborock.data.containers import UserData +from roborock.data.zeo.zeo_code_mappings import ZeoState from roborock.devices.cache import Cache, InMemoryCache from roborock.devices.device_manager import DeviceManager, UserParams, create_device_manager from roborock.protocol import MessageParser +from roborock.protocols.a01_protocol import A01_VERSION +from roborock.protocols.b01_q7_protocol import B01_VERSION from roborock.protocols.v1_protocol import LocalProtocolVersion -from roborock.roborock_message import RoborockMessage, RoborockMessageProtocol +from roborock.roborock_message import RoborockB01Props, RoborockMessage, RoborockMessageProtocol, RoborockZeoProtocol from roborock.web_api import RoborockApiClient from tests import mock_data, mqtt_packet from tests.fixtures.logging import CapturedRequestLog @@ -33,7 +39,7 @@ # The topic used for the user + device. This is determined from the fake Home # data API response. -TEST_TOPIC = "rr/m/o/user123/19648f94/abc123" +TEST_TOPIC_FORMAT = "rr/m/o/user123/19648f94/{duid}" TEST_RANDOM = 23 TEST_HOST = mock_data.TEST_LOCAL_API_HOST NETWORK_INFO = { @@ -113,36 +119,29 @@ def __init__(self) -> None: self.connect_nonce: int | None = None self.ack_nonce: int | None = None self.protocol = RoborockMessageProtocol.RPC_RESPONSE - self.version = LocalProtocolVersion.V1 + self.version: bytes = LocalProtocolVersion.V1.value.encode() - def build( + def build_v1( self, payload: bytes, protocol: RoborockMessageProtocol | None = None, ) -> bytes: """Build an encoded response message.""" self.seq_counter += 1 - return MessageParser.build( - RoborockMessage( - protocol=protocol if protocol is not None else self.protocol, - random=TEST_RANDOM, - seq=self.seq_counter, + return self._encrypt( + self._build_roborock_message( payload=payload, - version=self.version.value.encode(), + protocol=protocol if protocol is not None else self.protocol, ), - local_key=LOCAL_KEY, - connect_nonce=self.connect_nonce, - ack_nonce=self.ack_nonce, ) - def build_rpc( + def build_v1_rpc( self, data: dict[str, Any], - protocol: RoborockMessageProtocol | None = None, ) -> bytes: """Build an encoded RPC response message.""" self.timestamp_counter += 1 - return self.build( + return self.build_v1( payload=json.dumps( { "t": self.timestamp_counter, @@ -151,7 +150,58 @@ def build_rpc( }, } ).encode(), - protocol=protocol, + ) + + def build_a01_rpc(self, data: dict[str, Any]) -> bytes: + """Build an encoded A01 RPC response message.""" + self.timestamp_counter += 1 + return self._encrypt( + self._build_roborock_message( + payload=pad(json.dumps({"dps": data}).encode(), AES.block_size), + ), + ) + + def build_b01_q7_rpc(self, data: dict[str, Any] | str, code: int | None = None, msg_id: int | None = None) -> bytes: + """Build an encoded B01 RPC response message.""" + message: dict[str, Any] = { + "msgId": str(msg_id), + "data": data, + } + if code is not None: + message["code"] = code + return self._build_b01_dps(message) + + def _build_b01_dps(self, message: dict[str, Any] | str) -> bytes: + """Build an encoded B01 RPC response message given an inner message.""" + dps_payload = {"dps": {"10000": json.dumps(message)}} + self.seq_counter += 1 + return self._encrypt( + self._build_roborock_message( + payload=json.dumps(dps_payload).encode(), + ), + ) + + def _build_roborock_message( + self, + payload: bytes, + protocol: RoborockMessageProtocol | None = None, + ) -> RoborockMessage: + """Build a Roborock message.""" + return RoborockMessage( + protocol=protocol if protocol is not None else self.protocol, + random=TEST_RANDOM, + seq=self.seq_counter, + payload=payload, + version=self.version, + ) + + def _encrypt(self, message: RoborockMessage) -> bytes: + """Encrypt a message.""" + return MessageParser.build( + message, + local_key=LOCAL_KEY, + connect_nonce=self.connect_nonce, + ack_nonce=self.ack_nonce, ) @@ -173,11 +223,12 @@ async def test_v1_device( # Prepare MQTT requests response_builder = ResponseBuilder() + test_topic = TEST_TOPIC_FORMAT.format(duid="abc123") mqtt_responses: list[bytes] = [ *MQTT_DEFAULT_RESPONSES, # ACK the GET_NETWORK_INFO call. id is deterministic based on deterministic_message_fixtures mqtt_packet.gen_publish( - TEST_TOPIC, mid=2, payload=response_builder.build_rpc(data={"id": 9090, "result": NETWORK_INFO}) + test_topic, mid=2, payload=response_builder.build_v1_rpc(data={"id": 9090, "result": NETWORK_INFO}) ), ] for response in mqtt_responses: @@ -187,10 +238,10 @@ async def test_v1_device( response_builder.seq_counter = 0 local_responses: list[bytes] = [ # Queue HELLO response - response_builder.build(protocol=RoborockMessageProtocol.HELLO_RESPONSE, payload=b"ok"), + response_builder.build_v1(protocol=RoborockMessageProtocol.HELLO_RESPONSE, payload=b"ok"), # Feature discovery part 1 & 2 - response_builder.build_rpc(data={"id": 9094, "result": [mock_data.APP_GET_INIT_STATUS]}), - response_builder.build_rpc(data={"id": 9097, "result": [mock_data.STATUS]}), + response_builder.build_v1_rpc(data={"id": 9094, "result": [mock_data.APP_GET_INIT_STATUS]}), + response_builder.build_v1_rpc(data={"id": 9097, "result": [mock_data.STATUS]}), ] for payload in local_responses: local_response_queue.put_nowait(payload) @@ -238,7 +289,7 @@ async def test_v1_device( # Prepare local device responses. response_builder.seq_counter = 0 local_response_queue.put_nowait( - response_builder.build(protocol=RoborockMessageProtocol.HELLO_RESPONSE, payload=b"ok") + response_builder.build_v1(protocol=RoborockMessageProtocol.HELLO_RESPONSE, payload=b"ok") ) device_manager = await device_manager_factory(user_params) @@ -267,7 +318,7 @@ async def test_v1_device( assert device.v1_properties.status.state_name is None # Exercise a GET_STATUS call. id is deterministic based on deterministic_message_fixtures - local_response_queue.put_nowait(response_builder.build_rpc(data={"id": 9101, "result": [mock_data.STATUS]})) + local_response_queue.put_nowait(response_builder.build_v1_rpc(data={"id": 9101, "result": [mock_data.STATUS]})) # Verify GET_STATUS response await device.v1_properties.status.refresh() @@ -288,11 +339,12 @@ async def test_l01_device( """Test the device manager end to end flow with a l01 device.""" # Prepare MQTT requests mqtt_response_builder = ResponseBuilder() + test_topic = TEST_TOPIC_FORMAT.format(duid="abc123") mqtt_responses: list[bytes] = [ *MQTT_DEFAULT_RESPONSES, # ACK the GET_NETWORK_INFO call. id is deterministic based on deterministic_message_fixtures mqtt_packet.gen_publish( - TEST_TOPIC, mid=2, payload=mqtt_response_builder.build_rpc(data={"id": 9090, "result": NETWORK_INFO}) + test_topic, mid=2, payload=mqtt_response_builder.build_v1_rpc(data={"id": 9090, "result": NETWORK_INFO}) ), ] for response in mqtt_responses: @@ -300,20 +352,20 @@ async def test_l01_device( # Prepare local device responses. The ids are deterministic based on deterministic_message_fixtures local_response_builder = ResponseBuilder() - local_response_builder.version = LocalProtocolVersion.L01 + local_response_builder.version = LocalProtocolVersion.L01.value.encode() local_response_builder.connect_nonce = 9093 local_responses: list[bytes] = [ - # Initial V01 Hello request will fail and cause a retry with L01 + # Initial V1.0 Hello request will fail and cause a retry with L01 b"\x00", # Queue HELLO response with L01 - local_response_builder.build(protocol=RoborockMessageProtocol.HELLO_RESPONSE, payload=b"ok"), + local_response_builder.build_v1(protocol=RoborockMessageProtocol.HELLO_RESPONSE, payload=b"ok"), ] # Feature discovery requests are sent with an ack nonce based on the random sent in HELLO_RESPONSE local_response_builder.ack_nonce = TEST_RANDOM local_responses.extend( [ - local_response_builder.build_rpc(data={"id": 9094, "result": [mock_data.APP_GET_INIT_STATUS]}), - local_response_builder.build_rpc(data={"id": 9097, "result": [mock_data.STATUS]}), + local_response_builder.build_v1_rpc(data={"id": 9094, "result": [mock_data.APP_GET_INIT_STATUS]}), + local_response_builder.build_v1_rpc(data={"id": 9097, "result": [mock_data.STATUS]}), ] ) for payload in local_responses: @@ -392,3 +444,134 @@ async def test_q10_device( # In the future here we can verify receiving requests from the device assert snapshot == log + + +@pytest.mark.parametrize( + "home_data", + [ + ( + { + **HOME_DATA_RAW, + # Use a fake Q7 device and product profile as a placeholder + # until we add a json file based on the real one. + "devices": [ + { + **mock_data.Q10_DEVICE_DATA, + "name": "Roborock Q7 XX", + "productId": "product-id-q7", + }, + ], + "products": [ + { + **mock_data.SS07_PRODUCT_DATA, + "id": "product-id-q7", + "name": "Roborock Q7 Series", + "model": "roborock.vacuum.scXX", + "category": "robot.vacuum.cleaner", + }, + ], + } + ) + ], +) +async def test_q7_device( + mock_rest: Any, + push_mqtt_response: Callable[[bytes], None], + log: CapturedRequestLog, + device_manager_factory: Callable[[UserParams], Awaitable[DeviceManager]], + home_data: dict[str, Any], + snapshot: syrupy.SnapshotAssertion, +) -> None: + """Test the device manager end to end flow with a B01 Q10 device.""" + # Prepare MQTT requests + response_builder = ResponseBuilder() + response_builder.version = B01_VERSION + test_topic = TEST_TOPIC_FORMAT.format(duid="device-id-def456") + mqtt_responses: list[bytes] = [ + *MQTT_DEFAULT_RESPONSES, + # ACK the Query status call sent below. id is deterministic based on deterministic_message_fixtures + mqtt_packet.gen_publish( + test_topic, mid=2, payload=response_builder.build_b01_q7_rpc({"status": 2}, msg_id=9090) + ), + # ACK the start clean call sent below. id is deterministic based on deterministic_message_fixtures + mqtt_packet.gen_publish(test_topic, mid=2, payload=response_builder.build_b01_q7_rpc("ok", msg_id=9093)), + ] + for response in mqtt_responses: + push_mqtt_response(response) + + # Create the device manager + device_manager = await device_manager_factory(TEST_USER_PARAMS) + + # The mocked Home Data API returns a single v1 device + devices = await device_manager.get_devices() + assert len(devices) == 1 + device = devices[0] + assert device.duid == "device-id-def456" + assert device.name == "Roborock Q7 XX" + assert device.is_connected + assert not device.is_local_connected # Q7 does not support local connections + + # Query a value from the device. + assert device.b01_q7_properties + props = await device.b01_q7_properties.query_values([RoborockB01Props.STATUS]) + assert props + assert props.status == WorkStatusMapping.PAUSED + + # Send a command and block on an OK response. + await device.b01_q7_properties.start_clean() + + assert snapshot == log + + +@pytest.mark.parametrize( + "home_data", + [ + ( + { + **HOME_DATA_RAW, + "devices": [mock_data.ZEO_ONE_DEVICE_DATA], + "products": [mock_data.A102_PRODUCT_DATA], + } + ) + ], +) +async def test_a01_device( + mock_rest: Any, + push_mqtt_response: Callable[[bytes], None], + log: CapturedRequestLog, + device_manager_factory: Callable[[UserParams], Awaitable[DeviceManager]], + home_data: dict[str, Any], + snapshot: syrupy.SnapshotAssertion, +) -> None: + """Test the device manager end to end flow with an A01 device.""" + # Prepare MQTT requests + response_builder = ResponseBuilder() + response_builder.version = A01_VERSION + test_topic = TEST_TOPIC_FORMAT.format(duid="zeo_duid") + mqtt_responses: list[bytes] = [ + *MQTT_DEFAULT_RESPONSES, + # ACK the Query state call sent below. id is deterministic based on deterministic_message_fixtures + mqtt_packet.gen_publish(test_topic, mid=2, payload=response_builder.build_a01_rpc({"203": 6})), + ] + for response in mqtt_responses: + push_mqtt_response(response) + + # Create the device manager + device_manager = await device_manager_factory(TEST_USER_PARAMS) + + # The mocked Home Data API returns a single v1 device + devices = await device_manager.get_devices() + assert len(devices) == 1 + device = devices[0] + assert device.duid == "zeo_duid" + assert device.name == "Zeo One" + assert device.is_connected + assert not device.is_local_connected # Washing Machine does not support local connections + + # Query a value from the device. + assert device.zeo + props: dict[RoborockZeoProtocol, Any] = await device.zeo.query_values([RoborockZeoProtocol.STATE]) + assert props + assert props[RoborockZeoProtocol.STATE] == ZeoState.spinning.name + + assert snapshot == log diff --git a/tests/fixtures/logging_fixtures.py b/tests/fixtures/logging_fixtures.py index 542ec164..439c5891 100644 --- a/tests/fixtures/logging_fixtures.py +++ b/tests/fixtures/logging_fixtures.py @@ -52,6 +52,7 @@ def get_token_bytes(n: int) -> bytes: with ( patch("roborock.devices.local_channel.get_next_int", side_effect=get_next_int), + patch("roborock.protocols.b01_q7_protocol.get_next_int", side_effect=get_next_int), patch("roborock.protocols.v1_protocol.get_next_int", side_effect=get_next_int), patch("roborock.protocols.v1_protocol.get_timestamp", side_effect=get_timestamp), patch("roborock.protocols.v1_protocol.secrets.token_bytes", side_effect=get_token_bytes), diff --git a/tests/testdata/home_data_device_q10.json b/tests/testdata/home_data_device_q10.json index f8b0892c..68651dff 100644 --- a/tests/testdata/home_data_device_q10.json +++ b/tests/testdata/home_data_device_q10.json @@ -2,7 +2,7 @@ { "duid": "device-id-def456", "name": "Roborock Q10 S5+", - "localKey": "b1E53o6NAR6qF9a1", + "localKey": "key123key123key1", "productId": "product-id-q10-ss07", "fv": "03.10.0", "activeTime": 1767044247, diff --git a/tests/testdata/home_data_device_zeo_one.json b/tests/testdata/home_data_device_zeo_one.json index 4bd2a717..e313cd1e 100644 --- a/tests/testdata/home_data_device_zeo_one.json +++ b/tests/testdata/home_data_device_zeo_one.json @@ -3,6 +3,7 @@ "name": "Zeo One", "fv": "01.00.94", "productId": "product-id-zeo-one", + "localKey": "key123key123key1", "activeTime": 1699964128, "timeZoneId": "Europe/Berlin", "iconUrl": "",