From 3fb864eb12febcc6ec92fccf9435f6337cadf792 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 7 Dec 2025 10:16:05 -0800 Subject: [PATCH 1/3] fix: add test coverage for a01 traits --- tests/devices/traits/a01/test_init.py | 227 ++++++++++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 tests/devices/traits/a01/test_init.py diff --git a/tests/devices/traits/a01/test_init.py b/tests/devices/traits/a01/test_init.py new file mode 100644 index 00000000..45167f46 --- /dev/null +++ b/tests/devices/traits/a01/test_init.py @@ -0,0 +1,227 @@ +from collections.abc import Generator +from typing import Any +from unittest.mock import AsyncMock, call, patch + +import pytest + +from roborock.devices.mqtt_channel import MqttChannel +from roborock.devices.traits.a01 import DyadApi, ZeoApi +from roborock.roborock_message import RoborockDyadDataProtocol, RoborockZeoProtocol + + +@pytest.fixture(name="mock_channel") +def mock_channel_fixture() -> AsyncMock: + return AsyncMock(spec=MqttChannel) + + +@pytest.fixture(name="mock_send") +def mock_send_fixture(mock_channel) -> Generator[AsyncMock, None, None]: + with patch("roborock.devices.traits.a01.send_decoded_command") as mock_send: + yield mock_send + + +async def test_dyad_api_query_values(mock_channel: AsyncMock, mock_send: AsyncMock): + """Test that DyadApi currently returns raw values without conversion.""" + api = DyadApi(mock_channel) + + mock_send.return_value = { + RoborockDyadDataProtocol.POWER: 1, + RoborockDyadDataProtocol.STATUS: 6, + RoborockDyadDataProtocol.WATER_LEVEL: 3, + RoborockDyadDataProtocol.MESH_LEFT: 120, + RoborockDyadDataProtocol.BRUSH_LEFT: 90, + RoborockDyadDataProtocol.SILENT_MODE_START_TIME: 85, + RoborockDyadDataProtocol.RECENT_RUN_TIME: "3,4,5", + RoborockDyadDataProtocol.TOTAL_RUN_TIME: 123456, + } + result = await api.query_values( + [ + RoborockDyadDataProtocol.POWER, + RoborockDyadDataProtocol.STATUS, + RoborockDyadDataProtocol.WATER_LEVEL, + RoborockDyadDataProtocol.MESH_LEFT, + RoborockDyadDataProtocol.BRUSH_LEFT, + RoborockDyadDataProtocol.SILENT_MODE_START_TIME, + RoborockDyadDataProtocol.RECENT_RUN_TIME, + RoborockDyadDataProtocol.TOTAL_RUN_TIME, + ] + ) + assert result == { + # Note: Bugs here, returning raw values + RoborockDyadDataProtocol.POWER: 1, + RoborockDyadDataProtocol.STATUS: 6, + RoborockDyadDataProtocol.WATER_LEVEL: 3, + RoborockDyadDataProtocol.MESH_LEFT: 120, + RoborockDyadDataProtocol.BRUSH_LEFT: 90, + RoborockDyadDataProtocol.SILENT_MODE_START_TIME: 85, + RoborockDyadDataProtocol.RECENT_RUN_TIME: "3,4,5", + RoborockDyadDataProtocol.TOTAL_RUN_TIME: 123456, + } + + # Note: Bug here, this is the wrong encoding for the query + assert mock_send.call_args_list == [ + call( + mock_channel, + { + RoborockDyadDataProtocol.ID_QUERY: [209, 201, 207, 214, 215, 227, 229, 230], + }, + ), + ] + + +@pytest.mark.parametrize( + ("query", "response", "expected_result"), + [ + ( + [RoborockDyadDataProtocol.STATUS], + { + 7: 1, + RoborockDyadDataProtocol.STATUS: 3, + 9999: -3, + }, + { + # Note: Bug here, should return enum value + RoborockDyadDataProtocol.STATUS: 3, + # Note: Bug here, unknown value should not be returned + 7: 1, + 9999: -3, + }, + ), + ( + [RoborockDyadDataProtocol.SILENT_MODE_START_TIME], + { + RoborockDyadDataProtocol.SILENT_MODE_START_TIME: "invalid", + }, + { + # Note: Bug here, invalid value should not be returned + RoborockDyadDataProtocol.SILENT_MODE_START_TIME: "invalid", + }, + ), + ( + [RoborockDyadDataProtocol.SILENT_MODE_START_TIME], + { + RoborockDyadDataProtocol.SILENT_MODE_START_TIME: 85, + RoborockDyadDataProtocol.POWER: 2, + 9999: -3, + }, + { + # Note: Bug here, should return time value + RoborockDyadDataProtocol.SILENT_MODE_START_TIME: 85, + # Note: Bug here, additional values should not be returned + RoborockDyadDataProtocol.POWER: 2, + 9999: -3, + }, + ), + ], + ids=[ + "ignored-unknown-protocol", + "invalid-value", + "additional-returned-values", + ], +) +async def test_dyad_invalid_response_value( + mock_channel: AsyncMock, + mock_send: AsyncMock, + query: list[RoborockDyadDataProtocol], + response: dict[int, Any], + expected_result: dict[RoborockDyadDataProtocol, Any], +): + """Test that DyadApi currently returns raw values without conversion.""" + api = DyadApi(mock_channel) + + mock_send.return_value = response + result = await api.query_values(query) + assert result == expected_result + + +async def test_zeo_api_query_values(mock_channel: AsyncMock, mock_send: AsyncMock): + """Test that ZeoApi currently returns raw values without conversion.""" + api = ZeoApi(mock_channel) + + mock_send.return_value = { + RoborockZeoProtocol.STATE: 1, + RoborockZeoProtocol.MODE: 3, + RoborockZeoProtocol.WASHING_LEFT: 4, + } + result = await api.query_values( + [RoborockZeoProtocol.STATE, RoborockZeoProtocol.MODE, RoborockZeoProtocol.WASHING_LEFT] + ) + assert result == { + # Note: Bug here, should return enum values + RoborockZeoProtocol.STATE: 1, + RoborockZeoProtocol.MODE: 3, + RoborockZeoProtocol.WASHING_LEFT: 4, + } + # Note: Bug here, this is the wrong encoding for the query + assert mock_send.call_args_list == [ + call( + mock_channel, + { + RoborockZeoProtocol.ID_QUERY: [203, 204, 218], + }, + ), + ] + + +@pytest.mark.parametrize( + ("query", "response", "expected_result"), + [ + ( + [RoborockZeoProtocol.STATE], + { + 7: 1, + RoborockZeoProtocol.STATE: 1, + 9999: -3, + }, + { + # Note: Bug here, should return enum value + RoborockZeoProtocol.STATE: 1, + # Note: Bug here, unknown value should not be returned + 7: 1, + 9999: -3, + }, + ), + ( + [RoborockZeoProtocol.WASHING_LEFT], + { + RoborockZeoProtocol.WASHING_LEFT: "invalid", + }, + { + # Note: Bug here, invalid value should not be returned + RoborockZeoProtocol.WASHING_LEFT: "invalid", + }, + ), + ( + [RoborockZeoProtocol.STATE], + { + RoborockZeoProtocol.STATE: 1, + RoborockZeoProtocol.WASHING_LEFT: 2, + 9999: -3, + }, + { + RoborockZeoProtocol.STATE: 1, + # Note: Bug here, these values were not requested and should not be returned + RoborockZeoProtocol.WASHING_LEFT: 2, + 9999: -3, + }, + ), + ], + ids=[ + "ignored-unknown-protocol", + "invalid-value", + "additional-returned-values", + ], +) +async def test_zeo_invalid_response_value( + mock_channel: AsyncMock, + mock_send: AsyncMock, + query: list[RoborockZeoProtocol], + response: dict[int, Any], + expected_result: dict[RoborockZeoProtocol, Any], +): + """Test that ZeoApi currently returns raw values without conversion.""" + api = ZeoApi(mock_channel) + + mock_send.return_value = response + result = await api.query_values(query) + assert result == expected_result From 9a50602e6eb3c25a10555bdc1dc5ad6e3e4c12ee Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 7 Dec 2025 10:26:08 -0800 Subject: [PATCH 2/3] chrore: Add additional test values to exercise more conversions --- tests/devices/traits/a01/test_init.py | 41 ++++++++++++++++++++------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/tests/devices/traits/a01/test_init.py b/tests/devices/traits/a01/test_init.py index 45167f46..3dca5a79 100644 --- a/tests/devices/traits/a01/test_init.py +++ b/tests/devices/traits/a01/test_init.py @@ -33,6 +33,8 @@ async def test_dyad_api_query_values(mock_channel: AsyncMock, mock_send: AsyncMo RoborockDyadDataProtocol.SILENT_MODE_START_TIME: 85, RoborockDyadDataProtocol.RECENT_RUN_TIME: "3,4,5", RoborockDyadDataProtocol.TOTAL_RUN_TIME: 123456, + RoborockDyadDataProtocol.STAND_LOCK_AUTO_RUN: 1, + RoborockDyadDataProtocol.AUTO_DRY_MODE: 0, } result = await api.query_values( [ @@ -44,6 +46,8 @@ async def test_dyad_api_query_values(mock_channel: AsyncMock, mock_send: AsyncMo RoborockDyadDataProtocol.SILENT_MODE_START_TIME, RoborockDyadDataProtocol.RECENT_RUN_TIME, RoborockDyadDataProtocol.TOTAL_RUN_TIME, + RoborockDyadDataProtocol.STAND_LOCK_AUTO_RUN, + RoborockDyadDataProtocol.AUTO_DRY_MODE, ] ) assert result == { @@ -56,6 +60,8 @@ async def test_dyad_api_query_values(mock_channel: AsyncMock, mock_send: AsyncMo RoborockDyadDataProtocol.SILENT_MODE_START_TIME: 85, RoborockDyadDataProtocol.RECENT_RUN_TIME: "3,4,5", RoborockDyadDataProtocol.TOTAL_RUN_TIME: 123456, + RoborockDyadDataProtocol.STAND_LOCK_AUTO_RUN: 1, + RoborockDyadDataProtocol.AUTO_DRY_MODE: 0, } # Note: Bug here, this is the wrong encoding for the query @@ -63,7 +69,7 @@ async def test_dyad_api_query_values(mock_channel: AsyncMock, mock_send: AsyncMo call( mock_channel, { - RoborockDyadDataProtocol.ID_QUERY: [209, 201, 207, 214, 215, 227, 229, 230], + RoborockDyadDataProtocol.ID_QUERY: [209, 201, 207, 214, 215, 227, 229, 230, 222, 224], }, ), ] @@ -139,25 +145,38 @@ async def test_zeo_api_query_values(mock_channel: AsyncMock, mock_send: AsyncMoc api = ZeoApi(mock_channel) mock_send.return_value = { - RoborockZeoProtocol.STATE: 1, - RoborockZeoProtocol.MODE: 3, - RoborockZeoProtocol.WASHING_LEFT: 4, + 203: 6, # spinning + 207: 3, # medium + 226: 1, + 227: 0, + 224: 1, # Times after clean. Testing int value + 218: 0, # Washing left. Testing zero int value } result = await api.query_values( - [RoborockZeoProtocol.STATE, RoborockZeoProtocol.MODE, RoborockZeoProtocol.WASHING_LEFT] + [ + RoborockZeoProtocol.STATE, + RoborockZeoProtocol.TEMP, + RoborockZeoProtocol.DETERGENT_EMPTY, + RoborockZeoProtocol.SOFTENER_EMPTY, + RoborockZeoProtocol.TIMES_AFTER_CLEAN, + RoborockZeoProtocol.WASHING_LEFT, + ] ) assert result == { - # Note: Bug here, should return enum values - RoborockZeoProtocol.STATE: 1, - RoborockZeoProtocol.MODE: 3, - RoborockZeoProtocol.WASHING_LEFT: 4, + # Note: Bug here, should return enum/bool values + RoborockZeoProtocol.STATE: 6, + RoborockZeoProtocol.TEMP: 3, + RoborockZeoProtocol.DETERGENT_EMPTY: 1, + RoborockZeoProtocol.SOFTENER_EMPTY: 0, + RoborockZeoProtocol.TIMES_AFTER_CLEAN: 1, + RoborockZeoProtocol.WASHING_LEFT: 0, } # Note: Bug here, this is the wrong encoding for the query assert mock_send.call_args_list == [ call( mock_channel, { - RoborockZeoProtocol.ID_QUERY: [203, 204, 218], + RoborockZeoProtocol.ID_QUERY: [203, 207, 226, 227, 224, 218], }, ), ] @@ -174,7 +193,7 @@ async def test_zeo_api_query_values(mock_channel: AsyncMock, mock_send: AsyncMoc 9999: -3, }, { - # Note: Bug here, should return enum value + # Note: Bug here, should return enum/bool value RoborockZeoProtocol.STATE: 1, # Note: Bug here, unknown value should not be returned 7: 1, From db2af8e4c56d776de4583fc16e98271e282784f9 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 7 Dec 2025 10:27:53 -0800 Subject: [PATCH 3/3] chore: use raw return values --- tests/devices/traits/a01/test_init.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/devices/traits/a01/test_init.py b/tests/devices/traits/a01/test_init.py index 3dca5a79..366d8ddf 100644 --- a/tests/devices/traits/a01/test_init.py +++ b/tests/devices/traits/a01/test_init.py @@ -25,16 +25,16 @@ async def test_dyad_api_query_values(mock_channel: AsyncMock, mock_send: AsyncMo api = DyadApi(mock_channel) mock_send.return_value = { - RoborockDyadDataProtocol.POWER: 1, - RoborockDyadDataProtocol.STATUS: 6, - RoborockDyadDataProtocol.WATER_LEVEL: 3, - RoborockDyadDataProtocol.MESH_LEFT: 120, - RoborockDyadDataProtocol.BRUSH_LEFT: 90, - RoborockDyadDataProtocol.SILENT_MODE_START_TIME: 85, - RoborockDyadDataProtocol.RECENT_RUN_TIME: "3,4,5", - RoborockDyadDataProtocol.TOTAL_RUN_TIME: 123456, - RoborockDyadDataProtocol.STAND_LOCK_AUTO_RUN: 1, - RoborockDyadDataProtocol.AUTO_DRY_MODE: 0, + 209: 1, # POWER + 201: 6, # STATUS + 207: 3, # WATER_LEVEL + 214: 120, # MESH_LEFT + 215: 90, # BRUSH_LEFT + 227: 85, # SILENT_MODE_START_TIME + 229: "3,4,5", # RECENT_RUN_TIME + 230: 123456, # TOTAL_RUN_TIME + 222: 1, # STAND_LOCK_AUTO_RUN + 224: 0, # AUTO_DRY_MODE } result = await api.query_values( [