valetudo-map-parser 0.1.9b67__py3-none-any.whl → 0.1.9b69__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,7 +23,9 @@ class AsyncNumPy:
23
23
  return await make_async(np.copy, array)
24
24
 
25
25
  @staticmethod
26
- async def async_full(shape: tuple, fill_value: Any, dtype: np.dtype = None) -> np.ndarray:
26
+ async def async_full(
27
+ shape: tuple, fill_value: Any, dtype: np.dtype = None
28
+ ) -> np.ndarray:
27
29
  """Async array creation with fill value."""
28
30
  return await make_async(np.full, shape, fill_value, dtype=dtype)
29
31
 
@@ -42,20 +44,25 @@ class AsyncPIL:
42
44
  return await make_async(Image.fromarray, array, mode)
43
45
 
44
46
  @staticmethod
45
- async def async_resize(image: Image.Image, size: tuple, resample: int = None) -> Image.Image:
47
+ async def async_resize(
48
+ image: Image.Image, size: tuple, resample: int = None
49
+ ) -> Image.Image:
46
50
  """Async image resizing."""
47
51
  if resample is None:
48
52
  resample = Image.LANCZOS
49
53
  return await make_async(image.resize, size, resample)
50
54
 
51
55
  @staticmethod
52
- async def async_save_to_bytes(image: Image.Image, format_type: str = "WEBP", **kwargs) -> bytes:
56
+ async def async_save_to_bytes(
57
+ image: Image.Image, format_type: str = "WEBP", **kwargs
58
+ ) -> bytes:
53
59
  """Async image saving to bytes."""
60
+
54
61
  def save_to_bytes():
55
62
  buffer = io.BytesIO()
56
63
  image.save(buffer, format=format_type, **kwargs)
57
64
  return buffer.getvalue()
58
-
65
+
59
66
  return await make_async(save_to_bytes)
60
67
 
61
68
 
@@ -14,7 +14,6 @@ import logging
14
14
  import math
15
15
  import asyncio
16
16
  import inspect
17
- import threading
18
17
 
19
18
  import numpy as np
20
19
  from PIL import ImageDraw, ImageFont
@@ -27,72 +26,6 @@ from .types import Color, NumpyArray, PilPNG, Point, Tuple, Union
27
26
  _LOGGER = logging.getLogger(__name__)
28
27
 
29
28
 
30
- class ImageArrayPool:
31
- """Thread-safe memory pool for reusing image arrays to reduce allocation overhead."""
32
-
33
- def __init__(self, max_arrays_per_size: int = 3):
34
- self._pools = {} # {(width, height): [array1, array2, ...]}
35
- self._lock = threading.Lock()
36
- self._max_arrays_per_size = max_arrays_per_size
37
-
38
- def get_array(self, width: int, height: int, background_color: Color) -> NumpyArray:
39
- """Get a reusable array or create a new one if none available."""
40
- key = (width, height)
41
-
42
- with self._lock:
43
- if key in self._pools and self._pools[key]:
44
- # Reuse existing array
45
- array = self._pools[key].pop()
46
- _LOGGER.debug("Reused array from pool for size %dx%d", width, height)
47
- else:
48
- # Create new array
49
- array = np.empty((height, width, 4), dtype=np.uint8)
50
- _LOGGER.debug("Created new array for size %dx%d", width, height)
51
-
52
- # Fill with background color (outside lock for better performance)
53
- array[:] = background_color
54
- return array
55
-
56
- def return_array(self, array: NumpyArray) -> None:
57
- """Return an array to the pool for reuse."""
58
- if array is None:
59
- return
60
-
61
- height, width = array.shape[:2]
62
- key = (width, height)
63
-
64
- with self._lock:
65
- if key not in self._pools:
66
- self._pools[key] = []
67
-
68
- # Only keep up to max_arrays_per_size arrays per size
69
- if len(self._pools[key]) < self._max_arrays_per_size:
70
- self._pools[key].append(array)
71
- _LOGGER.debug("Returned array to pool for size %dx%d (pool size: %d)",
72
- width, height, len(self._pools[key]))
73
- else:
74
- _LOGGER.debug("Pool full for size %dx%d, discarding array", width, height)
75
-
76
- def clear_pool(self) -> None:
77
- """Clear all arrays from the pool."""
78
- with self._lock:
79
- total_arrays = sum(len(arrays) for arrays in self._pools.values())
80
- self._pools.clear()
81
- _LOGGER.debug("Cleared image array pool (%d arrays freed)", total_arrays)
82
-
83
- def get_pool_stats(self) -> dict:
84
- """Get statistics about the current pool state."""
85
- with self._lock:
86
- stats = {}
87
- for (width, height), arrays in self._pools.items():
88
- stats[f"{width}x{height}"] = len(arrays)
89
- return stats
90
-
91
-
92
- # Global shared pool instance for both Hypfer and Rand256 handlers
93
- _image_pool = ImageArrayPool()
94
-
95
-
96
29
  class Drawable:
