valetudo-map-parser 0.1.9b40__py3-none-any.whl → 0.1.9b42__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.
@@ -0,0 +1,447 @@
1
+ """
2
+ Enhanced Drawable Class.
3
+ Provides drawing utilities with element selection support.
4
+ Version: 0.1.9
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+
11
+ # math is not used in this file
12
+ from typing import Optional, Tuple
13
+
14
+ import numpy as np
15
+
16
+ from .drawable import Drawable
17
+ from .drawable_elements import DrawableElement, DrawingConfig
18
+
19
+
20
+ # Type aliases
21
+ NumpyArray = np.ndarray
22
+ Color = Tuple[int, int, int, int]
23
+
24
+ _LOGGER = logging.getLogger(__name__)
25
+
26
+
27
+ class EnhancedDrawable(Drawable):
28
+ """Enhanced drawing utilities with element selection support."""
29
+
30
+ def __init__(self, drawing_config: Optional[DrawingConfig] = None):
31
+ """Initialize with optional drawing configuration."""
32
+ super().__init__()
33
+ self.drawing_config = drawing_config or DrawingConfig()
34
+
35
+ @staticmethod
36
+ def blend_colors(base_color: Color, overlay_color: Color) -> Color:
37
+ """
38
+ Blend two RGBA colors, considering alpha channels.
39
+
40
+ Args:
41
+ base_color: The base RGBA color
42
+ overlay_color: The overlay RGBA color to blend on top
43
+
44
+ Returns:
45
+ The blended RGBA color
46
+ """
47
+ # Extract components
48
+ r1, g1, b1, a1 = base_color
49
+ r2, g2, b2, a2 = overlay_color
50
+
51
+ # Convert alpha to 0-1 range
52
+ a1 = a1 / 255.0
53
+ a2 = a2 / 255.0
54
+
55
+ # Calculate resulting alpha
56
+ a_out = a1 + a2 * (1 - a1)
57
+
58
+ # Avoid division by zero
59
+ if a_out < 0.0001:
60
+ return (0, 0, 0, 0)
61
+
62
+ # Calculate blended RGB components
63
+ r_out = (r1 * a1 + r2 * a2 * (1 - a1)) / a_out
64
+ g_out = (g1 * a1 + g2 * a2 * (1 - a1)) / a_out
65
+ b_out = (b1 * a1 + b2 * a2 * (1 - a1)) / a_out
66
+
67
+ # Convert back to 0-255 range and return as tuple
68
+ return (
69
+ int(max(0, min(255, r_out))),
70
+ int(max(0, min(255, g_out))),
71
+ int(max(0, min(255, b_out))),
72
+ int(max(0, min(255, a_out * 255))),
73
+ )
74
+
75
+ def blend_pixel(
76
+ self,
77
+ array: NumpyArray,
78
+ x: int,
79
+ y: int,
80
+ color: Color,
81
+ element: DrawableElement,
82
+ element_map: NumpyArray,
83
+ ) -> None:
84
+ """
85
+ Blend a pixel color with the existing color at the specified position.
86
+ Also updates the element map if the new element has higher z-index.
87
+
88
+ Args:
89
+ array: The image array to modify
90
+ x, y: Pixel coordinates
91
+ color: RGBA color to blend
92
+ element: The element being drawn
93
+ element_map: The element map to update
94
+ """
95
+ # Check bounds
96
+ if not (0 <= y < array.shape[0] and 0 <= x < array.shape[1]):
97
+ return
98
+
99
+ # Get current element at this position
100
+ current_element = element_map[y, x]
101
+
102
+ # Get z-indices for comparison
103
+ current_z = (
104
+ self.drawing_config.get_property(current_element, "z_index", 0)
105
+ if current_element
106
+ else 0
107
+ )
108
+ new_z = self.drawing_config.get_property(element, "z_index", 0)
109
+
110
+ # Get current color at this position
111
+ current_color = tuple(array[y, x])
112
+
113
+ # Blend colors
114
+ blended_color = self.blend_colors(current_color, color)
115
+
116
+ # Update pixel color
117
+ array[y, x] = blended_color
118
+
119
+ # Update element map if new element has higher z-index
120
+ if new_z >= current_z:
121
+ element_map[y, x] = element
122
+
123
+ async def draw_map(
124
+ self, map_data: dict, base_array: Optional[NumpyArray] = None
125
+ ) -> Tuple[NumpyArray, NumpyArray]:
126
+ """
127
+ Draw the map with selected elements.
128
+
129
+ Args:
130
+ map_data: The map data dictionary
131
+ base_array: Optional base array to draw on
132
+
133
+ Returns:
134
+ Tuple of (image_array, element_map)
135
+ """
136
+ # Get map dimensions
137
+ size_x = map_data.get("size", {}).get("x", 1024)
138
+ size_y = map_data.get("size", {}).get("y", 1024)
139
+
140
+ # Create empty image if none provided
141
+ if base_array is None:
142
+ background_color = self.drawing_config.get_property(
143
+ DrawableElement.FLOOR, "color", (200, 200, 200, 255)
144
+ )
145
+ base_array = await self.create_empty_image(size_x, size_y, background_color)
146
+
147
+ # Create a 2D map for element identification
148
+ element_map = np.zeros(
149
+ (base_array.shape[0], base_array.shape[1]), dtype=np.int32
150
+ )
151
+
152
+ # Draw elements in order of z-index
153
+ for element in self.drawing_config.get_drawing_order():
154
+ if element == DrawableElement.FLOOR:
155
+ base_array, element_map = await self._draw_floor(
156
+ map_data, base_array, element_map
157
+ )
158
+ elif element == DrawableElement.WALL:
159
+ base_array, element_map = await self._draw_walls(
160
+ map_data, base_array, element_map
161
+ )
162
+ elif element == DrawableElement.ROBOT:
163
+ base_array, element_map = await self._draw_robot(
164
+ map_data, base_array, element_map
165
+ )
166
+ elif element == DrawableElement.CHARGER:
167
+ base_array, element_map = await self._draw_charger(
168
+ map_data, base_array, element_map
169
+ )
170
+ elif element == DrawableElement.VIRTUAL_WALL:
171
+ base_array, element_map = await self._draw_virtual_walls(
172
+ map_data, base_array, element_map
173
+ )
174
+ elif element == DrawableElement.RESTRICTED_AREA:
175
+ base_array, element_map = await self._draw_restricted_areas(
176
+ map_data, base_array, element_map
177
+ )
178
+ elif element == DrawableElement.NO_MOP_AREA:
179
+ base_array, element_map = await self._draw_no_mop_areas(
180
+ map_data, base_array, element_map
181
+ )
182
+ elif element == DrawableElement.PATH:
183
+ base_array, element_map = await self._draw_path(
184
+ map_data, base_array, element_map
185
+ )
186
+ elif element == DrawableElement.PREDICTED_PATH:
187
+ base_array, element_map = await self._draw_predicted_path(
188
+ map_data, base_array, element_map
189
+ )
190
+ elif element == DrawableElement.GO_TO_TARGET:
191
+ base_array, element_map = await self._draw_go_to_target(
192
+ map_data, base_array, element_map
193
+ )
194
+ elif DrawableElement.ROOM_1 <= element <= DrawableElement.ROOM_15:
195
+ room_id = element - DrawableElement.ROOM_1 + 1
196
+ base_array, element_map = await self._draw_room(
197
+ map_data, room_id, base_array, element_map
198
+ )
199
+
200
+ return base_array, element_map
201
+
202
+ async def _draw_floor(
203
+ self, map_data: dict, array: NumpyArray, element_map: NumpyArray
204
+ ) -> Tuple[NumpyArray, NumpyArray]:
205
+ """Draw the floor layer."""
206
+ if not self.drawing_config.is_enabled(DrawableElement.FLOOR):
207
+ return array, element_map
208
+
209
+ # Implementation depends on the map data format
210
+ # This is a placeholder - actual implementation would use map_data to draw floor
211
+ # For now, we'll just mark the entire map as floor in the element map
212
+ element_map[:] = DrawableElement.FLOOR
213
+
214
+ return array, element_map
215
+
216
+ async def _draw_walls(
217
+ self, map_data: dict, array: NumpyArray, element_map: NumpyArray
218
+ ) -> Tuple[NumpyArray, NumpyArray]:
219
+ """Draw the walls."""
220
+ if not self.drawing_config.is_enabled(DrawableElement.WALL):
221
+ return array, element_map
222
+
223
+ # Get wall color from drawing config
224
+ wall_color = self.drawing_config.get_property(
225
+ DrawableElement.WALL, "color", (255, 255, 0, 255)
226
+ )
227
+
228
+ # Implementation depends on the map data format
229
+ # For Valetudo maps, we would look at the layers with type "wall"
230
+ # This is a simplified example - in a real implementation, we would extract the actual wall pixels
231
+
232
+ # Find wall data in map_data
233
+ wall_pixels = []
234
+ for layer in map_data.get("layers", []):
235
+ if layer.get("type") == "wall":
236
+ # Extract wall pixels from the layer
237
+ # This is a placeholder - actual implementation would depend on the map data format
238
+ wall_pixels = layer.get("pixels", [])
239
+ break
240
+
241
+ # Draw wall pixels with color blending
242
+ for x, y in wall_pixels:
243
+ # Use blend_pixel to properly blend colors and update element map
244
+ self.blend_pixel(array, x, y, wall_color, DrawableElement.WALL, element_map)
245
+
246
+ return array, element_map
247
+
248
+ async def _draw_robot(
249
+ self, map_data: dict, array: NumpyArray, element_map: NumpyArray
250
+ ) -> Tuple[NumpyArray, NumpyArray]:
251
+ """Draw the robot."""
252
+ if not self.drawing_config.is_enabled(DrawableElement.ROBOT):
253
+ return array, element_map
254
+
255
+ # Get robot color from drawing config
256
+ robot_color = self.drawing_config.get_property(
257
+ DrawableElement.ROBOT, "color", (255, 255, 204, 255)
258
+ )
259
+
260
+ # Extract robot position and angle from map_data
261
+ robot_position = map_data.get("robot", {}).get("position", None)
262
+ robot_angle = map_data.get("robot", {}).get("angle", 0)
263
+
264
+ if robot_position:
265
+ x, y = robot_position.get("x", 0), robot_position.get("y", 0)
266
+
267
+ # Draw robot with color blending
268
+ # Create a circle around the robot position
269
+ radius = 25 # Same as in the robot drawing method
270
+ for dy in range(-radius, radius + 1):
271
+ for dx in range(-radius, radius + 1):
272
+ if dx * dx + dy * dy <= radius * radius:
273
+ map_x, map_y = int(x + dx), int(y + dy)
274
+ # Use blend_pixel to properly blend colors and update element map
275
+ self.blend_pixel(
276
+ array,
277
+ map_x,
278
+ map_y,
279
+ robot_color,
280
+ DrawableElement.ROBOT,
281
+ element_map,
282
+ )
283
+
284
+ # TODO: Draw robot orientation indicator
285
+ # This would be a line or triangle showing the direction
286
+ # For now, we'll skip this part as it requires more complex drawing
287
+
288
+ return array, element_map
289
+
290
+ async def _draw_charger(
291
+ self, map_data: dict, array: NumpyArray, element_map: NumpyArray
292
+ ) -> Tuple[NumpyArray, NumpyArray]:
293
+ """Draw the charger."""
294
+ if not self.drawing_config.is_enabled(DrawableElement.CHARGER):
295
+ return array, element_map
296
+
297
+ # Get charger color from drawing config
298
+ charger_color = self.drawing_config.get_property(
299
+ DrawableElement.CHARGER, "color", (255, 128, 0, 255)
300
+ )
301
+
302
+ # Implementation depends on the map data format
303
+ # This would extract charger data from map_data and draw it
304
+
305
+ return array, element_map
306
+
307
+ async def _draw_virtual_walls(
308
+ self, map_data: dict, array: NumpyArray, element_map: NumpyArray
309
+ ) -> Tuple[NumpyArray, NumpyArray]:
310
+ """Draw virtual walls."""
311
+ if not self.drawing_config.is_enabled(DrawableElement.VIRTUAL_WALL):
312
+ return array, element_map
313
+
314
+ # Get virtual wall color from drawing config
315
+ wall_color = self.drawing_config.get_property(
316
+ DrawableElement.VIRTUAL_WALL, "color", (255, 0, 0, 255)
317
+ )
318
+
319
+ # Implementation depends on the map data format
320
+ # This would extract virtual wall data from map_data and draw it
321
+
322
+ return array, element_map
323
+
324
+ async def _draw_restricted_areas(
325
+ self, map_data: dict, array: NumpyArray, element_map: NumpyArray
326
+ ) -> Tuple[NumpyArray, NumpyArray]:
327
+ """Draw restricted areas."""
328
+ if not self.drawing_config.is_enabled(DrawableElement.RESTRICTED_AREA):
329
+ return array, element_map
330
+
331
+ # Get restricted area color from drawing config
332
+ area_color = self.drawing_config.get_property(
333
+ DrawableElement.RESTRICTED_AREA, "color", (255, 0, 0, 125)
334
+ )
335
+
336
+ # Implementation depends on the map data format
337
+ # This would extract restricted area data from map_data and draw it
338
+
339
+ return array, element_map
340
+
341
+ async def _draw_no_mop_areas(
342
+ self, map_data: dict, array: NumpyArray, element_map: NumpyArray
343
+ ) -> Tuple[NumpyArray, NumpyArray]:
344
+ """Draw no-mop areas."""
345
+ if not self.drawing_config.is_enabled(DrawableElement.NO_MOP_AREA):
346
+ return array, element_map
347
+
348
+ # Get no-mop area color from drawing config
349
+ area_color = self.drawing_config.get_property(
350
+ DrawableElement.NO_MOP_AREA, "color", (0, 0, 255, 125)
351
+ )
352
+
353
+ # Implementation depends on the map data format
354
+ # This would extract no-mop area data from map_data and draw it
355
+
356
+ return array, element_map
357
+
358
+ async def _draw_path(
359
+ self, map_data: dict, array: NumpyArray, element_map: NumpyArray
360
+ ) -> Tuple[NumpyArray, NumpyArray]:
361
+ """Draw the robot's path."""
362
+ if not self.drawing_config.is_enabled(DrawableElement.PATH):
363
+ return array, element_map
364
+
365
+ # Get path color from drawing config
366
+ path_color = self.drawing_config.get_property(
367
+ DrawableElement.PATH, "color", (238, 247, 255, 255)
368
+ )
369
+
370
+ # Implementation depends on the map data format
371
+ # This would extract path data from map_data and draw it
372
+
373
+ return array, element_map
374
+
375
+ async def _draw_predicted_path(
376
+ self, map_data: dict, array: NumpyArray, element_map: NumpyArray
377
+ ) -> Tuple[NumpyArray, NumpyArray]:
378
+ """Draw the predicted path."""
379
+ if not self.drawing_config.is_enabled(DrawableElement.PREDICTED_PATH):
380
+ return array, element_map
381
+
382
+ # Get predicted path color from drawing config
383
+ path_color = self.drawing_config.get_property(
384
+ DrawableElement.PREDICTED_PATH, "color", (238, 247, 255, 125)
385
+ )
386
+
387
+ # Implementation depends on the map data format
388
+ # This would extract predicted path data from map_data and draw it
389
+
390
+ return array, element_map
391
+
392
+ async def _draw_go_to_target(
393
+ self, map_data: dict, array: NumpyArray, element_map: NumpyArray
394
+ ) -> Tuple[NumpyArray, NumpyArray]:
395
+ """Draw the go-to target."""
396
+ if not self.drawing_config.is_enabled(DrawableElement.GO_TO_TARGET):
397
+ return array, element_map
398
+
399
+ # Get go-to target color from drawing config
400
+ target_color = self.drawing_config.get_property(
401
+ DrawableElement.GO_TO_TARGET, "color", (0, 255, 0, 255)
402
+ )
403
+
404
+ # Implementation depends on the map data format
405
+ # This would extract go-to target data from map_data and draw it
406
+
407
+ return array, element_map
408
+
409
+ async def _draw_room(
410
+ self, map_data: dict, room_id: int, array: NumpyArray, element_map: NumpyArray
411
+ ) -> Tuple[NumpyArray, NumpyArray]:
412
+ """Draw a specific room."""
413
+ element = getattr(DrawableElement, f"ROOM_{room_id}")
414
+ if not self.drawing_config.is_enabled(element):
415
+ return array, element_map
416
+
417
+ # Get room color from drawing config
418
+ room_color = self.drawing_config.get_property(
419
+ element,
420
+ "color",
421
+ (135, 206, 250, 255), # Default light blue
422
+ )
423
+
424
+ # Implementation depends on the map data format
425
+ # For Valetudo maps, we would look at the layers with type "segment"
426
+ # This is a simplified example - in a real implementation, we would extract the actual room pixels
427
+
428
+ # Find room data in map_data
429
+ room_pixels = []
430
+ for layer in map_data.get("layers", []):
431
+ if layer.get("type") == "segment" and str(
432
+ layer.get("metaData", {}).get("segmentId")
433
+ ) == str(room_id):
434
+ # Extract room pixels from the layer
435
+ # This is a placeholder - actual implementation would depend on the map data format
436
+ # For example, it might use compressed pixels or other data structures
437
+
438
+ # For demonstration, let's assume we have a list of (x, y) coordinates
439
+ room_pixels = layer.get("pixels", [])
440
+ break
441
+
442
+ # Draw room pixels with color blending
443
+ for x, y in room_pixels:
444
+ # Use blend_pixel to properly blend colors and update element map
445
+ self.blend_pixel(array, x, y, room_color, element, element_map)
446
+
447
+ return array, element_map
@@ -106,6 +106,7 @@ class CameraShared:
106
106
  self.trim_crop_data = None
