From 2b5bd0e31753b22479a8253e4b24da94022d76ff Mon Sep 17 00:00:00 2001 From: SCA075 <82227818+sca075@users.noreply.github.com> Date: Thu, 23 Oct 2025 17:44:56 +0200 Subject: [PATCH 01/27] adding const.py and update types.py to separate const and types Signed-off-by: Sandro Cantarella --- SCR/valetudo_map_parser/hypfer_handler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/SCR/valetudo_map_parser/hypfer_handler.py b/SCR/valetudo_map_parser/hypfer_handler.py index a125d4a..69b1383 100644 --- a/SCR/valetudo_map_parser/hypfer_handler.py +++ b/SCR/valetudo_map_parser/hypfer_handler.py @@ -16,6 +16,7 @@ from .config.async_utils import AsyncPIL from .config.drawable_elements import DrawableElement from .config.shared import CameraShared +from .const import COLORS from .config.types import ( LOGGER, CalibrationPoints, From c6ab4ee95be9efcf6b2dd3fdf129c43cdb997cbb Mon Sep 17 00:00:00 2001 From: SCA075 <82227818+sca075@users.noreply.github.com> Date: Tue, 4 Nov 2025 18:06:54 +0100 Subject: [PATCH 02/27] last files for 12 isort / ruff and lint Signed-off-by: Sandro Cantarella --- SCR/valetudo_map_parser/hypfer_handler.py | 1 - SCR/valetudo_map_parser/rand256_handler.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/SCR/valetudo_map_parser/hypfer_handler.py b/SCR/valetudo_map_parser/hypfer_handler.py index 69b1383..a125d4a 100644 --- a/SCR/valetudo_map_parser/hypfer_handler.py +++ b/SCR/valetudo_map_parser/hypfer_handler.py @@ -16,7 +16,6 @@ from .config.async_utils import AsyncPIL from .config.drawable_elements import DrawableElement from .config.shared import CameraShared -from .const import COLORS from .config.types import ( LOGGER, CalibrationPoints, diff --git a/SCR/valetudo_map_parser/rand256_handler.py b/SCR/valetudo_map_parser/rand256_handler.py index 3ed2a74..8e150e8 100644 --- a/SCR/valetudo_map_parser/rand256_handler.py +++ b/SCR/valetudo_map_parser/rand256_handler.py @@ -15,7 +15,6 @@ from .config.async_utils import AsyncPIL from .config.drawable_elements import DrawableElement -from .const import COLORS, DEFAULT_IMAGE_SIZE, DEFAULT_PIXEL_SIZE from .config.types import ( LOGGER, Colors, @@ -31,6 +30,7 @@ initialize_drawing_config, point_in_polygon, ) +from .const import COLORS, DEFAULT_IMAGE_SIZE, DEFAULT_PIXEL_SIZE from .map_data import RandImageData from .reimg_draw import ImageDraw from .rooms_handler import RandRoomsHandler From e6b006c8ffb6768475f7050f6a9a24e6c500f454 Mon Sep 17 00:00:00 2001 From: SCA075 <82227818+sca075@users.noreply.github.com> Date: Fri, 19 Dec 2025 08:07:11 +0100 Subject: [PATCH 03/27] remove duplicate import of const Signed-off-by: Sandro Cantarella --- SCR/valetudo_map_parser/rand256_handler.py | 1 - 1 file changed, 1 deletion(-) diff --git a/SCR/valetudo_map_parser/rand256_handler.py b/SCR/valetudo_map_parser/rand256_handler.py index 8e150e8..d1df01c 100644 --- a/SCR/valetudo_map_parser/rand256_handler.py +++ b/SCR/valetudo_map_parser/rand256_handler.py @@ -30,7 +30,6 @@ initialize_drawing_config, point_in_polygon, ) -from .const import COLORS, DEFAULT_IMAGE_SIZE, DEFAULT_PIXEL_SIZE from .map_data import RandImageData from .reimg_draw import ImageDraw from .rooms_handler import RandRoomsHandler From a5ca57172820a8526d0b04f8342001e7621919e9 Mon Sep 17 00:00:00 2001 From: SCA075 <82227818+sca075@users.noreply.github.com> Date: Sat, 20 Dec 2025 13:59:48 +0100 Subject: [PATCH 04/27] const was not properly imported Signed-off-by: SCA075 <82227818+sca075@users.noreply.github.com> --- SCR/valetudo_map_parser/rand256_handler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SCR/valetudo_map_parser/rand256_handler.py b/SCR/valetudo_map_parser/rand256_handler.py index d1df01c..8a68f09 100644 --- a/SCR/valetudo_map_parser/rand256_handler.py +++ b/SCR/valetudo_map_parser/rand256_handler.py @@ -15,6 +15,7 @@ from .config.async_utils import AsyncPIL from .config.drawable_elements import DrawableElement +from .const import COLORS, DEFAULT_IMAGE_SIZE, DEFAULT_PIXEL_SIZE from .config.types import ( LOGGER, Colors, @@ -542,4 +543,4 @@ def get_calibration_data(self, rotation_angle: int = 0) -> Any: calibration_point = {"vacuum": vacuum_point, "map": map_point} self.calibration_data.append(calibration_point) - return self.calibration_data + return self.calibration_data \ No newline at end of file From 0d4f63c615dbe43bd0c2fd80ac26f29fbecf4bca Mon Sep 17 00:00:00 2001 From: SCA075 <82227818+sca075@users.noreply.github.com> Date: Sat, 20 Dec 2025 17:17:49 +0100 Subject: [PATCH 05/27] minor changes in rand256_handler.py, shared.py removed snapshot at init bump version in pyproject.toml added to __init__.py Trims and Floor Data Signed-off-by: SCA075 <82227818+sca075@users.noreply.github.com> --- SCR/valetudo_map_parser/__init__.py | 4 +++- SCR/valetudo_map_parser/config/shared.py | 4 ---- SCR/valetudo_map_parser/rand256_handler.py | 2 +- pyproject.toml | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/SCR/valetudo_map_parser/__init__.py b/SCR/valetudo_map_parser/__init__.py index 37d3cd3..0d72b66 100644 --- a/SCR/valetudo_map_parser/__init__.py +++ b/SCR/valetudo_map_parser/__init__.py @@ -1,5 +1,5 @@ """Valetudo map parser. -Version: 0.1.13""" +Version: 0.1.14""" from pathlib import Path @@ -12,6 +12,7 @@ from .config.status_text.translations import translations as STATUS_TEXT_TRANSLATIONS from .config.types import ( CameraModes, + FloorData, ImageSize, JsonType, NumpyArray, @@ -20,6 +21,7 @@ RoomStore, SnapshotStore, TrimCropData, + TrimsData, UserLanguageStore, ) from .config.utils import ResizeParams, async_resize_image diff --git a/SCR/valetudo_map_parser/config/shared.py b/SCR/valetudo_map_parser/config/shared.py index 77443c5..b2257bd 100755 --- a/SCR/valetudo_map_parser/config/shared.py +++ b/SCR/valetudo_map_parser/config/shared.py @@ -311,10 +311,6 @@ def update_shared_data(self, device_info): instance.vacuum_status_position = device_info.get( CONF_VAC_STAT_POS, DEFAULT_VALUES["vac_status_position"] ) - # If enable_snapshots, check for png in www. - instance.enable_snapshots = device_info.get( - CONF_SNAPSHOTS_ENABLE, DEFAULT_VALUES["enable_www_snapshots"] - ) # Ensure trims are updated correctly trim_data = device_info.get("trims_data", DEFAULT_VALUES["trims_data"]) instance.trims = TrimsData.from_dict(trim_data) diff --git a/SCR/valetudo_map_parser/rand256_handler.py b/SCR/valetudo_map_parser/rand256_handler.py index 8a68f09..3ed2a74 100644 --- a/SCR/valetudo_map_parser/rand256_handler.py +++ b/SCR/valetudo_map_parser/rand256_handler.py @@ -543,4 +543,4 @@ def get_calibration_data(self, rotation_angle: int = 0) -> Any: calibration_point = {"vacuum": vacuum_point, "map": map_point} self.calibration_data.append(calibration_point) - return self.calibration_data \ No newline at end of file + return self.calibration_data diff --git a/pyproject.toml b/pyproject.toml index ecf107d..e2cb060 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "valetudo-map-parser" -version = "0.1.13" +version = "0.1.14" description = "A Python library to parse Valetudo map data returning a PIL Image object." authors = ["Sandro Cantarella "] license = "Apache-2.0" From c412fd7fb0f2dd8c9dbdcb20101e0c65a02e1fa8 Mon Sep 17 00:00:00 2001 From: SCA075 <82227818+sca075@users.noreply.github.com> Date: Sun, 21 Dec 2025 09:29:15 +0100 Subject: [PATCH 06/27] updated __init__.py Signed-off-by: SCA075 <82227818+sca075@users.noreply.github.com> --- SCR/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SCR/__init__.py b/SCR/__init__.py index 2217ee2..ebdf05f 100644 --- a/SCR/__init__.py +++ b/SCR/__init__.py @@ -1,2 +1,2 @@ """Valetudo map parser. -Version: 0.1.10""" +Version: 0.1.14""" From 11c09e24c037e90e4d0e355195d2add3f0c70585 Mon Sep 17 00:00:00 2001 From: SCA075 <82227818+sca075@users.noreply.github.com> Date: Sun, 21 Dec 2025 09:33:41 +0100 Subject: [PATCH 07/27] updated __init__.py all Signed-off-by: SCA075 <82227818+sca075@users.noreply.github.com> --- SCR/valetudo_map_parser/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/SCR/valetudo_map_parser/__init__.py b/SCR/valetudo_map_parser/__init__.py index 0d72b66..b0816d7 100644 --- a/SCR/valetudo_map_parser/__init__.py +++ b/SCR/valetudo_map_parser/__init__.py @@ -150,6 +150,7 @@ def get_default_font_path() -> str: "SENSOR_NO_DATA", # Classes and Handlers "CameraShared", + "FloorData", "CameraSharedManager", "ColorsManagement", "Drawable", @@ -172,6 +173,7 @@ def get_default_font_path() -> str: "RoomStore", "SnapshotStore", "TrimCropData", + "TrimsData", "UserLanguageStore", # Utilities "ResizeParams", From cb056f43243e8bd34480f0ac09a2e8fc36879852 Mon Sep 17 00:00:00 2001 From: SCA075 <82227818+sca075@users.noreply.github.com> Date: Sun, 21 Dec 2025 11:02:18 +0100 Subject: [PATCH 08/27] updated __init__.py all Signed-off-by: SCA075 <82227818+sca075@users.noreply.github.com> --- SCR/valetudo_map_parser/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SCR/valetudo_map_parser/__init__.py b/SCR/valetudo_map_parser/__init__.py index b0816d7..7fb2898 100644 --- a/SCR/valetudo_map_parser/__init__.py +++ b/SCR/valetudo_map_parser/__init__.py @@ -150,7 +150,6 @@ def get_default_font_path() -> str: "SENSOR_NO_DATA", # Classes and Handlers "CameraShared", - "FloorData", "CameraSharedManager", "ColorsManagement", "Drawable", @@ -165,6 +164,7 @@ def get_default_font_path() -> str: "StatusText", # Types "CameraModes", + "FloorData", "ImageSize", "JsonType", "NumpyArray", From 4e216569dbd2cb808f65623ba4e630cf3c393443 Mon Sep 17 00:00:00 2001 From: SCA075 <82227818+sca075@users.noreply.github.com> Date: Wed, 24 Dec 2025 07:32:01 +0100 Subject: [PATCH 09/27] update shared.py to load floor data from the config if present else uses old key trims_data Signed-off-by: SCA075 <82227818+sca075@users.noreply.github.com> --- SCR/valetudo_map_parser/config/shared.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/SCR/valetudo_map_parser/config/shared.py b/SCR/valetudo_map_parser/config/shared.py index b2257bd..03ebc5c 100755 --- a/SCR/valetudo_map_parser/config/shared.py +++ b/SCR/valetudo_map_parser/config/shared.py @@ -311,9 +311,20 @@ def update_shared_data(self, device_info): instance.vacuum_status_position = device_info.get( CONF_VAC_STAT_POS, DEFAULT_VALUES["vac_status_position"] ) - # Ensure trims are updated correctly - trim_data = device_info.get("trims_data", DEFAULT_VALUES["trims_data"]) - instance.trims = TrimsData.from_dict(trim_data) + # Check for new floors_data first + floors_data = device_info.get("floors_data", None) + current_floor = device_info.get("current_floor", "floor_0") # Default fallback + + if floors_data: + # NEW: Use floors_data + floor_trims = floors_data.get(current_floor, DEFAULT_VALUES["trims_data"]) + instance.trims = TrimsData.from_dict(floor_trims) + instance.current_floor = current_floor + else: + # OLD: Backward compatibility + trim_data = device_info.get("trims_data", DEFAULT_VALUES["trims_data"]) + instance.trims = TrimsData.from_dict(trim_data) + instance.current_floor = "floor_0" # Default # Robot size robot_size = device_info.get("robot_size", 25) try: From 0505a8c88ca4e6845d78d950ab258619c10a4c8b Mon Sep 17 00:00:00 2001 From: SCA075 <82227818+sca075@users.noreply.github.com> Date: Thu, 8 Jan 2026 20:47:21 +0100 Subject: [PATCH 10/27] update TrimsCropData to use the floors when init, else default values. Signed-off-by: SCA075 <82227818+sca075@users.noreply.github.com> --- SCR/valetudo_map_parser/config/types.py | 10 +++++++--- SCR/valetudo_map_parser/config/utils.py | 4 +++- SCR/valetudo_map_parser/const.py | 2 +- pyproject.toml | 4 ++-- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/SCR/valetudo_map_parser/config/types.py b/SCR/valetudo_map_parser/config/types.py index 79fec70..e0bbc8f 100644 --- a/SCR/valetudo_map_parser/config/types.py +++ b/SCR/valetudo_map_parser/config/types.py @@ -63,7 +63,8 @@ class RoomProperty(TypedDict): @dataclass class TrimCropData: """Dataclass for trim and crop data.""" - + + floor: str trim_left: int trim_up: int trim_right: int @@ -72,6 +73,7 @@ class TrimCropData: def to_dict(self) -> dict: """Convert dataclass to dictionary.""" return { + "floor": self.floor, "trim_left": self.trim_left, "trim_up": self.trim_up, "trim_right": self.trim_right, @@ -82,6 +84,7 @@ def to_dict(self) -> dict: def from_dict(data: dict): """Create dataclass from dictionary.""" return TrimCropData( + floor=data["floor"], trim_left=data["trim_left"], trim_up=data["trim_up"], trim_right=data["trim_right"], @@ -93,9 +96,10 @@ def to_list(self) -> list: return [self.trim_left, self.trim_up, self.trim_right, self.trim_down] @staticmethod - def from_list(data: list): - """Create dataclass from list.""" + def from_list(data: list, floor: Optional[str] = "floor_0"): + """Create dataclass from list [trim_left, trim_up, trim_right, trim_down].""" return TrimCropData( + floor=floor, trim_left=data[0], trim_up=data[1], trim_right=data[2], diff --git a/SCR/valetudo_map_parser/config/utils.py b/SCR/valetudo_map_parser/config/utils.py index 74e93c3..2830a39 100644 --- a/SCR/valetudo_map_parser/config/utils.py +++ b/SCR/valetudo_map_parser/config/utils.py @@ -275,7 +275,9 @@ def prepare_resize_params( def update_trims(self) -> None: """Update the trims.""" - self.shared.trims = TrimsData.from_list(self.crop_area) + self.shared.trims = TrimsData.from_list( + self.crop_area, floor=self.shared.current_floor + ) def get_charger_position(self) -> ChargerPosition | None: """Return the charger position.""" diff --git a/SCR/valetudo_map_parser/const.py b/SCR/valetudo_map_parser/const.py index 2506135..1a47661 100644 --- a/SCR/valetudo_map_parser/const.py +++ b/SCR/valetudo_map_parser/const.py @@ -83,7 +83,7 @@ "vac_status_position": True, "get_svg_file": False, "save_trims": True, - "trims_data": {"trim_left": 0, "trim_up": 0, "trim_right": 0, "trim_down": 0}, + "trims_data": {"floor": "floor_0", "trim_left": 0, "trim_up": 0, "trim_right": 0, "trim_down": 0}, "enable_www_snapshots": False, "color_charger": [255, 128, 0], "color_move": [238, 247, 255], diff --git a/pyproject.toml b/pyproject.toml index e2cb060..5dc3f5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "valetudo-map-parser" -version = "0.1.14" +version = "0.1.15" description = "A Python library to parse Valetudo map data returning a PIL Image object." authors = ["Sandro Cantarella "] license = "Apache-2.0" @@ -18,7 +18,7 @@ python = ">=3.13" numpy = ">=1.26.4" Pillow = ">=10.3.0" scipy = ">=1.12.0" -mvcrender = "==0.0.6" +mvcrender = "==0.0.7" [tool.poetry.group.dev.dependencies] ruff = "*" From 3b9d5b23540b596f25a80da0629565c73ab11acd Mon Sep 17 00:00:00 2001 From: SCA075 <82227818+sca075@users.noreply.github.com> Date: Sun, 11 Jan 2026 15:50:24 +0100 Subject: [PATCH 11/27] feat: Add carpet zone detection and rendering support - Add CARPET drawable element with configurable color and alpha - Implement carpet zone parsing from Valetudo JSON (PolygonMapEntity) - Add carpet rendering with customizable color (default: 50% of room_0) - Support disable_carpets flag to toggle carpet visibility - Fix COLORS list order to match base_color_keys mapping - Fix floor rendering to use correct MAP_BACKGROUND color - Add carpet color configuration to device_info Fixes color index mismatch that affected carpet and floor color updates Signed-off-by: SCA075 <82227818+sca075@users.noreply.github.com> --- SCR/valetudo_map_parser/config/colors.py | 6 ++ .../config/drawable_elements.py | 4 + SCR/valetudo_map_parser/config/shared.py | 1 - SCR/valetudo_map_parser/const.py | 6 ++ SCR/valetudo_map_parser/hypfer_draw.py | 76 +++++++++++++++++-- SCR/valetudo_map_parser/hypfer_handler.py | 4 +- SCR/valetudo_map_parser/map_data.py | 38 +++++++++- 7 files changed, 123 insertions(+), 12 deletions(-) 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..23bf60e 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, @@ -265,6 +268,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/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..9f7204b 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,68 @@ 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 _draw_carpets( + self, np_array: NumpyArray, carpet_zones: list + ) -> NumpyArray: + """Draw carpet zones with 50% of room color and alpha 255.""" + from .config.room_store import RoomStore + + # Get room store to map segment IDs to room indices + room_store = RoomStore(self.file_name) + room_keys = list(room_store.get_rooms().keys()) + + for carpet in carpet_zones: + try: + # Get the segment ID from carpet metadata + segment_id = carpet.get("metaData", {}).get("id") + if not segment_id: + continue + + # Find the room index for this segment ID + if segment_id in room_keys: + room_index = room_keys.index(segment_id) + else: + # Default to room 0 if segment ID not found + room_index = 0 + + # Get the room color + if room_index < len(self.img_h.shared.rooms_colors): + room_color = self.img_h.shared.rooms_colors[room_index] + else: + room_color = self.img_h.shared.rooms_colors[0] + + # Calculate carpet color: 50% of room color with alpha 255 + carpet_color = ( + room_color[0] // 2, + room_color[1] // 2, + room_color[2] // 2, + 255, # Full opacity + ) + + # Draw the carpet zone + np_array = await self.img_h.draw.zones( + np_array, [carpet], carpet_color + ) + + except (KeyError, TypeError, IndexError) as e: + _LOGGER.warning( + "%s: Error drawing carpet: %s", self.file_name, str(e) + ) + continue + 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: From 3bea98ea33e739ac80753441bfda49e9782f02c1 Mon Sep 17 00:00:00 2001 From: SCA075 <82227818+sca075@users.noreply.github.com> Date: Sun, 11 Jan 2026 16:04:30 +0100 Subject: [PATCH 12/27] feat: Add carpet zone detection and rendering support from_list Signed-off-by: SCA075 <82227818+sca075@users.noreply.github.com> --- SCR/valetudo_map_parser/config/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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"], From de0effa9ec475f0442bc1156579bc15c71b398f1 Mon Sep 17 00:00:00 2001 From: SCA075 <82227818+sca075@users.noreply.github.com> Date: Sun, 11 Jan 2026 16:12:48 +0100 Subject: [PATCH 13/27] fix: Add missing CARPET to element_color_mapping Fixes carpet color/alpha from device_info not being applied due to missing DrawableElement.CARPET entry in update_from_device_info(). Signed-off-by: SCA075 <82227818+sca075@users.noreply.github.com> --- SCR/valetudo_map_parser/config/drawable_elements.py | 1 + 1 file changed, 1 insertion(+) diff --git a/SCR/valetudo_map_parser/config/drawable_elements.py b/SCR/valetudo_map_parser/config/drawable_elements.py index 23bf60e..8982654 100644 --- a/SCR/valetudo_map_parser/config/drawable_elements.py +++ b/SCR/valetudo_map_parser/config/drawable_elements.py @@ -216,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 From 644b81e53e360db9e5401472a6830c9db9b0c3ad Mon Sep 17 00:00:00 2001 From: SCA075 <82227818+sca075@users.noreply.github.com> Date: Sun, 11 Jan 2026 16:23:00 +0100 Subject: [PATCH 14/27] Dead code removed. The _draw_carpets method was leftover from the initial implementation attempt and is not needed since we're using the simpler approach of passing color_carpet directly from device_info to the zones() drawing method. Signed-off-by: SCA075 <82227818+sca075@users.noreply.github.com> --- SCR/valetudo_map_parser/hypfer_draw.py | 51 -------------------------- pyproject.toml | 2 +- 2 files changed, 1 insertion(+), 52 deletions(-) diff --git a/SCR/valetudo_map_parser/hypfer_draw.py b/SCR/valetudo_map_parser/hypfer_draw.py index 9f7204b..fcba079 100755 --- a/SCR/valetudo_map_parser/hypfer_draw.py +++ b/SCR/valetudo_map_parser/hypfer_draw.py @@ -312,57 +312,6 @@ async def async_draw_zones( return np_array - async def _draw_carpets( - self, np_array: NumpyArray, carpet_zones: list - ) -> NumpyArray: - """Draw carpet zones with 50% of room color and alpha 255.""" - from .config.room_store import RoomStore - - # Get room store to map segment IDs to room indices - room_store = RoomStore(self.file_name) - room_keys = list(room_store.get_rooms().keys()) - - for carpet in carpet_zones: - try: - # Get the segment ID from carpet metadata - segment_id = carpet.get("metaData", {}).get("id") - if not segment_id: - continue - - # Find the room index for this segment ID - if segment_id in room_keys: - room_index = room_keys.index(segment_id) - else: - # Default to room 0 if segment ID not found - room_index = 0 - - # Get the room color - if room_index < len(self.img_h.shared.rooms_colors): - room_color = self.img_h.shared.rooms_colors[room_index] - else: - room_color = self.img_h.shared.rooms_colors[0] - - # Calculate carpet color: 50% of room color with alpha 255 - carpet_color = ( - room_color[0] // 2, - room_color[1] // 2, - room_color[2] // 2, - 255, # Full opacity - ) - - # Draw the carpet zone - np_array = await self.img_h.draw.zones( - np_array, [carpet], carpet_color - ) - - except (KeyError, TypeError, IndexError) as e: - _LOGGER.warning( - "%s: Error drawing carpet: %s", self.file_name, str(e) - ) - continue - - return np_array - async def async_draw_virtual_walls( self, m_json: JsonType, np_array: NumpyArray, color_no_go: Color ) -> NumpyArray: 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" From a67cd56760b3616e0e1d2cda84dbed8cd2512fa5 Mon Sep 17 00:00:00 2001 From: SCA075 <82227818+sca075@users.noreply.github.com> Date: Tue, 13 Jan 2026 20:54:42 +0100 Subject: [PATCH 15/27] Materials implementation and configuration Signed-off-by: SCA075 <82227818+sca075@users.noreply.github.com> --- .../config/drawable_elements.py | 37 +++ SCR/valetudo_map_parser/config/material.py | 230 ++++++++++++++++++ SCR/valetudo_map_parser/hypfer_draw.py | 65 +++++ SCR/valetudo_map_parser/map_data.py | 32 ++- 4 files changed, 357 insertions(+), 7 deletions(-) create mode 100644 SCR/valetudo_map_parser/config/material.py diff --git a/SCR/valetudo_map_parser/config/drawable_elements.py b/SCR/valetudo_map_parser/config/drawable_elements.py index 8982654..5aa5745 100644 --- a/SCR/valetudo_map_parser/config/drawable_elements.py +++ b/SCR/valetudo_map_parser/config/drawable_elements.py @@ -34,6 +34,7 @@ class DrawableElement(IntEnum): PREDICTED_PATH = 10 GO_TO_TARGET = 11 CARPET = 12 + MATERIAL_OVERLAY = 13 # Rooms (101-115 for up to 15 rooms) ROOM_1 = 101 @@ -98,6 +99,15 @@ def _set_default_properties(self): DrawableElement.CARPET: SupportedColor.CARPET, } + # Set default properties for material overlay + self._element_properties[DrawableElement.MATERIAL_OVERLAY] = { + "wood_color": (40, 40, 40), # RGB for wood lines + "wood_alpha": 38, # Alpha for wood lines + "tile_color": (40, 40, 40), # RGB for tile lines + "tile_alpha": 45, # Alpha for tile lines + "z_index": 11, # Draw materials above rooms but below walls + } + # Set z-index for each element type z_indices = { DrawableElement.FLOOR: 0, @@ -219,6 +229,32 @@ def update_from_device_info(self, device_info: dict) -> None: DrawableElement.CARPET: SupportedColor.CARPET, } + # Update material overlay properties if present + if "material_wood_color" in device_info: + self.set_property( + DrawableElement.MATERIAL_OVERLAY, + "wood_color", + device_info["material_wood_color"], + ) + if "material_wood_alpha" in device_info: + self.set_property( + DrawableElement.MATERIAL_OVERLAY, + "wood_alpha", + device_info["material_wood_alpha"], + ) + if "material_tile_color" in device_info: + self.set_property( + DrawableElement.MATERIAL_OVERLAY, + "tile_color", + device_info["material_tile_color"], + ) + if "material_tile_alpha" in device_info: + self.set_property( + DrawableElement.MATERIAL_OVERLAY, + "tile_alpha", + device_info["material_tile_alpha"], + ) + # Update room colors from device info for room_id in range(1, 16): room_element = getattr(DrawableElement, f"ROOM_{room_id}") @@ -274,6 +310,7 @@ def update_from_device_info(self, device_info: dict) -> None: "disable_path": DrawableElement.PATH, "disable_predicted_path": DrawableElement.PREDICTED_PATH, "disable_go_to_target": DrawableElement.GO_TO_TARGET, + "disable_material_overlay": DrawableElement.MATERIAL_OVERLAY, } # Process base element disable flags diff --git a/SCR/valetudo_map_parser/config/material.py b/SCR/valetudo_map_parser/config/material.py new file mode 100644 index 0000000..948ed57 --- /dev/null +++ b/SCR/valetudo_map_parser/config/material.py @@ -0,0 +1,230 @@ +from __future__ import annotations + +from dataclasses import dataclass +from functools import lru_cache +from typing import Final, Optional + +import numpy as np + +from .types import Color, NumpyArray + + +@dataclass(frozen=True, slots=True) +class _MaterialSpec: + cells: int + kind: str # "wood_h", "wood_v", "tile" + + +class MaterialTileRenderer: + """ + Material patterns rendered as small RGBA tiles. + + Wood is drawn as staggered rectangular planks (brick-like) with ONLY thin seams + (no extra inner grain line). + """ + + # Default neutral contour colors (RGBA) – keep subtle + _DEFAULT_WOOD_RGBA: Final[Color] = (40, 40, 40, 38) + _DEFAULT_TILE_RGBA: Final[Color] = (40, 40, 40, 45) + + # Current colors (can be updated via set_colors) + WOOD_RGBA: Color = _DEFAULT_WOOD_RGBA + TILE_RGBA: Color = _DEFAULT_TILE_RGBA + + _SPECS: Final[dict[str, _MaterialSpec]] = { + "wood_horizontal": _MaterialSpec(cells=36, kind="wood_h"), + "wood_vertical": _MaterialSpec(cells=36, kind="wood_v"), + "tile": _MaterialSpec(cells=4, kind="tile"), + } + + @classmethod + def set_colors(cls, wood_rgba: Optional[Color] = None, tile_rgba: Optional[Color] = None) -> None: + """ + Update the material colors used for rendering. + + Args: + wood_rgba: RGBA color for wood lines (default: use current) + tile_rgba: RGBA color for tile lines (default: use current) + """ + if wood_rgba is not None: + cls.WOOD_RGBA = wood_rgba + if tile_rgba is not None: + cls.TILE_RGBA = tile_rgba + + @classmethod + def reset_colors(cls) -> None: + """Reset material colors to defaults.""" + cls.WOOD_RGBA = cls._DEFAULT_WOOD_RGBA + cls.TILE_RGBA = cls._DEFAULT_TILE_RGBA + + @staticmethod + def _empty_rgba(h: int, w: int) -> NumpyArray: + return np.zeros((h, w, 4), dtype=np.uint8) + + @staticmethod + def _thin_px(pixel_size: int) -> int: + """Thin seam thickness in pixels (for pixel_size 5/7 -> 1 px).""" + return 1 if pixel_size <= 7 else 2 + + @staticmethod + def _paint_hline(tile: NumpyArray, y: int, thickness: int, rgba: Color) -> None: + h = tile.shape[0] + if y >= h: + return + y2 = min(h, y + thickness) + tile[y:y2, :, 0] = rgba[0] + tile[y:y2, :, 1] = rgba[1] + tile[y:y2, :, 2] = rgba[2] + tile[y:y2, :, 3] = rgba[3] + + @staticmethod + def _paint_vline(tile: NumpyArray, x: int, thickness: int, rgba: Color) -> None: + w = tile.shape[1] + if x >= w: + return + x2 = min(w, x + thickness) + tile[:, x:x2, 0] = rgba[0] + tile[:, x:x2, 1] = rgba[1] + tile[:, x:x2, 2] = rgba[2] + tile[:, x:x2, 3] = rgba[3] + + @staticmethod + def _paint_rect_outline( + tile: NumpyArray, + x0: int, + y0: int, + x1: int, + y1: int, + thickness: int, + rgba: Color, + ) -> None: + if x1 <= x0 or y1 <= y0: + return + MaterialTileRenderer._paint_hline(tile, y0, thickness, rgba) + MaterialTileRenderer._paint_hline(tile, max(y0, y1 - thickness), thickness, rgba) + MaterialTileRenderer._paint_vline(tile, x0, thickness, rgba) + MaterialTileRenderer._paint_vline(tile, max(x0, x1 - thickness), thickness, rgba) + + @staticmethod + def _wood_planks_horizontal(tile_px: int, pixel_size: int) -> NumpyArray: + """ + Horizontal wood planks as staggered rectangles. + ONLY thin seams (no inner lines). + """ + t = MaterialTileRenderer._empty_rgba(tile_px, tile_px) + seam = MaterialTileRenderer._thin_px(pixel_size) + + # Plank size in CELLS (tweak here) + plank_h_cells = 3 + plank_w_cells = 24 # longer planks -> looks less like tiles + + plank_h = plank_h_cells * pixel_size + plank_w = plank_w_cells * pixel_size + + rows = max(1, tile_px // plank_h) + cols = max(1, (tile_px + plank_w - 1) // plank_w) + + for r in range(rows + 1): + y0 = r * plank_h + y1 = y0 + plank_h + offset = (plank_w // 2) if (r % 2 == 1) else 0 + + for c in range(cols + 1): + x0 = c * plank_w - offset + x1 = x0 + plank_w + + cx0 = max(0, x0) + cy0 = max(0, y0) + cx1 = min(tile_px, x1) + cy1 = min(tile_px, y1) + + MaterialTileRenderer._paint_rect_outline( + t, cx0, cy0, cx1, cy1, seam, MaterialTileRenderer.WOOD_RGBA + ) + + return t + + @staticmethod + def _wood_planks_vertical(tile_px: int, pixel_size: int) -> NumpyArray: + """Vertical wood planks as staggered rectangles, ONLY thin seams.""" + t = MaterialTileRenderer._empty_rgba(tile_px, tile_px) + seam = MaterialTileRenderer._thin_px(pixel_size) + + plank_w_cells = 3 + plank_h_cells = 24 + + plank_w = plank_w_cells * pixel_size + plank_h = plank_h_cells * pixel_size + + cols = max(1, tile_px // plank_w) + rows = max(1, (tile_px + plank_h - 1) // plank_h) + + for c in range(cols + 1): + x0 = c * plank_w + x1 = x0 + plank_w + offset = (plank_h // 2) if (c % 2 == 1) else 0 + + for r in range(rows + 1): + y0 = r * plank_h - offset + y1 = y0 + plank_h + + cx0 = max(0, x0) + cy0 = max(0, y0) + cx1 = min(tile_px, x1) + cy1 = min(tile_px, y1) + + MaterialTileRenderer._paint_rect_outline( + t, cx0, cy0, cx1, cy1, seam, MaterialTileRenderer.WOOD_RGBA + ) + + return t + + @staticmethod + def _tile_pixels(cells: int, pixel_size: int) -> NumpyArray: + size = cells * pixel_size + t = MaterialTileRenderer._empty_rgba(size, size) + th = MaterialTileRenderer._thin_px(pixel_size) + rgba = MaterialTileRenderer.TILE_RGBA + MaterialTileRenderer._paint_hline(t, 0, th, rgba) + MaterialTileRenderer._paint_vline(t, 0, th, rgba) + return t + + @staticmethod + @lru_cache(maxsize=64) + def get_tile(material: str, pixel_size: int) -> Optional[NumpyArray]: + spec = MaterialTileRenderer._SPECS.get(material) + if spec is None or pixel_size <= 0: + return None + + if spec.kind == "tile": + return MaterialTileRenderer._tile_pixels(spec.cells, pixel_size) + + tile_px = spec.cells * pixel_size + if spec.kind == "wood_h": + return MaterialTileRenderer._wood_planks_horizontal(tile_px, pixel_size) + if spec.kind == "wood_v": + return MaterialTileRenderer._wood_planks_vertical(tile_px, pixel_size) + + return None + + @staticmethod + def tile_block(tile: NumpyArray, r0: int, r1: int, c0: int, c1: int) -> NumpyArray: + th, tw, _ = tile.shape + rows = (np.arange(r0, r1) % th).astype(np.intp, copy=False) + cols = (np.arange(c0, c1) % tw).astype(np.intp, copy=False) + return tile[rows[:, None], cols[None, :], :] + + @staticmethod + def apply_overlay_on_region( + image: NumpyArray, + tile: NumpyArray, + r0: int, + r1: int, + c0: int, + c1: int, + ) -> None: + region = image[r0:r1, c0:c1] + overlay = MaterialTileRenderer.tile_block(tile, r0, r1, c0, c1) + mask = overlay[..., 3] > 0 + if np.any(mask): + region[mask] = overlay[mask] diff --git a/SCR/valetudo_map_parser/hypfer_draw.py b/SCR/valetudo_map_parser/hypfer_draw.py index fcba079..36ff472 100755 --- a/SCR/valetudo_map_parser/hypfer_draw.py +++ b/SCR/valetudo_map_parser/hypfer_draw.py @@ -9,6 +9,7 @@ import logging from .config.drawable_elements import DrawableElement +from .config.material import MaterialTileRenderer from .config.types import Color, JsonType, NumpyArray, RobotPosition, RoomStore from .config.utils import point_in_polygon @@ -117,6 +118,22 @@ async def _process_room_layer( img_np_array, pixels, pixel_size, room_color ) + # Apply material overlay if present for this segment and enabled + if ( + layer_type == "segment" + and hasattr(self.img_h, "json_data") + and self.img_h.drawing_config.is_enabled(DrawableElement.MATERIAL_OVERLAY) + ): + segment_id = str(room_id + 1) + materials = getattr(self.img_h.json_data, "materials", {}) + material = materials.get(segment_id) + if material and material != "generic": + # Update MaterialTileRenderer colors from config + self._update_material_colors() + img_np_array = self._apply_material_overlay( + img_np_array, pixels, pixel_size, material + ) + # 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 @@ -126,6 +143,54 @@ async def _process_room_layer( return img_np_array, room_id + def _update_material_colors(self): + """Update MaterialTileRenderer colors from DrawingConfig.""" + wood_color = self.img_h.drawing_config.get_property( + DrawableElement.MATERIAL_OVERLAY, "wood_color", (40, 40, 40) + ) + wood_alpha = self.img_h.drawing_config.get_property( + DrawableElement.MATERIAL_OVERLAY, "wood_alpha", 38 + ) + tile_color = self.img_h.drawing_config.get_property( + DrawableElement.MATERIAL_OVERLAY, "tile_color", (40, 40, 40) + ) + tile_alpha = self.img_h.drawing_config.get_property( + DrawableElement.MATERIAL_OVERLAY, "tile_alpha", 45 + ) + + # Update MaterialTileRenderer with configured colors + MaterialTileRenderer.set_colors( + wood_rgba=(*wood_color, wood_alpha), + tile_rgba=(*tile_color, tile_alpha), + ) + + def _apply_material_overlay(self, img_np_array, pixels, pixel_size, material): + """Apply material texture overlay to a room segment.""" + try: + tile = MaterialTileRenderer.get_tile(material, pixel_size) + if tile is None: + return img_np_array + + if not pixels: + return img_np_array + + # Apply overlay directly to each pixel block + for x, y, count in pixels: + row = y * pixel_size + col = x * pixel_size + for i in range(count): + c_start = col + i * pixel_size + c_end = c_start + pixel_size + overlay = MaterialTileRenderer.tile_block(tile, row, row + pixel_size, c_start, c_end) + region = img_np_array[row:row + pixel_size, c_start:c_end] + alpha = overlay[..., 3:4] / 255.0 + region[:] = (1 - alpha) * region + alpha * overlay + + except Exception as e: + _LOGGER.warning("%s: Material overlay error: %s", self.file_name, str(e)) + + return img_np_array + def _get_active_room_color(self, room_id, room_color, color_zone_clean): """Adjust the room color if the room is active.""" if self.img_h.active_zones and room_id < len(self.img_h.active_zones): diff --git a/SCR/valetudo_map_parser/map_data.py b/SCR/valetudo_map_parser/map_data.py index 9722482..9538d94 100755 --- a/SCR/valetudo_map_parser/map_data.py +++ b/SCR/valetudo_map_parser/map_data.py @@ -62,6 +62,7 @@ class SegmentMeta(TypedDict, total=False): active: bool source: str area: int + material: str class MapLayerBase(TypedDict): @@ -251,7 +252,8 @@ def find_layers( json_obj: JsonType, layer_dict: dict[str, list[Any]] | None, active_list: list[int] | None, - ) -> tuple[dict[str, list[Any]], list[int]]: + materials_dict: dict[str, str] | None = None, + ) -> tuple[dict[str, list[Any]], list[int], dict[str, str]]: """ Recursively traverse a JSON-like structure to find MapLayer entries. @@ -259,15 +261,21 @@ def find_layers( json_obj: The JSON-like object (dicts/lists) to search. layer_dict: Optional mapping of layer_type to a list of compressed pixel data. active_list: Optional list of active segment flags. + materials_dict: Optional mapping of segment_id to material type. Returns: A tuple: - dict mapping layer types to their compressed pixel arrays. - list of integers marking active segment layers. + - dict mapping segment IDs to material types. """ if layer_dict is None: layer_dict = {} active_list = [] + materials_dict = {} + + if materials_dict is None: + materials_dict = {} if isinstance(json_obj, dict): if json_obj.get("__class") == "MapLayer": @@ -292,22 +300,28 @@ def find_layers( layer_dict.setdefault(layer_type, []).append(compressed_pixels) - # Safely extract "active" flag if present and convertible to int + # Safely extract "active" flag and material if present if layer_type == "segment": try: active_list.append(int(meta_data.get("active", 0))) except (ValueError, TypeError): pass # skip invalid/missing 'active' values + # Extract material if present + segment_id = meta_data.get("segmentId") + material = meta_data.get("material") + if segment_id and material: + materials_dict[segment_id] = material + # json_obj.items() yields (key, value), so we only want the values for _, value in json_obj.items(): - ImageData.find_layers(value, layer_dict, active_list) + ImageData.find_layers(value, layer_dict, active_list, materials_dict) elif isinstance(json_obj, list): for item in json_obj: - ImageData.find_layers(item, layer_dict, active_list) + ImageData.find_layers(item, layer_dict, active_list, materials_dict) - return layer_dict, active_list + return layer_dict, active_list, materials_dict @staticmethod def find_points_entities( @@ -786,6 +800,7 @@ class HyperMapData: layers: dict[str, list[Any]] = field(default_factory=dict) active_zones: list[int] = field(default_factory=list) virtual_walls: list[list[tuple[float, float]]] = field(default_factory=list) + materials: dict[str, str] = field(default_factory=dict) @classmethod async def async_from_valetudo_json(cls, json_data: Any) -> "HyperMapData": @@ -801,14 +816,15 @@ async def async_from_valetudo_json(cls, json_data: Any) -> "HyperMapData": areas = ImageData.find_zone_entities(json_data) layers = {} active_zones = [] + materials = {} # Hypothetical obstacles finder, if you have one obstacles = getattr(ImageData, "find_obstacles_entities", lambda *_: {})( json_data ) virtual_walls = ImageData.find_virtual_walls(json_data) pixel_size = int(json_data["pixelSize"]) - layers, active_zones = ImageData.find_layers( - json_data["layers"], layers, active_zones + layers, active_zones, materials = ImageData.find_layers( + json_data["layers"], layers, active_zones, materials ) entity_dict = ImageData.find_points_entities(json_data) @@ -824,6 +840,7 @@ async def async_from_valetudo_json(cls, json_data: Any) -> "HyperMapData": pixel_size=pixel_size, layers=layers, active_zones=active_zones, + materials=materials, ) def to_dict(self) -> dict[str, Any]: @@ -847,6 +864,7 @@ def from_dict(cls, data: dict[str, Any]) -> "HyperMapData": layers=data.get("layers", {}), active_zones=data.get("active_zones", []), virtual_walls=data.get("virtual_walls", []), + materials=data.get("materials", {}), ) def update_from_dict(self, updates: dict[str, Any]) -> None: From 317511db4d7fb4f95e1a1b439479b725fd29436c Mon Sep 17 00:00:00 2001 From: SCA075 <82227818+sca075@users.noreply.github.com> Date: Tue, 13 Jan 2026 21:52:28 +0100 Subject: [PATCH 16/27] removed test and unused code _update_material_colors() Signed-off-by: SCA075 <82227818+sca075@users.noreply.github.com> --- SCR/valetudo_map_parser/hypfer_draw.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/SCR/valetudo_map_parser/hypfer_draw.py b/SCR/valetudo_map_parser/hypfer_draw.py index 36ff472..3be0182 100755 --- a/SCR/valetudo_map_parser/hypfer_draw.py +++ b/SCR/valetudo_map_parser/hypfer_draw.py @@ -128,8 +128,6 @@ async def _process_room_layer( materials = getattr(self.img_h.json_data, "materials", {}) material = materials.get(segment_id) if material and material != "generic": - # Update MaterialTileRenderer colors from config - self._update_material_colors() img_np_array = self._apply_material_overlay( img_np_array, pixels, pixel_size, material ) @@ -143,27 +141,6 @@ async def _process_room_layer( return img_np_array, room_id - def _update_material_colors(self): - """Update MaterialTileRenderer colors from DrawingConfig.""" - wood_color = self.img_h.drawing_config.get_property( - DrawableElement.MATERIAL_OVERLAY, "wood_color", (40, 40, 40) - ) - wood_alpha = self.img_h.drawing_config.get_property( - DrawableElement.MATERIAL_OVERLAY, "wood_alpha", 38 - ) - tile_color = self.img_h.drawing_config.get_property( - DrawableElement.MATERIAL_OVERLAY, "tile_color", (40, 40, 40) - ) - tile_alpha = self.img_h.drawing_config.get_property( - DrawableElement.MATERIAL_OVERLAY, "tile_alpha", 45 - ) - - # Update MaterialTileRenderer with configured colors - MaterialTileRenderer.set_colors( - wood_rgba=(*wood_color, wood_alpha), - tile_rgba=(*tile_color, tile_alpha), - ) - def _apply_material_overlay(self, img_np_array, pixels, pixel_size, material): """Apply material texture overlay to a room segment.""" try: From dbe642fdd1ecf488c6af17f2d4e3bc3b735454bb Mon Sep 17 00:00:00 2001 From: SCA075 <82227818+sca075@users.noreply.github.com> Date: Tue, 13 Jan 2026 22:33:36 +0100 Subject: [PATCH 17/27] improved material.py drawings with mcvrender Signed-off-by: SCA075 <82227818+sca075@users.noreply.github.com> --- SCR/valetudo_map_parser/config/material.py | 53 +++++++++------------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/SCR/valetudo_map_parser/config/material.py b/SCR/valetudo_map_parser/config/material.py index 948ed57..e59441e 100644 --- a/SCR/valetudo_map_parser/config/material.py +++ b/SCR/valetudo_map_parser/config/material.py @@ -6,6 +6,7 @@ import numpy as np +from mvcrender.draw import line_u8 from .types import Color, NumpyArray @@ -67,29 +68,7 @@ def _thin_px(pixel_size: int) -> int: return 1 if pixel_size <= 7 else 2 @staticmethod - def _paint_hline(tile: NumpyArray, y: int, thickness: int, rgba: Color) -> None: - h = tile.shape[0] - if y >= h: - return - y2 = min(h, y + thickness) - tile[y:y2, :, 0] = rgba[0] - tile[y:y2, :, 1] = rgba[1] - tile[y:y2, :, 2] = rgba[2] - tile[y:y2, :, 3] = rgba[3] - - @staticmethod - def _paint_vline(tile: NumpyArray, x: int, thickness: int, rgba: Color) -> None: - w = tile.shape[1] - if x >= w: - return - x2 = min(w, x + thickness) - tile[:, x:x2, 0] = rgba[0] - tile[:, x:x2, 1] = rgba[1] - tile[:, x:x2, 2] = rgba[2] - tile[:, x:x2, 3] = rgba[3] - - @staticmethod - def _paint_rect_outline( + def _draw_rect_outline( tile: NumpyArray, x0: int, y0: int, @@ -98,12 +77,19 @@ def _paint_rect_outline( thickness: int, rgba: Color, ) -> None: + """Draw rectangle outline using mvcrender.line_u8.""" if x1 <= x0 or y1 <= y0: return - MaterialTileRenderer._paint_hline(tile, y0, thickness, rgba) - MaterialTileRenderer._paint_hline(tile, max(y0, y1 - thickness), thickness, rgba) - MaterialTileRenderer._paint_vline(tile, x0, thickness, rgba) - MaterialTileRenderer._paint_vline(tile, max(x0, x1 - thickness), thickness, rgba) + + # Draw four lines to form rectangle outline + # Top line + line_u8(tile, x0, y0, x1 - 1, y0, rgba, thickness) + # Bottom line + line_u8(tile, x0, y1 - 1, x1 - 1, y1 - 1, rgba, thickness) + # Left line + line_u8(tile, x0, y0, x0, y1 - 1, rgba, thickness) + # Right line + line_u8(tile, x1 - 1, y0, x1 - 1, y1 - 1, rgba, thickness) @staticmethod def _wood_planks_horizontal(tile_px: int, pixel_size: int) -> NumpyArray: @@ -138,7 +124,7 @@ def _wood_planks_horizontal(tile_px: int, pixel_size: int) -> NumpyArray: cx1 = min(tile_px, x1) cy1 = min(tile_px, y1) - MaterialTileRenderer._paint_rect_outline( + MaterialTileRenderer._draw_rect_outline( t, cx0, cy0, cx1, cy1, seam, MaterialTileRenderer.WOOD_RGBA ) @@ -173,7 +159,7 @@ def _wood_planks_vertical(tile_px: int, pixel_size: int) -> NumpyArray: cx1 = min(tile_px, x1) cy1 = min(tile_px, y1) - MaterialTileRenderer._paint_rect_outline( + MaterialTileRenderer._draw_rect_outline( t, cx0, cy0, cx1, cy1, seam, MaterialTileRenderer.WOOD_RGBA ) @@ -181,12 +167,17 @@ def _wood_planks_vertical(tile_px: int, pixel_size: int) -> NumpyArray: @staticmethod def _tile_pixels(cells: int, pixel_size: int) -> NumpyArray: + """Draw tile grid using mvcrender.line_u8.""" size = cells * pixel_size t = MaterialTileRenderer._empty_rgba(size, size) th = MaterialTileRenderer._thin_px(pixel_size) rgba = MaterialTileRenderer.TILE_RGBA - MaterialTileRenderer._paint_hline(t, 0, th, rgba) - MaterialTileRenderer._paint_vline(t, 0, th, rgba) + + # Draw horizontal line at top + line_u8(t, 0, 0, size - 1, 0, rgba, th) + # Draw vertical line at left + line_u8(t, 0, 0, 0, size - 1, rgba, th) + return t @staticmethod From 9ac664bb9e2171b44e165a274e446daa7c9eda5b Mon Sep 17 00:00:00 2001 From: SCA075 <82227818+sca075@users.noreply.github.com> Date: Fri, 16 Jan 2026 23:19:22 +0100 Subject: [PATCH 18/27] isort and ruffed code and added configurable colours for material.py Signed-off-by: SCA075 <82227818+sca075@users.noreply.github.com> --- SCR/valetudo_map_parser/config/colors.py | 25 +++++- .../config/drawable_elements.py | 6 +- SCR/valetudo_map_parser/config/material.py | 77 +++++++++---------- SCR/valetudo_map_parser/config/shared.py | 8 +- SCR/valetudo_map_parser/const.py | 16 +++- SCR/valetudo_map_parser/hypfer_draw.py | 29 +++++-- SCR/valetudo_map_parser/hypfer_handler.py | 6 +- SCR/valetudo_map_parser/map_data.py | 4 +- SCR/valetudo_map_parser/rand256_handler.py | 4 +- pyproject.toml | 2 +- 10 files changed, 118 insertions(+), 59 deletions(-) diff --git a/SCR/valetudo_map_parser/config/colors.py b/SCR/valetudo_map_parser/config/colors.py index 904da4d..aa96a33 100644 --- a/SCR/valetudo_map_parser/config/colors.py +++ b/SCR/valetudo_map_parser/config/colors.py @@ -9,9 +9,11 @@ from ..const import ( ALPHA_BACKGROUND, - ALPHA_CHARGER, ALPHA_CARPET, + ALPHA_CHARGER, ALPHA_GO_TO, + ALPHA_MATERIAL_TILE, + ALPHA_MATERIAL_WOOD, ALPHA_MOVE, ALPHA_NO_GO, ALPHA_ROBOT, @@ -35,9 +37,11 @@ ALPHA_WALL, ALPHA_ZONE_CLEAN, COLOR_BACKGROUND, - COLOR_CHARGER, COLOR_CARPET, + COLOR_CHARGER, COLOR_GO_TO, + COLOR_MATERIAL_TILE, + COLOR_MATERIAL_WOOD, COLOR_MOVE, COLOR_NO_GO, COLOR_ROBOT, @@ -77,6 +81,8 @@ color_text = (255, 255, 255, 255) color_grey = (125, 125, 125, 255) color_black = (0, 0, 0, 255) +color_material_wood = (40, 40, 40, 38) +color_material_tile = (40, 40, 40, 45) color_room_0 = (135, 206, 250, 255) color_room_1 = (176, 226, 255, 255) color_room_2 = (164, 211, 238, 255) @@ -116,6 +122,9 @@ base_colors_array = [ color_wall, color_zone_clean, + color_carpet, + color_material_wood, + color_material_tile, color_robot, color_background, color_move, @@ -129,6 +138,8 @@ base_colors_array[0], # color_wall base_colors_array[6], # color_no_go base_colors_array[7], # color_go_to + base_colors_array[8], # color_predicted_path + base_colors_array[9], # color_obstacle color_black, base_colors_array[2], # color_robot base_colors_array[5], # color_charger @@ -153,6 +164,9 @@ class SupportedColor(StrEnum): NO_GO = "color_no_go" ZONE_CLEAN = "color_zone_clean" CARPET = "color_carpet" + OBSTACLE = "color_obstacle" + TILE = "color_material_tile" + WOOD = "color_material_wood" MAP_BACKGROUND = "color_background" TEXT = "color_text" TRANSPARENT = "color_transparent" @@ -175,7 +189,10 @@ 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.CARPET: (67, 103, 125), + SupportedColor.OBSTACLE: (255, 0, 0), + SupportedColor.TILE: (40, 40, 40), + SupportedColor.WOOD: (40, 40, 40), SupportedColor.MAP_BACKGROUND: (0, 125, 255), SupportedColor.TEXT: (0, 0, 0), SupportedColor.TRANSPARENT: (0, 0, 0), @@ -310,6 +327,8 @@ def set_initial_colours(self, device_info: dict) -> None: (COLOR_NO_GO, color_no_go, ALPHA_NO_GO), (COLOR_GO_TO, color_go_to, ALPHA_GO_TO), (COLOR_TEXT, color_text, ALPHA_TEXT), + (COLOR_MATERIAL_WOOD, color_material_wood, ALPHA_MATERIAL_WOOD), + (COLOR_MATERIAL_TILE, color_material_tile, ALPHA_MATERIAL_TILE), ] room_color_keys = [ diff --git a/SCR/valetudo_map_parser/config/drawable_elements.py b/SCR/valetudo_map_parser/config/drawable_elements.py index 5aa5745..13b4db8 100644 --- a/SCR/valetudo_map_parser/config/drawable_elements.py +++ b/SCR/valetudo_map_parser/config/drawable_elements.py @@ -240,19 +240,19 @@ def update_from_device_info(self, device_info: dict) -> None: self.set_property( DrawableElement.MATERIAL_OVERLAY, "wood_alpha", - device_info["material_wood_alpha"], + device_info["alpha_material_wood"], ) if "material_tile_color" in device_info: self.set_property( DrawableElement.MATERIAL_OVERLAY, "tile_color", - device_info["material_tile_color"], + device_info["color_material_tile"], ) if "material_tile_alpha" in device_info: self.set_property( DrawableElement.MATERIAL_OVERLAY, "tile_alpha", - device_info["material_tile_alpha"], + device_info["alpha_material_tile"], ) # Update room colors from device info diff --git a/SCR/valetudo_map_parser/config/material.py b/SCR/valetudo_map_parser/config/material.py index e59441e..a367081 100644 --- a/SCR/valetudo_map_parser/config/material.py +++ b/SCR/valetudo_map_parser/config/material.py @@ -5,8 +5,9 @@ from typing import Final, Optional import numpy as np - from mvcrender.draw import line_u8 + +from .colors import color_material_tile, color_material_wood from .types import Color, NumpyArray @@ -16,6 +17,18 @@ class _MaterialSpec: kind: str # "wood_h", "wood_v", "tile" +@dataclass(frozen=True, slots=True) +class MaterialColors: + """Material colors for rendering.""" + + wood_rgba: Color = color_material_wood + tile_rgba: Color = color_material_tile + + +# Create a singleton instance for easy access +_material_colors = MaterialColors() + + class MaterialTileRenderer: """ Material patterns rendered as small RGBA tiles. @@ -24,40 +37,12 @@ class MaterialTileRenderer: (no extra inner grain line). """ - # Default neutral contour colors (RGBA) – keep subtle - _DEFAULT_WOOD_RGBA: Final[Color] = (40, 40, 40, 38) - _DEFAULT_TILE_RGBA: Final[Color] = (40, 40, 40, 45) - - # Current colors (can be updated via set_colors) - WOOD_RGBA: Color = _DEFAULT_WOOD_RGBA - TILE_RGBA: Color = _DEFAULT_TILE_RGBA - _SPECS: Final[dict[str, _MaterialSpec]] = { "wood_horizontal": _MaterialSpec(cells=36, kind="wood_h"), "wood_vertical": _MaterialSpec(cells=36, kind="wood_v"), "tile": _MaterialSpec(cells=4, kind="tile"), } - @classmethod - def set_colors(cls, wood_rgba: Optional[Color] = None, tile_rgba: Optional[Color] = None) -> None: - """ - Update the material colors used for rendering. - - Args: - wood_rgba: RGBA color for wood lines (default: use current) - tile_rgba: RGBA color for tile lines (default: use current) - """ - if wood_rgba is not None: - cls.WOOD_RGBA = wood_rgba - if tile_rgba is not None: - cls.TILE_RGBA = tile_rgba - - @classmethod - def reset_colors(cls) -> None: - """Reset material colors to defaults.""" - cls.WOOD_RGBA = cls._DEFAULT_WOOD_RGBA - cls.TILE_RGBA = cls._DEFAULT_TILE_RGBA - @staticmethod def _empty_rgba(h: int, w: int) -> NumpyArray: return np.zeros((h, w, 4), dtype=np.uint8) @@ -92,7 +77,9 @@ def _draw_rect_outline( line_u8(tile, x1 - 1, y0, x1 - 1, y1 - 1, rgba, thickness) @staticmethod - def _wood_planks_horizontal(tile_px: int, pixel_size: int) -> NumpyArray: + def _wood_planks_horizontal( + tile_px: int, pixel_size: int, color: Color + ) -> NumpyArray: """ Horizontal wood planks as staggered rectangles. ONLY thin seams (no inner lines). @@ -125,13 +112,15 @@ def _wood_planks_horizontal(tile_px: int, pixel_size: int) -> NumpyArray: cy1 = min(tile_px, y1) MaterialTileRenderer._draw_rect_outline( - t, cx0, cy0, cx1, cy1, seam, MaterialTileRenderer.WOOD_RGBA + t, cx0, cy0, cx1, cy1, seam, color ) return t @staticmethod - def _wood_planks_vertical(tile_px: int, pixel_size: int) -> NumpyArray: + def _wood_planks_vertical( + tile_px: int, pixel_size: int, color: Color + ) -> NumpyArray: """Vertical wood planks as staggered rectangles, ONLY thin seams.""" t = MaterialTileRenderer._empty_rgba(tile_px, tile_px) seam = MaterialTileRenderer._thin_px(pixel_size) @@ -160,18 +149,18 @@ def _wood_planks_vertical(tile_px: int, pixel_size: int) -> NumpyArray: cy1 = min(tile_px, y1) MaterialTileRenderer._draw_rect_outline( - t, cx0, cy0, cx1, cy1, seam, MaterialTileRenderer.WOOD_RGBA + t, cx0, cy0, cx1, cy1, seam, color ) return t @staticmethod - def _tile_pixels(cells: int, pixel_size: int) -> NumpyArray: + def _tile_pixels(cells: int, pixel_size: int, tile_rgba: Color) -> NumpyArray: """Draw tile grid using mvcrender.line_u8.""" size = cells * pixel_size t = MaterialTileRenderer._empty_rgba(size, size) th = MaterialTileRenderer._thin_px(pixel_size) - rgba = MaterialTileRenderer.TILE_RGBA + rgba = tile_rgba # Draw horizontal line at top line_u8(t, 0, 0, size - 1, 0, rgba, th) @@ -182,19 +171,29 @@ def _tile_pixels(cells: int, pixel_size: int) -> NumpyArray: @staticmethod @lru_cache(maxsize=64) - def get_tile(material: str, pixel_size: int) -> Optional[NumpyArray]: + def get_tile( + material: str, pixel_size: int, wood_rgba: Color = None, tile_rgba: Color = None + ) -> Optional[NumpyArray]: spec = MaterialTileRenderer._SPECS.get(material) if spec is None or pixel_size <= 0: return None + # Use provided colors or fall back to defaults + wood_color = wood_rgba if wood_rgba else color_material_wood + tile_color = tile_rgba if tile_rgba else color_material_tile + if spec.kind == "tile": - return MaterialTileRenderer._tile_pixels(spec.cells, pixel_size) + return MaterialTileRenderer._tile_pixels(spec.cells, pixel_size, tile_color) tile_px = spec.cells * pixel_size if spec.kind == "wood_h": - return MaterialTileRenderer._wood_planks_horizontal(tile_px, pixel_size) + return MaterialTileRenderer._wood_planks_horizontal( + tile_px, pixel_size, wood_color + ) if spec.kind == "wood_v": - return MaterialTileRenderer._wood_planks_vertical(tile_px, pixel_size) + return MaterialTileRenderer._wood_planks_vertical( + tile_px, pixel_size, wood_color + ) return None diff --git a/SCR/valetudo_map_parser/config/shared.py b/SCR/valetudo_map_parser/config/shared.py index b106bb2..aa62f07 100755 --- a/SCR/valetudo_map_parser/config/shared.py +++ b/SCR/valetudo_map_parser/config/shared.py @@ -312,11 +312,15 @@ def update_shared_data(self, device_info): ) # Check for new floors_data first floors_data = device_info.get("floors_data", None) - current_floor = device_info.get("current_floor", "floor_0") # Default fallback + current_floor = device_info.get( + "current_floor", "floor_0" + ) # Default fallback if floors_data: # NEW: Use floors_data - floor_trims = floors_data.get(current_floor, DEFAULT_VALUES["trims_data"]) + floor_trims = floors_data.get( + current_floor, DEFAULT_VALUES["trims_data"] + ) instance.trims = TrimsData.from_dict(floor_trims) instance.current_floor = current_floor else: diff --git a/SCR/valetudo_map_parser/const.py b/SCR/valetudo_map_parser/const.py index 6c6508d..a6514d1 100644 --- a/SCR/valetudo_map_parser/const.py +++ b/SCR/valetudo_map_parser/const.py @@ -85,7 +85,13 @@ "vac_status_position": True, "get_svg_file": False, "save_trims": True, - "trims_data": {"floor": "floor_0", "trim_left": 0, "trim_up": 0, "trim_right": 0, "trim_down": 0}, + "trims_data": { + "floor": "floor_0", + "trim_left": 0, + "trim_up": 0, + "trim_right": 0, + "trim_down": 0, + }, "enable_www_snapshots": False, "color_charger": [255, 128, 0], "color_move": [238, 247, 255], @@ -97,6 +103,8 @@ "color_carpet": [67, 103, 125], "color_background": [0, 125, 255], "color_text": [255, 255, 255], + "color_material_wood": [40, 40, 40], + "color_material_tile": [40, 40, 40], "alpha_charger": 255.0, "alpha_move": 255.0, "alpha_wall": 255.0, @@ -107,6 +115,8 @@ "alpha_carpet": 255.0, "alpha_background": 255.0, "alpha_text": 255.0, + "alpha_material_wood": 38.0, + "alpha_material_tile": 45.0, "color_room_0": [135, 206, 250], "color_room_1": [176, 226, 255], "color_room_2": [165, 105, 18], @@ -235,6 +245,8 @@ COLOR_CHARGER = "color_charger" COLOR_CARPET = "color_carpet" COLOR_MOVE = "color_move" +COLOR_MATERIAL_WOOD = "color_material_wood" +COLOR_MATERIAL_TILE = "color_material_tile" COLOR_ROBOT = "color_robot" COLOR_NO_GO = "color_no_go" COLOR_GO_TO = "color_go_to" @@ -269,6 +281,8 @@ ALPHA_NO_GO = "alpha_no_go" ALPHA_GO_TO = "alpha_go_to" ALPHA_BACKGROUND = "alpha_background" +ALPHA_MATERIAL_WOOD = "alpha_material_wood" +ALPHA_MATERIAL_TILE = "alpha_material_tile" ALPHA_ZONE_CLEAN = "alpha_zone_clean" ALPHA_WALL = "alpha_wall" ALPHA_TEXT = "alpha_text" diff --git a/SCR/valetudo_map_parser/hypfer_draw.py b/SCR/valetudo_map_parser/hypfer_draw.py index 3be0182..bd87f19 100755 --- a/SCR/valetudo_map_parser/hypfer_draw.py +++ b/SCR/valetudo_map_parser/hypfer_draw.py @@ -122,14 +122,25 @@ async def _process_room_layer( if ( layer_type == "segment" and hasattr(self.img_h, "json_data") - and self.img_h.drawing_config.is_enabled(DrawableElement.MATERIAL_OVERLAY) + and self.img_h.drawing_config.is_enabled( + DrawableElement.MATERIAL_OVERLAY + ) ): segment_id = str(room_id + 1) materials = getattr(self.img_h.json_data, "materials", {}) material = materials.get(segment_id) if material and material != "generic": + # Get material colors from shared.user_colors + # Index 10 = wood, Index 11 = tile + wood_rgba = self.img_h.shared.user_colors[10] + tile_rgba = self.img_h.shared.user_colors[11] img_np_array = self._apply_material_overlay( - img_np_array, pixels, pixel_size, material + img_np_array, + pixels, + pixel_size, + material, + wood_rgba, + tile_rgba, ) # Increment room_id only for segment layers, not for floor layers @@ -141,10 +152,14 @@ async def _process_room_layer( return img_np_array, room_id - def _apply_material_overlay(self, img_np_array, pixels, pixel_size, material): + def _apply_material_overlay( + self, img_np_array, pixels, pixel_size, material, wood_rgba, tile_rgba + ): """Apply material texture overlay to a room segment.""" try: - tile = MaterialTileRenderer.get_tile(material, pixel_size) + tile = MaterialTileRenderer.get_tile( + material, pixel_size, wood_rgba, tile_rgba + ) if tile is None: return img_np_array @@ -158,8 +173,10 @@ def _apply_material_overlay(self, img_np_array, pixels, pixel_size, material): for i in range(count): c_start = col + i * pixel_size c_end = c_start + pixel_size - overlay = MaterialTileRenderer.tile_block(tile, row, row + pixel_size, c_start, c_end) - region = img_np_array[row:row + pixel_size, c_start:c_end] + overlay = MaterialTileRenderer.tile_block( + tile, row, row + pixel_size, c_start, c_end + ) + region = img_np_array[row : row + pixel_size, c_start:c_end] alpha = overlay[..., 3:4] / 255.0 region[:] = (1 - alpha) * region + alpha * overlay diff --git a/SCR/valetudo_map_parser/hypfer_handler.py b/SCR/valetudo_map_parser/hypfer_handler.py index 04d9a67..3df4f76 100644 --- a/SCR/valetudo_map_parser/hypfer_handler.py +++ b/SCR/valetudo_map_parser/hypfer_handler.py @@ -231,7 +231,11 @@ 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"], colors.get("carpet") + 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 9538d94..1f6121c 100755 --- a/SCR/valetudo_map_parser/map_data.py +++ b/SCR/valetudo_map_parser/map_data.py @@ -294,7 +294,9 @@ def find_layers( compressed_pixels = [] for i in range(0, len(pixels), 2): if i + 1 < len(pixels): - compressed_pixels.extend([pixels[i], pixels[i + 1], 1]) + compressed_pixels.extend( + [pixels[i], pixels[i + 1], 1] + ) else: compressed_pixels = [] diff --git a/SCR/valetudo_map_parser/rand256_handler.py b/SCR/valetudo_map_parser/rand256_handler.py index 8a68f09..8e150e8 100644 --- a/SCR/valetudo_map_parser/rand256_handler.py +++ b/SCR/valetudo_map_parser/rand256_handler.py @@ -15,7 +15,6 @@ from .config.async_utils import AsyncPIL from .config.drawable_elements import DrawableElement -from .const import COLORS, DEFAULT_IMAGE_SIZE, DEFAULT_PIXEL_SIZE from .config.types import ( LOGGER, Colors, @@ -31,6 +30,7 @@ initialize_drawing_config, point_in_polygon, ) +from .const import COLORS, DEFAULT_IMAGE_SIZE, DEFAULT_PIXEL_SIZE from .map_data import RandImageData from .reimg_draw import ImageDraw from .rooms_handler import RandRoomsHandler @@ -543,4 +543,4 @@ def get_calibration_data(self, rotation_angle: int = 0) -> Any: calibration_point = {"vacuum": vacuum_point, "map": map_point} self.calibration_data.append(calibration_point) - return self.calibration_data \ No newline at end of file + return self.calibration_data diff --git a/pyproject.toml b/pyproject.toml index 428cf97..6122fda 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "valetudo-map-parser" -version = "0.1.16" +version = "0.1.17" description = "A Python library to parse Valetudo map data returning a PIL Image object." authors = ["Sandro Cantarella "] license = "Apache-2.0" From 64c47430eb97c81c06b7865ba4a9ae3463281236 Mon Sep 17 00:00:00 2001 From: SCA075 <82227818+sca075@users.noreply.github.com> Date: Fri, 16 Jan 2026 23:35:55 +0100 Subject: [PATCH 19/27] updated types.py isort and ruff Signed-off-by: SCA075 <82227818+sca075@users.noreply.github.com> --- SCR/valetudo_map_parser/config/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SCR/valetudo_map_parser/config/types.py b/SCR/valetudo_map_parser/config/types.py index 1501a66..72f7456 100644 --- a/SCR/valetudo_map_parser/config/types.py +++ b/SCR/valetudo_map_parser/config/types.py @@ -63,7 +63,7 @@ class RoomProperty(TypedDict): @dataclass class TrimCropData: """Dataclass for trim and crop data.""" - + floor: str trim_left: int trim_up: int From 7536d8fbf703d88a4e24e09768aaf66ccc0b7dee Mon Sep 17 00:00:00 2001 From: SCA075 <82227818+sca075@users.noreply.github.com> Date: Fri, 16 Jan 2026 23:43:48 +0100 Subject: [PATCH 20/27] updated drawable_elements.py with correct colors name for material Signed-off-by: SCA075 <82227818+sca075@users.noreply.github.com> --- SCR/valetudo_map_parser/config/drawable_elements.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/SCR/valetudo_map_parser/config/drawable_elements.py b/SCR/valetudo_map_parser/config/drawable_elements.py index 13b4db8..dd7cea9 100644 --- a/SCR/valetudo_map_parser/config/drawable_elements.py +++ b/SCR/valetudo_map_parser/config/drawable_elements.py @@ -230,25 +230,25 @@ def update_from_device_info(self, device_info: dict) -> None: } # Update material overlay properties if present - if "material_wood_color" in device_info: + if "color_material_wood" in device_info: self.set_property( DrawableElement.MATERIAL_OVERLAY, "wood_color", - device_info["material_wood_color"], + device_info["color_material_wood"], ) - if "material_wood_alpha" in device_info: + if "alpha_material_wood" in device_info: self.set_property( DrawableElement.MATERIAL_OVERLAY, "wood_alpha", device_info["alpha_material_wood"], ) - if "material_tile_color" in device_info: + if "color_material_tile" in device_info: self.set_property( DrawableElement.MATERIAL_OVERLAY, "tile_color", device_info["color_material_tile"], ) - if "material_tile_alpha" in device_info: + if "alpha_material_tile" in device_info: self.set_property( DrawableElement.MATERIAL_OVERLAY, "tile_alpha", From b00953316ef0f39ac7d23193ed558f3aad51a03b Mon Sep 17 00:00:00 2001 From: SCA075 <82227818+sca075@users.noreply.github.com> Date: Sat, 17 Jan 2026 00:02:19 +0100 Subject: [PATCH 21/27] double import correction in colors.py and indexing Signed-off-by: SCA075 <82227818+sca075@users.noreply.github.com> --- SCR/valetudo_map_parser/config/colors.py | 28 ++++++++++-------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/SCR/valetudo_map_parser/config/colors.py b/SCR/valetudo_map_parser/config/colors.py index 86f2e6a..f37fd46 100644 --- a/SCR/valetudo_map_parser/config/colors.py +++ b/SCR/valetudo_map_parser/config/colors.py @@ -9,7 +9,6 @@ from ..const import ( ALPHA_BACKGROUND, - ALPHA_CARPET, ALPHA_CHARGER, ALPHA_CARPET, ALPHA_GO_TO, @@ -38,7 +37,6 @@ ALPHA_WALL, ALPHA_ZONE_CLEAN, COLOR_BACKGROUND, - COLOR_CARPET, COLOR_CHARGER, COLOR_CARPET, COLOR_GO_TO, @@ -122,26 +120,24 @@ ] base_colors_array = [ - color_wall, - color_zone_clean, - color_carpet, - color_material_wood, - color_material_tile, - color_robot, - color_background, - color_move, - color_charger, - color_no_go, - color_go_to, - color_text, + color_wall, # [0] + color_zone_clean, # [1] + color_robot, # [2] + color_background, # [3] + color_move, # [4] + color_charger, # [5] + color_no_go, # [6] + color_go_to, # [7] + color_text, # [8] + color_carpet, # [9] + color_material_wood, # [10] + color_material_tile, # [11] ] color_array = [ base_colors_array[0], # color_wall base_colors_array[6], # color_no_go base_colors_array[7], # color_go_to - base_colors_array[8], # color_predicted_path - base_colors_array[9], # color_obstacle color_black, base_colors_array[2], # color_robot base_colors_array[5], # color_charger From f30bafdf051310b5c91059dd562a56f9c1b6255e Mon Sep 17 00:00:00 2001 From: SCA075 <82227818+sca075@users.noreply.github.com> Date: Sat, 17 Jan 2026 00:16:33 +0100 Subject: [PATCH 22/27] use MaterialColor instead of direct access. Signed-off-by: SCA075 <82227818+sca075@users.noreply.github.com> --- SCR/valetudo_map_parser/config/material.py | 9 ++++++--- SCR/valetudo_map_parser/hypfer_draw.py | 15 ++++++++------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/SCR/valetudo_map_parser/config/material.py b/SCR/valetudo_map_parser/config/material.py index a367081..1f10f7d 100644 --- a/SCR/valetudo_map_parser/config/material.py +++ b/SCR/valetudo_map_parser/config/material.py @@ -172,15 +172,18 @@ def _tile_pixels(cells: int, pixel_size: int, tile_rgba: Color) -> NumpyArray: @staticmethod @lru_cache(maxsize=64) def get_tile( - material: str, pixel_size: int, wood_rgba: Color = None, tile_rgba: Color = None + material: str, pixel_size: int, colors: MaterialColors = None ) -> Optional[NumpyArray]: spec = MaterialTileRenderer._SPECS.get(material) if spec is None or pixel_size <= 0: return None # Use provided colors or fall back to defaults - wood_color = wood_rgba if wood_rgba else color_material_wood - tile_color = tile_rgba if tile_rgba else color_material_tile + if colors is None: + colors = _material_colors + + wood_color = colors.wood_rgba + tile_color = colors.tile_rgba if spec.kind == "tile": return MaterialTileRenderer._tile_pixels(spec.cells, pixel_size, tile_color) diff --git a/SCR/valetudo_map_parser/hypfer_draw.py b/SCR/valetudo_map_parser/hypfer_draw.py index bd87f19..18f7562 100755 --- a/SCR/valetudo_map_parser/hypfer_draw.py +++ b/SCR/valetudo_map_parser/hypfer_draw.py @@ -9,7 +9,7 @@ import logging from .config.drawable_elements import DrawableElement -from .config.material import MaterialTileRenderer +from .config.material import MaterialColors, MaterialTileRenderer from .config.types import Color, JsonType, NumpyArray, RobotPosition, RoomStore from .config.utils import point_in_polygon @@ -132,15 +132,16 @@ async def _process_room_layer( if material and material != "generic": # Get material colors from shared.user_colors # Index 10 = wood, Index 11 = tile - wood_rgba = self.img_h.shared.user_colors[10] - tile_rgba = self.img_h.shared.user_colors[11] + material_colors = MaterialColors( + wood_rgba=self.img_h.shared.user_colors[10], + tile_rgba=self.img_h.shared.user_colors[11] + ) img_np_array = self._apply_material_overlay( img_np_array, pixels, pixel_size, material, - wood_rgba, - tile_rgba, + material_colors, ) # Increment room_id only for segment layers, not for floor layers @@ -153,12 +154,12 @@ async def _process_room_layer( return img_np_array, room_id def _apply_material_overlay( - self, img_np_array, pixels, pixel_size, material, wood_rgba, tile_rgba + self, img_np_array, pixels, pixel_size, material, material_colors ): """Apply material texture overlay to a room segment.""" try: tile = MaterialTileRenderer.get_tile( - material, pixel_size, wood_rgba, tile_rgba + material, pixel_size, material_colors ) if tile is None: return img_np_array From 43287e4999b800023035653d5d31734bcef857cd Mon Sep 17 00:00:00 2001 From: SCA075 <82227818+sca075@users.noreply.github.com> Date: Wed, 21 Jan 2026 20:52:07 +0100 Subject: [PATCH 23/27] feat: Add mop mode path width adjustment for Hypfer vacuums - Path width adjusts to robot_size - 2 when mop_mode=True (default: 5px) - Cache robot_size in ImageDraw for performance - Update handler to use cached robot_size - Test with mop_mode enabled in test.py When mop mode is active, path width visually represents mop coverage area. Signed-off-by: SCA075 <82227818+sca075@users.noreply.github.com> --- SCR/valetudo_map_parser/config/colors.py | 1 + SCR/valetudo_map_parser/config/shared.py | 1 + SCR/valetudo_map_parser/hypfer_draw.py | 9 ++++++++- SCR/valetudo_map_parser/hypfer_handler.py | 4 ++-- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/SCR/valetudo_map_parser/config/colors.py b/SCR/valetudo_map_parser/config/colors.py index f37fd46..603dd00 100644 --- a/SCR/valetudo_map_parser/config/colors.py +++ b/SCR/valetudo_map_parser/config/colors.py @@ -134,6 +134,7 @@ color_material_tile, # [11] ] +# Might be not used in the future we may remove it. color_array = [ base_colors_array[0], # color_wall base_colors_array[6], # color_no_go diff --git a/SCR/valetudo_map_parser/config/shared.py b/SCR/valetudo_map_parser/config/shared.py index aa62f07..a543f6b 100755 --- a/SCR/valetudo_map_parser/config/shared.py +++ b/SCR/valetudo_map_parser/config/shared.py @@ -124,6 +124,7 @@ def __init__(self, file_name): self.current_floor: str = "floor_0" self.skip_room_ids: List[str] = [] self.device_info = None + self.mop_mode: bool = False self._battery_state = None def vacuum_bat_charged(self) -> bool: diff --git a/SCR/valetudo_map_parser/hypfer_draw.py b/SCR/valetudo_map_parser/hypfer_draw.py index 18f7562..c0e3071 100755 --- a/SCR/valetudo_map_parser/hypfer_draw.py +++ b/SCR/valetudo_map_parser/hypfer_draw.py @@ -24,6 +24,7 @@ class ImageDraw: def __init__(self, image_handler): self.img_h = image_handler self.file_name = self.img_h.shared.file_name + self.robot_size = self.img_h.shared.robot_size async def draw_go_to_flag( self, np_array: NumpyArray, entity_dict: dict, color_go_to: Color @@ -415,6 +416,12 @@ async def async_draw_paths( np_array, predicted_pat2, 2, color_gray ) if path_pixels: + # Calculate path width based on mop mode + if self.img_h.shared.mop_mode: + path_width = max(1, self.robot_size - 2) + else: + path_width = 5 # Default width + for path in path_pixels: # Get the points from the current path and extend multiple paths. points = path.get("points", []) @@ -423,7 +430,7 @@ async def async_draw_paths( sublist, 2 ) np_array = await self.img_h.draw.lines( - np_array, self.img_h.shared.map_new_path, 5, color_move + np_array, self.img_h.shared.map_new_path, path_width, color_move ) return np_array diff --git a/SCR/valetudo_map_parser/hypfer_handler.py b/SCR/valetudo_map_parser/hypfer_handler.py index 3df4f76..42e3320 100644 --- a/SCR/valetudo_map_parser/hypfer_handler.py +++ b/SCR/valetudo_map_parser/hypfer_handler.py @@ -269,7 +269,7 @@ async def _draw_robot_if_enabled( y=robot_position[1], angle=robot_position_angle, fill=robot_color, - radius=self.shared.robot_size, + radius=self.imd.robot_size, robot_state=self.shared.vacuum_state, ) @@ -384,7 +384,7 @@ async def async_get_image_from_json( # Synchronize zooming state from ImageDraw to handler before auto-crop self.zooming = self.imd.img_h.zooming - # Resize the image + # Resize the image - auto_trim_and_zoom_image creates a new array img_np_array = self.auto_trim_and_zoom_image( img_np_array, colors["background"], From 22daf1ada42d1d676bcd09e63b89f6090f72442a Mon Sep 17 00:00:00 2001 From: SCA075 <82227818+sca075@users.noreply.github.com> Date: Wed, 21 Jan 2026 21:30:22 +0100 Subject: [PATCH 24/27] refactor: remove unused apply_overlay_on_region method from MaterialTileRenderer Remove dead code from MaterialTileRenderer class in material.py: - Deleted apply_overlay_on_region() static method (lines 210-223) - Method was never called anywhere in the codebase - Duplicated functionality already implemented in hypfer_draw._apply_material_overlay() - Removed implementation used inferior mask-based assignment vs proper alpha blending The active material overlay implementation in hypfer_draw.py continues to use: - MaterialTileRenderer.get_tile() for tile generation - MaterialTileRenderer.tile_block() for tile positioning - Proper alpha blending: region[:] = (1 - alpha) * region + alpha * overlay This cleanup eliminates 15 lines of unused code while maintaining all functional material overlay capabilities. Signed-off-by: SCA075 <82227818+sca075@users.noreply.github.com> --- SCR/valetudo_map_parser/config/material.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/SCR/valetudo_map_parser/config/material.py b/SCR/valetudo_map_parser/config/material.py index 1f10f7d..fe13869 100644 --- a/SCR/valetudo_map_parser/config/material.py +++ b/SCR/valetudo_map_parser/config/material.py @@ -206,18 +206,3 @@ def tile_block(tile: NumpyArray, r0: int, r1: int, c0: int, c1: int) -> NumpyArr rows = (np.arange(r0, r1) % th).astype(np.intp, copy=False) cols = (np.arange(c0, c1) % tw).astype(np.intp, copy=False) return tile[rows[:, None], cols[None, :], :] - - @staticmethod - def apply_overlay_on_region( - image: NumpyArray, - tile: NumpyArray, - r0: int, - r1: int, - c0: int, - c1: int, - ) -> None: - region = image[r0:r1, c0:c1] - overlay = MaterialTileRenderer.tile_block(tile, r0, r1, c0, c1) - mask = overlay[..., 3] > 0 - if np.any(mask): - region[mask] = overlay[mask] From 9b81c74364617c3fd5adab5d82f8294b89d3d535 Mon Sep 17 00:00:00 2001 From: SCA075 <82227818+sca075@users.noreply.github.com> Date: Thu, 22 Jan 2026 00:08:54 +0100 Subject: [PATCH 25/27] refactor: Replace hardcoded color indices with named ColorIndex enum Replace all hardcoded user_colors[x] array indices with a ColorIndex IntEnum to prevent breakage when color order changes and improve code maintainability. Changes: - Add ColorIndex IntEnum in config/colors.py with named constants for all user_colors array indices (WALL=0, ZONE_CLEAN=1, ROBOT=2, BACKGROUND=3, MOVE=4, CHARGER=5, CARPET=6, NO_GO=7, GO_TO=8, TEXT=9, MATERIAL_WOOD=10, MATERIAL_TILE=11) - Add 'material_wood' and 'material_tile' to COLORS list in const.py to match the actual colors being initialized - Replace user_colors[10] and user_colors[11] with ColorIndex.MATERIAL_WOOD and ColorIndex.MATERIAL_TILE in hypfer_draw.py - Replace user_colors[8] with ColorIndex.TEXT in config/utils.py Benefits: - Self-documenting code with named constants instead of magic numbers - Type-safe access to color indices - Easy to maintain and reorder colors without breaking existing code - Clear mapping between color names and their array positions Tested: Hypfer test passes successfully with all colors initialized correctly Signed-off-by: SCA075 <82227818+sca075@users.noreply.github.com> --- SCR/valetudo_map_parser/config/colors.py | 22 +++++++++++++++++++++- SCR/valetudo_map_parser/config/utils.py | 3 ++- SCR/valetudo_map_parser/const.py | 2 ++ SCR/valetudo_map_parser/hypfer_draw.py | 8 ++++---- 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/SCR/valetudo_map_parser/config/colors.py b/SCR/valetudo_map_parser/config/colors.py index 603dd00..02fcdf9 100644 --- a/SCR/valetudo_map_parser/config/colors.py +++ b/SCR/valetudo_map_parser/config/colors.py @@ -2,7 +2,7 @@ from __future__ import annotations -from enum import StrEnum +from enum import IntEnum, StrEnum from typing import Dict, List, Tuple import numpy as np @@ -151,6 +151,26 @@ ] +class ColorIndex(IntEnum): + """ + Named indices for user_colors array. + This prevents hardcoded indices and makes the code maintainable. + The order must match the order in set_initial_colours() base_color_keys. + """ + WALL = 0 + ZONE_CLEAN = 1 + ROBOT = 2 + BACKGROUND = 3 + MOVE = 4 + CHARGER = 5 + CARPET = 6 + NO_GO = 7 + GO_TO = 8 + TEXT = 9 + MATERIAL_WOOD = 10 + MATERIAL_TILE = 11 + + class SupportedColor(StrEnum): """Color of a supported map element.""" diff --git a/SCR/valetudo_map_parser/config/utils.py b/SCR/valetudo_map_parser/config/utils.py index 2830a39..95bfe1b 100644 --- a/SCR/valetudo_map_parser/config/utils.py +++ b/SCR/valetudo_map_parser/config/utils.py @@ -13,6 +13,7 @@ from ..map_data import HyperMapData from .async_utils import AsyncNumPy +from .colors import ColorIndex from .drawable import Drawable from .drawable_elements import DrawingConfig from .status_text.status_text import StatusText @@ -205,7 +206,7 @@ async def _add_status_text(self, new_image: PilPNG): Drawable.status_text( new_image, img_text[1], - self.shared.user_colors[8], + self.shared.user_colors[ColorIndex.TEXT], img_text[0], self.shared.vacuum_status_font, self.shared.vacuum_status_position, diff --git a/SCR/valetudo_map_parser/const.py b/SCR/valetudo_map_parser/const.py index a6514d1..66a0f77 100644 --- a/SCR/valetudo_map_parser/const.py +++ b/SCR/valetudo_map_parser/const.py @@ -44,6 +44,8 @@ "no_go", "go_to", "text", + "material_wood", + "material_tile", ] SENSOR_NO_DATA = { diff --git a/SCR/valetudo_map_parser/hypfer_draw.py b/SCR/valetudo_map_parser/hypfer_draw.py index c0e3071..a93bd75 100755 --- a/SCR/valetudo_map_parser/hypfer_draw.py +++ b/SCR/valetudo_map_parser/hypfer_draw.py @@ -8,6 +8,7 @@ import logging +from .config.colors import ColorIndex from .config.drawable_elements import DrawableElement from .config.material import MaterialColors, MaterialTileRenderer from .config.types import Color, JsonType, NumpyArray, RobotPosition, RoomStore @@ -131,11 +132,10 @@ async def _process_room_layer( materials = getattr(self.img_h.json_data, "materials", {}) material = materials.get(segment_id) if material and material != "generic": - # Get material colors from shared.user_colors - # Index 10 = wood, Index 11 = tile + # Get material colors from shared.user_colors using named indices material_colors = MaterialColors( - wood_rgba=self.img_h.shared.user_colors[10], - tile_rgba=self.img_h.shared.user_colors[11] + wood_rgba=self.img_h.shared.user_colors[ColorIndex.MATERIAL_WOOD], + tile_rgba=self.img_h.shared.user_colors[ColorIndex.MATERIAL_TILE] ) img_np_array = self._apply_material_overlay( img_np_array, From 9564f588294d70e2d7c9784bd44beffbf5a70cc7 Mon Sep 17 00:00:00 2001 From: SCA075 <82227818+sca075@users.noreply.github.com> Date: Thu, 22 Jan 2026 00:38:28 +0100 Subject: [PATCH 26/27] Release v0.2.0 - Major Color System Refactoring and Code Quality Improvements ## Version Update - Bumped version to 0.2.0 across pyproject.toml, __init__.py, and README.md ## Color System Enhancements ### ColorIndex Enum Implementation - Created ColorIndex IntEnum with named constants for all user_colors array indices - Replaced hardcoded numeric indices (user_colors[8], [10], [11]) with type-safe enum constants - Prevents breakage when color order changes in the future - Updated hypfer_draw.py to use ColorIndex.MATERIAL_WOOD and ColorIndex.MATERIAL_TILE - Updated utils.py to use ColorIndex.TEXT for status text rendering ### Material Colors Refactoring - Refactored MaterialTileRenderer to use MaterialColors dataclass - Made material overlay colors (wood and tile) user-configurable via device_info - Removed hardcoded class variables WOOD_RGBA/TILE_RGBA and set_colors()/reset_colors() methods - All rendering methods now accept color parameters instead of using class state - Added material color constants to DEFAULT_VALUES (color_material_wood, color_material_tile) - Updated material overlay implementation to extract colors from shared.user_colors ### Color Initialization Fix - Fixed initialize_user_colors() to use same base_color_keys order as set_initial_colours() and ColorIndex enum - Prevents color index mismatches during initialization - Added 'material_wood' and 'material_tile' to COLORS list in const.py ### Color Blending Implementation - Implemented color blending for semi-transparent elements using mvcrender's sample_and_blend_color - Applied blending to flag, obstacles, and charger elements when alpha < 255 - Optimized alpha blending to minimize temporary allocations in zone drawing ### Path Color Enhancements - Updated default path color to more vibrant blue (50, 150, 255) for better visibility - Implemented mop mode path width adjustment: path_width = max(1, robot_size - 2) when mop_mode=True - Added robot_size caching in ImageDraw to reduce shared data access - Path lines now support different colors when crossing different rooms ### Alpha Transparency Updates - Updated default alpha values: path (200.0), walls (150.0) - Semi-transparent elements (restricted areas, no-mop areas, predicted path) default to alpha=125.0 ## Code Quality Improvements ### Logging Cleanup - Converted 24+ INFO logs to DEBUG level across 5 files to reduce production log noise - hypfer_handler.py: 5 logs converted - rand256_handler.py: 4 logs converted - hypfer_draw.py: 1 log converted + fixed misleading else clause - reimg_draw.py: 8 logs converted + fixed else clause pattern - drawable_elements.py: 6 logs converted + removed duplicate logging - Fixed misleading else-clause logging patterns in virtual walls and entity dict methods - Removed duplicate log statements in enable_element/disable_element methods ### Dead Code Removal - Removed unused material_mvcrender.py (test/alternative implementation) - Removed unused apply_overlay_on_region() static method from MaterialTileRenderer ## Documentation Updates - Updated Python requirement documentation from 3.12 to 3.13 in README.md - Added complete dependency list with version constraints: - Pillow (>=10.3.0) - NumPy (>=1.26.4) - SciPy (>=1.12.0) - mvcrender (==0.0.7) ## Performance Optimizations - Added mop_mode attribute to CameraShared class for tracking vacuum operation mode - Cached robot_size in ImageDraw to avoid repeated shared data access - Optimized color blending operations to reduce memory allocations ## Deferred Issues (Medium Priority) - Pylint disabled in CI since refactoring (to be re-enabled) - Potential null reference in _convert_to_binary when last_image is None - Room ID indexing inconsistency (0-based vs 1-based) - Potential race conditions in async shared data access - Hardcoded /tmp/ paths in tests (Windows incompatible) - Virtual walls logging pattern needs review Signed-off-by: SCA075 <82227818+sca075@users.noreply.github.com> --- README.md | 10 ++++--- SCR/valetudo_map_parser/__init__.py | 2 +- SCR/valetudo_map_parser/config/colors.py | 27 ++++++++++++++----- .../config/drawable_elements.py | 20 ++------------ SCR/valetudo_map_parser/hypfer_draw.py | 4 +-- SCR/valetudo_map_parser/hypfer_handler.py | 10 +++---- SCR/valetudo_map_parser/rand256_handler.py | 8 +++--- SCR/valetudo_map_parser/reimg_draw.py | 19 ++++++------- pyproject.toml | 2 +- 9 files changed, 51 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 4ac68bc..f09a821 100644 --- a/README.md +++ b/README.md @@ -30,10 +30,12 @@ pip install valetudo_map_parser ``` ### Requirements: -- Python 3.12 or higher +- Python 3.13 or higher - Dependencies: - - Pillow (PIL) for image processing - - NumPy for array operations + - Pillow (>=10.3.0) for image processing + - NumPy (>=1.26.4) for array operations + - SciPy (>=1.12.0) for scientific computing + - mvcrender (==0.0.7) for high-performance rendering ### Usage: The library is configured using a dictionary format. See our [sample code](https://github.com/sca075/Python-package-valetudo-map-parser/blob/main/tests/test.py) for implementation examples. @@ -46,7 +48,7 @@ Key functionalities: - Supports asynchronous operations ### Development Status: -Current version: 0.1.9.b41 +Current version: 0.2.0 - Full functionality available in versions >= 0.1.9 - Actively maintained and enhanced - Uses Poetry for dependency management diff --git a/SCR/valetudo_map_parser/__init__.py b/SCR/valetudo_map_parser/__init__.py index 7fb2898..68ba02c 100644 --- a/SCR/valetudo_map_parser/__init__.py +++ b/SCR/valetudo_map_parser/__init__.py @@ -1,5 +1,5 @@ """Valetudo map parser. -Version: 0.1.14""" +Version: 0.2.0""" from pathlib import Path diff --git a/SCR/valetudo_map_parser/config/colors.py b/SCR/valetudo_map_parser/config/colors.py index 02fcdf9..085964c 100644 --- a/SCR/valetudo_map_parser/config/colors.py +++ b/SCR/valetudo_map_parser/config/colors.py @@ -404,17 +404,30 @@ def set_initial_colours(self, device_info: dict) -> None: def initialize_user_colors(self, device_info: dict) -> List[Color]: """ Initialize user-defined colors with defaults as fallback. + The order MUST match ColorIndex enum and set_initial_colours() base_color_keys. :param device_info: Dictionary containing user-defined colors. :return: List of RGBA colors for map elements. """ + # Define the canonical order matching ColorIndex enum + base_color_keys = [ + (COLOR_WALL, color_wall, ALPHA_WALL), + (COLOR_ZONE_CLEAN, color_zone_clean, ALPHA_ZONE_CLEAN), + (COLOR_ROBOT, color_robot, ALPHA_ROBOT), + (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), + (COLOR_MATERIAL_WOOD, color_material_wood, ALPHA_MATERIAL_WOOD), + (COLOR_MATERIAL_TILE, color_material_tile, ALPHA_MATERIAL_TILE), + ] + colors = [] - for key in SupportedColor: - if key.startswith(SupportedColor.COLOR_ROOM_PREFIX): - continue # Skip room colors for user_colors - rgb = device_info.get(key, DefaultColors.COLORS_RGB.get(key)) - alpha = device_info.get( - f"alpha_{key}", DefaultColors.DEFAULT_ALPHA.get(f"alpha_{key}") - ) + for color_key, default_color, alpha_key in base_color_keys: + rgb = device_info.get(color_key, default_color[:3] if default_color else (0, 0, 0)) + alpha = device_info.get(alpha_key, 255.0) colors.append(self.add_alpha_to_color(rgb, alpha)) return colors diff --git a/SCR/valetudo_map_parser/config/drawable_elements.py b/SCR/valetudo_map_parser/config/drawable_elements.py index dd7cea9..7484e5f 100644 --- a/SCR/valetudo_map_parser/config/drawable_elements.py +++ b/SCR/valetudo_map_parser/config/drawable_elements.py @@ -148,27 +148,17 @@ def enable_element(self, element_code: DrawableElement) -> None: """Enable drawing of a specific element.""" if element_code in self._enabled_elements: self._enabled_elements[element_code] = True - LOGGER.info( + LOGGER.debug( "Enabled element %s (%s)", element_code.name, element_code.value ) - LOGGER.info( - "Element %s is now enabled: %s", - element_code.name, - self._enabled_elements[element_code], - ) def disable_element(self, element_code: DrawableElement) -> None: """Disable drawing of a specific element.""" if element_code in self._enabled_elements: self._enabled_elements[element_code] = False - LOGGER.info( + LOGGER.debug( "Disabled element %s (%s)", element_code.name, element_code.value ) - LOGGER.info( - "Element %s is now enabled: %s", - element_code.name, - self._enabled_elements[element_code], - ) def set_elements(self, element_codes: List[DrawableElement]) -> None: """Enable only the specified elements, disable all others.""" @@ -317,9 +307,6 @@ def update_from_device_info(self, device_info: dict) -> None: for disable_key, element in element_disable_mapping.items(): if device_info.get(disable_key, False): self.disable_element(element) - LOGGER.info( - "Disabled %s element from device_info setting", element.name - ) # Process room disable flags (1-15) for room_id in range(1, 16): @@ -327,6 +314,3 @@ def update_from_device_info(self, device_info: dict) -> None: if device_info.get(disable_key, False): room_element = getattr(DrawableElement, f"ROOM_{room_id}") self.disable_element(room_element) - LOGGER.info( - "Disabled ROOM_%d element from device_info setting", room_id - ) diff --git a/SCR/valetudo_map_parser/hypfer_draw.py b/SCR/valetudo_map_parser/hypfer_draw.py index a93bd75..2ed68f4 100755 --- a/SCR/valetudo_map_parser/hypfer_draw.py +++ b/SCR/valetudo_map_parser/hypfer_draw.py @@ -381,9 +381,9 @@ async def async_draw_virtual_walls( virtual_walls = self.img_h.data.find_virtual_walls(m_json) except (ValueError, KeyError): virtual_walls = None - else: - _LOGGER.info("%s: Got virtual walls.", self.file_name) + if virtual_walls: + _LOGGER.debug("%s: Got virtual walls.", self.file_name) np_array = await self.img_h.draw.draw_virtual_walls( np_array, virtual_walls, color_no_go ) diff --git a/SCR/valetudo_map_parser/hypfer_handler.py b/SCR/valetudo_map_parser/hypfer_handler.py index 42e3320..92a4778 100644 --- a/SCR/valetudo_map_parser/hypfer_handler.py +++ b/SCR/valetudo_map_parser/hypfer_handler.py @@ -215,9 +215,9 @@ async def _prepare_data_tasks(self, m_json, entity_dict): data_tasks.append(self._prepare_goto_data(entity_dict)) path_enabled = self.drawing_config.is_enabled(DrawableElement.PATH) - LOGGER.info("%s: PATH element enabled: %s", self.file_name, path_enabled) + LOGGER.debug("%s: PATH element enabled: %s", self.file_name, path_enabled) if path_enabled: - LOGGER.info("%s: Drawing path", self.file_name) + LOGGER.debug("%s: Drawing path", self.file_name) data_tasks.append(self._prepare_path_data(m_json)) if data_tasks: @@ -248,7 +248,7 @@ async def _draw_dynamic_elements( img_np_array, m_json, colors["move"], self.color_grey ) else: - LOGGER.info("%s: Skipping path drawing", self.file_name) + LOGGER.debug("%s: Skipping path drawing", self.file_name) return img_np_array @@ -334,7 +334,7 @@ async def async_get_image_from_json( room_id, robot_position, robot_position_angle ) - LOGGER.info("%s: Completed base Layers", self.file_name) + LOGGER.debug("%s: Completed base Layers", self.file_name) # Copy the new array in base layer. # Delete old base layer before creating new one to free memory if self.img_base_layer is not None: @@ -436,7 +436,7 @@ def get_calibration_data(self, rotation_angle: int = 0) -> CalibrationPoints: """Get the calibration data from the JSON data. this will create the attribute calibration points.""" calibration_data = [] - LOGGER.info("Getting %s Calibrations points.", self.file_name) + LOGGER.debug("Getting %s Calibrations points.", self.file_name) # Define the map points (fixed) map_points = self.get_map_points() diff --git a/SCR/valetudo_map_parser/rand256_handler.py b/SCR/valetudo_map_parser/rand256_handler.py index 8e150e8..933db68 100644 --- a/SCR/valetudo_map_parser/rand256_handler.py +++ b/SCR/valetudo_map_parser/rand256_handler.py @@ -139,12 +139,12 @@ async def get_image_from_rrm( try: if (m_json is not None) and (not isinstance(m_json, tuple)): - LOGGER.info("%s: Composing the image for the camera.", self.file_name) + LOGGER.debug("%s: Composing the image for the camera.", self.file_name) self.json_data = m_json size_x, size_y = self.data.get_rrm_image_size(m_json) self.img_size = DEFAULT_IMAGE_SIZE self.json_id = str(uuid.uuid4()) # image id - LOGGER.info("Vacuum Data ID: %s", self.json_id) + LOGGER.debug("Vacuum Data ID: %s", self.json_id) ( img_np_array, @@ -220,7 +220,7 @@ async def _initialize_base_layer( colors["background"], DEFAULT_PIXEL_SIZE, ) - LOGGER.info("%s: Completed base Layers", self.file_name) + LOGGER.debug("%s: Completed base Layers", self.file_name) if room_id > 0 and not self.room_propriety: self.room_propriety = await self.get_rooms_attributes(destinations) @@ -526,7 +526,7 @@ def get_calibration_data(self, rotation_angle: int = 0) -> Any: """Return the map calibration data.""" if not self.calibration_data and self.crop_img_size: self.calibration_data = [] - LOGGER.info( + LOGGER.debug( "%s: Getting Calibrations points %s", self.file_name, str(self.crop_area), diff --git a/SCR/valetudo_map_parser/reimg_draw.py b/SCR/valetudo_map_parser/reimg_draw.py index 63c1604..aa52948 100644 --- a/SCR/valetudo_map_parser/reimg_draw.py +++ b/SCR/valetudo_map_parser/reimg_draw.py @@ -65,7 +65,7 @@ async def async_segment_data( ) except ValueError as e: self.img_h.segment_data = None - LOGGER.info("%s: No segments data found: %s", self.file_name, e) + LOGGER.debug("%s: No segments data found: %s", self.file_name, e) async def async_draw_base_layer( self, @@ -82,13 +82,13 @@ async def async_draw_base_layer( walls_data = self.data.get_rrm_walls(m_json) floor_data = self.data.get_rrm_floor(m_json) - LOGGER.info("%s: Empty image with background color", self.file_name) + LOGGER.debug("%s: Empty image with background color", self.file_name) img_np_array = await self.draw.create_empty_image( self.img_h.img_size["x"], self.img_h.img_size["y"], color_background ) room_id = 0 if self.img_h.frame_number == 0: - LOGGER.info("%s: Overlapping Layers", self.file_name) + LOGGER.debug("%s: Overlapping Layers", self.file_name) # checking if there are segments too (sorted pixels in the raw data). await self.async_segment_data(m_json, size_x, size_y, pos_top, pos_left) @@ -143,10 +143,10 @@ async def _draw_segments( room_id = 0 rooms_list = [color_wall] if not segment_data: - LOGGER.info("%s: No segments data found.", self.file_name) + LOGGER.debug("%s: No segments data found.", self.file_name) return room_id, img_np_array - LOGGER.info("%s: Drawing segments.", self.file_name) + LOGGER.debug("%s: Drawing segments.", self.file_name) for pixels in segment_data: room_color = self.img_h.shared.rooms_colors[room_id] rooms_list.append(room_color) @@ -233,7 +233,7 @@ async def async_draw_zones( zone_clean = None if zone_clean: - LOGGER.info("%s: Got zones.", self.file_name) + LOGGER.debug("%s: Got zones.", self.file_name) return await self.draw.zones(np_array, zone_clean, color_zone_clean) return np_array @@ -247,7 +247,7 @@ async def async_draw_virtual_restrictions( virtual_walls = None if virtual_walls: - LOGGER.info("%s: Got virtual walls.", self.file_name) + LOGGER.debug("%s: Got virtual walls.", self.file_name) np_array = await self.draw.draw_virtual_walls( np_array, virtual_walls, color_no_go ) @@ -291,8 +291,9 @@ async def async_get_entity_data(self, m_json: JsonType) -> dict or None: entity_dict = self.data_sup.find_points_entities(m_json) except (ValueError, KeyError): entity_dict = None - else: - LOGGER.info("%s: Got the points in the json.", self.file_name) + + if entity_dict: + LOGGER.debug("%s: Got the points in the json.", self.file_name) return entity_dict async def async_get_robot_position(self, m_json: JsonType) -> tuple | None: diff --git a/pyproject.toml b/pyproject.toml index 6122fda..3a3c88d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "valetudo-map-parser" -version = "0.1.17" +version = "0.2.0" description = "A Python library to parse Valetudo map data returning a PIL Image object." authors = ["Sandro Cantarella "] license = "Apache-2.0" From e3469b7010586ebb0b2e03d181c9d93ce5f923c5 Mon Sep 17 00:00:00 2001 From: SCA075 <82227818+sca075@users.noreply.github.com> Date: Sun, 25 Jan 2026 16:36:20 +0100 Subject: [PATCH 27/27] chore: bump version to 0.2.1 with mvcrender 0.0.8 dependency update - Updated mvcrender dependency from 0.0.7 to 0.0.8 (critical trim swap fix) - Updated TrimsData.from_list() docstring to reflect actual implementation - Added investigation note for future parameter mapping review This patch release addresses critical autocrop trim coordinate handling with the updated mvcrender dependency. Signed-off-by: SCA075 <82227818+sca075@users.noreply.github.com> --- SCR/__init__.py | 2 +- SCR/valetudo_map_parser/__init__.py | 2 +- SCR/valetudo_map_parser/config/types.py | 7 +++++-- SCR/valetudo_map_parser/hypfer_handler.py | 2 ++ SCR/valetudo_map_parser/rand256_handler.py | 2 ++ pyproject.toml | 4 ++-- 6 files changed, 13 insertions(+), 6 deletions(-) diff --git a/SCR/__init__.py b/SCR/__init__.py index ebdf05f..cb5a7a9 100644 --- a/SCR/__init__.py +++ b/SCR/__init__.py @@ -1,2 +1,2 @@ """Valetudo map parser. -Version: 0.1.14""" +Version: 0.2.1""" diff --git a/SCR/valetudo_map_parser/__init__.py b/SCR/valetudo_map_parser/__init__.py index 68ba02c..001835c 100644 --- a/SCR/valetudo_map_parser/__init__.py +++ b/SCR/valetudo_map_parser/__init__.py @@ -1,5 +1,5 @@ """Valetudo map parser. -Version: 0.2.0""" +Version: 0.2.1""" from pathlib import Path diff --git a/SCR/valetudo_map_parser/config/types.py b/SCR/valetudo_map_parser/config/types.py index 72f7456..a202859 100644 --- a/SCR/valetudo_map_parser/config/types.py +++ b/SCR/valetudo_map_parser/config/types.py @@ -327,7 +327,10 @@ def to_dict(self) -> dict: @classmethod def from_list(cls, crop_area: List[int], floor: Optional[str] = None): """ - Initialize TrimsData from a list [trim_up, trim_left, trim_down, trim_right] + Initialize TrimsData from a list [left, up, right, down] (mvcrender crop_area format). + NOTE: This matches the current implementation but needs investigation - mvcrender + documentation states crop_area is [left, up, right, down] but the mapping here + assigns crop_area[0] to trim_up and crop_area[1] to trim_left which seems reversed. """ return cls( trim_up=crop_area[0], @@ -339,7 +342,7 @@ def from_list(cls, crop_area: List[int], floor: Optional[str] = None): def clear(self) -> dict: """Clear all the trims.""" - self.floor = "" + self.floor = "floor_0" self.trim_up = 0 self.trim_left = 0 self.trim_down = 0 diff --git a/SCR/valetudo_map_parser/hypfer_handler.py b/SCR/valetudo_map_parser/hypfer_handler.py index 92a4778..e3a2c3a 100644 --- a/SCR/valetudo_map_parser/hypfer_handler.py +++ b/SCR/valetudo_map_parser/hypfer_handler.py @@ -392,6 +392,8 @@ async def async_get_image_from_json( int(self.shared.image_rotate), self.zooming, ) + # Update shared trims with calculated crop values + self.update_trims() # If the image is None return None and log the error. if img_np_array is None: LOGGER.warning("%s: Image array is None.", self.file_name) diff --git a/SCR/valetudo_map_parser/rand256_handler.py b/SCR/valetudo_map_parser/rand256_handler.py index 933db68..bdc0f38 100644 --- a/SCR/valetudo_map_parser/rand256_handler.py +++ b/SCR/valetudo_map_parser/rand256_handler.py @@ -380,6 +380,8 @@ async def _draw_map_elements( zoom=self.zooming, rand256=True, ) + # Update shared trims with calculated crop values + self.update_trims() return img_np_array async def _finalize_image(self, pil_img): diff --git a/pyproject.toml b/pyproject.toml index 3a3c88d..7adc95a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "valetudo-map-parser" -version = "0.2.0" +version = "0.2.1" description = "A Python library to parse Valetudo map data returning a PIL Image object." authors = ["Sandro Cantarella "] license = "Apache-2.0" @@ -18,7 +18,7 @@ python = ">=3.13" numpy = ">=1.26.4" Pillow = ">=10.3.0" scipy = ">=1.12.0" -mvcrender = "==0.0.7" +mvcrender = "==0.0.8" [tool.poetry.group.dev.dependencies] ruff = "*"