97
30
  """
98
31
  Collection of drawing utility functions for the image handlers.
@@ -112,27 +45,13 @@ class Drawable:
112
45
  async def create_empty_image(
113
46
  width: int, height: int, background_color: Color
114
47
  ) -> NumpyArray:
115
- """Create the empty background image NumPy array using memory pool for better performance.
48
+ """Create the empty background image NumPy array.
116
49
  Background color is specified as an RGBA tuple.
117
- Optimized: Uses shared memory pool to reuse arrays and reduce allocation overhead."""
118
- # Get array from shared pool (reuses memory when possible)
119
- return _image_pool.get_array(width, height, background_color)
120
-
121
- @staticmethod
122
- def return_image_to_pool(image_array: NumpyArray) -> None:
123
- """Return an image array to the memory pool for reuse.
124
- Call this when you're done with an image array to enable memory reuse."""
125
- _image_pool.return_array(image_array)
126
-
127
- @staticmethod
128
- def get_pool_stats() -> dict:
129
- """Get statistics about the current memory pool state."""
130
- return _image_pool.get_pool_stats()
131
-
132
- @staticmethod
133
- def clear_image_pool() -> None:
134
- """Clear all arrays from the memory pool."""
135
- _image_pool.clear_pool()
50
+ Optimized: Uses np.empty + broadcast instead of np.full for better performance."""
51
+ # Use np.empty + broadcast instead of np.full (avoids double initialization)
52
+ img_array = np.empty((height, width, 4), dtype=np.uint8)
53
+ img_array[:] = background_color # Broadcast color to all pixels efficiently
54
+ return img_array
136
55
 
137
56
  @staticmethod
138
57
  async def from_json_to_image(
@@ -392,6 +311,79 @@ class Drawable:
392
311
 
393
312
  return layer
394
313
 
314
+ @staticmethod
315
+ def draw_lines_batch(
316
+ layer: NumpyArray,
317
+ line_segments: list,
318
+ color: Color,
319
+ width: int = 3,
320
+ ) -> NumpyArray:
321
+ """
322
+ Draw multiple line segments with batch processing for better performance.
323
+
324
+ Args:
325
+ layer: The numpy array to draw on
326
+ line_segments: List of tuples [(x1, y1, x2, y2), ...]
327
+ color: Color to draw with
328
+ width: Width of the lines
329
+ """
330
+ if not line_segments:
331
+ return layer
332
+
333
+ # Pre-calculate blended color once for the entire batch
334
+ # Use the first line segment for color sampling
335
+ x1, y1, x2, y2 = line_segments[0]
336
+ blended_color = get_blended_color(x1, y1, x2, y2, layer, color)
337
+
338
+ # Fast path for fully opaque colors - skip individual blending
339
+ if color[3] == 255:
340
+ blended_color = color
341
+
342
+ # Process all line segments with the same blended color
343
+ for x1, y1, x2, y2 in line_segments:
344
+ # Ensure coordinates are integers
345
+ x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2)
346
+
347
+ # Calculate line length
348
+ length = max(abs(x2 - x1), abs(y2 - y1))
349
+ if length == 0: # Handle case of a single point
350
+ # Draw a dot with the specified width
351
+ for i in range(-width // 2, (width + 1) // 2):
352
+ for j in range(-width // 2, (width + 1) // 2):
353
+ if (
354
+ 0 <= x1 + i < layer.shape[1]
355
+ and 0 <= y1 + j < layer.shape[0]
356
+ ):
357
+ layer[y1 + j, x1 + i] = blended_color
358
+ continue
359
+
360
+ # Create parametric points along the line
361
+ t = np.linspace(0, 1, length + 1) # Reduced from length * 2 to length + 1
362
+ x_coords = np.round(x1 * (1 - t) + x2 * t).astype(int)
363
+ y_coords = np.round(y1 * (1 - t) + y2 * t).astype(int)
364
+
365
+ # Draw the line with the specified width
366
+ if width == 1:
367
+ # Fast path for width=1
368
+ for x, y in zip(x_coords, y_coords):
369
+ if 0 <= x < layer.shape[1] and 0 <= y < layer.shape[0]:
370
+ layer[y, x] = blended_color
371
+ else:
372
+ # For thicker lines, draw a rectangle at each point
373
+ half_width = width // 2
374
+ for x, y in zip(x_coords, y_coords):
375
+ for i in range(-half_width, half_width + 1):
376
+ for j in range(-half_width, half_width + 1):
377
+ if (
378
+ i * i + j * j
379
+ <= half_width * half_width # Make it round
380
+ and 0 <= x + i < layer.shape[1]
381
+ and 0 <= y + j < layer.shape[0]
382
+ ):
383
+ layer[y + j, x + i] = blended_color
384
+
385
+ return layer
386
+
395
387
  @staticmethod
396
388
  async def draw_virtual_walls(
397
389
  layer: NumpyArray, virtual_walls, color: Color
@@ -410,14 +402,16 @@ class Drawable:
410
402
  async def lines(arr: NumpyArray, coords, width: int, color: Color) -> NumpyArray:
411
403
  """
412
404
  Join the coordinates creating a continuous line (path).