107
107
  self.trims = TrimsData.from_dict(DEFAULT_VALUES["trims_data"]) # Trims data
108
108
  self.skip_room_ids: List[str] = []
109
+ self.device_info = None # Store the device_info
109
110
 
110
111
  def update_user_colors(self, user_colors):
111
112
  """Update the user colors."""
@@ -125,7 +126,8 @@ class CameraShared:
125
126
 
126
127
  def reset_trims(self) -> dict:
127
128
  """Reset the trims."""
128
- return self.trims.clear()
129
+ self.trims = TrimsData.from_dict(DEFAULT_VALUES["trims_data"])
130
+ return self.trims
129
131
 
130
132
  async def batch_update(self, **kwargs):
131
133
  """Batch update multiple attributes."""
@@ -186,6 +188,12 @@ class CameraSharedManager:
186
188
  instance = self.get_instance() # Retrieve the correct instance
187
189
 
188
190
  try:
191
+ # Store the device_info in the instance
192
+ instance.device_info = device_info
193
+ _LOGGER.info(
194
+ "%s: Stored device_info in shared instance", instance.file_name
195
+ )
196
+
189
197
  instance.attr_calibration_points = None
190
198
 
191
199
  # Initialize shared data with defaults from DEFAULT_VALUES
@@ -239,6 +247,26 @@ class CameraSharedManager:
239
247
  )
