Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 5 additions & 2 deletions 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.2.7"""
Version: 0.3.0b2"""

from pathlib import Path

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

Expand Down Expand Up @@ -168,11 +169,13 @@ def get_default_font_path() -> str:
"CameraShared",
"CameraSharedManager",
"ColorsManagement",
"CongaMapImageHandler",
"Drawable",
"DrawableElement",
"DrawingConfig",
"HyperMapData",
"HypferMapImageHandler",
"ImageData",
"RRMapParser",
"RandRoomsHandler",
"ReImageHandler",
Expand Down
30 changes: 20 additions & 10 deletions SCR/valetudo_map_parser/config/shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from PIL import Image

from ..const import (
ALLOWED_IMAGE_FORMAT,
ATTR_CALIBRATION_POINTS,
ATTR_CAMERA_MODE,
ATTR_CONTENT_TYPE,
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions SCR/valetudo_map_parser/config/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
28 changes: 28 additions & 0 deletions SCR/valetudo_map_parser/conga_draw.py
Original file line number Diff line number Diff line change
@@ -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.
"""
67 changes: 67 additions & 0 deletions SCR/valetudo_map_parser/conga_handler.py
Original file line number Diff line number Diff line change
@@ -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)
Comment on lines +32 to +35
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Assignments and reads of is_conga:\n'
rg -n -C2 '\bis_conga\b'

printf '\nConga handler construction sites:\n'
rg -n -C2 '\bCongaMapImageHandler\s*\('

Repository: sca075/Python-package-valetudo-map-parser

Length of output: 925


Set shared_data.is_conga = True in the Conga handler constructor to keep state synchronized.

CameraShared.is_conga is initialized to False and is never set to True when the Conga handler is active. While the flag is not currently read elsewhere in the codebase, keeping shared state synchronized with the active handler is a valid code hygiene practice.

Suggested fix
     def __init__(self, shared_data: CameraShared) -> None:
         """Initialise the Conga image handler."""
         super().__init__(shared_data)
+        self.shared.is_conga = True
         self.imd = CongaImageDraw(self)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@SCR/valetudo_map_parser/conga_handler.py` around lines 32 - 35, The Conga
handler constructor does not set the shared flag; in the __init__ method of the
Conga handler (the def __init__(self, shared_data: CameraShared) -> None:
constructor that currently calls super().__init__(shared_data) and creates
CongaImageDraw(self)), set shared_data.is_conga = True to keep
CameraShared.is_conga synchronized with the active handler; update that
constructor to assign the flag immediately after calling super().__init__ (and
consider leaving a brief comment noting the state synchronization).


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

Expand Down
4 changes: 1 addition & 3 deletions SCR/valetudo_map_parser/reimg_draw.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 <gsca075@gmail.com>"]
license = "Apache-2.0"
Expand Down
Loading