413
- Optimized with vectorized operations for better performance.
405
+ Optimized with batch processing for better performance.
414
406
  """
415
407
 
416
408
  # Handle case where arr might be a coroutine (shouldn't happen but let's be safe)
417
409
  if inspect.iscoroutine(arr):
418
410
  arr = await arr
419
411
 
420
- for i, coord in enumerate(coords):
412
+ # Collect all line segments for batch processing
413
+ line_segments = []
414
+ for coord in coords:
421
415
  x0, y0 = coord[0]
422
416
  try:
423
417
  x1, y1 = coord[1]
@@ -428,15 +422,16 @@ class Drawable:
428
422
  if x0 == x1 and y0 == y1:
429
423
  continue
430
424
 
431
- # Get blended color for this line segment
432
- blended_color = get_blended_color(x0, y0, x1, y1, arr, color)
425
+ line_segments.append((x0, y0, x1, y1))
433
426
 
434
- # Use the optimized line drawing method
435
- arr = Drawable._line(arr, x0, y0, x1, y1, blended_color, width)
427
+ # Process all line segments in batches
428
+ batch_size = 100 # Process 100 lines at a time
429
+ for i in range(0, len(line_segments), batch_size):
430
+ batch = line_segments[i : i + batch_size]
431
+ arr = Drawable.draw_lines_batch(arr, batch, color, width)
436
432
 
437
- # Yield control every 100 operations to prevent blocking
438
- if i % 100 == 0:
439
- await asyncio.sleep(0)
433
+ # Yield control between batches to prevent blocking
434
+ await asyncio.sleep(0)
440
435
 
441
436
  return arr
442
437
 
@@ -594,7 +589,11 @@ class Drawable:
594
589
  # Create tasks for parallel zone processing
595
590
  zone_tasks = []
596
591
  for zone in coordinates:
597
- zone_tasks.append(Drawable._process_single_zone(layers.copy(), zone, color, dot_radius, dot_spacing))
592
+ zone_tasks.append(
593
+ Drawable._process_single_zone(
594
+ layers.copy(), zone, color, dot_radius, dot_spacing
595
+ )
596
+ )
598
597
 
599
598
  # Execute all zone processing tasks in parallel
600
599
  zone_results = await asyncio.gather(*zone_tasks, return_exceptions=True)
@@ -648,7 +647,9 @@ class Drawable:
648
647
  y_indices, x_indices = np.ogrid[y_min:y_max, x_min:x_max]
649
648
 
650
649
  # Create a circular mask
651
- mask = (y_indices - y) ** 2 + (x_indices - x) ** 2 <= dot_radius**2
650
+ mask = (y_indices - y) ** 2 + (
651
+ x_indices - x
652
+ ) ** 2 <= dot_radius**2
652
653
 
653
654
  # Apply the color to the masked region
654
655
  layers[y_min:y_max, x_min:x_max][mask] = blended_color
@@ -656,7 +657,9 @@ class Drawable:
656
657
  return layers
657
658
 
658
659
  @staticmethod
659
- async def _process_single_zone(layers: NumpyArray, zone, color: Color, dot_radius: int, dot_spacing: int) -> NumpyArray:
660
+ async def _process_single_zone(
661
+ layers: NumpyArray, zone, color: Color, dot_radius: int, dot_spacing: int
662
+ ) -> NumpyArray:
660
663
  """Process a single zone for parallel execution."""
661
664
  await asyncio.sleep(0) # Yield control
662
665
 
@@ -694,7 +697,9 @@ class Drawable:
694
697
 
695
698
  if y_min < y_max and x_min < x_max:
696
699
  y_indices, x_indices = np.ogrid[y_min:y_max, x_min:x_max]
697
- mask = (y_indices - y) ** 2 + (x_indices - x) ** 2 <= dot_radius**2
700
+ mask = (y_indices - y) ** 2 + (
701
+ x_indices - x
702
+ ) ** 2 <= dot_radius**2
698
703
  layers[y_min:y_max, x_min:x_max][mask] = blended_color
699
704
 
700
705
  return layers
@@ -1,12 +1,13 @@
1
1
  """
2
2
  Class Camera Shared.
3
3
  Keep the data between the modules.
