Skip to content

Commit 3bfa659

Browse files
authored
Rand256 Paser Zones and Share data (#15)
* change local _LOGGER to LOGGER and shared.py optimized parsed zone clean tested for rand. Signed-off-by: Sandro Cantarella <sandro@Sandros-Mac-mini.fritz.box> * typing and other parser touches. Signed-off-by: Sandro Cantarella <sandro@Sandros-Mac-mini.fritz.box> * bump mvcrender and change the async to sync usage. Signed-off-by: Sandro Cantarella <sandro@Sandros-Mac-mini.fritz.box> --------- Signed-off-by: Sandro Cantarella <sandro@Sandros-Mac-mini.fritz.box>
1 parent 8e0f92b commit 3bfa659

File tree

10 files changed

+197
-711
lines changed

10 files changed

+197
-711
lines changed

SCR/valetudo_map_parser/config/rand256_parser.py

Lines changed: 129 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ class Types(Enum):
2424
VIRTUAL_WALLS = 10
2525
CURRENTLY_CLEANED_BLOCKS = 11
2626
FORBIDDEN_MOP_ZONES = 12
27+
OBSTACLES = 13
28+
IGNORED_OBSTACLES = 14
29+
OBSTACLES_WITH_PHOTO = 15
30+
IGNORED_OBSTACLES_WITH_PHOTO = 16
31+
CARPET_MAP = 17
32+
MOP_PATH = 18
33+
NO_CARPET_AREAS = 19
34+
DIGEST = 1024
2735

2836
class Tools:
2937
"""Tools for coordinate transformations."""
@@ -33,6 +41,7 @@ class Tools:
3341

3442
def __init__(self):
3543
"""Initialize the parser."""
44+
self.is_valid = False
3645
self.map_data: Dict[str, Any] = {}
3746

3847
# Xiaomi/Roborock style byte extraction methods
@@ -67,6 +76,61 @@ def _get_int32_signed(data: bytes, address: int) -> int:
6776
value = RRMapParser._get_int32(data, address)
6877
return value if value < 0x80000000 else value - 0x100000000
6978

79+
@staticmethod
80+
def _parse_carpet_map(data: bytes) -> set[int]:
81+
carpet_map = set()
82+
83+
for i, v in enumerate(data):
84+
if v:
85+
carpet_map.add(i)
86+
return carpet_map
87+
88+
@staticmethod
89+
def _parse_area(header: bytes, data: bytes) -> list:
90+
area_pairs = RRMapParser._get_int16(header, 0x08)
91+
areas = []
92+
for area_start in range(0, area_pairs * 16, 16):
93+
x0 = RRMapParser._get_int16(data, area_start + 0)
94+
y0 = RRMapParser._get_int16(data, area_start + 2)
95+
x1 = RRMapParser._get_int16(data, area_start + 4)
96+
y1 = RRMapParser._get_int16(data, area_start + 6)
97+
x2 = RRMapParser._get_int16(data, area_start + 8)
98+
y2 = RRMapParser._get_int16(data, area_start + 10)
99+
x3 = RRMapParser._get_int16(data, area_start + 12)
100+
y3 = RRMapParser._get_int16(data, area_start + 14)
101+
areas.append(
102+
[
103+
x0,
104+
RRMapParser.Tools.DIMENSION_MM - y0,
105+
x1,
106+
RRMapParser.Tools.DIMENSION_MM - y1,
107+
x2,
108+
RRMapParser.Tools.DIMENSION_MM - y2,
109+
x3,
110+
RRMapParser.Tools.DIMENSION_MM - y3,
111+
]
112+
)
113+
return areas
114+
115+
@staticmethod
116+
def _parse_zones(data: bytes, header: bytes) -> list:
117+
zone_pairs = RRMapParser._get_int16(header, 0x08)
118+
zones = []
119+
for zone_start in range(0, zone_pairs * 8, 8):
120+
x0 = RRMapParser._get_int16(data, zone_start + 0)
121+
y0 = RRMapParser._get_int16(data, zone_start + 2)
122+
x1 = RRMapParser._get_int16(data, zone_start + 4)
123+
y1 = RRMapParser._get_int16(data, zone_start + 6)
124+
zones.append(
125+
[
126+
x0,
127+
RRMapParser.Tools.DIMENSION_MM - y0,
128+
x1,
129+
RRMapParser.Tools.DIMENSION_MM - y1,
130+
]
131+
)
132+
return zones
133+
70134
@staticmethod
71135
def _parse_object_position(block_data_length: int, data: bytes) -> Dict[str, Any]:
72136
"""Parse object position using Xiaomi method."""
@@ -82,6 +146,19 @@ def _parse_object_position(block_data_length: int, data: bytes) -> Dict[str, Any
82146
angle = raw_angle
83147
return {"position": [x, y], "angle": angle}
84148

149+
150+
@staticmethod
151+
def _parse_walls(data: bytes, header: bytes) -> list:
152+
wall_pairs = RRMapParser._get_int16(header, 0x08)
153+
walls = []
154+
for wall_start in range(0, wall_pairs * 8, 8):
155+
x0 = RRMapParser._get_int16(data, wall_start + 0)
156+
y0 = RRMapParser._get_int16(data, wall_start + 2)
157+
x1 = RRMapParser._get_int16(data, wall_start + 4)
158+
y1 = RRMapParser._get_int16(data, wall_start + 6)
159+
walls.append([x0, RRMapParser.Tools.DIMENSION_MM - y0, x1, RRMapParser.Tools.DIMENSION_MM - y1])
160+
return walls
161+
85162
@staticmethod
86163
def _parse_path_block(buf: bytes, offset: int, length: int) -> Dict[str, Any]:
87164
"""Parse path block using EXACT same method as working parser."""
@@ -127,59 +204,45 @@ def parse(self, map_buf: bytes) -> Dict[str, Any]:
127204
return {}
128205

129206
def parse_blocks(self, raw: bytes, pixels: bool = True) -> Dict[int, Any]:
130-
"""Parse all blocks using Xiaomi method."""
131207
blocks = {}
132208
map_header_length = self._get_int16(raw, 0x02)
133209
block_start_position = map_header_length
134-
135210
while block_start_position < len(raw):
136211
try:
137-
# Parse block header using Xiaomi method
138212
block_header_length = self._get_int16(raw, block_start_position + 0x02)
139213
header = self._get_bytes(raw, block_start_position, block_header_length)
140214
block_type = self._get_int16(header, 0x00)
141215
block_data_length = self._get_int32(header, 0x04)
142216
block_data_start = block_start_position + block_header_length
143217
data = self._get_bytes(raw, block_data_start, block_data_length)
144-
145-
# Parse different block types
146-
if block_type == self.Types.ROBOT_POSITION.value:
147-
blocks[block_type] = self._parse_object_position(
148-
block_data_length, data
149-
)
150-
elif block_type == self.Types.CHARGER_LOCATION.value:
151-
blocks[block_type] = self._parse_object_position(
152-
block_data_length, data
153-
)
154-
elif block_type == self.Types.PATH.value:
155-
blocks[block_type] = self._parse_path_block(
156-
raw, block_start_position, block_data_length
157-
)
158-
elif block_type == self.Types.GOTO_PREDICTED_PATH.value:
159-
blocks[block_type] = self._parse_path_block(
160-
raw, block_start_position, block_data_length
161-
)
162-
elif block_type == self.Types.GOTO_TARGET.value:
163-
blocks[block_type] = {"position": self._parse_goto_target(data)}
164-
elif block_type == self.Types.IMAGE.value:
165-
# Get header length for Gen1/Gen3 detection
166-
header_length = self._get_int8(header, 2)
167-
blocks[block_type] = self._parse_image_block(
168-
raw,
169-
block_start_position,
170-
block_data_length,
171-
header_length,
172-
pixels,
173-
)
174-
175-
# Move to next block using Xiaomi method
176-
block_start_position = (
177-
block_start_position + block_data_length + self._get_int8(header, 2)
178-
)
179-
218+
match block_type:
219+
case self.Types.DIGEST.value:
220+
self.is_valid = True
221+
case self.Types.ROBOT_POSITION.value | self.Types.CHARGER_LOCATION.value:
222+
blocks[block_type] = self._parse_object_position(block_data_length, data)
223+
case self.Types.PATH.value | self.Types.GOTO_PREDICTED_PATH.value:
224+
blocks[block_type] = self._parse_path_block(raw, block_start_position, block_data_length)
225+
case self.Types.CURRENTLY_CLEANED_ZONES.value:
226+
blocks[block_type] = {"zones": self._parse_zones(data, header)}
227+
case self.Types.FORBIDDEN_ZONES.value:
228+
blocks[block_type] = {"forbidden_zones": self._parse_area(header, data)}
229+
case self.Types.FORBIDDEN_MOP_ZONES.value:
230+
blocks[block_type] = {"forbidden_mop_zones": self._parse_area(header, data)}
231+
case self.Types.GOTO_TARGET.value:
232+
blocks[block_type] = {"position": self._parse_goto_target(data)}
233+
case self.Types.VIRTUAL_WALLS.value:
234+
blocks[block_type] = {"virtual_walls": self._parse_walls(data, header)}
235+
case self.Types.CARPET_MAP.value:
236+
data = RRMapParser._get_bytes(raw, block_data_start, block_data_length)
237+
blocks[block_type] = {"carpet_map": self._parse_carpet_map(data)}
238+
case self.Types.IMAGE.value:
239+
header_length = self._get_int8(header, 2)
240+
blocks[block_type] = self._parse_image_block(
241+
raw, block_start_position, block_data_length, header_length, pixels)
242+
243+
block_start_position = block_start_position + block_data_length + self._get_int8(header, 2)
180244
except (struct.error, IndexError):
181245
break
182-
183246
return blocks
184247

185248
def _parse_image_block(
@@ -365,8 +428,32 @@ def parse_rrm_data(
365428
]
366429

367430
# Add missing fields to match expected JSON format
368-
parsed_map_data["forbidden_zones"] = []
369-
parsed_map_data["virtual_walls"] = []
431+
parsed_map_data["currently_cleaned_zones"] = (
432+
blocks[self.Types.CURRENTLY_CLEANED_ZONES.value]["zones"]
433+
if self.Types.CURRENTLY_CLEANED_ZONES.value in blocks
434+
else []
435+
)
436+
parsed_map_data["forbidden_zones"] = (
437+
blocks[self.Types.FORBIDDEN_ZONES.value]["forbidden_zones"]
438+
if self.Types.FORBIDDEN_ZONES.value in blocks
439+
else []
440+
)
441+
parsed_map_data["forbidden_mop_zones"] = (
442+
blocks[self.Types.FORBIDDEN_MOP_ZONES.value]["forbidden_mop_zones"]
443+
if self.Types.FORBIDDEN_MOP_ZONES.value in blocks
444+
else []
445+
)
446+
parsed_map_data["virtual_walls"] = (
447+
blocks[self.Types.VIRTUAL_WALLS.value]["virtual_walls"]
448+
if self.Types.VIRTUAL_WALLS.value in blocks
449+
else []
450+
)
451+
parsed_map_data["carpet_areas"] = (
452+
blocks[self.Types.CARPET_MAP.value]["carpet_map"]
453+
if self.Types.CARPET_MAP.value in blocks
454+
else []
455+
)
456+
parsed_map_data["is_valid"] = self.is_valid
370457

371458
return parsed_map_data
372459

@@ -388,8 +475,3 @@ def parse_data(
388475
except (struct.error, IndexError, ValueError):
389476
return None
390477
return self.map_data
391-
392-
@staticmethod
393-
def get_int32(data: bytes, address: int) -> int:
394-
"""Get a 32-bit integer from the data - kept for compatibility."""
395-
return struct.unpack_from("<i", data, address)[0]

SCR/valetudo_map_parser/config/shared.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@
1212
from .types import (
1313
ATTR_CALIBRATION_POINTS,
1414
ATTR_CAMERA_MODE,
15+
ATTR_CONTENT_TYPE,
1516
ATTR_MARGINS,
1617
ATTR_OBSTACLES,
1718
ATTR_POINTS,
1819
ATTR_ROOMS,
1920
ATTR_ROTATE,
20-
ATTR_SNAPSHOT,
21+
ATTR_IMAGE_LAST_UPDATED,
2122
ATTR_VACUUM_BATTERY,
2223
ATTR_VACUUM_CHARGING,
2324
ATTR_VACUUM_JSON_ID,
@@ -179,12 +180,14 @@ async def batch_get(self, *args):
179180
def generate_attributes(self) -> dict:
180181
"""Generate and return the shared attribute's dictionary."""
181182
attrs = {
183+
ATTR_IMAGE_LAST_UPDATED: self.image_last_updated,
184+
ATTR_CONTENT_TYPE: self.image_format,
185+
ATTR_VACUUM_JSON_ID: self.vac_json_id,
182186
ATTR_CAMERA_MODE: self.camera_mode,
187+
ATTR_VACUUM_STATUS: self.vacuum_state,
183188
ATTR_VACUUM_BATTERY: f"{self.vacuum_battery}%",
184189
ATTR_VACUUM_CHARGING: self.vacuum_bat_charged(),
185190
ATTR_VACUUM_POSITION: self.current_room,
186-
ATTR_VACUUM_STATUS: self.vacuum_state,
187-
ATTR_VACUUM_JSON_ID: self.vac_json_id,
188191
ATTR_CALIBRATION_POINTS: self.attr_calibration_points,
189192
}
190193
if self.obstacles_pos and self.vacuum_ips:
@@ -193,8 +196,6 @@ def generate_attributes(self) -> dict:
193196
)
194197
attrs[ATTR_OBSTACLES] = self.obstacles_data
195198

196-
attrs[ATTR_SNAPSHOT] = self.snapshot_take if self.enable_snapshots else False
197-
198199
shared_attrs = {
199200
ATTR_ROOMS: self.map_rooms,
200201
ATTR_ZONES: self.map_pred_zones,
@@ -211,10 +212,8 @@ def to_dict(self) -> dict:
211212
return {
212213
"image": {
213214
"binary": self.binary_image,
214-
"pil_image_size": self.new_image.size,
215-
"size": self.new_image.size if self.new_image else None,
216-
"format": self.image_format,
217-
"updated": self.image_last_updated,
215+
"pil_image": self.new_image,
216+
"size": self.new_image.size if self.new_image else (10, 10),
218217
},
219218
"attributes": self.generate_attributes(),
220219
}

SCR/valetudo_map_parser/config/types.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,29 @@
1818

1919
LOGGER = logging.getLogger(__package__)
2020

21+
2122
class Spot(TypedDict):
2223
name: str
2324
coordinates: List[int] # [x, y]
2425

26+
2527
class Zone(TypedDict):
2628
name: str
2729
coordinates: List[List[int]] # [[x1, y1, x2, y2, repeats], ...]
2830

31+
2932
class Room(TypedDict):
3033
name: str
3134
id: int
3235

36+
37+
# list[dict[str, str | list[int]]] | list[dict[str, str | list[list[int]]]] | list[dict[str, str | int]] | int]'
3338
class Destinations(TypedDict, total=False):
3439
spots: NotRequired[Optional[List[Spot]]]
3540
zones: NotRequired[Optional[List[Zone]]]
3641
rooms: NotRequired[Optional[List[Room]]]
37-
updated: NotRequired[Optional[int]]
42+
updated: NotRequired[Optional[float]]
43+
3844

3945
class RoomProperty(TypedDict):
4046
number: int
@@ -227,9 +233,11 @@ async def async_set_vacuum_json(self, vacuum_id: str, json_data: Any) -> None:
227233
Point = Tuple[int, int]
228234

229235
CAMERA_STORAGE = "valetudo_camera"
236+
ATTR_IMAGE_LAST_UPDATED = "image_last_updated"
230237
ATTR_ROTATE = "rotate_image"
231238
ATTR_CROP = "crop_image"
232239
ATTR_MARGINS = "margins"
240+
ATTR_CONTENT_TYPE = "content_type"
233241
CONF_OFFSET_TOP = "offset_top"
234242
CONF_OFFSET_BOTTOM = "offset_bottom"
235243
CONF_OFFSET_LEFT = "offset_left"

0 commit comments

Comments
 (0)