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": "",