valetudo-map-parser 0.1.10rc5__tar.gz → 0.1.10rc7__tar.gz

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.
Files changed (36) hide show
  1. {valetudo_map_parser-0.1.10rc5 → valetudo_map_parser-0.1.10rc7}/PKG-INFO +2 -2
  2. {valetudo_map_parser-0.1.10rc5 → valetudo_map_parser-0.1.10rc7}/SCR/valetudo_map_parser/config/rand256_parser.py +129 -47
  3. {valetudo_map_parser-0.1.10rc5 → valetudo_map_parser-0.1.10rc7}/SCR/valetudo_map_parser/config/shared.py +83 -93
  4. {valetudo_map_parser-0.1.10rc5 → valetudo_map_parser-0.1.10rc7}/SCR/valetudo_map_parser/config/types.py +26 -1
  5. {valetudo_map_parser-0.1.10rc5 → valetudo_map_parser-0.1.10rc7}/SCR/valetudo_map_parser/config/utils.py +60 -10
  6. {valetudo_map_parser-0.1.10rc5 → valetudo_map_parser-0.1.10rc7}/SCR/valetudo_map_parser/hypfer_handler.py +2 -5
  7. {valetudo_map_parser-0.1.10rc5 → valetudo_map_parser-0.1.10rc7}/SCR/valetudo_map_parser/map_data.py +4 -3
  8. {valetudo_map_parser-0.1.10rc5 → valetudo_map_parser-0.1.10rc7}/SCR/valetudo_map_parser/rand256_handler.py +13 -17
  9. {valetudo_map_parser-0.1.10rc5 → valetudo_map_parser-0.1.10rc7}/SCR/valetudo_map_parser/reimg_draw.py +13 -18
  10. {valetudo_map_parser-0.1.10rc5 → valetudo_map_parser-0.1.10rc7}/pyproject.toml +2 -2
  11. valetudo_map_parser-0.1.10rc5/SCR/valetudo_map_parser/hypfer_rooms_handler.py +0 -599
  12. {valetudo_map_parser-0.1.10rc5 → valetudo_map_parser-0.1.10rc7}/LICENSE +0 -0
  13. {valetudo_map_parser-0.1.10rc5 → valetudo_map_parser-0.1.10rc7}/NOTICE.txt +0 -0
  14. {valetudo_map_parser-0.1.10rc5 → valetudo_map_parser-0.1.10rc7}/README.md +0 -0
  15. {valetudo_map_parser-0.1.10rc5 → valetudo_map_parser-0.1.10rc7}/SCR/valetudo_map_parser/__init__.py +0 -0
  16. {valetudo_map_parser-0.1.10rc5 → valetudo_map_parser-0.1.10rc7}/SCR/valetudo_map_parser/config/__init__.py +0 -0
  17. {valetudo_map_parser-0.1.10rc5 → valetudo_map_parser-0.1.10rc7}/SCR/valetudo_map_parser/config/async_utils.py +0 -0
  18. {valetudo_map_parser-0.1.10rc5 → valetudo_map_parser-0.1.10rc7}/SCR/valetudo_map_parser/config/auto_crop.py +0 -0
  19. {valetudo_map_parser-0.1.10rc5 → valetudo_map_parser-0.1.10rc7}/SCR/valetudo_map_parser/config/color_utils.py +0 -0
  20. {valetudo_map_parser-0.1.10rc5 → valetudo_map_parser-0.1.10rc7}/SCR/valetudo_map_parser/config/colors.py +0 -0
  21. {valetudo_map_parser-0.1.10rc5 → valetudo_map_parser-0.1.10rc7}/SCR/valetudo_map_parser/config/drawable.py +0 -0
  22. {valetudo_map_parser-0.1.10rc5 → valetudo_map_parser-0.1.10rc7}/SCR/valetudo_map_parser/config/drawable_elements.py +0 -0
  23. {valetudo_map_parser-0.1.10rc5 → valetudo_map_parser-0.1.10rc7}/SCR/valetudo_map_parser/config/enhanced_drawable.py +0 -0
  24. {valetudo_map_parser-0.1.10rc5 → valetudo_map_parser-0.1.10rc7}/SCR/valetudo_map_parser/config/fonts/FiraSans.ttf +0 -0
  25. {valetudo_map_parser-0.1.10rc5 → valetudo_map_parser-0.1.10rc7}/SCR/valetudo_map_parser/config/fonts/Inter-VF.ttf +0 -0
  26. {valetudo_map_parser-0.1.10rc5 → valetudo_map_parser-0.1.10rc7}/SCR/valetudo_map_parser/config/fonts/Lato-Regular.ttf +0 -0
  27. {valetudo_map_parser-0.1.10rc5 → valetudo_map_parser-0.1.10rc7}/SCR/valetudo_map_parser/config/fonts/MPLUSRegular.ttf +0 -0
  28. {valetudo_map_parser-0.1.10rc5 → valetudo_map_parser-0.1.10rc7}/SCR/valetudo_map_parser/config/fonts/NotoKufiArabic-VF.ttf +0 -0
  29. {valetudo_map_parser-0.1.10rc5 → valetudo_map_parser-0.1.10rc7}/SCR/valetudo_map_parser/config/fonts/NotoSansCJKhk-VF.ttf +0 -0
  30. {valetudo_map_parser-0.1.10rc5 → valetudo_map_parser-0.1.10rc7}/SCR/valetudo_map_parser/config/fonts/NotoSansKhojki.ttf +0 -0
  31. {valetudo_map_parser-0.1.10rc5 → valetudo_map_parser-0.1.10rc7}/SCR/valetudo_map_parser/config/optimized_element_map.py +0 -0
  32. {valetudo_map_parser-0.1.10rc5 → valetudo_map_parser-0.1.10rc7}/SCR/valetudo_map_parser/config/status_text/status_text.py +0 -0
  33. {valetudo_map_parser-0.1.10rc5 → valetudo_map_parser-0.1.10rc7}/SCR/valetudo_map_parser/config/status_text/translations.py +0 -0
  34. {valetudo_map_parser-0.1.10rc5 → valetudo_map_parser-0.1.10rc7}/SCR/valetudo_map_parser/hypfer_draw.py +0 -0
  35. {valetudo_map_parser-0.1.10rc5 → valetudo_map_parser-0.1.10rc7}/SCR/valetudo_map_parser/py.typed +0 -0
  36. {valetudo_map_parser-0.1.10rc5 → valetudo_map_parser-0.1.10rc7}/SCR/valetudo_map_parser/rooms_handler.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: valetudo-map-parser
