From 93ee0a851fde4c2267c46d18407d22c5ce7fd7b3 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 4 Oct 2025 08:15:48 -0700 Subject: [PATCH 1/9] feat: Add v1 rooms support to the device traits API --- roborock/cli.py | 11 +++ roborock/devices/device_manager.py | 8 +- roborock/devices/traits/v1/__init__.py | 15 ++- roborock/devices/traits/v1/rooms.py | 94 +++++++++++++++++++ tests/devices/test_v1_device.py | 2 +- tests/devices/traits/v1/fixtures.py | 2 +- tests/devices/traits/v1/test_rooms.py | 68 ++++++++++++++ .../__snapshots__/test_v1_protocol.ambr | 21 +++++ .../v1_protocol/get_room_mapping.json | 1 + 9 files changed, 212 insertions(+), 10 deletions(-) create mode 100644 roborock/devices/traits/v1/rooms.py create mode 100644 tests/devices/traits/v1/test_rooms.py create mode 100644 tests/protocols/testdata/v1_protocol/get_room_mapping.json diff --git a/roborock/cli.py b/roborock/cli.py index d98582df..8ceb1d51 100644 --- a/roborock/cli.py +++ b/roborock/cli.py @@ -475,6 +475,16 @@ async def reset_consumable(ctx, device_id: str, consumable: str): click.echo(f"Reset {consumable} for device {device_id}") +@session.command() +@click.option("--device_id", required=True) +@click.pass_context +@async_command +async def rooms(ctx, device_id: str): + """Get device room mapping info.""" + context: RoborockContext = ctx.obj + await _display_v1_trait(context, device_id, lambda v1: v1.rooms) + + @click.command() @click.option("--device_id", required=True) @click.option("--cmd", required=True) @@ -719,6 +729,7 @@ def write_markdown_table(product_features: dict[str, dict[str, any]], all_featur cli.add_command(maps) cli.add_command(consumables) cli.add_command(reset_consumable) +cli.add_command(rooms) def main(): diff --git a/roborock/devices/device_manager.py b/roborock/devices/device_manager.py index 0361c8cc..b01bb671 100644 --- a/roborock/devices/device_manager.py +++ b/roborock/devices/device_manager.py @@ -35,7 +35,7 @@ HomeDataApi = Callable[[], Awaitable[HomeData]] -DeviceCreator = Callable[[HomeDataDevice, HomeDataProduct], RoborockDevice] +DeviceCreator = Callable[[HomeData, HomeDataDevice, HomeDataProduct], RoborockDevice] class DeviceVersion(enum.StrEnum): @@ -84,7 +84,7 @@ async def discover_devices(self) -> list[RoborockDevice]: for duid, (device, product) in device_products.items(): if duid in self._devices: continue - new_device = self._device_creator(device, product) + new_device = self._device_creator(home_data, device, product) await new_device.connect() new_devices[duid] = new_device @@ -143,13 +143,13 @@ async def create_device_manager( mqtt_params = create_mqtt_params(user_data.rriot) mqtt_session = await create_lazy_mqtt_session(mqtt_params) - def device_creator(device: HomeDataDevice, product: HomeDataProduct) -> RoborockDevice: + def device_creator(home_data: HomeData, device: HomeDataDevice, product: HomeDataProduct) -> RoborockDevice: channel: Channel trait: Trait match device.pv: case DeviceVersion.V1: channel = create_v1_channel(user_data, mqtt_params, mqtt_session, device, cache) - trait = v1.create(product, channel.rpc_channel, channel.mqtt_rpc_channel) + trait = v1.create(product, home_data, channel.rpc_channel, channel.mqtt_rpc_channel) case DeviceVersion.A01: channel = create_mqtt_channel(user_data, mqtt_params, mqtt_session, device) trait = a01.create(product, channel) diff --git a/roborock/devices/traits/v1/__init__.py b/roborock/devices/traits/v1/__init__.py index 7edf5f43..065d4744 100644 --- a/roborock/devices/traits/v1/__init__.py +++ b/roborock/devices/traits/v1/__init__.py @@ -12,6 +12,7 @@ from .consumeable import ConsumableTrait from .do_not_disturb import DoNotDisturbTrait from .maps import MapsTrait +from .rooms import RoomsTrait from .status import StatusTrait from .volume import SoundVolumeTrait @@ -41,14 +42,18 @@ class PropertiesApi(Trait): dnd: DoNotDisturbTrait clean_summary: CleanSummaryTrait sound_volume: SoundVolumeTrait + rooms: RoomsTrait maps: MapsTrait consumables: ConsumableTrait # In the future optional fields can be added below based on supported features - def __init__(self, product: HomeDataProduct, rpc_channel: V1RpcChannel, mqtt_rpc_channel: V1RpcChannel) -> None: - """Initialize the V1TraitProps with None values.""" + def __init__( + self, product: HomeDataProduct, home_data: HomeData, rpc_channel: V1RpcChannel, mqtt_rpc_channel: V1RpcChannel + ) -> None: + """Initialize the V1TraitProps.""" self.status = StatusTrait(product) + self.rooms = RoomsTrait(home_data) self.maps = MapsTrait(self.status) # This is a hack to allow setting the rpc_channel on all traits. This is @@ -67,6 +72,8 @@ def __init__(self, product: HomeDataProduct, rpc_channel: V1RpcChannel, mqtt_rpc trait._rpc_channel = rpc_channel -def create(product: HomeDataProduct, rpc_channel: V1RpcChannel, mqtt_rpc_channel: V1RpcChannel) -> PropertiesApi: +def create( + product: HomeDataProduct, home_data: HomeData, rpc_channel: V1RpcChannel, mqtt_rpc_channel: V1RpcChannel +) -> PropertiesApi: """Create traits for V1 devices.""" - return PropertiesApi(product, rpc_channel, mqtt_rpc_channel) + return PropertiesApi(product, home_data, rpc_channel, mqtt_rpc_channel) diff --git a/roborock/devices/traits/v1/rooms.py b/roborock/devices/traits/v1/rooms.py new file mode 100644 index 00000000..47d0020e --- /dev/null +++ b/roborock/devices/traits/v1/rooms.py @@ -0,0 +1,94 @@ +"""Trait for managing room mappings on Roborock devices.""" + +import logging +from dataclasses import dataclass + +from roborock.containers import HomeData, RoborockBase, RoomMapping +from roborock.devices.traits.v1 import common +from roborock.roborock_typing import RoborockCommand + +_LOGGER = logging.getLogger(__name__) + +_DEFAULT_NAME = "Unknown" + + +@dataclass +class NamedRoomMapping(RoomMapping): + """Dataclass representing a mapping of a room segment to a name. + + The name information is not provided by the device directly, but is provided + from the HomeData based on the iot_id from the room. + """ + + name: str | None = None + """The human-readable name of the room, if available.""" + + +@dataclass +class Rooms(RoborockBase): + """Dataclass representing a collection of room mappings.""" + + rooms: list[NamedRoomMapping] | None = None + """List of room mappings.""" + + @property + def room_map(self) -> dict[int, NamedRoomMapping]: + """Returns a mapping of segment_id to NamedRoomMapping.""" + if self.rooms is None: + return {} + return {room.segment_id: room for room in self.rooms} + + +@common.mqtt_rpc_channel +class RoomsTrait(Rooms, common.V1TraitMixin): + """Trait for managing the room mappings of Roborock devices.""" + + command = RoborockCommand.GET_ROOM_MAPPING + + def __init__(self, home_data: HomeData) -> None: + """Initialize the RoomsTrait.""" + super().__init__() + self._home_data = home_data + + @property + def _iot_id_room_name_map(self) -> dict[str, str]: + """Returns a dictionary of Room IOT IDs to room names.""" + return {str(room.id): room.name for room in self._home_data.rooms or ()} + + def _parse_response(self, response: common.V1ResponseData) -> Rooms: + """Parse the response from the device into a list of NamedRoomMapping.""" + if not isinstance(response, list): + raise ValueError(f"Unexpected RoomsTrait response format: {response!r}") + name_map = self._iot_id_room_name_map + segment_pairs = _extract_segment_pairs(response) + return Rooms( + rooms=[ + NamedRoomMapping(segment_id=segment_id, iot_id=iot_id, name=name_map.get(iot_id, _DEFAULT_NAME)) + for segment_id, iot_id in segment_pairs + ] + ) + + +def _extract_segment_pairs(response: list) -> list[tuple[int, str]]: + """Extract segment_id and iot_id pairs from the response. + + The response format can be either a flat list of [segment_id, iot_id] or a + list of lists, where each inner list is a pair of [segment_id, iot_id]. This + function normalizes the response into a list of (segment_id, iot_id) tuples + + NOTE: We currently only have samples of the list of lists format in + tests/protocols/testdata so improving test coverage with samples from a real + device with this format would be helpful. + """ + if len(response) == 2 and not isinstance(response[0], list): + segment_id, iot_id = response[0], response[1] + return [(segment_id, iot_id)] + + segment_pairs: list[tuple[int, str]] = [] + for part in response: + if not isinstance(part, list) or len(part) != 2: + _LOGGER.warning("Unexpected room mapping entry format: %r", part) + continue + segment_id, iot_id = part[0], part[1] + segment_pairs.append((segment_id, iot_id)) + return segment_pairs diff --git a/tests/devices/test_v1_device.py b/tests/devices/test_v1_device.py index 30515b48..3628772f 100644 --- a/tests/devices/test_v1_device.py +++ b/tests/devices/test_v1_device.py @@ -47,7 +47,7 @@ def device_fixture(channel: AsyncMock, rpc_channel: AsyncMock, mqtt_rpc_channel: return RoborockDevice( device_info=HOME_DATA.devices[0], channel=channel, - trait=v1.create(HOME_DATA.products[0], rpc_channel, mqtt_rpc_channel), + trait=v1.create(HOME_DATA.products[0], HOME_DATA, rpc_channel, mqtt_rpc_channel), ) diff --git a/tests/devices/traits/v1/fixtures.py b/tests/devices/traits/v1/fixtures.py index 8def9231..431741e0 100644 --- a/tests/devices/traits/v1/fixtures.py +++ b/tests/devices/traits/v1/fixtures.py @@ -39,5 +39,5 @@ def device_fixture(channel: AsyncMock, mock_rpc_channel: AsyncMock, mock_mqtt_rp return RoborockDevice( device_info=HOME_DATA.devices[0], channel=channel, - trait=v1.create(HOME_DATA.products[0], mock_rpc_channel, mock_mqtt_rpc_channel), + trait=v1.create(HOME_DATA.products[0], HOME_DATA, mock_rpc_channel, mock_mqtt_rpc_channel), ) diff --git a/tests/devices/traits/v1/test_rooms.py b/tests/devices/traits/v1/test_rooms.py new file mode 100644 index 00000000..76244cb3 --- /dev/null +++ b/tests/devices/traits/v1/test_rooms.py @@ -0,0 +1,68 @@ +"""Tests for the RoomMapping related functionality.""" + +from unittest.mock import AsyncMock + +import pytest + +from roborock.devices.device import RoborockDevice +from roborock.devices.traits.v1.rooms import RoomsTrait +from roborock.devices.traits.v1.status import StatusTrait +from roborock.roborock_typing import RoborockCommand + +# Rooms from mock_data.HOME_DATA +# {"id": 2362048, "name": "Example room 1"}, +# {"id": 2362044, "name": "Example room 2"}, +# {"id": 2362041, "name": "Example room 3"}, +ROOM_MAPPING_DATA = [[16, "2362048"], [17, "2362044"], [18, "2362041"]] + + +@pytest.fixture +def status_trait(device: RoborockDevice) -> StatusTrait: + """Create a StatusTrait instance with mocked dependencies.""" + assert device.v1_properties + return device.v1_properties.status + + +@pytest.fixture +def rooms_trait(device: RoborockDevice) -> RoomsTrait: + """Create a RoomsTrait instance with mocked dependencies.""" + assert device.v1_properties + return device.v1_properties.rooms + + +async def test_refresh_rooms_trait( + rooms_trait: RoomsTrait, + mock_rpc_channel: AsyncMock, + mock_mqtt_rpc_channel: AsyncMock, +) -> None: + """Test successfully getting room mapping.""" + # Setup mock to return the sample room mapping + mock_mqtt_rpc_channel.send_command.side_effect = [ + ROOM_MAPPING_DATA, + ] + # Before refresh, rooms should be empty + assert not rooms_trait.rooms + + # Load the room mapping information + refreshed_trait = await rooms_trait.refresh() + + # Verify the room mappings are now populated + assert refreshed_trait.rooms + rooms = refreshed_trait.rooms + assert len(rooms) == 3 + + assert rooms[0].segment_id == 16 + assert rooms[0].name == "Example room 1" + assert rooms[0].iot_id == "2362048" + + assert rooms[1].segment_id == 17 + assert rooms[1].name == "Example room 2" + assert rooms[1].iot_id == "2362044" + + assert rooms[2].segment_id == 18 + assert rooms[2].name == "Example room 3" + assert rooms[2].iot_id == "2362041" + + # Verify the RPC call was made correctly + assert mock_mqtt_rpc_channel.send_command.call_count == 1 + mock_mqtt_rpc_channel.send_command.assert_any_call(RoborockCommand.GET_ROOM_MAPPING) diff --git a/tests/protocols/__snapshots__/test_v1_protocol.ambr b/tests/protocols/__snapshots__/test_v1_protocol.ambr index 0b736df6..ab486230 100644 --- a/tests/protocols/__snapshots__/test_v1_protocol.ambr +++ b/tests/protocols/__snapshots__/test_v1_protocol.ambr @@ -93,6 +93,27 @@ ] ''' # --- +# name: test_decode_rpc_payload[get_room_mapping] + 20001 +# --- +# name: test_decode_rpc_payload[get_room_mapping].1 + ''' + [ + [ + 16, + "3031886" + ], + [ + 17, + "3031880" + ], + [ + 18, + "3031883" + ] + ] + ''' +# --- # name: test_decode_rpc_payload[get_status] 20001 # --- diff --git a/tests/protocols/testdata/v1_protocol/get_room_mapping.json b/tests/protocols/testdata/v1_protocol/get_room_mapping.json new file mode 100644 index 00000000..ef363f0c --- /dev/null +++ b/tests/protocols/testdata/v1_protocol/get_room_mapping.json @@ -0,0 +1 @@ +{"t":1759590351,"dps":{"102":"{\"id\":20001,\"result\":[[16,\"3031886\"],[17,\"3031880\"],[18,\"3031883\"]]}"}} From d9cb16e861f5f8131e6c4238e681020b8b5b84f4 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 4 Oct 2025 08:18:39 -0700 Subject: [PATCH 2/9] chore: Switch the rooms trait back to the local API --- roborock/devices/traits/v1/rooms.py | 1 - tests/devices/traits/v1/test_rooms.py | 7 +++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/roborock/devices/traits/v1/rooms.py b/roborock/devices/traits/v1/rooms.py index 47d0020e..b3e160e7 100644 --- a/roborock/devices/traits/v1/rooms.py +++ b/roborock/devices/traits/v1/rooms.py @@ -39,7 +39,6 @@ def room_map(self) -> dict[int, NamedRoomMapping]: return {room.segment_id: room for room in self.rooms} -@common.mqtt_rpc_channel class RoomsTrait(Rooms, common.V1TraitMixin): """Trait for managing the room mappings of Roborock devices.""" diff --git a/tests/devices/traits/v1/test_rooms.py b/tests/devices/traits/v1/test_rooms.py index 76244cb3..e2eb9c32 100644 --- a/tests/devices/traits/v1/test_rooms.py +++ b/tests/devices/traits/v1/test_rooms.py @@ -33,11 +33,10 @@ def rooms_trait(device: RoborockDevice) -> RoomsTrait: async def test_refresh_rooms_trait( rooms_trait: RoomsTrait, mock_rpc_channel: AsyncMock, - mock_mqtt_rpc_channel: AsyncMock, ) -> None: """Test successfully getting room mapping.""" # Setup mock to return the sample room mapping - mock_mqtt_rpc_channel.send_command.side_effect = [ + mock_rpc_channel.send_command.side_effect = [ ROOM_MAPPING_DATA, ] # Before refresh, rooms should be empty @@ -64,5 +63,5 @@ async def test_refresh_rooms_trait( assert rooms[2].iot_id == "2362041" # Verify the RPC call was made correctly - assert mock_mqtt_rpc_channel.send_command.call_count == 1 - mock_mqtt_rpc_channel.send_command.assert_any_call(RoborockCommand.GET_ROOM_MAPPING) + assert mock_rpc_channel.send_command.call_count == 1 + mock_rpc_channel.send_command.assert_any_call(RoborockCommand.GET_ROOM_MAPPING) From eded27d8b5d4ff42c475ff0ab1b94ceab268ea32 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 5 Oct 2025 10:43:08 -0700 Subject: [PATCH 3/9] chore: fix typing --- roborock/devices/traits/v1/rooms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roborock/devices/traits/v1/rooms.py b/roborock/devices/traits/v1/rooms.py index b3e160e7..42c73949 100644 --- a/roborock/devices/traits/v1/rooms.py +++ b/roborock/devices/traits/v1/rooms.py @@ -20,7 +20,7 @@ class NamedRoomMapping(RoomMapping): from the HomeData based on the iot_id from the room. """ - name: str | None = None + name: str """The human-readable name of the room, if available.""" From a91c708ae889ca0ddc168d6fb090777c304baf29 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 5 Oct 2025 10:50:21 -0700 Subject: [PATCH 4/9] fix: fix room mapping parsing bug and add addtiional format samples --- roborock/devices/traits/v1/rooms.py | 8 ++++---- tests/devices/traits/v1/test_rooms.py | 23 ++++++++++++++--------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/roborock/devices/traits/v1/rooms.py b/roborock/devices/traits/v1/rooms.py index 42c73949..b4b36f92 100644 --- a/roborock/devices/traits/v1/rooms.py +++ b/roborock/devices/traits/v1/rooms.py @@ -75,9 +75,9 @@ def _extract_segment_pairs(response: list) -> list[tuple[int, str]]: list of lists, where each inner list is a pair of [segment_id, iot_id]. This function normalizes the response into a list of (segment_id, iot_id) tuples - NOTE: We currently only have samples of the list of lists format in - tests/protocols/testdata so improving test coverage with samples from a real - device with this format would be helpful. + NOTE: We currently only partial samples of the room mapping formats, so + improving test coverage with samples from a real device with this format + would be helpful. """ if len(response) == 2 and not isinstance(response[0], list): segment_id, iot_id = response[0], response[1] @@ -85,7 +85,7 @@ def _extract_segment_pairs(response: list) -> list[tuple[int, str]]: segment_pairs: list[tuple[int, str]] = [] for part in response: - if not isinstance(part, list) or len(part) != 2: + if not isinstance(part, list) or len(part) < 2: _LOGGER.warning("Unexpected room mapping entry format: %r", part) continue segment_id, iot_id = part[0], part[1] diff --git a/tests/devices/traits/v1/test_rooms.py b/tests/devices/traits/v1/test_rooms.py index e2eb9c32..6e02365d 100644 --- a/tests/devices/traits/v1/test_rooms.py +++ b/tests/devices/traits/v1/test_rooms.py @@ -3,18 +3,13 @@ from unittest.mock import AsyncMock import pytest +from typing import Any from roborock.devices.device import RoborockDevice from roborock.devices.traits.v1.rooms import RoomsTrait from roborock.devices.traits.v1.status import StatusTrait from roborock.roborock_typing import RoborockCommand -# Rooms from mock_data.HOME_DATA -# {"id": 2362048, "name": "Example room 1"}, -# {"id": 2362044, "name": "Example room 2"}, -# {"id": 2362041, "name": "Example room 3"}, -ROOM_MAPPING_DATA = [[16, "2362048"], [17, "2362044"], [18, "2362041"]] - @pytest.fixture def status_trait(device: RoborockDevice) -> StatusTrait: @@ -30,15 +25,25 @@ def rooms_trait(device: RoborockDevice) -> RoomsTrait: return device.v1_properties.rooms +# Rooms from mock_data.HOME_DATA +# {"id": 2362048, "name": "Example room 1"}, +# {"id": 2362044, "name": "Example room 2"}, +# {"id": 2362041, "name": "Example room 3"}, +@pytest.mark.parametrize( + ("room_mapping_data"), + [ + ([[16, "2362048"], [17, "2362044"], [18, "2362041"]]), + ([[16, "2362048", 6], [17, "2362044", 14], [18, "2362041", 13]]), + ] +) async def test_refresh_rooms_trait( rooms_trait: RoomsTrait, mock_rpc_channel: AsyncMock, + room_mapping_data: list[Any], ) -> None: """Test successfully getting room mapping.""" # Setup mock to return the sample room mapping - mock_rpc_channel.send_command.side_effect = [ - ROOM_MAPPING_DATA, - ] + mock_rpc_channel.send_command.side_effect = [room_mapping_data] # Before refresh, rooms should be empty assert not rooms_trait.rooms From c2fe37fab9bd1a3305f6dc711d405a870879d5c9 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 5 Oct 2025 10:55:34 -0700 Subject: [PATCH 5/9] chore: add additional example room mapping --- .../__snapshots__/test_v1_protocol.ambr | 44 +++++++++++++++++++ .../v1_protocol/get_room_mapping2.json | 1 + 2 files changed, 45 insertions(+) create mode 100644 tests/protocols/testdata/v1_protocol/get_room_mapping2.json diff --git a/tests/protocols/__snapshots__/test_v1_protocol.ambr b/tests/protocols/__snapshots__/test_v1_protocol.ambr index ab486230..95337182 100644 --- a/tests/protocols/__snapshots__/test_v1_protocol.ambr +++ b/tests/protocols/__snapshots__/test_v1_protocol.ambr @@ -93,6 +93,50 @@ ] ''' # --- +# name: test_decode_rpc_payload[get_room_mapping2] + 20001 +# --- +# name: test_decode_rpc_payload[get_room_mapping2].1 + ''' + [ + [ + 16, + "2537178", + 6 + ], + [ + 17, + "2537175", + 14 + ], + [ + 18, + "2537174", + 13 + ], + [ + 19, + "2537176", + 14 + ], + [ + 20, + "10655627", + 12 + ], + [ + 21, + "2537145", + 2 + ], + [ + 22, + "2537147", + 12 + ] + ] + ''' +# --- # name: test_decode_rpc_payload[get_room_mapping] 20001 # --- diff --git a/tests/protocols/testdata/v1_protocol/get_room_mapping2.json b/tests/protocols/testdata/v1_protocol/get_room_mapping2.json new file mode 100644 index 00000000..49fd1195 --- /dev/null +++ b/tests/protocols/testdata/v1_protocol/get_room_mapping2.json @@ -0,0 +1 @@ +{"t":1759590351,"dps":{"102":"{\"id\":20001,\"result\":[[16, \"2537178\", 6], [17, \"2537175\", 14], [18, \"2537174\", 13], [19, \"2537176\", 14], [20, \"10655627\", 12], [21, \"2537145\", 2], [22, \"2537147\", 12]]}"}} From 3b36cefe998a815b8e09c47ec1b69b5efd082155 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 5 Oct 2025 10:59:29 -0700 Subject: [PATCH 6/9] fix: update test --- tests/protocols/test_v1_protocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/protocols/test_v1_protocol.py b/tests/protocols/test_v1_protocol.py index 554e65f6..455bf2ae 100644 --- a/tests/protocols/test_v1_protocol.py +++ b/tests/protocols/test_v1_protocol.py @@ -209,7 +209,7 @@ def test_create_map_response_decoder_invalid_endpoint(caplog: pytest.LogCaptureF decoder = create_map_response_decoder(SECURITY_DATA) assert decoder(message) is None - assert "Received map response requested not made by this device, ignoring." in caplog.text + assert "Received map response not requested by this device, ignoring." in caplog.text def test_create_map_response_decoder_invalid_payload(): From a4b3b0184fbe1dd3db3c50d022d71aeabb774070 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 5 Oct 2025 11:17:07 -0700 Subject: [PATCH 7/9] chore: abort bad merges --- roborock/devices/traits/v1/common.py | 4 ++-- tests/protocols/test_v1_protocol.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/roborock/devices/traits/v1/common.py b/roborock/devices/traits/v1/common.py index fc13838f..e02ec873 100644 --- a/roborock/devices/traits/v1/common.py +++ b/roborock/devices/traits/v1/common.py @@ -38,7 +38,7 @@ class V1TraitMixin(ABC): command: ClassVar[RoborockCommand] @classmethod - def _parse_type_response(cls, response: V1ResponseData) -> Self: + def _parse_type_response(cls, response: V1ResponseData) -> RoborockBase: """Parse the response from the device into a a RoborockBase. Subclasses should override this method to implement custom parsing @@ -53,7 +53,7 @@ def _parse_type_response(cls, response: V1ResponseData) -> Self: raise ValueError(f"Unexpected {cls} response format: {response!r}") return cls.from_dict(response) - def _parse_response(self, response: V1ResponseData) -> Self: + def _parse_response(self, response: V1ResponseData) -> RoborockBase: """Parse the response from the device into a a RoborockBase. This is used by subclasses that want to override the class diff --git a/tests/protocols/test_v1_protocol.py b/tests/protocols/test_v1_protocol.py index 455bf2ae..554e65f6 100644 --- a/tests/protocols/test_v1_protocol.py +++ b/tests/protocols/test_v1_protocol.py @@ -209,7 +209,7 @@ def test_create_map_response_decoder_invalid_endpoint(caplog: pytest.LogCaptureF decoder = create_map_response_decoder(SECURITY_DATA) assert decoder(message) is None - assert "Received map response not requested by this device, ignoring." in caplog.text + assert "Received map response requested not made by this device, ignoring." in caplog.text def test_create_map_response_decoder_invalid_payload(): From 5ce28ce452897d4aa9850d8a55996e159715b18b Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 5 Oct 2025 11:18:41 -0700 Subject: [PATCH 8/9] chore: fix lint errors --- tests/devices/traits/v1/test_rooms.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/devices/traits/v1/test_rooms.py b/tests/devices/traits/v1/test_rooms.py index 6e02365d..320a916a 100644 --- a/tests/devices/traits/v1/test_rooms.py +++ b/tests/devices/traits/v1/test_rooms.py @@ -3,6 +3,7 @@ from unittest.mock import AsyncMock import pytest + from typing import Any from roborock.devices.device import RoborockDevice From 6c2a220e537572969a297c0ab45a1e1d7d8224b4 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 5 Oct 2025 11:18:49 -0700 Subject: [PATCH 9/9] chore: fix lint errors --- tests/devices/traits/v1/test_rooms.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/devices/traits/v1/test_rooms.py b/tests/devices/traits/v1/test_rooms.py index 320a916a..271ecd61 100644 --- a/tests/devices/traits/v1/test_rooms.py +++ b/tests/devices/traits/v1/test_rooms.py @@ -1,11 +1,10 @@ """Tests for the RoomMapping related functionality.""" +from typing import Any from unittest.mock import AsyncMock import pytest -from typing import Any - from roborock.devices.device import RoborockDevice from roborock.devices.traits.v1.rooms import RoomsTrait from roborock.devices.traits.v1.status import StatusTrait @@ -35,7 +34,7 @@ def rooms_trait(device: RoborockDevice) -> RoomsTrait: [ ([[16, "2362048"], [17, "2362044"], [18, "2362041"]]), ([[16, "2362048", 6], [17, "2362044", 14], [18, "2362041", 13]]), - ] + ], ) async def test_refresh_rooms_trait( rooms_trait: RoomsTrait,