Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
2b5bd0e
adding const.py and update types.py to separate const and types
sca075 Oct 23, 2025
c6ab4ee
last files for 12 isort / ruff and lint
sca075 Nov 4, 2025
e6b006c
remove duplicate import of const
sca075 Dec 19, 2025
a5ca571
const was not properly imported
sca075 Dec 20, 2025
0d4f63c
minor changes in rand256_handler.py, shared.py removed snapshot at in…
sca075 Dec 20, 2025
c412fd7
updated __init__.py
sca075 Dec 21, 2025
11c09e2
updated __init__.py all
sca075 Dec 21, 2025
cb056f4
updated __init__.py all
sca075 Dec 21, 2025
4e21656
update shared.py to load floor data from the config if present else u…
sca075 Dec 24, 2025
0505a8c
update TrimsCropData to use the floors when init, else default values.
sca075 Jan 8, 2026
9426afb
Merge branch 'main' into dev_main
sca075 Jan 8, 2026
3b9d5b2
feat: Add carpet zone detection and rendering support
sca075 Jan 11, 2026
3bea98e
feat: Add carpet zone detection and rendering support
sca075 Jan 11, 2026
de0effa
fix: Add missing CARPET to element_color_mapping
sca075 Jan 11, 2026
644b81e
Dead code removed. The _draw_carpets method was leftover from the ini…
sca075 Jan 11, 2026
f3af432
Merge branch 'main' into dev_main
sca075 Jan 11, 2026
a67cd56
Materials implementation and configuration
sca075 Jan 13, 2026
317511d
removed test and unused code _update_material_colors()
sca075 Jan 13, 2026
dbe642f
improved material.py drawings with mcvrender
sca075 Jan 13, 2026
9ac664b
isort and ruffed code and added configurable colours for material.py
sca075 Jan 16, 2026
a7ce494
Merge branch 'main' into dev_main
sca075 Jan 16, 2026
64c4743
updated types.py isort and ruff
sca075 Jan 16, 2026
7536d8f
updated drawable_elements.py with correct colors name for material
sca075 Jan 16, 2026
b009533
double import correction in colors.py and indexing
sca075 Jan 16, 2026
f30bafd
use MaterialColor instead of direct access.
sca075 Jan 16, 2026
43287e4
feat: Add mop mode path width adjustment for Hypfer vacuums
sca075 Jan 21, 2026
22daf1a
refactor: remove unused apply_overlay_on_region method from MaterialT…
sca075 Jan 21, 2026
d48fd44
Merge branch 'main' into dev_main
sca075 Jan 21, 2026
9b81c74
refactor: Replace hardcoded color indices with named ColorIndex enum
sca075 Jan 21, 2026
9564f58
Release v0.2.0 - Major Color System Refactoring and Code Quality Impr…
sca075 Jan 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion SCR/valetudo_map_parser/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Valetudo map parser.
Version: 0.1.14"""
Version: 0.2.0"""

from pathlib import Path

Expand Down
50 changes: 42 additions & 8 deletions SCR/valetudo_map_parser/config/colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -150,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."""

Expand Down Expand Up @@ -383,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

Expand Down
20 changes: 2 additions & 18 deletions SCR/valetudo_map_parser/config/drawable_elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -317,16 +307,10 @@ 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):
disable_key = f"disable_room_{room_id}"
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
)
15 changes: 0 additions & 15 deletions SCR/valetudo_map_parser/config/material.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
1 change: 1 addition & 0 deletions SCR/valetudo_map_parser/config/shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion SCR/valetudo_map_parser/config/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions SCR/valetudo_map_parser/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
"no_go",
"go_to",
"text",
"material_wood",
"material_tile",
]

SENSOR_NO_DATA = {
Expand Down
21 changes: 14 additions & 7 deletions SCR/valetudo_map_parser/hypfer_draw.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -24,6 +25,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
Expand Down Expand Up @@ -130,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,
Expand Down Expand Up @@ -380,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
)
Expand Down Expand Up @@ -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", [])
Expand All @@ -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

Expand Down
14 changes: 7 additions & 7 deletions SCR/valetudo_map_parser/hypfer_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand All @@ -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,
)

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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"],
Expand Down Expand Up @@ -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()
Expand Down
8 changes: 4 additions & 4 deletions SCR/valetudo_map_parser/rand256_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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),
Expand Down
Loading