3
- Version: 0.1.10rc5
3
+ Version: 0.1.10rc7
4
4
  Summary: A Python library to parse Valetudo map data returning a PIL Image object.
5
5
  License: Apache-2.0
6
6
  License-File: LICENSE
@@ -13,7 +13,7 @@ Classifier: Programming Language :: Python :: 3
13
13
  Classifier: Programming Language :: Python :: 3.13
14
14
  Classifier: Programming Language :: Python :: 3.14
15
15
  Requires-Dist: Pillow (>=10.3.0)
16
- Requires-Dist: mvcrender (>=0.0.2)
16
+ Requires-Dist: mvcrender (>=0.0.4)
17
17
  Requires-Dist: numpy (>=1.26.4)
18
18
  Requires-Dist: scipy (>=1.12.0)
19
19
  Project-URL: Bug Tracker, https://github.com/sca075/Python-package-valetudo-map-parser/issues
@@ -24,6 +24,14 @@ class RRMapParser:
24
24
  VIRTUAL_WALLS = 10
25
25
  CURRENTLY_CLEANED_BLOCKS = 11
26
26
  FORBIDDEN_MOP_ZONES = 12
27
+ OBSTACLES = 13
28
+ IGNORED_OBSTACLES = 14
29
+ OBSTACLES_WITH_PHOTO = 15
30
+ IGNORED_OBSTACLES_WITH_PHOTO = 16
31
+ CARPET_MAP = 17
32
+ MOP_PATH = 18
33
+ NO_CARPET_AREAS = 19
34
+ DIGEST = 1024
27
35
 
28
36
  class Tools:
29
37
  """Tools for coordinate transformations."""
@@ -33,6 +41,7 @@ class RRMapParser:
33
41
 
34
42
  def __init__(self):
35
43
  """Initialize the parser."""
44
+ self.is_valid = False
36
45
  self.map_data: Dict[str, Any] = {}
37
46
 