4
- Version: v2024.12.0
4
+ Version: v0.1.9
5
5
  """
6
6
 
7
7
  import asyncio
8
8
  import logging
9
9
  from typing import List
10
+ from PIL import Image
10
11
 
11
12
  from .types import (
12
13
  ATTR_CALIBRATION_POINTS,
@@ -39,6 +40,7 @@ from .types import (
39
40
  CameraModes,
40
41
  Colors,
41
42
  TrimsData,
43
+ PilPNG,
42
44
  )
43
45
 
44
46
 
@@ -58,9 +60,13 @@ class CameraShared:
58
60
  self.rand256_active_zone: list = [] # Active zone for rand256
59
61
  self.is_rand: bool = False # MQTT rand data
60
62
  self._new_mqtt_message = False # New MQTT message
61
- self.last_image = None # Last image received
62
- self.current_image = None # Current image
63
- self.binary_image = None # Current image in binary format
63
+ # Initialize last_image with default gray image (250x150 minimum)
64
+ self.last_image = Image.new(
65
+ "RGBA", (250, 150), (128, 128, 128, 255)
66
+ ) # Gray default image
67
+ self.new_image: PilPNG | None = None # New image received
68
+ self.binary_image: bytes | None = None # Current image in binary format
69
+ self.image_last_updated: float = 0.0 # Last image update time
64
70
  self.image_format = "image/pil" # Image format
65
71
  self.image_size = None # Image size
66
72
  self.image_auto_zoom: bool = False # Auto zoom image
@@ -113,9 +119,7 @@ class CameraShared:
113
119
  self.skip_room_ids: List[str] = []
114
120
  self.device_info = None # Store the device_info
115
121
 
116
-
117
-
118
- def _state_charging(self) -> bool:
122
+ def vacuum_bat_charged(self) -> bool:
119
123
  """Check if the vacuum is charging."""
120
124
  return (self.vacuum_state == "docked") and (int(self.vacuum_battery) < 100)
121
125
 
@@ -193,7 +197,7 @@ class CameraShared:
193
197
  attrs = {
194
198
  ATTR_CAMERA_MODE: self.camera_mode,
195
199
  ATTR_VACUUM_BATTERY: f"{self.vacuum_battery}%",
196
- ATTR_VACUUM_CHARGING: self._state_charging(),
200
+ ATTR_VACUUM_CHARGING: self.vacuum_bat_charged,
197
201
  ATTR_VACUUM_POSITION: self.current_room,
198
202
  ATTR_VACUUM_STATUS: self.vacuum_state,
199
203
  ATTR_VACUUM_JSON_ID: self.vac_json_id,
@@ -228,12 +232,13 @@ class CameraShared:
228
232
  class CameraSharedManager:
229
233
  """Camera Shared Manager class."""
230
234
 
231
- def __init__(self, file_name, device_info):
235
+ def __init__(self, file_name: str, device_info: dict = None):
232
236
  self._instances = {}
233
237
  self._lock = asyncio.Lock()
234
238
  self.file_name = file_name
235
- self.device_info = device_info
236
- self.update_shared_data(device_info)
239
+ if device_info:
240
+ self.device_info = device_info
241
+ self.update_shared_data(device_info)
237
242
 
238
243
  # Automatically initialize shared data for the instance
239
244
  # self._init_shared_data(device_info)
@@ -1,5 +1,6 @@
1
1
  """Utility code for the valetudo map parser."""
2
2
 
3
+ import datetime
3
4
  import hashlib
4
5
  import json
5
6
  from dataclasses import dataclass
@@ -10,7 +11,7 @@ import numpy as np
10
11
  from PIL import Image, ImageOps
11
12
 
12
13
  from .drawable import Drawable
13
- from .drawable_elements import DrawableElement, DrawingConfig
14
+ from .drawable_elements import DrawingConfig
14
15
  from .enhanced_drawable import EnhancedDrawable
15
16
  from .types import (
16
17
  LOGGER,
@@ -79,6 +80,107 @@ class BaseHandler:
79
80
  """Return the robot position."""
80
81
  return self.robot_pos
81
82
 
83
+ async def async_get_image(
84
+ self,
85
+ m_json: dict | None,
86
+ destinations: list | None = None,
87
+ bytes_format: bool = False,
88
+ ) -> PilPNG | None:
89
+ """
90
+ Unified async function to get PIL image from JSON data for both Hypfer and Rand256 handlers.
91
+
92
+ This function:
93
+ 1. Calls the appropriate async_get_image_from_json method
94
+ 2. Stores the processed data in shared.new_image
95
+ 3. Backs up previous data to shared.last_image
96
+ 4. Updates shared.image_last_updated with current datetime
97
+
98
+ @param m_json: The JSON data to use to draw the image
99
+ @param destinations: MQTT destinations for labels (used by Rand256)
100
+ @param bytes_format: If True, also convert to PNG bytes and store in shared.binary_image
101
+ @return: PIL Image or None
102
+ """
103
+ try:
104
+ # Backup current image to last_image before processing new one
105
+ if hasattr(self.shared, "new_image") and self.shared.new_image is not None:
106
+ self.shared.last_image = self.shared.new_image
107
+
108
+ # Call the appropriate handler method based on handler type
109
+ if hasattr(self, "get_image_from_rrm"):
110
+ # This is a Rand256 handler
111
+ new_image = await self.get_image_from_rrm(
112
+ m_json=m_json,
113
+ destinations=destinations,
114
+ return_webp=False, # Always return PIL Image
115
+ )
116
+ elif hasattr(self, "async_get_image_from_json"):
117
+ # This is a Hypfer handler
118
+ new_image = await self.async_get_image_from_json(
119
+ m_json=m_json,
120
+ return_webp=False, # Always return PIL Image
121
+ )
122
+ else:
123
+ LOGGER.warning(
124
+ "%s: Handler type not recognized for async_get_image",
125
+ self.file_name,
126
+ )
127
+ return (
128
+ self.shared.last_image
129
+ if hasattr(self.shared, "last_image")
130
+ else None
131
+ )
132
+
133
+ # Store the new image in shared data
134
+ if new_image is not None:
135
+ self.shared.new_image = new_image
136
+
137
+ # Convert to binary (PNG bytes) if requested
138
+ if bytes_format:
139
+ try:
140
+ png_buffer = io.BytesIO()
141
+ new_image.save(png_buffer, format="PNG")
142
+ self.shared.binary_image = png_buffer.getvalue()
143
+ png_buffer.close()
144
+ LOGGER.debug(
145
+ "%s: Binary image conversion completed", self.file_name
146
+ )
147
+ except Exception as e:
148
+ LOGGER.warning(
149
+ "%s: Failed to convert image to binary: %s",
150
+ self.file_name,
151
+ str(e),
152
+ )
153
+ self.shared.binary_image = None
154
+ else:
155
+ self.shared.binary_image = None
156
+
157
+ # Update the timestamp with current datetime
158
+ self.shared.image_last_updated = datetime.datetime.now().timestamp()
159
+ LOGGER.debug(
160
+ "%s: Image processed and stored in shared data", self.file_name
161
+ )
162
+ return new_image
163
+ else:
164
+ LOGGER.warning(
165
+ "%s: Failed to generate image from JSON data", self.file_name
166
+ )
167
+ return (
168
+ self.shared.last_image
169
+ if hasattr(self.shared, "last_image")
170
+ else None
171
+ )
172
+
173
+ except Exception as e:
174
+ LOGGER.error(
175
+ "%s: Error in async_get_image: %s",
176
+ self.file_name,
177
+ str(e),
178
+ exc_info=True,
179
+ )
180
+ return (
181
+ self.shared.last_image if hasattr(self.shared, "last_image") else None
182
+ )
183
+
82
184
  def get_charger_position(self) -> ChargerPosition | None:
83
185
  """Return the charger position."""
84
186
  return self.charger_pos
@@ -337,39 +337,30 @@ class ImageDraw:
337
337
  _LOGGER.info("%s: Got zones.", self.file_name)
338
338
 
339
339
  if zone_clean:
340
- # Prepare zone drawing tasks for parallel execution
341
- zone_tasks = []
340
+ # Process zones sequentially to avoid memory-intensive array copies
341
+ # This is more memory-efficient than parallel processing with copies
342
342
 
343
343
  # Active zones
344
344
  zones_active = zone_clean.get("active_zone")
345
345
  if zones_active:
346
- zone_tasks.append(
347
- self.img_h.draw.zones(np_array.copy(), zones_active, color_zone_clean)
346
+ np_array = await self.img_h.draw.zones(
347
+ np_array, zones_active, color_zone_clean
348
348
  )
349
349
 
350
350
  # No-go zones
351
351
  no_go_zones = zone_clean.get("no_go_area")
352
352
  if no_go_zones:
353
- zone_tasks.append(
354
- self.img_h.draw.zones(np_array.copy(), no_go_zones, color_no_go)
353
+ np_array = await self.img_h.draw.zones(
354
+ np_array, no_go_zones, color_no_go
355
355
  )
356
356
 
357
357
  # No-mop zones
358
358
  no_mop_zones = zone_clean.get("no_mop_area")
359
359
  if no_mop_zones:
360
- zone_tasks.append(
361
- self.img_h.draw.zones(np_array.copy(), no_mop_zones, color_no_go)
360
+ np_array = await self.img_h.draw.zones(
361
+ np_array, no_mop_zones, color_no_go
362
362
  )
363
363
 
364
- # Execute all zone drawing tasks in parallel
365
- if zone_tasks:
366
- zone_results = await asyncio.gather(*zone_tasks)
367
- # Merge results back into the main array
368
- for result in zone_results:
369
- # Simple overlay - in practice you might want more sophisticated blending
370
- mask = result != np_array
371
- np_array[mask] = result[mask]
372
-
373
364
  return np_array
374
365
 
375
366
  async def async_draw_virtual_walls(
@@ -439,7 +430,6 @@ class ImageDraw:
439
430
  def _check_active_zone_and_set_zooming(self) -> None:
440
431
  """Helper function to check active zones and set zooming state."""
441
432
  if self.img_h.active_zones and self.img_h.robot_in_room:
442
-
443
433
  segment_id = str(self.img_h.robot_in_room["id"])
444
434
  room_store = RoomStore(self.file_name)
445
435
  room_keys = list(room_store.get_rooms().keys())
@@ -615,7 +605,6 @@ class ImageDraw:
615
605
 
616
606
  # Handle active zones - Map segment ID to active_zones position
617
607
  if self.img_h.active_zones:
618
-
619
608
  segment_id = str(self.img_h.robot_in_room["id"])
620
609
  room_store = RoomStore(self.file_name)
621
610
  room_keys = list(room_store.get_rooms().keys())
@@ -8,11 +8,10 @@ Version: 0.1.9
8
8
  from __future__ import annotations
9
9
 
10
10
  import asyncio
11
- import json
12
11
 
13
12
  from PIL import Image
14
13
 
15
- from .config.async_utils import AsyncNumPy, AsyncPIL, AsyncParallel
14
+ from .config.async_utils import AsyncNumPy, AsyncPIL
16
15
  from .config.auto_crop import AutoCrop
17
16
  from .config.drawable_elements import DrawableElement
18
17
  from .config.shared import CameraShared
@@ -25,6 +24,7 @@ from .config.types import (
25
24
  RoomsProperties,
26
25
  RoomStore,
27
26
  WebPBytes,
27
+ JsonType,
28
28
  )
29
29
  from .config.utils import (
30
30
  BaseHandler,
@@ -100,7 +100,7 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
100
100
  # noinspection PyUnresolvedReferences,PyUnboundLocalVariable
101
101
  async def async_get_image_from_json(
102
102
  self,
103
- m_json: json | None,
103
+ m_json: JsonType | None,
104
104
  return_webp: bool = False,
105
105
  ) -> WebPBytes | Image.Image | None:
106
106
  """Get the image from the JSON data.
