Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion roborock/devices/traits/v1/home.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ async def discover_home(self) -> None:
self._discovery_completed = True
try:
self._home_map_content = {
k: self._map_content.converter.parse_map_content(base64.b64decode(v))
k: await self._map_content.converter.async_parse_map_content(base64.b64decode(v))
for k, v in (device_cache_data.home_map_content_base64 or {}).items()
}
except (ValueError, RoborockException) as ex:
Expand Down
34 changes: 34 additions & 0 deletions roborock/devices/traits/v1/map_content.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
"""Trait for fetching the map content from Roborock devices."""

import asyncio
import logging
from dataclasses import dataclass

from vacuum_map_parser_base.map_data import MapData

from roborock.data import RoborockBase
from roborock.devices.traits.v1 import common
from roborock.exceptions import RoborockParsingException
from roborock.map.map_parser import MapParser, MapParserConfig
from roborock.roborock_typing import RoborockCommand

Expand Down Expand Up @@ -78,6 +80,20 @@ def parse_map_content(self, response: bytes) -> MapContent:
raw_api_response=response,
)

async def async_parse_map_content(self, response: bytes) -> MapContent:
"""Parse the map content from raw bytes asynchronously.

Args:
response: The raw bytes of the map data from the API.

Returns:
MapContent: The parsed map content.

Raises:
RoborockException: If the map data cannot be parsed.
"""
return await asyncio.to_thread(self.parse_map_content, response)


@common.map_rpc_channel
class MapContentTrait(MapContent, common.V1TraitMixin):
Expand All @@ -90,3 +106,21 @@ def __init__(self, map_parser_config: MapParserConfig | None = None) -> None:
"""Initialize MapContentTrait."""
super().__init__()
self.converter = MapContentConverter(MapParser(map_parser_config or MapParserConfig()))

async def refresh(self) -> None:
"""Refresh the contents of this trait without blocking the event loop."""
response = await self.rpc_channel.send_command(self.command)
if self.raw_api_response is not None and self.raw_api_response == response:
return
if not isinstance(response, bytes):
raise ValueError(f"Unexpected MapContentTrait response format: {type(response)}")
try:
Comment thread
allenporter marked this conversation as resolved.
new_data = await self.converter.async_parse_map_content(response)
except (TypeError, ValueError) as err:
raise RoborockParsingException(
trait_name=type(self).__name__,
command=self.command,
payload=response,
inner_error=err,
) from err
common.merge_trait_values(self, new_data)
48 changes: 48 additions & 0 deletions tests/devices/traits/v1/test_map_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,51 @@ async def test_refresh_map_content_trait(
assert map_content_trait.map_data is not None

mock_map_rpc_channel.send_command.assert_called_once_with(RoborockCommand.GET_MAP_V1)


async def test_refresh_unchanged_content_skips_parsing(
map_content_trait: MapContentTrait,
mock_map_rpc_channel: AsyncMock,
) -> None:
"""Test that refresh avoids re-parsing when the map content is unchanged."""
map_data = b"dummy_map_bytes"
mock_map_rpc_channel.send_command.return_value = map_data
mock_parsed_data = ParsedMapData(
image_content=b"dummy_image_content",
map_data=MagicMock(),
)

with patch("roborock.devices.traits.v1.map_content.MapParser.parse", return_value=mock_parsed_data) as mock_parse:
await map_content_trait.refresh()
assert mock_parse.call_count == 1
assert map_content_trait.image_content == b"dummy_image_content"

# Second refresh with identical response should not re-parse
await map_content_trait.refresh()
assert mock_parse.call_count == 1

# Third refresh with new response parses again
new_map_data = b"new_map_bytes"
new_parsed_data = ParsedMapData(
image_content=b"new_image_content",
map_data=MagicMock(),
)
mock_map_rpc_channel.send_command.return_value = new_map_data
mock_parse.return_value = new_parsed_data
await map_content_trait.refresh()
assert mock_parse.call_count == 2
assert map_content_trait.image_content == b"new_image_content"


async def test_async_parse_map_content(map_content_trait: MapContentTrait) -> None:
"""Test async_parse_map_content on MapContentConverter."""
map_data = b"dummy_map_bytes"
mock_parsed_data = ParsedMapData(
image_content=b"dummy_image_content",
map_data=MagicMock(),
)
with patch("roborock.devices.traits.v1.map_content.MapParser.parse", return_value=mock_parsed_data) as mock_parse:
result = await map_content_trait.converter.async_parse_map_content(map_data)
mock_parse.assert_called_once_with(map_data)
assert result.image_content == b"dummy_image_content"
assert result.raw_api_response == map_data
Loading