66
77import json
88import logging
9- from dataclasses import dataclass
10- from unittest .mock import AsyncMock , Mock , patch
9+ from unittest .mock import AsyncMock , Mock
1110
1211import pytest
1312
14- from roborock .containers import NetworkInfo , RoborockBase , UserData , S5MaxStatus , RoborockStateCode
13+ from roborock .containers import NetworkInfo , RoborockStateCode , S5MaxStatus , UserData
1514from roborock .devices .local_channel import LocalChannel , LocalSession
1615from roborock .devices .mqtt_channel import MqttChannel
1716from roborock .devices .v1_channel import V1Channel
3736)
3837TEST_RESPONSE = RoborockMessage (
3938 protocol = RoborockMessageProtocol .RPC_RESPONSE ,
40- payload = json .dumps ({"dps" : {"102" : json .dumps ({"id" : 12345 , "result" : {"state" : RoborockStateCode .cleaning }})}}).encode (),
39+ payload = json .dumps (
40+ {"dps" : {"102" : json .dumps ({"id" : 12345 , "result" : {"state" : RoborockStateCode .cleaning }})}}
41+ ).encode (),
4142)
4243TEST_NETWORK_INFO_RESPONSE = RoborockMessage (
4344 protocol = RoborockMessageProtocol .RPC_RESPONSE ,
@@ -62,6 +63,19 @@ def setup_mock_mqtt_channel() -> Mock:
6263 return mock_mqtt
6364
6465
66+ @pytest .fixture (name = "mqtt_responses" , autouse = True )
67+ def setup_mqtt_responses (mock_mqtt_channel : Mock ) -> list [RoborockMessage ]:
68+ """Fixture to provide a list of mock MQTT responses."""
69+
70+ responses : list [RoborockMessage ] = [TEST_NETWORK_INFO_RESPONSE ]
71+
72+ def send_command (* args ) -> RoborockMessage :
73+ return responses .pop (0 )
74+
75+ mock_mqtt_channel .send_command .side_effect = send_command
76+ return responses
77+
78+
6579@pytest .fixture (name = "mock_local_channel" )
6680def setup_mock_local_channel () -> Mock :
6781 """Mock Local channel for testing."""
@@ -148,9 +162,8 @@ async def test_v1_channel_subscribe_both_connections_success(
148162 mock_local_channel .subscribe .return_value = local_unsub
149163
150164 # Mock network info retrieval
151- with patch .object (v1_channel , "_get_networking_info" , return_value = TEST_NETWORKING_INFO ):
152- callback = Mock ()
153- unsub = await v1_channel .subscribe (callback )
165+ callback = Mock ()
166+ unsub = await v1_channel .subscribe (callback )
154167
155168 # Verify both connections established
156169 mock_mqtt_channel .subscribe .assert_called_once ()
@@ -189,8 +202,7 @@ async def test_v1_channel_local_connection_warning_logged(
189202 mock_mqtt_channel .subscribe .return_value = Mock ()
190203 mock_local_channel .connect .side_effect = RoborockException ("Local connection failed" )
191204
192- with patch .object (v1_channel , "_get_networking_info" , return_value = TEST_NETWORKING_INFO ):
193- await v1_channel .subscribe (Mock ())
205+ await v1_channel .subscribe (Mock ())
194206
195207 assert "Could not establish local connection for device abc123" in warning_caplog .text
196208 assert "Local connection failed" in warning_caplog .text
@@ -205,16 +217,12 @@ async def test_v1_channel_send_decoded_command_local_preferred(
205217 mock_local_channel : Mock ,
206218) -> None :
207219 """Test command sending prefers local connection when available."""
208- # Setup: both connections available
209- mock_mqtt_channel .subscribe .return_value = Mock ()
210- mock_local_channel .subscribe .return_value = Mock ()
211- mock_local_channel .send_command .return_value = TEST_RESPONSE
212-
213220 # Establish connections
214- with patch . object ( v1_channel , "_get_networking_info" , return_value = TEST_NETWORKING_INFO ):
215- await v1_channel . subscribe ( Mock () )
221+ await v1_channel . subscribe ( Mock ())
222+ mock_mqtt_channel . send_command . reset_mock ( return_value = False )
216223
217224 # Send command
225+ mock_local_channel .send_command .return_value = TEST_RESPONSE
218226 result = await v1_channel .send_decoded_command (
219227 RoborockCommand .CHANGE_SOUND_VOLUME ,
220228 response_type = S5MaxStatus ,
@@ -230,21 +238,19 @@ async def test_v1_channel_send_decoded_command_fallback_to_mqtt(
230238 v1_channel : V1Channel ,
231239 mock_mqtt_channel : Mock ,
232240 mock_local_channel : Mock ,
241+ mqtt_responses : list [RoborockMessage ],
233242) -> None :
234243 """Test command sending falls back to MQTT when local fails."""
235- # Setup: both connections available initially
236- mock_mqtt_channel .subscribe .return_value = Mock ()
237- mock_local_channel .subscribe .return_value = Mock ()
238- mock_mqtt_channel .send_command .return_value = TEST_RESPONSE
239244
240245 # Establish connections
241- with patch . object ( v1_channel , "_get_networking_info" , return_value = TEST_NETWORKING_INFO ):
242- await v1_channel . subscribe ( Mock () )
246+ await v1_channel . subscribe ( Mock ())
247+ mock_mqtt_channel . send_command . reset_mock ( return_value = False )
243248
244249 # Local command fails
245250 mock_local_channel .send_command .side_effect = RoborockException ("Local failed" )
246251
247252 # Send command
253+ mqtt_responses .append (TEST_RESPONSE )
248254 result = await v1_channel .send_decoded_command (
249255 RoborockCommand .CHANGE_SOUND_VOLUME ,
250256 response_type = S5MaxStatus ,
@@ -260,22 +266,19 @@ async def test_v1_channel_send_decoded_command_mqtt_only(
260266 v1_channel : V1Channel ,
261267 mock_mqtt_channel : Mock ,
262268 mock_local_channel : Mock ,
269+ mqtt_responses : list [RoborockMessage ],
263270) -> None :
264271 """Test command sending works with MQTT only."""
265272 # Setup: only MQTT connection
266- mock_mqtt_channel .subscribe .return_value = Mock ()
273+ # mock_mqtt_channel.subscribe.return_value = Mock()
267274 mock_local_channel .connect .side_effect = RoborockException ("No local" )
268275
269- responses = [TEST_NETWORK_INFO_RESPONSE , TEST_RESPONSE ]
270-
271- def send_command (* args ) -> RoborockMessage :
272- return responses .pop (0 )
273-
274- mock_mqtt_channel .send_command .side_effect = send_command
275-
276276 await v1_channel .subscribe (Mock ())
277+ mock_mqtt_channel .send_command .assert_called_once () # network info
278+ mock_mqtt_channel .send_command .reset_mock (return_value = False )
277279
278280 # Send command
281+ mqtt_responses .append (TEST_RESPONSE )
279282 result = await v1_channel .send_decoded_command (
280283 RoborockCommand .CHANGE_SOUND_VOLUME ,
281284 response_type = S5MaxStatus ,
@@ -293,17 +296,13 @@ async def test_v1_channel_send_decoded_command_with_params(
293296 mock_local_channel : Mock ,
294297) -> None :
295298 """Test command sending with parameters."""
296- # Setup: local connection only
297- mock_mqtt_channel .subscribe .return_value = Mock ()
298- mock_local_channel .subscribe .return_value = Mock ()
299- mock_local_channel .send_command .return_value = TEST_RESPONSE
300299
301- with patch .object (v1_channel , "_get_networking_info" , return_value = TEST_NETWORKING_INFO ):
302- await v1_channel .subscribe (Mock ())
300+ await v1_channel .subscribe (Mock ())
303301
304302 # Send command with params
303+ mock_local_channel .send_command .return_value = TEST_RESPONSE
305304 test_params = {"volume" : 80 }
306- result = await v1_channel .send_decoded_command (
305+ await v1_channel .send_decoded_command (
307306 RoborockCommand .CHANGE_SOUND_VOLUME ,
308307 response_type = S5MaxStatus ,
309308 params = test_params ,
@@ -312,11 +311,17 @@ async def test_v1_channel_send_decoded_command_with_params(
312311 # Verify command was sent with correct params
313312 mock_local_channel .send_command .assert_called_once ()
314313 call_args = mock_local_channel .send_command .call_args
315- assert call_args [1 ]["params" ] == test_params
316- assert result .state == RoborockStateCode .cleaning
317-
318-
319- # V1Channel message handling tests
314+ sent_message = call_args [0 ][0 ]
315+ assert sent_message
316+ assert isinstance (sent_message , RoborockMessage )
317+ assert sent_message .payload
318+ payload = sent_message .payload .decode ()
319+ json_data = json .loads (payload )
320+ assert "dps" in json_data
321+ assert "101" in json_data ["dps" ]
322+ decoded_payload = json .loads (json_data ["dps" ]["101" ])
323+ assert decoded_payload ["method" ] == "change_sound_volume"
324+ assert decoded_payload ["params" ] == {"volume" : 80 }
320325
321326
322327async def test_v1_channel_subscription_receives_mqtt_messages (
@@ -332,8 +337,7 @@ async def test_v1_channel_subscription_receives_mqtt_messages(
332337 mock_local_channel .connect .side_effect = RoborockException ("Local failed" )
333338
334339 # Subscribe
335- with patch .object (v1_channel , "_get_networking_info" , return_value = TEST_NETWORKING_INFO ):
336- await v1_channel .subscribe (callback )
340+ await v1_channel .subscribe (callback )
337341
338342 # Get the MQTT callback that was registered
339343 mqtt_callback = mock_mqtt_channel .subscribe .call_args [0 ][0 ]
@@ -359,8 +363,7 @@ async def test_v1_channel_subscription_receives_local_messages(
359363 mock_local_channel .subscribe .return_value = Mock ()
360364
361365 # Subscribe
362- with patch .object (v1_channel , "_get_networking_info" , return_value = TEST_NETWORKING_INFO ):
363- await v1_channel .subscribe (callback )
366+ await v1_channel .subscribe (callback )
364367
365368 # Get the local callback that was registered
366369 local_callback = mock_local_channel .subscribe .call_args [0 ][0 ]
@@ -476,13 +479,11 @@ async def test_v1_channel_connection_state_properties(v1_channel: V1Channel) ->
476479 assert not v1_channel .is_local_connected
477480
478481
479- # V1Channel integration tests
480-
481-
482482async def test_v1_channel_full_subscribe_and_command_flow (
483483 mock_mqtt_channel : Mock ,
484484 mock_local_session : Mock ,
485485 mock_local_channel : Mock ,
486+ mqtt_responses : list [RoborockMessage ],
486487) -> None :
487488 """Test the complete flow from subscription to command execution."""
488489 # Setup: successful connections and responses
@@ -501,9 +502,9 @@ async def test_v1_channel_full_subscribe_and_command_flow(
501502 )
502503
503504 # Mock network info for local connection
504- with patch . object ( v1_channel , "_get_networking_info" , return_value = TEST_NETWORKING_INFO ):
505- callback = Mock ( )
506- unsub = await v1_channel . subscribe ( callback )
505+ callback = Mock ()
506+ unsub = await v1_channel . subscribe ( callback )
507+ mock_mqtt_channel . send_command . reset_mock ( return_value = False )
507508
508509 # Verify both connections established
509510 assert v1_channel .is_mqtt_connected
@@ -536,31 +537,29 @@ async def test_v1_channel_graceful_degradation_local_to_mqtt(
536537 mock_mqtt_channel : Mock ,
537538 mock_local_session : Mock ,
538539 mock_local_channel : Mock ,
540+ mqtt_responses : list [RoborockMessage ],
539541) -> None :
540542 """Test graceful degradation from local to MQTT during operation."""
541- # Setup: both connections succeed initially
542- mock_mqtt_channel .subscribe .return_value = Mock ()
543- mock_local_channel .subscribe .return_value = Mock ()
544-
545543 v1_channel = V1Channel (
546544 device_uid = TEST_DEVICE_UID ,
547545 security_data = TEST_SECURITY_DATA ,
548546 mqtt_channel = mock_mqtt_channel ,
549547 local_session = mock_local_session ,
550548 )
551549
552- with patch . object ( v1_channel , "_get_networking_info" , return_value = TEST_NETWORKING_INFO ):
553- await v1_channel . subscribe ( Mock () )
550+ await v1_channel . subscribe ( Mock ())
551+ mock_mqtt_channel . send_command . reset_mock ( return_value = False )
554552
555553 # First command: local works
556554 mock_local_channel .send_command .return_value = TEST_RESPONSE
557555 result1 = await v1_channel .send_decoded_command (RoborockCommand .GET_STATUS , response_type = S5MaxStatus )
558556 assert result1 .state == RoborockStateCode .cleaning
559557 mock_local_channel .send_command .assert_called_once ()
558+ mock_mqtt_channel .send_command .assert_not_called ()
560559
561560 # Second command: local fails, falls back to MQTT
562561 mock_local_channel .send_command .side_effect = RoborockException ("Local failed" )
563- mock_mqtt_channel . send_command . return_value = TEST_RESPONSE
562+ mqtt_responses . append ( TEST_RESPONSE )
564563 result2 = await v1_channel .send_decoded_command (RoborockCommand .GET_STATUS , response_type = S5MaxStatus )
565564 assert result2 .state == RoborockStateCode .cleaning
566565
0 commit comments