@@ -232,13 +232,6 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
232
232
  disabled_rooms if layer_type == "wall" else None,
233
233
  )
234
234
 
235
- # Update element map for this layer
236
- if is_room_layer and 0 < room_id <= 15:
237
- # Mark the room in the element map
238
- room_element = getattr(
239
- DrawableElement, f"ROOM_{room_id}", None
240
- )
241
-
242
235
  # Draw the virtual walls if enabled
243
236
  if self.drawing_config.is_enabled(DrawableElement.VIRTUAL_WALL):
244
237
  img_np_array = await self.imd.async_draw_virtual_walls(
@@ -308,14 +301,16 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
308
301
 
309
302
  # Prepare path data extraction
310
303
  path_enabled = self.drawing_config.is_enabled(DrawableElement.PATH)
311
- LOGGER.info("%s: PATH element enabled: %s", self.file_name, path_enabled)
304
+ LOGGER.info(
305
+ "%s: PATH element enabled: %s", self.file_name, path_enabled
306
+ )
312
307
  if path_enabled:
313
308
  LOGGER.info("%s: Drawing path", self.file_name)
314
309
  data_tasks.append(self._prepare_path_data(m_json))
315
310
 
316
- # Execute data preparation in parallel if we have tasks
311
+ # Await all data preparation tasks if any were created
317
312
  if data_tasks:
318
- prepared_data = await AsyncParallel.parallel_data_preparation(*data_tasks)
313
+ await asyncio.gather(*data_tasks)
319
314
 
320
315
  # Process drawing operations sequentially (since they modify the same array)
321
316
  # Draw zones if enabled
@@ -390,9 +385,7 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
390
385
  if self.check_zoom_and_aspect_ratio():
391
386
  # Convert to PIL for resizing
392
387
  pil_img = await AsyncPIL.async_fromarray(img_np_array, mode="RGBA")
393
- # Return array to pool for reuse instead of just deleting
394
- from .config.drawable import Drawable
395
- Drawable.return_image_to_pool(img_np_array)
388
+ del img_np_array
396
389
  resize_params = prepare_resize_params(self, pil_img, False)
397
390
  resized_image = await self.async_resize_images(resize_params)
398
391
 
@@ -407,17 +400,13 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
407
400
  if return_webp:
408
401
  # Convert directly from NumPy to WebP for better performance
409
402
  webp_bytes = await numpy_to_webp_bytes(img_np_array)
410
- # Return array to pool for reuse instead of just deleting
411
- from .config.drawable import Drawable
412
- Drawable.return_image_to_pool(img_np_array)
403
+ del img_np_array
413
404
  LOGGER.debug("%s: Frame Completed.", self.file_name)
414
405
  return webp_bytes
415
406
  else:
416
407
  # Convert to PIL Image (original behavior)
417
408
  pil_img = await AsyncPIL.async_fromarray(img_np_array, mode="RGBA")
418
- # Return array to pool for reuse instead of just deleting
419
- from .config.drawable import Drawable
420
- Drawable.return_image_to_pool(img_np_array)
409
+ del img_np_array
421
410
  LOGGER.debug("%s: Frame Completed.", self.file_name)
422
411
  return pil_img
423
412
  except (RuntimeError, RuntimeWarning) as e:
@@ -506,7 +495,8 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
506
495
  except (ValueError, KeyError):
507
496
  return None
508
497
 
509
- async def _prepare_goto_data(self, entity_dict):
498
+ @staticmethod
499
+ async def _prepare_goto_data(entity_dict):
510
500
  """Prepare go-to flag data for parallel processing."""
511
501
  await asyncio.sleep(0) # Yield control
512
502
  # Extract go-to target data from entity_dict
@@ -516,6 +506,6 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
516
506
  """Prepare path data for parallel processing."""
517
507
  await asyncio.sleep(0) # Yield control
518
508
  try:
519
- return self.data.find_path_entities(m_json)
509
+ return self.data.find_paths_entities(m_json)
520
510
  except (ValueError, KeyError):
521
511
  return None
@@ -19,14 +19,14 @@ class ImageData:
19
19
  @staticmethod
20
20
  def sublist(lst, n):
21
21
  """Split a list into n chunks of specified size."""
22
- return [lst[i: i + n] for i in range(0, len(lst), n)]
22
+ return [lst[i : i + n] for i in range(0, len(lst), n)]
23
23
 
24
24
  @staticmethod
25
25
  def sublist_join(lst, n):
26
26
  """Join the lists in a unique list of n elements."""
27
27
  arr = np.array(lst)
28
28
  num_windows = len(lst) - n + 1
29
- result = [arr[i: i + n].tolist() for i in range(num_windows)]
29
+ result = [arr[i : i + n].tolist() for i in range(num_windows)]
30
30
  return result
31
31
 
32
32
  @staticmethod
@@ -39,15 +39,19 @@ class ImageData:
39
39
  points = obstacle.get("points", [])
40
40
  image_id = obstacle.get("metaData", {}).get("id")
41
41
  if label and points:
42
- obstacle_positions.append({
43
- "label": label,
44
- "points": {"x": points[0], "y": points[1]},
45
- "id": image_id,
46
- })
42
+ obstacle_positions.append(
43
+ {
44
+ "label": label,
45
+ "points": {"x": points[0], "y": points[1]},
46
+ "id": image_id,
47
+ }
48
+ )
47
49
  return obstacle_positions
48
50
 
49
51
  @staticmethod
50
- def find_layers(json_obj: JsonType, layer_dict: dict, active_list: list) -> tuple[dict, list]:
52
+ def find_layers(
53
+ json_obj: JsonType, layer_dict: dict, active_list: list
54
+ ) -> tuple[dict, list]:
51
55
  """Find the layers in the json object."""
52
56
  layer_dict = {} if layer_dict is None else layer_dict
53
57
  active_list = [] if active_list is None else active_list
@@ -56,7 +60,9 @@ class ImageData:
56
60
  layer_type = json_obj.get("type")
57
61
  active_type = json_obj.get("metaData")
58
62
  if layer_type:
59
- layer_dict.setdefault(layer_type, []).append(json_obj.get("compressedPixels", []))
63
+ layer_dict.setdefault(layer_type, []).append(
64
+ json_obj.get("compressedPixels", [])
65
+ )
60
66
  if layer_type == "segment":
61
67
  active_list.append(int(active_type.get("active", 0)))
62
68
  for value in json_obj.values():
@@ -121,7 +127,10 @@ class ImageData:
121
127
 
122
128
  def _recursive(obj):
123
129
  if isinstance(obj, dict):
124
- if obj.get("__class") == "LineMapEntity" and obj.get("type") == "virtual_wall":
130
+ if (
131
+ obj.get("__class") == "LineMapEntity"
132
+ and obj.get("type") == "virtual_wall"
133
+ ):
125
134
  walls.append(obj["points"])
126
135
  for value in obj.values():
127
136
  _recursive(value)
@@ -133,7 +142,9 @@ class ImageData:
133
142
  return walls
134
143
 
135
144
  @staticmethod
136
- async def async_get_rooms_coordinates(pixels: list, pixel_size: int = 5, rand: bool = False) -> tuple:
145
+ async def async_get_rooms_coordinates(
146
+ pixels: list, pixel_size: int = 5, rand: bool = False
147
+ ) -> tuple:
137
148
  """Extract the room coordinates from the vacuum pixels data."""
138
149
  df = pd.DataFrame(pixels, columns=["x", "y", "length"])
139
150
  if rand:
@@ -7,15 +7,13 @@ Version: 0.1.9.a6
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- import asyncio
11
10
  import logging
12
11
  import uuid
13
12
  from typing import Any
14
13
 
15
14
  import numpy as np
16
- from PIL import Image
17
15
 
18
- from .config.async_utils import AsyncNumPy, AsyncPIL, AsyncParallel
16
+ from .config.async_utils import AsyncNumPy, AsyncPIL
19
17
  from .config.auto_crop import AutoCrop
20
18
  from .config.drawable_elements import DrawableElement
21
19
  from .config.types import (
@@ -147,7 +145,7 @@ class ReImageHandler(BaseHandler, AutoCrop):
147
145
  m_json: JsonType, # json data
148
146
  destinations: None = None, # MQTT destinations for labels
149
147
  return_webp: bool = False,
150
- ) -> WebPBytes | Image.Image | None:
148
+ ) -> WebPBytes | PilPNG | None:
151
149
  """Generate Images from the json data.
152
150
  @param m_json: The JSON data to use to draw the image.
153
151
  @param destinations: MQTT destinations for labels (unused).
@@ -168,14 +166,6 @@ class ReImageHandler(BaseHandler, AutoCrop):
168
166
  self.json_id = str(uuid.uuid4()) # image id
169
167
  _LOGGER.info("Vacuum Data ID: %s", self.json_id)
170
168
 
171
- # Prepare parallel data extraction tasks
172
- data_tasks = []
173
- data_tasks.append(self._prepare_zone_data(m_json))
174
- data_tasks.append(self._prepare_path_data(m_json))
175
-
176
- # Execute data preparation tasks in parallel
177
- zone_data, path_data = await asyncio.gather(*data_tasks, return_exceptions=True)
178
-
179
169
  (
180
170
  img_np_array,
181
171
  robot_position,
@@ -202,16 +192,12 @@ class ReImageHandler(BaseHandler, AutoCrop):
202
192
  if return_webp:
203
193
  # Convert directly to WebP bytes for better performance
204
194
  webp_bytes = await numpy_to_webp_bytes(img_np_array)
205
- # Return array to pool for reuse instead of just deleting
206
- from .config.drawable import Drawable
207
- Drawable.return_image_to_pool(img_np_array)
195
+ del img_np_array # free memory
208
196
  return webp_bytes
209
197
  else:
210
198
  # Convert to PIL Image using async utilities
211
199
  pil_img = await AsyncPIL.async_fromarray(img_np_array, mode="RGBA")
212
- # Return array to pool for reuse instead of just deleting
213
- from .config.drawable import Drawable
214
- Drawable.return_image_to_pool(img_np_array)
200
+ del img_np_array # free memory
215
201
  return await self._finalize_image(pil_img)
216
202
 
217
203
  except (RuntimeError, RuntimeWarning) as e:
@@ -311,11 +297,6 @@ class ReImageHandler(BaseHandler, AutoCrop):
311
297
  original_rooms_pos = self.rooms_pos
312
298
  self.rooms_pos = temp_rooms_pos
313
299
 
314
- # Perform robot room detection to check active zones
315
- robot_room_result = await self.async_get_robot_in_room(
316
- robot_position[0], robot_position[1], robot_position_angle
317
- )
318
-
319
300
  # Restore original rooms_pos
320
301
  self.rooms_pos = original_rooms_pos
321
302
 
@@ -691,21 +672,3 @@ class ReImageHandler(BaseHandler, AutoCrop):
691
672
  async def async_copy_array(self, original_array):
692
673
  """Copy the array using async utilities."""
693
674
  return await AsyncNumPy.async_copy(original_array)
694
-
695
- async def _prepare_zone_data(self, m_json):
696
- """Prepare zone data for parallel processing."""
697
- await asyncio.sleep(0) # Yield control
698
- try:
699
- return self.data.find_zone_entities(m_json)
700
- except (ValueError, KeyError):
701
- return None
702
-
703
- async def _prepare_path_data(self, m_json):
704
- """Prepare path data for parallel processing."""
705
- await asyncio.sleep(0) # Yield control
706
- try:
707
- return self.data.find_path_entities(m_json)
708
- except (ValueError, KeyError):
709
- return None
710
-
711
-
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: valetudo-map-parser
3
- Version: 0.1.9b67
3
+ Version: 0.1.9b69
4
4
  Summary: A Python library to parse Valetudo map data returning a PIL Image object.
5
5
  License: Apache-2.0
6
6
  Author: Sandro Cantarella
@@ -1,27 +1,27 @@
1
1
  valetudo_map_parser/__init__.py,sha256=XO_eJwFDyU7hXJ4tAa2zY-n-SM2_kmIGMWDKY3GcauY,1163
2
2
  valetudo_map_parser/config/__init__.py,sha256=DQ9plV3ZF_K25Dp5ZQHPDoG-40dQoJNdNi-dfNeR3Zc,48
3
- valetudo_map_parser/config/async_utils.py,sha256=r96x4rVis2y57KSnWfVxraSGi0cOz0fsOg-njRIggfs,3169
3
+ valetudo_map_parser/config/async_utils.py,sha256=e1j9uTtg4dhPVWvB2_XgqaH4aeSjRAPz-puRMbGoOs8,3204
4
4
  valetudo_map_parser/config/auto_crop.py,sha256=Aes7vfv4z8ihYvGaH5Nryj6Y9mHDerZLIeyvePjf9aQ,19259
5
5
  valetudo_map_parser/config/color_utils.py,sha256=nXD6WeNmdFdoMxPDW-JFpjnxJSaZR1jX-ouNfrx6zvE,4502
6
6
  valetudo_map_parser/config/colors.py,sha256=DG-oPQoN5gsnwDbEsuFr8a0hRCxmbFHObWa4_5pr-70,29910
7
- valetudo_map_parser/config/drawable.py,sha256=d9hdEjmlXnbnEKgy2RiKVGiUlU8pbWF14MfNlVpDgcw,40947
7
+ valetudo_map_parser/config/drawable.py,sha256=nAOoPQmNz6LJ4-WN7WF5iHTYEAzT43YVnTpiqzjNva0,41180
8
8
  valetudo_map_parser/config/drawable_elements.py,sha256=o-5oiXmfqPwNQLzKIhkEcZD_A47rIU9E0CqKgWipxgc,11516
9
9
  valetudo_map_parser/config/enhanced_drawable.py,sha256=QlGxlUMVgECUXPtFwIslyjubWxQuhIixsRymWV3lEvk,12586
10
10
  valetudo_map_parser/config/optimized_element_map.py,sha256=52BCnkvVv9bre52LeVIfT8nhnEIpc0TuWTv1xcNu0Rk,15744
11
11
  valetudo_map_parser/config/rand256_parser.py,sha256=LU3y7XvRRQxVen9iwom0dOaDnJJvhZdg97NqOYRZFas,16279
12
- valetudo_map_parser/config/shared.py,sha256=J_66BuhgqRJUjXKBwKh8qih1iyiTq9ZiaZwduIROYmE,12560
12
+ valetudo_map_parser/config/shared.py,sha256=98CgGDY0tbc5BSg2TIHbGcDFZZ2acgIYnoPjAwENmBU,12885
13
13
  valetudo_map_parser/config/types.py,sha256=saL7pULKAdTRQ_ShR2arT8IV472e9MBC_SohTthlGp8,17567
14
- valetudo_map_parser/config/utils.py,sha256=fnzzNywE0Z1qw968hUa_yGslq3YhOP7daFj21miuhjI,31354
15
- valetudo_map_parser/hypfer_draw.py,sha256=ZK_WybvukHd8nNk2mq5icrOu1Ue3SVCIC6_Hc9bTg0Q,29396
16
- valetudo_map_parser/hypfer_handler.py,sha256=X-okVYDeHRWfwx08XIvqimoI-mghqekTe3GlyzV62gA,24214
14
+ valetudo_map_parser/config/utils.py,sha256=lRLvQbqCQ44knnIJr-UAlkEOO1d14mFCmHKDor989VE,35468
15
+ valetudo_map_parser/hypfer_draw.py,sha256=vlrfBSDpHNnjh0dRA1BE5_4MsKMMWS_O1QErrd-VRKE,28995
16
+ valetudo_map_parser/hypfer_handler.py,sha256=U452BEi3kmU0O27aNN7BY42J_zrpjhDuebpqxwGxe50,23366
17
17
  valetudo_map_parser/hypfer_rooms_handler.py,sha256=NkpOA6Gdq-2D3lLAxvtNuuWMvPXHxeMY2TO5RZLSHlU,22652
18
- valetudo_map_parser/map_data.py,sha256=Op0LTCakcTJ1Q0rxQhl6BpgSby_6nJenCQS2Y2FHtRk,17243
18
+ valetudo_map_parser/map_data.py,sha256=Flq5t9QQQiD5ylObIniHhPP1VB7VhNoMcMeJrOey3Go,17433
19
19
  valetudo_map_parser/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
- valetudo_map_parser/rand256_handler.py,sha256=vrfoX7RM8NFhpbzrc2BSLEA8VGx4B_sXBG_uAZSO8W0,29216
20
+ valetudo_map_parser/rand256_handler.py,sha256=daaSQ5ktMUYMnYxJkjS75UdBchpXVZ58HIomwHBFivs,27651
21
21
  valetudo_map_parser/reimg_draw.py,sha256=1q8LkNTPHEA9Tsapc_JnVw51kpPYNhaBU-KmHkefCQY,12507
22
22
  valetudo_map_parser/rooms_handler.py,sha256=ovqQtAjauAqwUNPR0aX27P2zhheQmqfaFhDE3_AwYWk,17821
23
- valetudo_map_parser-0.1.9b67.dist-info/LICENSE,sha256=Lh-qBbuRV0-jiCIBhfV7NgdwFxQFOXH3BKOzK865hRs,10480
24
- valetudo_map_parser-0.1.9b67.dist-info/METADATA,sha256=0ItPS5MJeqacL3gkVvxoL9T9jvRJi5o9rFmNwByD7cA,3353
25
- valetudo_map_parser-0.1.9b67.dist-info/NOTICE.txt,sha256=5lTOuWiU9aiEnJ2go8sc7lTJ7ntMBx0g0GFnNrswCY4,2533
26
- valetudo_map_parser-0.1.9b67.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
27
- valetudo_map_parser-0.1.9b67.dist-info/RECORD,,
23
+ valetudo_map_parser-0.1.9b69.dist-info/LICENSE,sha256=Lh-qBbuRV0-jiCIBhfV7NgdwFxQFOXH3BKOzK865hRs,10480
24
+ valetudo_map_parser-0.1.9b69.dist-info/METADATA,sha256=Ex2vOq_SjHDzJqk5dNNL78hXqOnkhz9Y7anOUC-XsmE,3353
25
+ valetudo_map_parser-0.1.9b69.dist-info/NOTICE.txt,sha256=5lTOuWiU9aiEnJ2go8sc7lTJ7ntMBx0g0GFnNrswCY4,2533
26
+ valetudo_map_parser-0.1.9b69.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
27
+ valetudo_map_parser-0.1.9b69.dist-info/RECORD,,