Skip to content

Commit e4b53f7

Browse files
committed
feat: add module for parsing map content
1 parent c0c082b commit e4b53f7

File tree

2 files changed

+127
-0
lines changed

2 files changed

+127
-0
lines changed

roborock/map/map_parser.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
"""Module for parsing v1 Roborock map content."""
2+
3+
import io
4+
import logging
5+
from dataclasses import dataclass, field
6+
7+
from vacuum_map_parser_base.config.color import ColorsPalette, SupportedColor
8+
from vacuum_map_parser_base.config.drawable import Drawable
9+
from vacuum_map_parser_base.config.image_config import ImageConfig
10+
from vacuum_map_parser_base.config.size import Size, Sizes
11+
from vacuum_map_parser_base.map_data import MapData
12+
from vacuum_map_parser_roborock.map_data_parser import RoborockMapDataParser
13+
14+
from roborock.exceptions import RoborockException
15+
16+
_LOGGER = logging.getLogger(__name__)
17+
18+
DEFAULT_DRAWABLES = {
19+
Drawable.CHARGER: True,
20+
Drawable.CLEANED_AREA: False,
21+
Drawable.GOTO_PATH: False,
22+
Drawable.IGNORED_OBSTACLES: False,
23+
Drawable.IGNORED_OBSTACLES_WITH_PHOTO: False,
24+
Drawable.MOP_PATH: False,
25+
Drawable.NO_CARPET_AREAS: False,
26+
Drawable.NO_GO_AREAS: False,
27+
Drawable.NO_MOPPING_AREAS: False,
28+
Drawable.OBSTACLES: False,
29+
Drawable.OBSTACLES_WITH_PHOTO: False,
30+
Drawable.PATH: True,
31+
Drawable.PREDICTED_PATH: False,
32+
Drawable.VACUUM_POSITION: True,
33+
Drawable.VIRTUAL_WALLS: False,
34+
Drawable.ZONES: False,
35+
}
36+
DEFAULT_MAP_SCALE = 4
37+
MAP_FILE_FORMAT = "PNG"
38+
39+
40+
def _default_drawable_factory() -> list[Drawable]:
41+
return [drawable for drawable, default_value in DEFAULT_DRAWABLES.items() if default_value]
42+
43+
44+
@dataclass
45+
class MapParserConfig:
46+
"""Configuration for the Roborock map parser."""
47+
48+
drawables: list[Drawable] = field(default_factory=_default_drawable_factory)
49+
"""List of drawables to include in the map rendering."""
50+
51+
show_background: bool = True
52+
"""Whether to show the background of the map."""
53+
54+
map_scale: int = DEFAULT_MAP_SCALE
55+
"""Scale factor for the map."""
56+
57+
58+
@dataclass
59+
class ParsedMapData:
60+
"""Roborock Map Data.
61+
62+
This class holds the parsed map data and the rendered image.
63+
"""
64+
65+
image_content: bytes | None
66+
"""The rendered image of the map in PNG format."""
67+
68+
map_data: MapData | None
69+
"""The parsed map data which contains metadata for points on the map."""
70+
71+
72+
class MapParser:
73+
"""Roborock Map Parser.
74+
75+
This class is used to parse the map data from the device and render it into an image.
76+
"""
77+
78+
def __init__(self, config: MapParserConfig) -> None:
79+
"""Initialize the MapParser."""
80+
self._map_parser = _create_map_data_parser(config)
81+
82+
def parse(self, map_bytes: bytes) -> ParsedMapData | None:
83+
"""Parse map_bytes and return MapData and the image."""
84+
try:
85+
parsed_map = self._map_parser.parse(map_bytes)
86+
except (IndexError, ValueError) as err:
87+
raise RoborockException("Failed to parse map data") from err
88+
if parsed_map.image is None:
89+
raise RoborockException("Failed to render map image")
90+
img_byte_arr = io.BytesIO()
91+
parsed_map.image.data.save(img_byte_arr, format=MAP_FILE_FORMAT)
92+
return ParsedMapData(image_content=img_byte_arr.getvalue(), map_data=parsed_map)
93+
94+
95+
def _create_map_data_parser(config: MapParserConfig) -> RoborockMapDataParser:
96+
"""Create a RoborockMapDataParser based on the config entry."""
97+
colors = ColorsPalette()
98+
if not config.show_background:
99+
colors = ColorsPalette({SupportedColor.MAP_OUTSIDE: (0, 0, 0, 0)})
100+
return RoborockMapDataParser(
101+
colors,
102+
Sizes({k: v * config.map_scale for k, v in Sizes.SIZES.items() if k != Size.MOP_PATH_WIDTH}),
103+
config.drawables,
104+
ImageConfig(scale=config.map_scale),
105+
[],
106+
)

tests/map/test_map_parser.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""Tests for the map parser."""
2+
3+
from pathlib import Path
4+
5+
import pytest
6+
from roborock.exceptions import RoborockException
7+
from roborock.map.map_parser import MapParser, MapParserConfig
8+
9+
MAP_DATA_FILE = Path(__file__).parent / "raw_map_data"
10+
DEFAULT_MAP_CONFIG = MapParserConfig()
11+
12+
13+
@pytest.mark.parametrize("map_content", [b"", b"12345"])
14+
def test_invalid_map_content(map_content: bytes):
15+
"""Test that parsing map data returns the expected image and data."""
16+
parser = MapParser(DEFAULT_MAP_CONFIG)
17+
with pytest.raises(RoborockException, match="Failed to parse map data"):
18+
parser.parse(map_content)
19+
20+
21+
# We can add additional tests here in the future that actually parse valid map data

0 commit comments

Comments
 (0)