-
Notifications
You must be signed in to change notification settings - Fork 52
feat: Add v1 rooms support to the device traits API #516
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
93ee0a8
feat: Add v1 rooms support to the device traits API
allenporter d9cb16e
chore: Switch the rooms trait back to the local API
allenporter eded27d
chore: fix typing
allenporter a91c708
fix: fix room mapping parsing bug and add addtiional format samples
allenporter c2fe37f
chore: add additional example room mapping
allenporter 3b36cef
fix: update test
allenporter a4b3b01
chore: abort bad merges
allenporter 5ce28ce
chore: fix lint errors
allenporter 6c2a220
chore: fix lint errors
allenporter File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| """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 | ||
| """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} | ||
|
|
||
|
|
||
| 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 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] | ||
| 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 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| """Tests for the RoomMapping related functionality.""" | ||
|
|
||
| from typing import Any | ||
| 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 | ||
|
|
||
|
|
||
| @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 | ||
|
|
||
|
|
||
| # 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] | ||
| # 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_rpc_channel.send_command.call_count == 1 | ||
| mock_rpc_channel.send_command.assert_any_call(RoborockCommand.GET_ROOM_MAPPING) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| {"t":1759590351,"dps":{"102":"{\"id\":20001,\"result\":[[16,\"3031886\"],[17,\"3031880\"],[18,\"3031883\"]]}"}} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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]]}"}} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.