From 1b2efd8719b90a6ac8b671f1d43e7a2d2fd27ddf Mon Sep 17 00:00:00 2001 From: infy1995 Date: Thu, 12 Nov 2026 20:56:01 +0100 Subject: [PATCH 1/2] feat: add V1 dust collection mode setter and actions --- docs/V1_API_COMMANDS.md | 23 +++- roborock/data/v1/v1_code_mappings.py | 9 +- .../devices/traits/v1/dust_collection_mode.py | 38 +++++- .../traits/v1/test_dust_collection_mode.py | 129 ++++++++++++++++++ 4 files changed, 194 insertions(+), 5 deletions(-) diff --git a/docs/V1_API_COMMANDS.md b/docs/V1_API_COMMANDS.md index 68776ef41..5799774b3 100644 --- a/docs/V1_API_COMMANDS.md +++ b/docs/V1_API_COMMANDS.md @@ -1202,18 +1202,39 @@ Return example: Source: Roborock S7 MaxV Ultra +Saros 10R return example: + +```json + {'mode': 4} +``` + **Supported devices:** * Roborock S7 MaxV Ultra: ✅ * Roborock S8 Pro Ultra: ✅ +* Roborock Saros 10R: ✅ ### set_dust_collection_mode -Description: +Description: Set the dock dust collection mode. Parameters: + mode: 0 smart, 1 light, 2 balanced, 3 strong, 4 max. The app offers 0, 1, 2 and 4. + +Example: + +```json + {'mode': 4} +``` + +Source: Roborock Saros 10R + +**Supported devices:** + +* Roborock Saros 10R: ✅ + ### get_wash_towel_mode Description: diff --git a/roborock/data/v1/v1_code_mappings.py b/roborock/data/v1/v1_code_mappings.py index c5d5037f7..d6a75405f 100644 --- a/roborock/data/v1/v1_code_mappings.py +++ b/roborock/data/v1/v1_code_mappings.py @@ -390,13 +390,18 @@ class RoborockDockTypeCode(RoborockEnum): class RoborockDockDustCollectionModeCode(RoborockEnum): - """Describes the dust collection mode of the vacuum cleaner.""" + """Describes the dust collection mode of the vacuum cleaner. + + Codes follow the Roborock app: 0 smart, 1 quick (``light``), 2 daily (``balanced``), + 3 strong, 4 max. The app offers every mode except ``strong``, which is kept so that + reading it does not fail. + """ - # TODO: Get the correct values for various different docks unknown = -9999 smart = 0 light = 1 balanced = 2 + strong = 3 max = 4 diff --git a/roborock/devices/traits/v1/dust_collection_mode.py b/roborock/devices/traits/v1/dust_collection_mode.py index e738e30d1..72975b35b 100644 --- a/roborock/devices/traits/v1/dust_collection_mode.py +++ b/roborock/devices/traits/v1/dust_collection_mode.py @@ -1,18 +1,52 @@ """Trait for dust collection mode.""" -from roborock.data import DustCollectionMode +from roborock.data import DustCollectionMode, RoborockDockDustCollectionModeCode from roborock.device_features import RoborockDockFeatures from roborock.devices.traits.v1 import common +from roborock.exceptions import RoborockUnsupportedFeature from roborock.roborock_typing import RoborockCommand +_DUST_COLLECTION_MODE_OPTIONS = [ + RoborockDockDustCollectionModeCode.smart, + RoborockDockDustCollectionModeCode.light, + RoborockDockDustCollectionModeCode.balanced, + RoborockDockDustCollectionModeCode.max, +] + def _supports_dust_collection_mode(dock_features: RoborockDockFeatures) -> bool: return dock_features.is_collectable class DustCollectionModeTrait(DustCollectionMode, common.V1TraitMixin): - """Trait for dust collection mode.""" + """Trait for the dock dust collection mode. + + ``set_dust_collection_mode`` changes how strongly the dock empties the + dustbin. ``start_dust_collection`` and ``stop_dust_collection`` control a + manual dust collection directly. + """ command = RoborockCommand.GET_DUST_COLLECTION_MODE converter = common.DefaultConverter(DustCollectionMode) requires_dock_features = _supports_dust_collection_mode + + @property + def dust_collection_mode_options(self) -> list[RoborockDockDustCollectionModeCode]: + """Return the dust collection modes offered by the app.""" + return list(_DUST_COLLECTION_MODE_OPTIONS) + + async def set_dust_collection_mode(self, mode: RoborockDockDustCollectionModeCode) -> None: + """Set the dust collection mode.""" + if mode not in self.dust_collection_mode_options: + raise RoborockUnsupportedFeature(f"Dust collection mode {mode.name} is not supported") + await self.rpc_channel.send_command(RoborockCommand.SET_DUST_COLLECTION_MODE, params={"mode": mode.value}) + # Optimistic update to avoid an extra refresh + self.mode = mode + + async def start_dust_collection(self) -> None: + """Start a manual dust collection.""" + await self.rpc_channel.send_command(RoborockCommand.APP_START_COLLECT_DUST) + + async def stop_dust_collection(self) -> None: + """Stop the running dust collection.""" + await self.rpc_channel.send_command(RoborockCommand.APP_STOP_COLLECT_DUST) diff --git a/tests/devices/traits/v1/test_dust_collection_mode.py b/tests/devices/traits/v1/test_dust_collection_mode.py index f8e847b4d..3255179df 100644 --- a/tests/devices/traits/v1/test_dust_collection_mode.py +++ b/tests/devices/traits/v1/test_dust_collection_mode.py @@ -7,6 +7,7 @@ from roborock.data import RoborockDockDustCollectionModeCode, RoborockDockTypeCode from roborock.devices.device import RoborockDevice from roborock.devices.traits.v1.dust_collection_mode import DustCollectionModeTrait +from roborock.exceptions import RoborockException, RoborockUnsupportedFeature from roborock.roborock_typing import RoborockCommand from tests.devices.traits.v1.helpers import dock_types_with_capability @@ -60,3 +61,131 @@ async def test_unsupported_dust_collection_mode( ) -> None: """Test that the trait is not available for unsupported dock types.""" assert dust_collection_mode is None + + +@pytest.mark.parametrize(("dock_type_code"), [RoborockDockTypeCode.o4_dock]) +async def test_options( + dust_collection_mode: DustCollectionModeTrait | None, + dock_type_code: RoborockDockTypeCode, +) -> None: + """Test the dust collection modes offered by the app.""" + assert dust_collection_mode is not None + assert dust_collection_mode.dust_collection_mode_options == [ + RoborockDockDustCollectionModeCode.smart, + RoborockDockDustCollectionModeCode.light, + RoborockDockDustCollectionModeCode.balanced, + RoborockDockDustCollectionModeCode.max, + ] + + +@pytest.mark.parametrize(("dock_type_code"), [RoborockDockTypeCode.o4_dock]) +@pytest.mark.parametrize( + ("mode"), + [ + RoborockDockDustCollectionModeCode.smart, + RoborockDockDustCollectionModeCode.light, + RoborockDockDustCollectionModeCode.balanced, + RoborockDockDustCollectionModeCode.max, + ], +) +async def test_set_dust_collection_mode( + dust_collection_mode: DustCollectionModeTrait | None, + mock_rpc_channel: AsyncMock, + dock_type_code: RoborockDockTypeCode, + mode: RoborockDockDustCollectionModeCode, +) -> None: + """Test setting an offered dust collection mode.""" + assert dust_collection_mode is not None + + await dust_collection_mode.set_dust_collection_mode(mode) + + mock_rpc_channel.send_command.assert_called_once_with( + RoborockCommand.SET_DUST_COLLECTION_MODE, params={"mode": mode.value} + ) + assert dust_collection_mode.mode == mode + + +@pytest.mark.parametrize(("dock_type_code"), [RoborockDockTypeCode.o4_dock]) +@pytest.mark.parametrize( + ("mode"), + [RoborockDockDustCollectionModeCode.strong, RoborockDockDustCollectionModeCode.unknown], +) +async def test_set_rejects_unoffered( + dust_collection_mode: DustCollectionModeTrait | None, + mock_rpc_channel: AsyncMock, + dock_type_code: RoborockDockTypeCode, + mode: RoborockDockDustCollectionModeCode, +) -> None: + """Test that a mode the app does not offer is rejected without sending.""" + assert dust_collection_mode is not None + + with pytest.raises(RoborockUnsupportedFeature): + await dust_collection_mode.set_dust_collection_mode(mode) + + mock_rpc_channel.send_command.assert_not_called() + + +@pytest.mark.parametrize(("dock_type_code"), [RoborockDockTypeCode.o4_dock]) +async def test_set_failure_keeps_state( + dust_collection_mode: DustCollectionModeTrait | None, + mock_rpc_channel: AsyncMock, + dock_type_code: RoborockDockTypeCode, +) -> None: + """Test that a failed command leaves the mode unchanged.""" + assert dust_collection_mode is not None + dust_collection_mode.mode = RoborockDockDustCollectionModeCode.max + mock_rpc_channel.send_command.side_effect = RoborockException("boom") + + with pytest.raises(RoborockException): + await dust_collection_mode.set_dust_collection_mode(RoborockDockDustCollectionModeCode.smart) + + assert dust_collection_mode.mode == RoborockDockDustCollectionModeCode.max + + +@pytest.mark.parametrize(("dock_type_code"), [RoborockDockTypeCode.o4_dock]) +@pytest.mark.parametrize( + ("response", "expected"), + [ + ({"mode": 4}, RoborockDockDustCollectionModeCode.max), # Saros 10R + ({"mode": 3}, RoborockDockDustCollectionModeCode.strong), + ({"mode": 5}, RoborockDockDustCollectionModeCode.unknown), + ], +) +async def test_refresh_unknown_code( + dust_collection_mode: DustCollectionModeTrait | None, + mock_rpc_channel: AsyncMock, + dock_type_code: RoborockDockTypeCode, + response: dict[str, int], + expected: RoborockDockDustCollectionModeCode, +) -> None: + """Test that refresh decodes known codes and tolerates unknown ones.""" + assert dust_collection_mode is not None + mock_rpc_channel.send_command.side_effect = [response] + + await dust_collection_mode.refresh() + + mock_rpc_channel.send_command.assert_called_once_with(RoborockCommand.GET_DUST_COLLECTION_MODE) + assert dust_collection_mode.mode == expected + + +@pytest.mark.parametrize(("dock_type_code"), [RoborockDockTypeCode.o4_dock]) +@pytest.mark.parametrize( + ("method_name", "command"), + [ + ("start_dust_collection", RoborockCommand.APP_START_COLLECT_DUST), + ("stop_dust_collection", RoborockCommand.APP_STOP_COLLECT_DUST), + ], +) +async def test_dust_collection_actions( + dust_collection_mode: DustCollectionModeTrait | None, + mock_rpc_channel: AsyncMock, + dock_type_code: RoborockDockTypeCode, + method_name: str, + command: RoborockCommand, +) -> None: + """Test starting and stopping a manual dust collection.""" + assert dust_collection_mode is not None + + await getattr(dust_collection_mode, method_name)() + + mock_rpc_channel.send_command.assert_called_once_with(command) From 3dfce1d50e748772fbb3fd9af1d4b0705a3e369b Mon Sep 17 00:00:00 2001 From: infy1995 Date: Thu, 12 Nov 2026 20:56:08 +0100 Subject: [PATCH 2/2] feat: add V1 dust collection frequency trait --- docs/V1_API_COMMANDS.md | 44 ++++++ roborock/data/v1/v1_code_mappings.py | 9 ++ roborock/data/v1/v1_containers.py | 8 + roborock/devices/traits/v1/__init__.py | 4 + .../traits/v1/dust_collection_frequency.py | 45 ++++++ .../v1/test_dust_collection_frequency.py | 144 ++++++++++++++++++ 6 files changed, 254 insertions(+) create mode 100644 roborock/devices/traits/v1/dust_collection_frequency.py create mode 100644 tests/devices/traits/v1/test_dust_collection_frequency.py diff --git a/docs/V1_API_COMMANDS.md b/docs/V1_API_COMMANDS.md index 5799774b3..0fc0bf9c1 100644 --- a/docs/V1_API_COMMANDS.md +++ b/docs/V1_API_COMMANDS.md @@ -32,6 +32,7 @@ Commands can have multiple parameters that can change from one model to another. * [get_customize_clean_mode](#get_customize_clean_mode) * [get_dnd_timer](#get_dnd_timer) * [get_dust_collection_mode](#get_dust_collection_mode) +* [get_dust_collection_switch_status](#get_dust_collection_switch_status) * [get_clean_follow_ground_material_status](#get_clean_follow_ground_material_status) * [get_identify_furniture_status](#get_identify_furniture_status) * [get_identify_ground_material_status](#get_identify_ground_material_status) @@ -66,6 +67,7 @@ Commands can have multiple parameters that can change from one model to another. * [set_customize_clean_mode](#set_customize_clean_mode) * [set_dnd_timer](#set_dnd_timer) * [set_dust_collection_mode](#set_dust_collection_mode) +* [set_dust_collection_switch_status](#set_dust_collection_switch_status) * [set_fds_endpoint](#set_fds_endpoint) * [set_identify_furniture_status](#set_identify_furniture_status) * [set_identify_ground_material_status](#set_identify_ground_material_status) @@ -1235,6 +1237,48 @@ Source: Roborock Saros 10R * Roborock Saros 10R: ✅ +### get_dust_collection_switch_status + +Description: Get how often the dock empties the dustbin automatically (app: auto-empty frequency). + +Parameters: None + +Returns: + + status: 0 off, 1 balanced, 2 high. Mirrors `auto_dust_collection` in `get_status`. + +Return example: + +```json + {'status': 2} +``` + +Source: Roborock Saros 10R + +**Supported devices:** + +* Roborock Saros 10R: ✅ + +### set_dust_collection_switch_status + +Description: Set how often the dock empties the dustbin automatically. + +Parameters: + + status: 0 off, 1 balanced, 2 high. + +Example: + +```json + {'status': 2} +``` + +Source: Roborock Saros 10R + +**Supported devices:** + +* Roborock Saros 10R: ✅ + ### get_wash_towel_mode Description: diff --git a/roborock/data/v1/v1_code_mappings.py b/roborock/data/v1/v1_code_mappings.py index d6a75405f..3bbd4ba64 100644 --- a/roborock/data/v1/v1_code_mappings.py +++ b/roborock/data/v1/v1_code_mappings.py @@ -405,6 +405,15 @@ class RoborockDockDustCollectionModeCode(RoborockEnum): max = 4 +class RoborockDockDustCollectionFrequencyCode(RoborockEnum): + """Describes how often the dock empties the dustbin automatically (app: auto-empty frequency).""" + + unknown = -1 + off = 0 + balanced = 1 + high = 2 + + class RoborockStateCode(RoborockEnum): unknown = 0 starting = 1 diff --git a/roborock/data/v1/v1_containers.py b/roborock/data/v1/v1_containers.py index 65bcba64a..3a42ca78e 100644 --- a/roborock/data/v1/v1_containers.py +++ b/roborock/data/v1/v1_containers.py @@ -27,6 +27,7 @@ DustBagStatus, RoborockChargeStatus, RoborockCleanType, + RoborockDockDustCollectionFrequencyCode, RoborockDockDustCollectionModeCode, RoborockDockErrorCode, RoborockDockState, @@ -510,6 +511,13 @@ class DustCollectionMode(RoborockBase): mode: RoborockDockDustCollectionModeCode | None = None +@dataclass +class DustCollectionFrequency(RoborockBase): + """Automatic dust collection frequency, as returned by GET_DUST_COLLECTION_SWITCH_STATUS.""" + + status: RoborockDockDustCollectionFrequencyCode | None = None + + @dataclass class WashTowelMode(RoborockBase): wash_mode: WashTowelModes | None = None diff --git a/roborock/devices/traits/v1/__init__.py b/roborock/devices/traits/v1/__init__.py index d8af621b2..6bf7f3c1b 100644 --- a/roborock/devices/traits/v1/__init__.py +++ b/roborock/devices/traits/v1/__init__.py @@ -77,6 +77,7 @@ customize_clean_mode, device_features, do_not_disturb, + dust_collection_frequency, dust_collection_mode, flow_led_status, home, @@ -102,6 +103,7 @@ from .customize_clean_mode import CustomizeCleanModeTrait from .device_features import DeviceFeaturesTrait from .do_not_disturb import DoNotDisturbTrait +from .dust_collection_frequency import DustCollectionFrequencyTrait from .dust_collection_mode import DustCollectionModeTrait from .flow_led_status import FlowLedStatusTrait from .home import HomeTrait @@ -131,6 +133,7 @@ "customize_clean_mode", "device_features", "do_not_disturb", + "dust_collection_frequency", "dust_collection_mode", "flow_led_status", "home", @@ -178,6 +181,7 @@ class PropertiesApi(Trait): flow_led_status: FlowLedStatusTrait | None = None valley_electricity_timer: ValleyElectricityTimerTrait | None = None dust_collection_mode: DustCollectionModeTrait | None = None + dust_collection_frequency: DustCollectionFrequencyTrait | None = None wash_towel_mode: WashTowelModeTrait | None = None smart_wash_params: SmartWashParamsTrait | None = None obstacle_photos: ObstaclePhotoTrait | None = None diff --git a/roborock/devices/traits/v1/dust_collection_frequency.py b/roborock/devices/traits/v1/dust_collection_frequency.py new file mode 100644 index 000000000..95f5943d5 --- /dev/null +++ b/roborock/devices/traits/v1/dust_collection_frequency.py @@ -0,0 +1,45 @@ +"""Trait for the automatic dust collection frequency.""" + +from roborock.data import DustCollectionFrequency, RoborockDockDustCollectionFrequencyCode +from roborock.device_features import RoborockDockFeatures +from roborock.devices.traits.v1 import common +from roborock.exceptions import RoborockUnsupportedFeature +from roborock.roborock_typing import RoborockCommand + +_FREQUENCY_OPTIONS = [ + RoborockDockDustCollectionFrequencyCode.off, + RoborockDockDustCollectionFrequencyCode.balanced, + RoborockDockDustCollectionFrequencyCode.high, +] + + +def _supports_dust_collection_frequency(dock_features: RoborockDockFeatures) -> bool: + return dock_features.is_collectable + + +class DustCollectionFrequencyTrait(DustCollectionFrequency, common.V1TraitMixin): + """Automatic dust collection frequency of the dock (app: auto-empty frequency). + + The same value is reported as ``auto_dust_collection`` on the device status, + which only picks up a change made here on its next refresh. Verified on the + Saros 10R; other collecting docks are assumed to answer the same command. + """ + + command = RoborockCommand.GET_DUST_COLLECTION_SWITCH_STATUS + converter = common.DefaultConverter(DustCollectionFrequency) + requires_dock_features = _supports_dust_collection_frequency + + @property + def frequency_options(self) -> list[RoborockDockDustCollectionFrequencyCode]: + """Return the auto-empty frequencies offered by the app.""" + return list(_FREQUENCY_OPTIONS) + + async def set_frequency(self, frequency: RoborockDockDustCollectionFrequencyCode) -> None: + """Set the automatic dust collection frequency.""" + if frequency not in self.frequency_options: + raise RoborockUnsupportedFeature(f"Dust collection frequency {frequency.name} is not supported") + await self.rpc_channel.send_command( + RoborockCommand.SET_DUST_COLLECTION_SWITCH_STATUS, params={"status": frequency.value} + ) + # Optimistic update to avoid an extra refresh + self.status = frequency diff --git a/tests/devices/traits/v1/test_dust_collection_frequency.py b/tests/devices/traits/v1/test_dust_collection_frequency.py new file mode 100644 index 000000000..bd624b283 --- /dev/null +++ b/tests/devices/traits/v1/test_dust_collection_frequency.py @@ -0,0 +1,144 @@ +"""Tests for the DustCollectionFrequencyTrait class.""" + +from unittest.mock import AsyncMock + +import pytest + +from roborock.data import RoborockDockDustCollectionFrequencyCode, RoborockDockTypeCode +from roborock.devices.device import RoborockDevice +from roborock.devices.traits.v1.dust_collection_frequency import DustCollectionFrequencyTrait +from roborock.exceptions import RoborockException, RoborockUnsupportedFeature +from roborock.roborock_typing import RoborockCommand +from tests.devices.traits.v1.helpers import dock_types_with_capability + + +@pytest.fixture(name="dust_collection_frequency") +def dust_collection_frequency_trait( + device: RoborockDevice, + discover_features_fixture: None, +) -> DustCollectionFrequencyTrait | None: + """Create a DustCollectionFrequencyTrait instance with mocked dependencies.""" + assert device.v1_properties + return device.v1_properties.dust_collection_frequency + + +@pytest.mark.parametrize( + ("dock_type_code"), + dock_types_with_capability("is_collectable"), +) +async def test_dust_collection_frequency_available( + dust_collection_frequency: DustCollectionFrequencyTrait | None, + dock_type_code: RoborockDockTypeCode, +) -> None: + """Test that the trait is available for collecting docks.""" + assert dust_collection_frequency is not None + + +@pytest.mark.parametrize( + ("dock_type_code"), + dock_types_with_capability("is_collectable", expected=False), +) +async def test_unsupported_dust_collection_frequency( + dust_collection_frequency: DustCollectionFrequencyTrait | None, + dock_type_code: RoborockDockTypeCode, +) -> None: + """Test that the trait is not available for unsupported dock types.""" + assert dust_collection_frequency is None + + +@pytest.mark.parametrize(("dock_type_code"), [RoborockDockTypeCode.o4_dock]) +@pytest.mark.parametrize( + ("response", "expected"), + [ + ({"status": 2}, RoborockDockDustCollectionFrequencyCode.high), # Saros 10R + ({"status": 0}, RoborockDockDustCollectionFrequencyCode.off), + ({"status": 7}, RoborockDockDustCollectionFrequencyCode.unknown), + ], +) +async def test_refresh_unknown_code( + dust_collection_frequency: DustCollectionFrequencyTrait | None, + mock_rpc_channel: AsyncMock, + dock_type_code: RoborockDockTypeCode, + response: dict[str, int], + expected: RoborockDockDustCollectionFrequencyCode, +) -> None: + """Test that refresh decodes known codes and tolerates unknown ones.""" + assert dust_collection_frequency is not None + mock_rpc_channel.send_command.side_effect = [response] + + await dust_collection_frequency.refresh() + + mock_rpc_channel.send_command.assert_called_once_with(RoborockCommand.GET_DUST_COLLECTION_SWITCH_STATUS) + assert dust_collection_frequency.status == expected + + +@pytest.mark.parametrize(("dock_type_code"), [RoborockDockTypeCode.o4_dock]) +async def test_options( + dust_collection_frequency: DustCollectionFrequencyTrait | None, + dock_type_code: RoborockDockTypeCode, +) -> None: + """Test the auto-empty frequencies offered by the app.""" + assert dust_collection_frequency is not None + assert dust_collection_frequency.frequency_options == [ + RoborockDockDustCollectionFrequencyCode.off, + RoborockDockDustCollectionFrequencyCode.balanced, + RoborockDockDustCollectionFrequencyCode.high, + ] + + +@pytest.mark.parametrize(("dock_type_code"), [RoborockDockTypeCode.o4_dock]) +@pytest.mark.parametrize( + ("frequency"), + [ + RoborockDockDustCollectionFrequencyCode.off, + RoborockDockDustCollectionFrequencyCode.balanced, + RoborockDockDustCollectionFrequencyCode.high, + ], +) +async def test_set_frequency( + dust_collection_frequency: DustCollectionFrequencyTrait | None, + mock_rpc_channel: AsyncMock, + dock_type_code: RoborockDockTypeCode, + frequency: RoborockDockDustCollectionFrequencyCode, +) -> None: + """Test setting an offered auto-empty frequency.""" + assert dust_collection_frequency is not None + + await dust_collection_frequency.set_frequency(frequency) + + mock_rpc_channel.send_command.assert_called_once_with( + RoborockCommand.SET_DUST_COLLECTION_SWITCH_STATUS, params={"status": frequency.value} + ) + assert dust_collection_frequency.status == frequency + + +@pytest.mark.parametrize(("dock_type_code"), [RoborockDockTypeCode.o4_dock]) +async def test_set_rejects_unoffered( + dust_collection_frequency: DustCollectionFrequencyTrait | None, + mock_rpc_channel: AsyncMock, + dock_type_code: RoborockDockTypeCode, +) -> None: + """Test that a frequency the app does not offer is rejected without sending.""" + assert dust_collection_frequency is not None + + with pytest.raises(RoborockUnsupportedFeature): + await dust_collection_frequency.set_frequency(RoborockDockDustCollectionFrequencyCode.unknown) + + mock_rpc_channel.send_command.assert_not_called() + + +@pytest.mark.parametrize(("dock_type_code"), [RoborockDockTypeCode.o4_dock]) +async def test_set_failure_keeps_state( + dust_collection_frequency: DustCollectionFrequencyTrait | None, + mock_rpc_channel: AsyncMock, + dock_type_code: RoborockDockTypeCode, +) -> None: + """Test that a failed command leaves the frequency unchanged.""" + assert dust_collection_frequency is not None + dust_collection_frequency.status = RoborockDockDustCollectionFrequencyCode.high + mock_rpc_channel.send_command.side_effect = RoborockException("boom") + + with pytest.raises(RoborockException): + await dust_collection_frequency.set_frequency(RoborockDockDustCollectionFrequencyCode.off) + + assert dust_collection_frequency.status == RoborockDockDustCollectionFrequencyCode.high