240
248
  instance.trims = TrimsData.from_dict(trim_data)
241
249
 
250
+ # Log disable_obstacles and disable_path settings
251
+ if "disable_obstacles" in device_info:
252
+ _LOGGER.info(
253
+ "%s: device_info contains disable_obstacles: %s",
254
+ instance.file_name,
255
+ device_info["disable_obstacles"],
256
+ )
257
+ if "disable_path" in device_info:
258
+ _LOGGER.info(
259
+ "%s: device_info contains disable_path: %s",
260
+ instance.file_name,
261
+ device_info["disable_path"],
262
+ )
263
+ if "disable_elements" in device_info:
264
+ _LOGGER.info(
265
+ "%s: device_info contains disable_elements: %s",
266
+ instance.file_name,
267
+ device_info["disable_elements"],
268
+ )
269
+
242
270
  except TypeError as ex:
243
271
  _LOGGER.error("Shared data can't be initialized due to a TypeError! %s", ex)
244
272
  except AttributeError as ex:
@@ -16,7 +16,8 @@ from PIL import Image
16
16
 
17
17
  DEFAULT_ROOMS = 1
18
18
 
19
- MY_LOGGER = logging.getLogger(__name__)
19
+ LOGGER = logging.getLogger(__package__)
20
+
20
21
 