38
47
  # Xiaomi/Roborock style byte extraction methods
@@ -67,6 +76,61 @@ class RRMapParser:
67
76
  value = RRMapParser._get_int32(data, address)
68
77
  return value if value < 0x80000000 else value - 0x100000000
69
78
 
79
+ @staticmethod
80
+ def _parse_carpet_map(data: bytes) -> set[int]:
81
+ carpet_map = set()
82
+
83
+ for i, v in enumerate(data):
84
+ if v:
85
+ carpet_map.add(i)
86
+ return carpet_map
87
+
88
+ @staticmethod
89
+ def _parse_area(header: bytes, data: bytes) -> list:
90
+ area_pairs = RRMapParser._get_int16(header, 0x08)
91
+ areas = []
92
+ for area_start in range(0, area_pairs * 16, 16):
93
+ x0 = RRMapParser._get_int16(data, area_start + 0)
94
+ y0 = RRMapParser._get_int16(data, area_start + 2)
95
+ x1 = RRMapParser._get_int16(data, area_start + 4)
96
+ y1 = RRMapParser._get_int16(data, area_start + 6)
97
+ x2 = RRMapParser._get_int16(data, area_start + 8)
98
+ y2 = RRMapParser._get_int16(data, area_start + 10)
99
+ x3 = RRMapParser._get_int16(data, area_start + 12)
100
+ y3 = RRMapParser._get_int16(data, area_start + 14)
101
+ areas.append(
102
+ [
103
+ x0,
104
+ RRMapParser.Tools.DIMENSION_MM - y0,
105
+ x1,
106
+ RRMapParser.Tools.DIMENSION_MM - y1,
107
+ x2,
108
+ RRMapParser.Tools.DIMENSION_MM - y2,
109
+ x3,
110
+ RRMapParser.Tools.DIMENSION_MM - y3,
111
+ ]
112
+ )
113
+ return areas
114
+
115
+ @staticmethod
116
+ def _parse_zones(data: bytes, header: bytes) -> list:
117
+ zone_pairs = RRMapParser._get_int16(header, 0x08)
118
+ zones = []
119
+ for zone_start in range(0, zone_pairs * 8, 8):
120
+ x0 = RRMapParser._get_int16(data, zone_start + 0)
121
+ y0 = RRMapParser._get_int16(data, zone_start + 2)
122
+ x1 = RRMapParser._get_int16(data, zone_start + 4)
123
+ y1 = RRMapParser._get_int16(data, zone_start + 6)
124
+ zones.append(
125
+ [
126
+ x0,
127
+ RRMapParser.Tools.DIMENSION_MM - y0,
128
+ x1,
129
+ RRMapParser.Tools.DIMENSION_MM - y1,
130
+ ]
131
+ )
132
+ return zones
133
+
70
134
  @staticmethod
71
135
  def _parse_object_position(block_data_length: int, data: bytes) -> Dict[str, Any]:
72
136
  """Parse object position using Xiaomi method."""
@@ -82,6 +146,19 @@ class RRMapParser:
82
146
  angle = raw_angle
83
147
  return {"position": [x, y], "angle": angle}
84
148
 
149
+
150
+ @staticmethod
151
+ def _parse_walls(data: bytes, header: bytes) -> list:
152
+ wall_pairs = RRMapParser._get_int16(header, 0x08)
153
+ walls = []
154
+ for wall_start in range(0, wall_pairs * 8, 8):
155
+ x0 = RRMapParser._get_int16(data, wall_start + 0)
156
+ y0 = RRMapParser._get_int16(data, wall_start + 2)
157
+ x1 = RRMapParser._get_int16(data, wall_start + 4)
158
+ y1 = RRMapParser._get_int16(data, wall_start + 6)
159
+ walls.append([x0, RRMapParser.Tools.DIMENSION_MM - y0, x1, RRMapParser.Tools.DIMENSION_MM - y1])
160
+ return walls
161
+
85
162
  @staticmethod
86
163
  def _parse_path_block(buf: bytes, offset: int, length: int) -> Dict[str, Any]:
87
164
  """Parse path block using EXACT same method as working parser."""
@@ -127,59 +204,45 @@ class RRMapParser:
127
204
  return {}
128
205
 
129
206
  def parse_blocks(self, raw: bytes, pixels: bool = True) -> Dict[int, Any]:
