Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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.2.6"""
Comment thread
sca075 marked this conversation as resolved.
Outdated

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
13 changes: 7 additions & 6 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 @@ -151,18 +151,17 @@ 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"""
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)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

def get_content_type(self)->str:
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 +405,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
19 changes: 18 additions & 1 deletion SCR/valetudo_map_parser/config/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import numpy as np
from PIL import Image, ImageOps

from ..map_data import HyperMapData
from ..map_data import HyperMapData, ImageData
from .async_utils import AsyncNumPy
from .colors import ColorIndex
from .drawable import Drawable
Expand Down Expand Up @@ -80,6 +80,23 @@ def __init__(self):
self.drawing_config: Optional[DrawingConfig] = None
self.draw: Optional[Drawable] = None

@staticmethod
def detect_vacuum_type(json_data: dict) -> str:
"""Detect the vacuum type from the raw JSON map data.

Returns:
'conga' — Conga vacuum (congaPixels present in layers)
'hypfer' — Standard Hypfer/Valetudo vacuum
'rand256' — Rand256 / Valetudo Re vacuum (rrm key present)
"""
if not isinstance(json_data, dict):
return "hypfer"
if "rrm" in json_data or json_data.get("__class") == "RRMap":
return "rand256"
if ImageData.is_conga_map(json_data):
return "conga"
return "hypfer"
Comment thread
sca075 marked this conversation as resolved.
Outdated

def get_frame_number(self) -> int:
"""Return the frame number of the image."""
return self.frame_number
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
Copy Markdown

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_image_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
14 changes: 13 additions & 1 deletion SCR/valetudo_map_parser/map_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,18 @@ def _extract_segment_metadata(
if segment_id and material:
materials_dict[segment_id] = material

@staticmethod
def is_conga_map(json_data: Any) -> bool:
"""Detect whether the JSON data originates from a Conga vacuum.

Conga maps are identified by the presence of a 'congaPixels' field
in at least one layer entry.
"""
layers = json_data.get("layers", []) if isinstance(json_data, dict) else []
return any(
"congaPixels" in layer for layer in layers if isinstance(layer, dict)
)

@staticmethod
def _process_map_layer(
json_obj: dict,
Expand All @@ -287,7 +299,7 @@ def _process_map_layer(

compressed_pixels = json_obj.get("compressedPixels")
if compressed_pixels is None:
pixels = json_obj.get("pixels", [])
pixels = json_obj.get("pixels") or json_obj.get("congaPixels", [])
compressed_pixels = ImageData._convert_pixels_to_compressed(pixels)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

layer_dict.setdefault(layer_type, []).append(compressed_pixels)
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.0b0"
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