diff --git a/SCR/valetudo_map_parser/__init__.py b/SCR/valetudo_map_parser/__init__.py index f5651ed..3854cdb 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.7""" +Version: 0.3.0b2""" from pathlib import Path @@ -31,6 +31,7 @@ pil_to_pil_bytes, pil_to_png_bytes, ) +from .conga_handler import CongaMapImageHandler from .const import ( ALLOWED_IMAGE_FORMAT, ATTR_CALIBRATION_POINTS, @@ -90,7 +91,7 @@ SENSOR_NO_DATA, ) from .hypfer_handler import HypferMapImageHandler -from .map_data import HyperMapData +from .map_data import HyperMapData, ImageData from .rand256_handler import ReImageHandler from .rooms_handler import RandRoomsHandler, RoomsHandler @@ -168,11 +169,13 @@ def get_default_font_path() -> str: "CameraShared", "CameraSharedManager", "ColorsManagement", + "CongaMapImageHandler", "Drawable", "DrawableElement", "DrawingConfig", "HyperMapData", "HypferMapImageHandler", + "ImageData", "RRMapParser", "RandRoomsHandler", "ReImageHandler", diff --git a/SCR/valetudo_map_parser/config/shared.py b/SCR/valetudo_map_parser/config/shared.py index 6a455e9..3ba7e43 100755 --- a/SCR/valetudo_map_parser/config/shared.py +++ b/SCR/valetudo_map_parser/config/shared.py @@ -11,6 +11,7 @@ from PIL import Image from ..const import ( + ALLOWED_IMAGE_FORMAT, ATTR_CALIBRATION_POINTS, ATTR_CAMERA_MODE, ATTR_CONTENT_TYPE, @@ -27,7 +28,6 @@ ATTR_VACUUM_POSITION, ATTR_VACUUM_STATUS, ATTR_ZONES, - ALLOWED_IMAGE_FORMAT, CONF_ASPECT_RATIO, CONF_AUTO_ZOOM, CONF_OBSTACLE_LINK_IP, @@ -71,6 +71,7 @@ def __init__(self, file_name): self.rand256_active_zone: list = [] self.rand256_zone_coordinates: list = [] self.is_rand: bool = False + self.is_conga: bool = False self._new_mqtt_message = False self.last_image = Image.new("RGBA", (250, 150), (128, 128, 128, 255)) self.new_image: PilPNG | None = None @@ -151,18 +152,25 @@ def vacuum_bat_charged(self) -> bool: ) return (self.vacuum_state == "docked") and (self._battery_state == "charging") - def set_content_type(self, new_image_format:str = "image/pil") -> None: - """ Set image format / content type""" - if new_image_format not in ALLOWED_IMAGE_FORMAT.keys(): - self._image_format = "image/pil" - return - self._image_format = ALLOWED_IMAGE_FORMAT.get(new_image_format) + def set_content_type(self, new_image_format: str = "pil") -> None: + """Set image format / content type. - def get_content_type(self)->str: + Accepts either a short key ('pil', 'png', 'jpeg') or the full MIME + value returned by get_content_type() ('image/pil', 'image/png', + 'image/jpeg'), so that a round-trip set→get→set preserves the format. + Unknown values fall back to 'image/pil'. + """ + if new_image_format in ALLOWED_IMAGE_FORMAT: + self._image_format = ALLOWED_IMAGE_FORMAT[new_image_format] + elif new_image_format in ALLOWED_IMAGE_FORMAT.values(): + self._image_format = new_image_format + else: + self._image_format = ALLOWED_IMAGE_FORMAT["pil"] + + def get_content_type(self) -> str: """Return the current set _image_format""" return self._image_format - @staticmethod def _compose_obstacle_links( vacuum_host_ip: str, @@ -406,7 +414,9 @@ def update_shared_data(self, device_info): # Obstacle link configuration instance.obstacle_link_ip = device_info.get(CONF_OBSTACLE_LINK_IP) - instance.obstacle_link_protocol = device_info.get(CONF_OBSTACLE_LINK_PROTOCOL) + instance.obstacle_link_protocol = device_info.get( + CONF_OBSTACLE_LINK_PROTOCOL + ) obstacle_link_port = device_info.get(CONF_OBSTACLE_LINK_PORT) if obstacle_link_port is not None: try: diff --git a/SCR/valetudo_map_parser/config/utils.py b/SCR/valetudo_map_parser/config/utils.py index 2a5d518..a455c7b 100644 --- a/SCR/valetudo_map_parser/config/utils.py +++ b/SCR/valetudo_map_parser/config/utils.py @@ -151,6 +151,9 @@ async def _generate_new_image( m_json=m_json, destinations=destinations, ) + + if hasattr(self, "async_get_conga_from_json"): + return await self.async_get_conga_from_json(m_json=m_json) if hasattr(self, "async_get_image_from_json"): self.json_data = await HyperMapData.async_from_valetudo_json(m_json) diff --git a/SCR/valetudo_map_parser/conga_draw.py b/SCR/valetudo_map_parser/conga_draw.py new file mode 100644 index 0000000..9d46b4b --- /dev/null +++ b/SCR/valetudo_map_parser/conga_draw.py @@ -0,0 +1,28 @@ +""" +Image Draw Class for Conga Vacuums. +Extends HypferImageDraw to provide a Conga-specific drawing layer. +Pixel data is already normalised to compressedPixels by the time it reaches +this class, so all Hypfer drawing routines apply unchanged. +The canvas is rendered at CONGA_SCALE × the native Conga size so each +boundary pixel becomes a dense CONGA_SCALE×CONGA_SCALE block, producing +filled-looking rooms. Entity coordinates are pre-scaled in the handler. +Version: 0.1.2 +""" + +from __future__ import annotations + +from .hypfer_draw import ImageDraw as HypferImageDraw + + +# Scale factor: Conga pixelSize=1 vs standard pixelSize=5. +# The handler scales the canvas and all entity coordinates by this factor. +CONGA_SCALE = 5 + + +class CongaImageDraw(HypferImageDraw): + """Drawing handler for Conga vacuums. + + Inherits all drawing logic from HypferImageDraw unchanged. + Coordinate scaling is handled entirely in CongaMapImageHandler so + the draw methods receive already-scaled values. + """ diff --git a/SCR/valetudo_map_parser/conga_handler.py b/SCR/valetudo_map_parser/conga_handler.py new file mode 100644 index 0000000..ca115c4 --- /dev/null +++ b/SCR/valetudo_map_parser/conga_handler.py @@ -0,0 +1,67 @@ +""" +Conga Image Handler Class. +Extends HypferMapImageHandler for Conga vacuums. +Conga maps have pixelSize=1 and canvas size 800×800. Before rendering we +deep-copy the JSON and multiply pixelSize, canvas size, and all entity +coordinates by CONGA_SCALE so that every map pixel becomes a CONGA_SCALE×CONGA_SCALE +block, producing solid filled rooms. Entity positions stay consistent with +the scaled map coordinate space, so no post-upscale is required. +Version: 0.1.4 +""" + +from __future__ import annotations + +import copy + +from PIL import Image + +from .config.shared import CameraShared +from .config.types import JsonType +from .conga_draw import CONGA_SCALE, CongaImageDraw +from .hypfer_handler import HypferMapImageHandler +from .map_data import HyperMapData + + +class CongaMapImageHandler(HypferMapImageHandler): + """Image Handler for Conga vacuums. + + Scales pixelSize, canvas size, and all entity coordinates by CONGA_SCALE + before handing the JSON to the standard Hypfer render pipeline. + """ + + def __init__(self, shared_data: CameraShared) -> None: + """Initialise the Conga image handler.""" + super().__init__(shared_data) + self.imd = CongaImageDraw(self) + + @staticmethod + def _scale_conga_json(m_json: JsonType, scale: int) -> JsonType: + """Return a deep copy of m_json with coordinates scaled by *scale*. + + Patches: + - pixelSize → scale + - size.x / size.y → original × scale + - every entity's points list → each value × scale + """ + scaled = copy.deepcopy(m_json) + scaled["pixelSize"] = scale + size = scaled.get("size", {}) + scaled["size"] = { + "x": int(size.get("x", 800)) * scale, + "y": int(size.get("y", 800)) * scale, + } + for entity in scaled.get("entities", []): + pts = entity.get("points") + if isinstance(pts, list): + entity["points"] = [v * scale for v in pts] + return scaled + + async def async_get_conga_from_json( + self, m_json: JsonType | None + ) -> Image.Image | None: + """Scale Conga JSON to pixel_size=CONGA_SCALE, then render normally.""" + if m_json is None: + return None + scaled_json = self._scale_conga_json(m_json, CONGA_SCALE) + self.json_data = await HyperMapData.async_from_valetudo_json(scaled_json) + return await super().async_get_image_from_json(m_json=scaled_json) diff --git a/SCR/valetudo_map_parser/const.py b/SCR/valetudo_map_parser/const.py index 9254fbb..0a751dc 100644 --- a/SCR/valetudo_map_parser/const.py +++ b/SCR/valetudo_map_parser/const.py @@ -31,10 +31,10 @@ NAME = "MQTT Vacuum Camera" ALLOWED_IMAGE_FORMAT: dict[str, str] = { - "pil": "image/pil", - "png": "image/png", - "jpeg" : "image/jpeg" - } + "pil": "image/pil", + "png": "image/png", + "jpeg": "image/jpeg", +} DEFAULT_IMAGE_SIZE = { "x": 5120, diff --git a/SCR/valetudo_map_parser/map_data.py b/SCR/valetudo_map_parser/map_data.py index 20061f0..af2cb14 100755 --- a/SCR/valetudo_map_parser/map_data.py +++ b/SCR/valetudo_map_parser/map_data.py @@ -286,8 +286,8 @@ def _process_map_layer( return compressed_pixels = json_obj.get("compressedPixels") - if compressed_pixels is None: - pixels = json_obj.get("pixels", []) + if not compressed_pixels: + pixels = json_obj.get("pixels") or json_obj.get("congaPixels", []) compressed_pixels = ImageData._convert_pixels_to_compressed(pixels) layer_dict.setdefault(layer_type, []).append(compressed_pixels) @@ -840,9 +840,9 @@ async def async_from_valetudo_json(cls, json_data: Any) -> "HyperMapData": json_data ) virtual_walls = ImageData.find_virtual_walls(json_data) - pixel_size = int(json_data["pixelSize"]) + pixel_size = int(json_data.get("pixelSize", 5)) layers, active_zones, materials = ImageData.find_layers( - json_data["layers"], layers, active_zones, materials + json_data.get("layers", []), layers, active_zones, materials ) entity_dict = ImageData.find_points_entities(json_data) diff --git a/SCR/valetudo_map_parser/reimg_draw.py b/SCR/valetudo_map_parser/reimg_draw.py index f17c039..b79fb32 100644 --- a/SCR/valetudo_map_parser/reimg_draw.py +++ b/SCR/valetudo_map_parser/reimg_draw.py @@ -275,9 +275,7 @@ async def async_draw_path( self.data.rrm_valetudo_path_array(path_pixel["points"]), 2 ) except KeyError as e: - LOGGER.debug( - "%s: Error extracting paths data: %s", self.file_name, str(e) - ) + LOGGER.debug("%s: Error extracting paths data: %s", self.file_name, str(e)) finally: if path_pixel_formatted: np_array = await self.draw.lines( diff --git a/pyproject.toml b/pyproject.toml index 78da225..9015154 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "valetudo-map-parser" -version = "0.2.9" +version = "0.3.0b2" description = "A Python library to parse Valetudo map data returning a PIL Image object." authors = ["Sandro Cantarella "] license = "Apache-2.0"