130
- """Parse all blocks using Xiaomi method."""
131
207
  blocks = {}
132
208
  map_header_length = self._get_int16(raw, 0x02)
133
209
  block_start_position = map_header_length
134
-
135
210
  while block_start_position < len(raw):
136
211
  try:
137
- # Parse block header using Xiaomi method
138
212
  block_header_length = self._get_int16(raw, block_start_position + 0x02)
139
213
  header = self._get_bytes(raw, block_start_position, block_header_length)
140
214
  block_type = self._get_int16(header, 0x00)
141
215
  block_data_length = self._get_int32(header, 0x04)
142
216
  block_data_start = block_start_position + block_header_length
143
217
  data = self._get_bytes(raw, block_data_start, block_data_length)
144
-
145
- # Parse different block types
146
- if block_type == self.Types.ROBOT_POSITION.value:
147
- blocks[block_type] = self._parse_object_position(
148
- block_data_length, data
149
- )
150
- elif block_type == self.Types.CHARGER_LOCATION.value:
151
- blocks[block_type] = self._parse_object_position(
152
- block_data_length, data
153
- )
154
- elif block_type == self.Types.PATH.value:
155
- blocks[block_type] = self._parse_path_block(
156
- raw, block_start_position, block_data_length
157
- )
158
- elif block_type == self.Types.GOTO_PREDICTED_PATH.value:
159
- blocks[block_type] = self._parse_path_block(
160
- raw, block_start_position, block_data_length
161
- )
162
- elif block_type == self.Types.GOTO_TARGET.value:
163
- blocks[block_type] = {"position": self._parse_goto_target(data)}
164
- elif block_type == self.Types.IMAGE.value:
165
- # Get header length for Gen1/Gen3 detection
166
- header_length = self._get_int8(header, 2)
167
- blocks[block_type] = self._parse_image_block(
168
- raw,
169
- block_start_position,
170
- block_data_length,
171
- header_length,
172
- pixels,
173
- )
174
-
175
- # Move to next block using Xiaomi method
176
- block_start_position = (
177
- block_start_position + block_data_length + self._get_int8(header, 2)
178
- )
179
-
218
+ match block_type:
219
+ case self.Types.DIGEST.value:
220
+ self.is_valid = True
221
+ case self.Types.ROBOT_POSITION.value | self.Types.CHARGER_LOCATION.value:
222
+ blocks[block_type] = self._parse_object_position(block_data_length, data)
223
+ case self.Types.PATH.value | self.Types.GOTO_PREDICTED_PATH.value:
224
+ blocks[block_type] = self._parse_path_block(raw, block_start_position, block_data_length)
225
+ case self.Types.CURRENTLY_CLEANED_ZONES.value:
226
+ blocks[block_type] = {"zones": self._parse_zones(data, header)}
227
+ case self.Types.FORBIDDEN_ZONES.value:
228
+ blocks[block_type] = {"forbidden_zones": self._parse_area(header, data)}
229
+ case self.Types.FORBIDDEN_MOP_ZONES.value:
230
+ blocks[block_type] = {"forbidden_mop_zones": self._parse_area(header, data)}
231
+ case self.Types.GOTO_TARGET.value:
232
+ blocks[block_type] = {"position": self._parse_goto_target(data)}
233
+ case self.Types.VIRTUAL_WALLS.value:
234
+ blocks[block_type] = {"virtual_walls": self._parse_walls(data, header)}
235
+ case self.Types.CARPET_MAP.value:
236
+ data = RRMapParser._get_bytes(raw, block_data_start, block_data_length)
237
+ blocks[block_type] = {"carpet_map": self._parse_carpet_map(data)}
238
+ case self.Types.IMAGE.value:
239
+ header_length = self._get_int8(header, 2)
240
+ blocks[block_type] = self._parse_image_block(
241
+ raw, block_start_position, block_data_length, header_length, pixels)
242
+
243
+ block_start_position = block_start_position + block_data_length + self._get_int8(header, 2)
180
244
  except (struct.error, IndexError):
181
245
  break
182
-
183
246
  return blocks
184
247
 
185
248
  def _parse_image_block(
@@ -365,8 +428,32 @@ class RRMapParser:
365
428
  ]
366
429
 
367
430
  # Add missing fields to match expected JSON format
