diff --git a/SCR/valetudo_map_parser/config/colors.py b/SCR/valetudo_map_parser/config/colors.py index 6640336..904da4d 100644 --- a/SCR/valetudo_map_parser/config/colors.py +++ b/SCR/valetudo_map_parser/config/colors.py @@ -10,6 +10,7 @@ from ..const import ( ALPHA_BACKGROUND, ALPHA_CHARGER, + ALPHA_CARPET, ALPHA_GO_TO, ALPHA_MOVE, ALPHA_NO_GO, @@ -35,6 +36,7 @@ ALPHA_ZONE_CLEAN, COLOR_BACKGROUND, COLOR_CHARGER, + COLOR_CARPET, COLOR_GO_TO, COLOR_MOVE, COLOR_NO_GO, @@ -64,6 +66,7 @@ color_transparent = (0, 0, 0, 0) color_charger = (0, 128, 0, 255) +color_carpet = (67, 103, 125, 255) color_move = (238, 247, 255, 255) color_robot = (255, 255, 204, 255) color_no_go = (255, 0, 0, 255) @@ -149,6 +152,7 @@ class SupportedColor(StrEnum): GO_TO = "color_go_to" NO_GO = "color_no_go" ZONE_CLEAN = "color_zone_clean" + CARPET = "color_carpet" MAP_BACKGROUND = "color_background" TEXT = "color_text" TRANSPARENT = "color_transparent" @@ -171,6 +175,7 @@ class DefaultColors: SupportedColor.GO_TO: (0, 255, 0), SupportedColor.NO_GO: (255, 0, 0), SupportedColor.ZONE_CLEAN: (255, 255, 255), + SupportedColor.CARPET: (67, 103, 125), # 50% of room_0 default color (135, 206, 250) SupportedColor.MAP_BACKGROUND: (0, 125, 255), SupportedColor.TEXT: (0, 0, 0), SupportedColor.TRANSPARENT: (0, 0, 0), @@ -301,6 +306,7 @@ def set_initial_colours(self, device_info: dict) -> None: (COLOR_BACKGROUND, color_background, ALPHA_BACKGROUND), (COLOR_MOVE, color_move, ALPHA_MOVE), (COLOR_CHARGER, color_charger, ALPHA_CHARGER), + (COLOR_CARPET, color_carpet, ALPHA_CARPET), (COLOR_NO_GO, color_no_go, ALPHA_NO_GO), (COLOR_GO_TO, color_go_to, ALPHA_GO_TO), (COLOR_TEXT, color_text, ALPHA_TEXT), diff --git a/SCR/valetudo_map_parser/config/drawable_elements.py b/SCR/valetudo_map_parser/config/drawable_elements.py index f15dbc2..8982654 100644 --- a/SCR/valetudo_map_parser/config/drawable_elements.py +++ b/SCR/valetudo_map_parser/config/drawable_elements.py @@ -33,6 +33,7 @@ class DrawableElement(IntEnum): PATH = 9 PREDICTED_PATH = 10 GO_TO_TARGET = 11 + CARPET = 12 # Rooms (101-115 for up to 15 rooms) ROOM_1 = 101 @@ -94,6 +95,7 @@ def _set_default_properties(self): DrawableElement.GO_TO_TARGET: SupportedColor.GO_TO, DrawableElement.NO_MOP_AREA: SupportedColor.NO_GO, # Using NO_GO for no-mop areas DrawableElement.OBSTACLE: SupportedColor.NO_GO, # Using NO_GO for obstacles + DrawableElement.CARPET: SupportedColor.CARPET, } # Set z-index for each element type @@ -105,6 +107,7 @@ def _set_default_properties(self): DrawableElement.VIRTUAL_WALL: 30, DrawableElement.RESTRICTED_AREA: 25, DrawableElement.NO_MOP_AREA: 25, + DrawableElement.CARPET: 15, # Draw carpets above floor but below walls DrawableElement.OBSTACLE: 15, DrawableElement.PATH: 35, DrawableElement.PREDICTED_PATH: 34, @@ -213,6 +216,7 @@ def update_from_device_info(self, device_info: dict) -> None: DrawableElement.GO_TO_TARGET: SupportedColor.GO_TO, DrawableElement.NO_MOP_AREA: SupportedColor.NO_GO, DrawableElement.OBSTACLE: SupportedColor.NO_GO, + DrawableElement.CARPET: SupportedColor.CARPET, } # Update room colors from device info @@ -265,6 +269,7 @@ def update_from_device_info(self, device_info: dict) -> None: "disable_virtual_walls": DrawableElement.VIRTUAL_WALL, "disable_restricted_areas": DrawableElement.RESTRICTED_AREA, "disable_no_mop_areas": DrawableElement.NO_MOP_AREA, + "disable_carpets": DrawableElement.CARPET, "disable_obstacles": DrawableElement.OBSTACLE, "disable_path": DrawableElement.PATH, "disable_predicted_path": DrawableElement.PREDICTED_PATH, diff --git a/SCR/valetudo_map_parser/config/shared.py b/SCR/valetudo_map_parser/config/shared.py index 03ebc5c..b106bb2 100755 --- a/SCR/valetudo_map_parser/config/shared.py +++ b/SCR/valetudo_map_parser/config/shared.py @@ -32,7 +32,6 @@ CONF_OFFSET_LEFT, CONF_OFFSET_RIGHT, CONF_OFFSET_TOP, - CONF_SNAPSHOTS_ENABLE, CONF_VAC_STAT, CONF_VAC_STAT_FONT, CONF_VAC_STAT_POS, diff --git a/SCR/valetudo_map_parser/config/types.py b/SCR/valetudo_map_parser/config/types.py index e0bbc8f..1501a66 100644 --- a/SCR/valetudo_map_parser/config/types.py +++ b/SCR/valetudo_map_parser/config/types.py @@ -84,7 +84,7 @@ def to_dict(self) -> dict: def from_dict(data: dict): """Create dataclass from dictionary.""" return TrimCropData( - floor=data["floor"], + floor=data.get("floor", "floor_0"), trim_left=data["trim_left"], trim_up=data["trim_up"], trim_right=data["trim_right"], diff --git a/SCR/valetudo_map_parser/const.py b/SCR/valetudo_map_parser/const.py index 1a47661..6c6508d 100644 --- a/SCR/valetudo_map_parser/const.py +++ b/SCR/valetudo_map_parser/const.py @@ -40,8 +40,10 @@ "background", "move", "charger", + "carpet", "no_go", "go_to", + "text", ] SENSOR_NO_DATA = { @@ -92,6 +94,7 @@ "color_go_to": [0, 255, 0], "color_no_go": [255, 0, 0], "color_zone_clean": [255, 255, 255], + "color_carpet": [67, 103, 125], "color_background": [0, 125, 255], "color_text": [255, 255, 255], "alpha_charger": 255.0, @@ -101,6 +104,7 @@ "alpha_go_to": 255.0, "alpha_no_go": 125.0, "alpha_zone_clean": 125.0, + "alpha_carpet": 255.0, "alpha_background": 255.0, "alpha_text": 255.0, "color_room_0": [135, 206, 250], @@ -229,6 +233,7 @@ """Base Colours RGB""" COLOR_CHARGER = "color_charger" +COLOR_CARPET = "color_carpet" COLOR_MOVE = "color_move" COLOR_ROBOT = "color_robot" COLOR_NO_GO = "color_no_go" @@ -258,6 +263,7 @@ """Alpha for RGBA Colours""" ALPHA_CHARGER = "alpha_charger" +ALPHA_CARPET = "alpha_carpet" ALPHA_MOVE = "alpha_move" ALPHA_ROBOT = "alpha_robot" ALPHA_NO_GO = "alpha_no_go" diff --git a/SCR/valetudo_map_parser/hypfer_draw.py b/SCR/valetudo_map_parser/hypfer_draw.py index 183b60a..fcba079 100755 --- a/SCR/valetudo_map_parser/hypfer_draw.py +++ b/SCR/valetudo_map_parser/hypfer_draw.py @@ -94,8 +94,6 @@ async def _process_room_layer( # The room_id is 0-based, but DrawableElement.ROOM_x is 1-based current_room_id = room_id + 1 if 1 <= current_room_id <= 15: - # Use the DrawableElement imported at the top of the file - room_element = getattr(DrawableElement, f"ROOM_{current_room_id}", None) if room_element and hasattr(self.img_h.drawing_config, "is_enabled"): draw_room = self.img_h.drawing_config.is_enabled(room_element) @@ -104,7 +102,11 @@ async def _process_room_layer( room_color = self.img_h.shared.rooms_colors[room_id] try: - if layer_type == "segment": + if layer_type == "floor": + # Floor layers always use color_room_0 (rooms_colors[0]) + # This ensures consistency across different vacuum models + room_color = self.img_h.shared.rooms_colors[0] + elif layer_type == "segment": room_color = self._get_active_room_color( room_id, room_color, color_zone_clean ) @@ -115,8 +117,9 @@ async def _process_room_layer( img_np_array, pixels, pixel_size, room_color ) - # Always increment the room_id, even if the room is not drawn - room_id = (room_id + 1) % 16 # Cycle room_id back to 0 after 15 + # Increment room_id only for segment layers, not for floor layers + if layer_type == "segment": + room_id = (room_id + 1) % 16 # Cycle room_id back to 0 after 15 except IndexError as e: _LOGGER.warning("%s: Image Draw Error: %s", self.file_name, str(e)) @@ -262,6 +265,7 @@ async def async_draw_zones( np_array: NumpyArray, color_zone_clean: Color, color_no_go: Color, + color_carpet: Color = None, ) -> NumpyArray: """Get the zone clean from the JSON data with parallel processing.""" @@ -295,6 +299,17 @@ async def async_draw_zones( np_array, no_mop_zones, color_no_go ) + # Carpet zones + carpet_zones = zone_clean.get("carpet") + if ( + carpet_zones + and color_carpet + and self.img_h.drawing_config.is_enabled(DrawableElement.CARPET) + ): + np_array = await self.img_h.draw.zones( + np_array, carpet_zones, color_carpet + ) + return np_array async def async_draw_virtual_walls( diff --git a/SCR/valetudo_map_parser/hypfer_handler.py b/SCR/valetudo_map_parser/hypfer_handler.py index a125d4a..04d9a67 100644 --- a/SCR/valetudo_map_parser/hypfer_handler.py +++ b/SCR/valetudo_map_parser/hypfer_handler.py @@ -130,7 +130,7 @@ async def _draw_layer_if_enabled( if is_wall_layer and not self.drawing_config.is_enabled(DrawableElement.WALL): return room_id, img_np_array # Skip walls - # Draw the layer + # Draw the layer (floor layers are always drawn when present) room_id, img_np_array = await self.imd.async_draw_base_layer( img_np_array, compressed_pixels_list, @@ -231,7 +231,7 @@ async def _draw_dynamic_elements( """Draw dynamic elements like zones, paths, and go-to targets.""" if self.drawing_config.is_enabled(DrawableElement.RESTRICTED_AREA): img_np_array = await self.imd.async_draw_zones( - m_json, img_np_array, colors["zone_clean"], colors["no_go"] + m_json, img_np_array, colors["zone_clean"], colors["no_go"], colors.get("carpet") ) if self.drawing_config.is_enabled(DrawableElement.GO_TO_TARGET): diff --git a/SCR/valetudo_map_parser/map_data.py b/SCR/valetudo_map_parser/map_data.py index 3c74d58..9722482 100755 --- a/SCR/valetudo_map_parser/map_data.py +++ b/SCR/valetudo_map_parser/map_data.py @@ -117,7 +117,22 @@ class PathMapEntity(TypedDict): metaData: dict[str, object] # flexible for now -Entity = PointMapEntity | PathMapEntity +class PolygonMeta(TypedDict, total=False): + """Metadata for polygon entities including ID.""" + + id: str + + +class PolygonMapEntity(TypedDict): + """Polygon-based map entity (zones, carpets, etc.).""" + + __class__: Literal["PolygonMapEntity"] + type: str + points: list[int] + metaData: NotRequired[PolygonMeta] + + +Entity = PointMapEntity | PathMapEntity | PolygonMapEntity # --- Top-level Map --- @@ -259,9 +274,24 @@ def find_layers( layer_type = json_obj.get("type") meta_data = json_obj.get("metaData") or {} if layer_type: - layer_dict.setdefault(layer_type, []).append( - json_obj.get("compressedPixels", []) - ) + # Get compressedPixels, or fall back to pixels if not present + compressed_pixels = json_obj.get("compressedPixels") + if compressed_pixels is None: + # If compressedPixels is missing, convert pixels array to compressed format + # pixels format: [x, y, x, y, ...] (pairs) + # compressedPixels format: [x, y, count, x, y, count, ...] (triplets) + pixels = json_obj.get("pixels", []) + if pixels: + # Convert pairs to triplets by adding count=1 for each pixel + compressed_pixels = [] + for i in range(0, len(pixels), 2): + if i + 1 < len(pixels): + compressed_pixels.extend([pixels[i], pixels[i + 1], 1]) + else: + compressed_pixels = [] + + layer_dict.setdefault(layer_type, []).append(compressed_pixels) + # Safely extract "active" flag if present and convertible to int if layer_type == "segment": try: diff --git a/pyproject.toml b/pyproject.toml index 5dc3f5a..428cf97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "valetudo-map-parser" -version = "0.1.15" +version = "0.1.16" description = "A Python library to parse Valetudo map data returning a PIL Image object." authors = ["Sandro Cantarella "] license = "Apache-2.0"