21
22
  Color = Union[Tuple[int, int, int], Tuple[int, int, int, int]]
22
23
  Colors = Dict[str, Color]
@@ -613,20 +614,22 @@ class CameraModes:
613
614
  class TrimsData:
614
615
  """Dataclass to store and manage trims data."""
615
616
 
616
- trim_left: int = 0
617
+ floor: str = ""
617
618
  trim_up: int = 0
618
- trim_right: int = 0
619
+ trim_left: int = 0
619
620
  trim_down: int = 0
621
+ trim_right: int = 0
620
622
 
621
623
  @classmethod
622
624
  def from_json(cls, json_data: str):
623
625
  """Create a TrimsConfig instance from a JSON string."""
624
626
  data = json.loads(json_data)
625
627
  return cls(
626
- trim_left=data.get("trim_left", 0),
628
+ floor=data.get("floor", ""),
627
629
  trim_up=data.get("trim_up", 0),
628
- trim_right=data.get("trim_right", 0),
630
+ trim_left=data.get("trim_left", 0),
629
631
  trim_down=data.get("trim_down", 0),
632
+ trim_right=data.get("trim_right", 0),
630
633
  )
631
634
 
632
635
  def to_json(self) -> str:
@@ -644,8 +647,13 @@ class TrimsData:
644
647
 
645
648
  def clear(self) -> dict:
646
649
  """Clear all the trims."""
647
- self.trim_left = 0
650
+ self.floor = ""
648
651
  self.trim_up = 0
649
- self.trim_right = 0
652
+ self.trim_left = 0
650
653
  self.trim_down = 0
654
+ self.trim_right = 0
651
655
  return asdict(self)
656
+
657
+ def self_instance(self):
658
+ """Return self instance."""
659
+ return self.self_instance()