368
- parsed_map_data["forbidden_zones"] = []
369
- parsed_map_data["virtual_walls"] = []
431
+ parsed_map_data["currently_cleaned_zones"] = (
432
+ blocks[self.Types.CURRENTLY_CLEANED_ZONES.value]["zones"]
433
+ if self.Types.CURRENTLY_CLEANED_ZONES.value in blocks
434
+ else []
435
+ )
436
+ parsed_map_data["forbidden_zones"] = (
437
+ blocks[self.Types.FORBIDDEN_ZONES.value]["forbidden_zones"]
438
+ if self.Types.FORBIDDEN_ZONES.value in blocks
439
+ else []
440
+ )
441
+ parsed_map_data["forbidden_mop_zones"] = (
442
+ blocks[self.Types.FORBIDDEN_MOP_ZONES.value]["forbidden_mop_zones"]
443
+ if self.Types.FORBIDDEN_MOP_ZONES.value in blocks
444
+ else []
445
+ )
446
+ parsed_map_data["virtual_walls"] = (
447
+ blocks[self.Types.VIRTUAL_WALLS.value]["virtual_walls"]
448
+ if self.Types.VIRTUAL_WALLS.value in blocks
449
+ else []
450
+ )
451
+ parsed_map_data["carpet_areas"] = (
452
+ blocks[self.Types.CARPET_MAP.value]["carpet_map"]
453
+ if self.Types.CARPET_MAP.value in blocks
454
+ else []
455
+ )
456
+ parsed_map_data["is_valid"] = self.is_valid
370
457
 
371
458
  return parsed_map_data
372
459
 
@@ -388,8 +475,3 @@ class RRMapParser:
388
475
  except (struct.error, IndexError, ValueError):
389
476
  return None
390
477
  return self.map_data
391
-
392
- @staticmethod
393
- def get_int32(data: bytes, address: int) -> int:
394
- """Get a 32-bit integer from the data - kept for compatibility."""
395
- return struct.unpack_from("<i", data, address)[0]
@@ -1,7 +1,7 @@
1
1
  """
2
2
  Class Camera Shared.
3
3
  Keep the data between the modules.
4
- Version: v0.1.10
4
+ Version: v0.1.12
5
5
  """
6
6
 
7
7
  import asyncio
@@ -12,12 +12,13 @@ from PIL import Image
12
12
  from .types import (
13
13
  ATTR_CALIBRATION_POINTS,
14
14
  ATTR_CAMERA_MODE,
15
+ ATTR_CONTENT_TYPE,
15
16
  ATTR_MARGINS,
16
17
  ATTR_OBSTACLES,
17
18
  ATTR_POINTS,
18
19
  ATTR_ROOMS,
19
20
  ATTR_ROTATE,
20
- ATTR_SNAPSHOT,
21
+ ATTR_IMAGE_LAST_UPDATED,
21
22
  ATTR_VACUUM_BATTERY,
22
23
  ATTR_VACUUM_CHARGING,
23
24
  ATTR_VACUUM_JSON_ID,
@@ -54,71 +55,68 @@ class CameraShared:
54
55
  """
55
56
 
56
57
  def __init__(self, file_name):
57
- self.camera_mode: str = CameraModes.MAP_VIEW # Camera mode
58
- self.frame_number: int = 0 # camera Frame number
59
- self.destinations: list = [] # MQTT rand destinations
60
- self.rand256_active_zone: list = [] # Active zone for rand256
61
- self.rand256_zone_coordinates: list = [] # Active zone coordinates for rand256
62
- self.is_rand: bool = False # MQTT rand data
63
- self._new_mqtt_message = False # New MQTT message
64
- # Initialize last_image with default gray image (250x150 minimum)
65
- self.last_image = Image.new(
66
- "RGBA", (250, 150), (128, 128, 128, 255)
67
- ) # Gray default image
68
- self.new_image: PilPNG | None = None # New image received
69
- self.binary_image: bytes | None = None # Current image in binary format
70
- self.image_last_updated: float = 0.0 # Last image update time
71
- self.image_format = "image/pil" # Image format
72
- self.image_size = None # Image size
73
- self.robot_size = None # Robot size
74
- self.image_auto_zoom: bool = False # Auto zoom image
75
- self.image_zoom_lock_ratio: bool = True # Zoom lock ratio
76
- self.image_ref_height: int = 0 # Image reference height
77
- self.image_ref_width: int = 0 # Image reference width
78
- self.image_aspect_ratio: str = "None" # Change Image aspect ratio
79
- self.image_grab = True # Grab image from MQTT
80
- self.image_rotate: int = 0 # Rotate image
81
- self.drawing_limit: float = 0.0 # Drawing CPU limit
82
- self.current_room = None # Current room of rhe vacuum
83
- self.user_colors = Colors # User base colors
84
- self.rooms_colors = Colors # Rooms colors
85
- self.vacuum_battery = 0 # Vacuum battery state
86
- self.vacuum_connection = False # Vacuum connection state
87
- self.vacuum_state = None # Vacuum state
88
- self.charger_position = None # Vacuum Charger position
89
- self.show_vacuum_state = None # Show vacuum state on the map
58
+ self.camera_mode: str = CameraModes.MAP_VIEW
59
+ self.frame_number: int = 0
60
+ self.destinations: list = []
61
+ self.rand256_active_zone: list = []
62
+ self.rand256_zone_coordinates: list = []
63
+ self.is_rand: bool = False
64
+ self._new_mqtt_message = False
65
+ self.last_image = Image.new("RGBA", (250, 150), (128, 128, 128, 255))
66
+ self.new_image: PilPNG | None = None
67
+ self.binary_image: bytes | None = None
68
+ self.image_last_updated: float = 0.0
69
+ self.image_format = "image/pil"
70
+ self.image_size = None
71
+ self.robot_size = None
72
+ self.image_auto_zoom: bool = False
73
+ self.image_zoom_lock_ratio: bool = True
74
+ self.image_ref_height: int = 0
75
+ self.image_ref_width: int = 0
76
+ self.image_aspect_ratio: str = "None"
77
+ self.image_grab = True
78
+ self.image_rotate: int = 0
79
+ self.drawing_limit: float = 0.0
80
+ self.current_room = None
81
+ self.user_colors = Colors
82
+ self.rooms_colors = Colors
83
+ self.vacuum_battery = 0
84
+ self.vacuum_connection = False
85
+ self.vacuum_state = None
86
+ self.charger_position = None
87
+ self.show_vacuum_state = None
90
88
  self.vacuum_status_font: str = (
91
- "custom_components/mqtt_vacuum_camera/utils/fonts/FiraSans.ttf" # Font
89
+ "custom_components/mqtt_vacuum_camera/utils/fonts/FiraSans.ttf"
92
90
  )
93
- self.vacuum_status_size: int = 50 # Vacuum status size
94
- self.vacuum_status_position: bool = True # Vacuum status text image top
95
- self.snapshot_take = False # Take snapshot
96
- self.vacuum_error = None # Vacuum error
97
- self.vacuum_api = None # Vacuum API
98
- self.vacuum_ips = None # Vacuum IPs
99
- self.vac_json_id = None # Vacuum json id
100
- self.margins = "100" # Image margins
101
- self.obstacles_data = None # Obstacles data
102
- self.obstacles_pos = None # Obstacles position
103
- self.offset_top = 0 # Image offset top
104
- self.offset_down = 0 # Image offset down
105
- self.offset_left = 0 # Image offset left
106
- self.offset_right = 0 # Image offset right
107
- self.export_svg = False # Export SVG
108
- self.svg_path = None # SVG Export path
109
- self.enable_snapshots = False # Enable snapshots
110
- self.file_name = file_name # vacuum friendly name as File name
111
- self.attr_calibration_points = None # Calibration points of the image
112
- self.map_rooms = None # Rooms data from the vacuum
113
- self.map_pred_zones = None # Predefined zones data
114
- self.map_pred_points = None # Predefined points data
115
- self.map_new_path = None # New path data
116
- self.map_old_path = None # Old path data
117
- self.user_language = None # User language
91
+ self.vacuum_status_size: int = 50
92
+ self.vacuum_status_position: bool = True
93
+ self.snapshot_take = False
94
+ self.vacuum_error = None
95
+ self.vacuum_api = None
96
+ self.vacuum_ips = None
97
+ self.vac_json_id = None
98
+ self.margins = "100"
99
+ self.obstacles_data = None
100
+ self.obstacles_pos = None
101
+ self.offset_top = 0
102
+ self.offset_down = 0
103
+ self.offset_left = 0
104
+ self.offset_right = 0
105
+ self.export_svg = False
106
+ self.svg_path = None
107
+ self.enable_snapshots = False
108
+ self.file_name = file_name
109
+ self.attr_calibration_points = None
110
+ self.map_rooms = None
111
+ self.map_pred_zones = None
112
+ self.map_pred_points = None
113
+ self.map_new_path = None
114
+ self.map_old_path = None
115
+ self.user_language = None
118
116
  self.trim_crop_data = None
119
- self.trims = TrimsData.from_dict(DEFAULT_VALUES["trims_data"]) # Trims data
117
+ self.trims = TrimsData.from_dict(DEFAULT_VALUES["trims_data"])
120
118
  self.skip_room_ids: List[str] = []
121
- self.device_info = None # Store the device_info
119
+ self.device_info = None
122
120
 
123
121
  def vacuum_bat_charged(self) -> bool:
124
122
  """Check if the vacuum is charging."""
@@ -126,49 +124,35 @@ class CameraShared:
126
124
 
127
125
  @staticmethod
128
126
  def _compose_obstacle_links(vacuum_host_ip: str, obstacles: list) -> list | None:
129
- """
130
- Compose JSON with obstacle details including the image link.
131
- """
127
+ """Compose JSON with obstacle details including the image link."""
132
128
  obstacle_links = []
133
129
  if not obstacles or not vacuum_host_ip:
134
130
  return None
135
131
 
136
132
  for obstacle in obstacles:
137
- # Extract obstacle details
138
133
  label = obstacle.get("label", "")
139
134
  points = obstacle.get("points", {})
140
135
  image_id = obstacle.get("id", "None")
141
136
 
142
137
  if label and points and image_id and vacuum_host_ip:
143
- # Append formatted obstacle data
144
138
  if image_id != "None":
145
- # Compose the link
146
139
  image_link = (
147
140
  f"http://{vacuum_host_ip}"
148
141
  f"/api/v2/robot/capabilities/ObstacleImagesCapability/img/{image_id}"
149
142
  )
150
143
  obstacle_links.append(
151
- {
152
- "point": points,
153
- "label": label,
154
- "link": image_link,
155
- }
144
+ {"point": points, "label": label, "link": image_link}
156
145
  )
157
146
  else:
158
- obstacle_links.append(
159
- {
160
- "point": points,
161
- "label": label,
162
- }
163
- )
147
+ obstacle_links.append({"point": points, "label": label})
164
148
  return obstacle_links
165
149
 
166
150
  def update_user_colors(self, user_colors):
167
- """Update the user colors."""
151
+ """Update user colors palette"""
168
152
  self.user_colors = user_colors
169
153
 
170
154
  def get_user_colors(self):
171
- """Get the user colors."""
155
+ """Return user colors"""
172
156
  return self.user_colors
173
157
 
174
158
  def update_rooms_colors(self, user_colors):
@@ -176,7 +160,7 @@ class CameraShared:
176
160
  self.rooms_colors = user_colors
177
161
 
178
162
  def get_rooms_colors(self):
179
- """Get the rooms colors."""
163
+ """Return rooms colors"""
180
164
  return self.rooms_colors
181
165
 
182
166
  def reset_trims(self) -> dict:
@@ -185,7 +169,7 @@ class CameraShared:
185
169
  return self.trims
186
170
 
187
171
  async def batch_update(self, **kwargs):
188
- """Batch update multiple attributes."""
172
+ """Update the data of Shared in Batch"""
189
173
  for key, value in kwargs.items():
190
174
  setattr(self, key, value)
191
175
 
@@ -196,12 +180,14 @@ class CameraShared:
196
180
  def generate_attributes(self) -> dict:
197
181
  """Generate and return the shared attribute's dictionary."""
198
182
  attrs = {
183
+ ATTR_IMAGE_LAST_UPDATED: self.image_last_updated,
184
+ ATTR_CONTENT_TYPE: self.image_format,
185
+ ATTR_VACUUM_JSON_ID: self.vac_json_id,
199
186
  ATTR_CAMERA_MODE: self.camera_mode,
187
+ ATTR_VACUUM_STATUS: self.vacuum_state,
200
188
  ATTR_VACUUM_BATTERY: f"{self.vacuum_battery}%",
201
189
  ATTR_VACUUM_CHARGING: self.vacuum_bat_charged(),
202
190
  ATTR_VACUUM_POSITION: self.current_room,
203
- ATTR_VACUUM_STATUS: self.vacuum_state,
204
- ATTR_VACUUM_JSON_ID: self.vac_json_id,
205
191
  ATTR_CALIBRATION_POINTS: self.attr_calibration_points,
206
192
  }
207
193
  if self.obstacles_pos and self.vacuum_ips:
@@ -210,24 +196,28 @@ class CameraShared:
210
196
  )
211
197
  attrs[ATTR_OBSTACLES] = self.obstacles_data
212
198
 
213
- if self.enable_snapshots:
214
- attrs[ATTR_SNAPSHOT] = self.snapshot_take
215
- else:
216
- attrs[ATTR_SNAPSHOT] = False
217
-
218
- # Add dynamic shared attributes if they are available
219
199
  shared_attrs = {
220
200
  ATTR_ROOMS: self.map_rooms,
221
201
  ATTR_ZONES: self.map_pred_zones,
222
202
  ATTR_POINTS: self.map_pred_points,
223
203
  }
224
-
225
204
  for key, value in shared_attrs.items():
226
205
  if value is not None:
227
206
  attrs[key] = value
228
207
 
229
208
  return attrs
230
209
 
210
+ def to_dict(self) -> dict:
211
+ """Return a dictionary with image and attributes data."""
212
+ return {
213
+ "image": {
214
+ "binary": self.binary_image,
215
+ "pil_image": self.new_image,
216
+ "size": self.new_image.size if self.new_image else (10, 10),
217
+ },
218
+ "attributes": self.generate_attributes(),
219
+ }
220
+
231
221
 
232
222
  class CameraSharedManager:
233
223
  """Camera Shared Manager class."""
@@ -8,7 +8,7 @@ import json
8
8
  import logging
9
9
  import threading
10
10
  from dataclasses import asdict, dataclass
11
- from typing import Any, Dict, Optional, Tuple, TypedDict, Union
11
+ from typing import Any, Dict, Optional, Tuple, TypedDict, Union, List, NotRequired
12
12
 
13
13
  import numpy as np
14
14
  from PIL import Image
@@ -19,6 +19,29 @@ DEFAULT_ROOMS = 1
19
19
  LOGGER = logging.getLogger(__package__)
20
20
 
21
21
 
22
+ class Spot(TypedDict):
23
+ name: str
24
+ coordinates: List[int] # [x, y]
25
+
26
+
27
+ class Zone(TypedDict):
28
+ name: str
29
+ coordinates: List[List[int]] # [[x1, y1, x2, y2, repeats], ...]
30
+
31
+
32
+ class Room(TypedDict):
33
+ name: str
34
+ id: int
35
+
36
+
37
+ # list[dict[str, str | list[int]]] | list[dict[str, str | list[list[int]]]] | list[dict[str, str | int]] | int]'
38
+ class Destinations(TypedDict, total=False):
39
+ spots: NotRequired[Optional[List[Spot]]]
40
+ zones: NotRequired[Optional[List[Zone]]]
41
+ rooms: NotRequired[Optional[List[Room]]]
42
+ updated: NotRequired[Optional[float]]
43
+
44
+
22
45
  class RoomProperty(TypedDict):
23
46
  number: int
24
47
  outline: list[tuple[int, int]]
@@ -210,9 +233,11 @@ NumpyArray = np.ndarray
210
233
  Point = Tuple[int, int]
211
234
 
212
235
  CAMERA_STORAGE = "valetudo_camera"
236
+ ATTR_IMAGE_LAST_UPDATED = "image_last_updated"
213
237
  ATTR_ROTATE = "rotate_image"
214
238
  ATTR_CROP = "crop_image"
215
239
  ATTR_MARGINS = "margins"
240
+ ATTR_CONTENT_TYPE = "content_type"
216
241
  CONF_OFFSET_TOP = "offset_top"
217
242
  CONF_OFFSET_BOTTOM = "offset_bottom"
218
243
  CONF_OFFSET_LEFT = "offset_left"