valetudo-map-parser 0.1.9b65__py3-none-any.whl → 0.1.9b68__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.
@@ -39,6 +39,7 @@ from .types import (
39
39
  CameraModes,
40
40
  Colors,
41
41
  TrimsData,
42
+ PilPNG,
42
43
  )
43
44
 
44
45
 
@@ -58,9 +59,10 @@ class CameraShared:
58
59
  self.rand256_active_zone: list = [] # Active zone for rand256
59
60
  self.is_rand: bool = False # MQTT rand data
60
61
  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
62
+ self.last_image = PilPNG | None # Last image received
63
+ self.new_image: PilPNG | None = None # New image received
64
+ self.binary_image: bytes | None = None # Current image in binary format
65
+ self.image_last_updated: float = 0.0 # Last image update time
64
66
  self.image_format = "image/pil" # Image format
65
67
  self.image_size = None # Image size
66
68
  self.image_auto_zoom: bool = False # Auto zoom image
@@ -115,7 +117,7 @@ class CameraShared:
115
117
 
116
118
 
117
119
 
118
- def _state_charging(self) -> bool:
120
+ def vacuum_bat_charged(self) -> bool:
119
121
  """Check if the vacuum is charging."""
120
122
  return (self.vacuum_state == "docked") and (int(self.vacuum_battery) < 100)
121
123
 
@@ -193,7 +195,7 @@ class CameraShared:
193
195
  attrs = {
194
196
  ATTR_CAMERA_MODE: self.camera_mode,
195
197
  ATTR_VACUUM_BATTERY: f"{self.vacuum_battery}%",
196
- ATTR_VACUUM_CHARGING: self._state_charging(),
198
+ ATTR_VACUUM_CHARGING: self.vacuum_bat_charged,
197
199
  ATTR_VACUUM_POSITION: self.current_room,
198
200
  ATTR_VACUUM_STATUS: self.vacuum_state,
199
201
  ATTR_VACUUM_JSON_ID: self.vac_json_id,
@@ -228,12 +230,13 @@ class CameraShared:
228
230
  class CameraSharedManager:
229
231
  """Camera Shared Manager class."""
230
232
 
231
- def __init__(self, file_name, device_info):
233
+ def __init__(self, file_name: str, device_info: dict = None):
232
234
  self._instances = {}
233
235
  self._lock = asyncio.Lock()
234
236
  self.file_name = file_name
235
- self.device_info = device_info
236
- self.update_shared_data(device_info)
237
+ if device_info:
238
+ self.device_info = device_info
239
+ self.update_shared_data(device_info)
237
240
 
238
241
  # Automatically initialize shared data for the instance
239
242
  # 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
@@ -79,6 +80,67 @@ class BaseHandler:
79
80
  """Return the robot position."""
80
81
  return self.robot_pos
81
82
 
83
+ async def async_get_pil_image(
84
+ self,
85
+ m_json: dict | None,
86
+ destinations: list | None = None,
87
+ ) -> PilPNG | None:
88
+ """
89
+ Unified async function to get PIL image from JSON data for both Hypfer and Rand256 handlers.
90
+
91
+ This function:
92
+ 1. Calls the appropriate async_get_image_from_json method
93
+ 2. Stores the processed data in shared.new_image
94
+ 3. Backs up previous data to shared.last_image
95
+ 4. Updates shared.image_last_updated with current datetime
96
+
97
+ @param m_json: The JSON data to use to draw the image
98
+ @param destinations: MQTT destinations for labels (used by Rand256)
99
+ @return: PIL Image or None
100
+ """
101
+ try:
102
+ # Backup current image to last_image before processing new one
103
+ if hasattr(self.shared, 'new_image') and self.shared.new_image is not None:
104
+ self.shared.last_image = self.shared.new_image
105
+
106
+ # Call the appropriate handler method based on handler type
107
+ if hasattr(self, 'get_image_from_rrm'):
108
+ # This is a Rand256 handler
109
+ new_image = await self.get_image_from_rrm(
110
+ m_json=m_json,
111
+ destinations=destinations,
112
+ return_webp=False # Always return PIL Image
113
+ )
114
+ elif hasattr(self, 'async_get_image_from_json'):
115
+ # This is a Hypfer handler
116
+ new_image = await self.async_get_image_from_json(
117
+ m_json=m_json,
118
+ return_webp=False # Always return PIL Image
119
+ )
120
+ else:
121
+ LOGGER.warning("%s: Handler type not recognized for async_get_pil_image", self.file_name)
122
+ return None
123
+
124
+ # Store the new image in shared data
125
+ if new_image is not None:
126
+ self.shared.new_image = new_image
127
+ # Update the timestamp with current datetime
128
+ self.shared.image_last_updated = datetime.datetime.now().timestamp()
129
+ LOGGER.debug("%s: Image processed and stored in shared data", self.file_name)
130
+ else:
131
+ LOGGER.warning("%s: Failed to generate image from JSON data", self.file_name)
132
+
133
+ return new_image
134
+
135
+ except Exception as e:
136
+ LOGGER.error(
137
+ "%s: Error in async_get_pil_image: %s",
138
+ self.file_name,
139
+ str(e),
140
+ exc_info=True
141
+ )
142
+ return None
143
+
82
144
  def get_charger_position(self) -> ChargerPosition | None:
83
145
  """Return the charger position."""
84
146
  return self.charger_pos
@@ -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(
@@ -313,10 +306,6 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
313
306
  LOGGER.info("%s: Drawing path", self.file_name)
314
307
  data_tasks.append(self._prepare_path_data(m_json))
315
308
 
316
- # Execute data preparation in parallel if we have tasks
317
- if data_tasks:
318
- prepared_data = await AsyncParallel.parallel_data_preparation(*data_tasks)
319
-
320
309
  # Process drawing operations sequentially (since they modify the same array)
321
310
  # Draw zones if enabled
322
311
  if self.drawing_config.is_enabled(DrawableElement.RESTRICTED_AREA):
@@ -500,7 +489,8 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
500
489
  except (ValueError, KeyError):
501
490
  return None
502
491
 
503
- async def _prepare_goto_data(self, entity_dict):
492
+ @staticmethod
493
+ async def _prepare_goto_data(entity_dict):
504
494
  """Prepare go-to flag data for parallel processing."""
505
495
  await asyncio.sleep(0) # Yield control
506
496
  # Extract go-to target data from entity_dict
@@ -510,6 +500,6 @@ class HypferMapImageHandler(BaseHandler, AutoCrop):
510
500
  """Prepare path data for parallel processing."""
511
501
  await asyncio.sleep(0) # Yield control
512
502
  try:
513
- return self.data.find_path_entities(m_json)
503
+ return self.data.find_paths_entities(m_json)
514
504
  except (ValueError, KeyError):
515
505
  return None
@@ -12,8 +12,8 @@ import uuid
12
12
  from typing import Any
13
13
 
14
14
  import numpy as np
15
- from PIL import Image
16
15
 
16
+ from .config.async_utils import AsyncNumPy, AsyncPIL
17
17
  from .config.auto_crop import AutoCrop
18
18
  from .config.drawable_elements import DrawableElement
19
19
  from .config.types import (
@@ -145,7 +145,7 @@ class ReImageHandler(BaseHandler, AutoCrop):
145
145
  m_json: JsonType, # json data
146
146
  destinations: None = None, # MQTT destinations for labels
147
147
  return_webp: bool = False,
148
- ) -> WebPBytes | Image.Image | None:
148
+ ) -> WebPBytes | PilPNG | None:
149
149
  """Generate Images from the json data.
150
150
  @param m_json: The JSON data to use to draw the image.
151
151
  @param destinations: MQTT destinations for labels (unused).
@@ -195,8 +195,8 @@ class ReImageHandler(BaseHandler, AutoCrop):
195
195
  del img_np_array # free memory
196
196
  return webp_bytes
197
197
  else:
198
- # Convert to PIL Image (original behavior)
199
- pil_img = Image.fromarray(img_np_array, mode="RGBA")
198
+ # Convert to PIL Image using async utilities
199
+ pil_img = await AsyncPIL.async_fromarray(img_np_array, mode="RGBA")
200
200
  del img_np_array # free memory
201
201
  return await self._finalize_image(pil_img)
202
202
 
@@ -297,11 +297,6 @@ class ReImageHandler(BaseHandler, AutoCrop):
297
297
  original_rooms_pos = self.rooms_pos
298
298
  self.rooms_pos = temp_rooms_pos
299
299
 
300
- # Perform robot room detection to check active zones
301
- robot_room_result = await self.async_get_robot_in_room(
302
- robot_position[0], robot_position[1], robot_position_angle
303
- )
304
-
305
300
  # Restore original rooms_pos
306
301
  self.rooms_pos = original_rooms_pos
307
302
 
@@ -673,3 +668,9 @@ class ReImageHandler(BaseHandler, AutoCrop):
673
668
  property_name=property_name,
674
669
  value=value,
675
670
  )
671
+
672
+ async def async_copy_array(self, original_array):
673
+ """Copy the array using async utilities."""
674
+ return await AsyncNumPy.async_copy(original_array)
675
+
676
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: valetudo-map-parser
3
- Version: 0.1.9b65
3
+ Version: 0.1.9b68
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
@@ -9,20 +9,19 @@ valetudo_map_parser/config/drawable_elements.py,sha256=o-5oiXmfqPwNQLzKIhkEcZD_A
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/rand25_parser.py,sha256=kIayyqVZBfQfAMkiArzqrrj9vqZB3pkgT0Y5ufrQmGA,16448
13
- valetudo_map_parser/config/shared.py,sha256=J_66BuhgqRJUjXKBwKh8qih1iyiTq9ZiaZwduIROYmE,12560
12
+ valetudo_map_parser/config/shared.py,sha256=AQ73878TGCxbHQhgrAxSROLqFE-Zz4fJhTdoi9gBvJo,12736
14
13
  valetudo_map_parser/config/types.py,sha256=saL7pULKAdTRQ_ShR2arT8IV472e9MBC_SohTthlGp8,17567
15
- valetudo_map_parser/config/utils.py,sha256=fnzzNywE0Z1qw968hUa_yGslq3YhOP7daFj21miuhjI,31354
14
+ valetudo_map_parser/config/utils.py,sha256=MR1UIOwHWYJ8lFrPYhfyi9IV7C08S-DqCyIPppsB2tM,33913
16
15
  valetudo_map_parser/hypfer_draw.py,sha256=ZK_WybvukHd8nNk2mq5icrOu1Ue3SVCIC6_Hc9bTg0Q,29396
17
- valetudo_map_parser/hypfer_handler.py,sha256=5RphiGaLFmlaajcmR0RKfkTevEpjXVylD1fn12rmzw4,23733
16
+ valetudo_map_parser/hypfer_handler.py,sha256=f9SCphArA-LO2ySrTKpxn6k4htM-JRrbrxKFh3AjnD8,23171
18
17
  valetudo_map_parser/hypfer_rooms_handler.py,sha256=NkpOA6Gdq-2D3lLAxvtNuuWMvPXHxeMY2TO5RZLSHlU,22652
19
18
  valetudo_map_parser/map_data.py,sha256=Op0LTCakcTJ1Q0rxQhl6BpgSby_6nJenCQS2Y2FHtRk,17243
20
19
  valetudo_map_parser/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
- valetudo_map_parser/rand256_handler.py,sha256=8X26-ApkwzfsbuXcdvt-sHWC0ibzjp3VbK0ZRCT77QE,27697
20
+ valetudo_map_parser/rand256_handler.py,sha256=6Qt-xFJ3PITQNSAJTaEo6jl6TdgFms7ay1rnk4vekgk,27653
22
21
  valetudo_map_parser/reimg_draw.py,sha256=1q8LkNTPHEA9Tsapc_JnVw51kpPYNhaBU-KmHkefCQY,12507
23
22
  valetudo_map_parser/rooms_handler.py,sha256=ovqQtAjauAqwUNPR0aX27P2zhheQmqfaFhDE3_AwYWk,17821
24
- valetudo_map_parser-0.1.9b65.dist-info/LICENSE,sha256=Lh-qBbuRV0-jiCIBhfV7NgdwFxQFOXH3BKOzK865hRs,10480
25
- valetudo_map_parser-0.1.9b65.dist-info/METADATA,sha256=oTnaDNVUpi4QHTCyIkIqSrAY3ATiPdwLjr0U2c-juVc,3353
26
- valetudo_map_parser-0.1.9b65.dist-info/NOTICE.txt,sha256=5lTOuWiU9aiEnJ2go8sc7lTJ7ntMBx0g0GFnNrswCY4,2533
27
- valetudo_map_parser-0.1.9b65.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
28
- valetudo_map_parser-0.1.9b65.dist-info/RECORD,,
23
+ valetudo_map_parser-0.1.9b68.dist-info/LICENSE,sha256=Lh-qBbuRV0-jiCIBhfV7NgdwFxQFOXH3BKOzK865hRs,10480
24
+ valetudo_map_parser-0.1.9b68.dist-info/METADATA,sha256=hBV8Inc5QpIw-TcGWC-sLqhRb60TlFq1OVkMmm1iBek,3353
25
+ valetudo_map_parser-0.1.9b68.dist-info/NOTICE.txt,sha256=5lTOuWiU9aiEnJ2go8sc7lTJ7ntMBx0g0GFnNrswCY4,2533
26
+ valetudo_map_parser-0.1.9b68.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
27
+ valetudo_map_parser-0.1.9b68.dist-info/RECORD,,
@@ -1,412 +0,0 @@
1
- """
2
- Version: v2024.08.2
3
- - This parser is the python version of @rand256 valetudo_mapper.
4
- - This class is extracting the vacuum binary map_data.
5
- - Additional functions are to get in our image_handler the images datas.
6
- """
7
-
8
- import math
9
- import struct
10
- from enum import Enum
11
- from typing import Any, Callable, Dict, List, Optional, TypeVar
12
-
13
-
14
- _CallableT = TypeVar("_CallableT", bound=Callable[..., Any])
15
-
16
-
17
- def callback(func: _CallableT) -> _CallableT:
18
- """Annotation to mark method as safe to call from within the event loop."""
19
- setattr(func, "_hass_callback", True) # Attach a custom attribute to the function
20
- return func # Return the function without modifying its behavior
21
-
22
-
23
- # noinspection PyTypeChecker
24
- class RRMapParser:
25
- """Parse the map data from the Rand256 vacuum."""
26
-
27
- def __init__(self):
28
- self.map_data = None
29
-
30
- class Tools:
31
- """Tools for the RRMapParser."""
32
-
33
- DIMENSION_PIXELS = 1024
34
- DIMENSION_MM = 50 * 1024
35
-
36
- class Types(Enum):
37
- """Types of blocks in the RRMapParser."""
38
-
39
- CHARGER_LOCATION = 1
40
- IMAGE = 2
41
- PATH = 3
42
- GOTO_PATH = 4
43
- GOTO_PREDICTED_PATH = 5
44
- CURRENTLY_CLEANED_ZONES = 6
45
- GOTO_TARGET = 7
46
- ROBOT_POSITION = 8
47
- FORBIDDEN_ZONES = 9
48
- VIRTUAL_WALLS = 10
49
- CURRENTLY_CLEANED_BLOCKS = 11
50
- FORBIDDEN_MOP_ZONES = 12
51
- DIGEST = 1024
52
-
53
- @staticmethod
54
- def parse_block(
55
- buf: bytes,
56
- offset: int,
57
- result: Optional[Dict[int, Any]] = None,
58
- pixels: bool = False,
59
- ) -> Dict[int, Any]:
60
- """Parse a block of data from the map data."""
61
- result = result or {}
62
- if len(buf) <= offset:
63
- return result
64
-
65
- type_ = struct.unpack("<H", buf[offset : offset + 2])[0]
66
- hlength = struct.unpack("<H", buf[offset + 2 : offset + 4])[0]
67
- length = struct.unpack("<I", buf[offset + 4 : offset + 8])[0]
68
-
69
- if type_ in (
70
- RRMapParser.Types.ROBOT_POSITION.value,
71
- RRMapParser.Types.CHARGER_LOCATION.value,
72
- ):
73
- result[type_] = {
74
- "position": [
75
- int.from_bytes(buf[offset + 8 : offset + 10], byteorder="little"),
76
- int.from_bytes(buf[offset + 12 : offset + 14], byteorder="little"),
77
- ],
78
- "angle": (
79
- struct.unpack("<i", buf[offset + 16 : offset + 20])[0]
80
- if length >= 12
81
- else 0
82
- ),
83
- }
84
- elif type_ == RRMapParser.Types.IMAGE.value:
85
- RRMapParser._parse_image_block(buf, offset, length, hlength, result, pixels)
86
- elif type_ in (
87
- RRMapParser.Types.PATH.value,
88
- RRMapParser.Types.GOTO_PATH.value,
89
- RRMapParser.Types.GOTO_PREDICTED_PATH.value,
90
- ):
91
- result[type_] = RRMapParser._parse_path_block(buf, offset, length)
92
- elif type_ == RRMapParser.Types.GOTO_TARGET.value:
93
- result[type_] = {
94
- "position": [
95
- struct.unpack("<H", buf[offset + 8 : offset + 10])[0],
96
- struct.unpack("<H", buf[offset + 10 : offset + 12])[0],
97
- ]
98
- }
99
- elif type_ == RRMapParser.Types.CURRENTLY_CLEANED_ZONES.value:
100
- result[type_] = RRMapParser._parse_cleaned_zones(buf, offset, length)
101
- elif type_ in (
102
- RRMapParser.Types.FORBIDDEN_ZONES.value,
103
- RRMapParser.Types.FORBIDDEN_MOP_ZONES.value,
104
- RRMapParser.Types.VIRTUAL_WALLS.value,
105
- ):
106
- result[type_] = RRMapParser._parse_forbidden_zones(buf, offset, length)
107
- return RRMapParser.parse_block(buf, offset + length + hlength, result, pixels)
108
-
109
- @staticmethod
110
- def _parse_image_block(
111
- buf: bytes,
112
- offset: int,
113
- length: int,
114
- hlength: int,
115
- result: Dict[int, Any],
116
- pixels: bool,
117
- ) -> None:
118
- """Parse the image block of the map data."""
119
- g3offset = 4 if hlength > 24 else 0
120
- parameters = {
121
- "segments": {
122
- "count": (
123
- struct.unpack("<i", buf[offset + 8 : offset + 12])[0]
124
- if g3offset
125
- else 0
126
- ),
127
- "id": [],
128
- },
129
- "position": {
130
- "top": struct.unpack(
131
- "<i", buf[offset + 8 + g3offset : offset + 12 + g3offset]
132
- )[0],
133
- "left": struct.unpack(
134
- "<i", buf[offset + 12 + g3offset : offset + 16 + g3offset]
135
- )[0],
136
- },
137
- "dimensions": {
138
- "height": struct.unpack(
139
- "<i", buf[offset + 16 + g3offset : offset + 20 + g3offset]
140
- )[0],
141
- "width": struct.unpack(
142
- "<i", buf[offset + 20 + g3offset : offset + 24 + g3offset]
143
- )[0],
144
- },
145
- "pixels": {"floor": [], "walls": [], "segments": {}},
146
- }
147
- parameters["position"]["top"] = (
148
- RRMapParser.Tools.DIMENSION_PIXELS
149
- - parameters["position"]["top"]
150
- - parameters["dimensions"]["height"]
151
- )
152
- if (
153
- parameters["dimensions"]["height"] > 0
154
- and parameters["dimensions"]["width"] > 0
155
- ):
156
- for i in range(length):
157
- segment_type = (
158
- struct.unpack(
159
- "<B",
160
- buf[offset + 24 + g3offset + i : offset + 25 + g3offset + i],
161
- )[0]
162
- & 0x07
163
- )
164
- if segment_type == 0:
165
- continue
166
- if segment_type == 1 and pixels:
167
- parameters["pixels"]["walls"].append(i)
168
- else:
169
- s = (
170
- struct.unpack(
171
- "<B",
172
- buf[
173
- offset + 24 + g3offset + i : offset + 25 + g3offset + i
174
- ],
175
- )[0]
176
- >> 3
177
- )
178
- if s == 0 and pixels:
179
- parameters["pixels"]["floor"].append(i)
180
- elif s != 0:
181
- if s not in parameters["segments"]["id"]:
182
- parameters["segments"]["id"].append(s)
183
- parameters["segments"]["pixels_seg_" + str(s)] = []
184
- if pixels:
185
- parameters["segments"]["pixels_seg_" + str(s)].append(i)
186
- result[RRMapParser.Types.IMAGE.value] = parameters
187
-
188
- @staticmethod
189
- def _parse_path_block(buf: bytes, offset: int, length: int) -> Dict[str, Any]:
190
- """Parse a path block of the map data."""
191
- points = [
192
- [
193
- struct.unpack("<H", buf[offset + 20 + i : offset + 22 + i])[0],
194
- struct.unpack("<H", buf[offset + 22 + i : offset + 24 + i])[0],
195
- ]
196
- for i in range(0, length, 4)
197
- ]
198
- return {
199
- "current_angle": struct.unpack("<I", buf[offset + 16 : offset + 20])[0],
200
- "points": points,
201
- }
202
-
203
- @staticmethod
204
- def _parse_cleaned_zones(buf: bytes, offset: int, length: int) -> List[List[int]]:
205
- """Parse the cleaned zones block of the map data."""
206
- zone_count = struct.unpack("<I", buf[offset + 8 : offset + 12])[0]
207
- return (
208
- [
209
- [
210
- struct.unpack("<H", buf[offset + 12 + i : offset + 14 + i])[0],
211
- struct.unpack("<H", buf[offset + 14 + i : offset + 16 + i])[0],
212
- struct.unpack("<H", buf[offset + 16 + i : offset + 18 + i])[0],
213
- struct.unpack("<H", buf[offset + 18 + i : offset + 20 + i])[0],
214
- ]
215
- for i in range(0, length, 8)
216
- ]
217
- if zone_count > 0
218
- else []
219
- )
220
-
221
- @staticmethod
222
- def _parse_forbidden_zones(buf: bytes, offset: int, length: int) -> List[List[int]]:
223
- """Parse the forbidden zones block of the map data."""
224
- zone_count = struct.unpack("<I", buf[offset + 8 : offset + 12])[0]
225
- return (
226
- [
227
- [
228
- struct.unpack("<H", buf[offset + 12 + i : offset + 14 + i])[0],
229
- struct.unpack("<H", buf[offset + 14 + i : offset + 16 + i])[0],
230
- struct.unpack("<H", buf[offset + 16 + i : offset + 18 + i])[0],
231
- struct.unpack("<H", buf[offset + 18 + i : offset + 20 + i])[0],
232
- struct.unpack("<H", buf[offset + 20 + i : offset + 22 + i])[0],
233
- struct.unpack("<H", buf[offset + 22 + i : offset + 24 + i])[0],
234
- struct.unpack("<H", buf[offset + 24 + i : offset + 26 + i])[0],
235
- struct.unpack("<H", buf[offset + 26 + i : offset + 28 + i])[0],
236
- ]
237
- for i in range(0, length, 16)
238
- ]
239
- if zone_count > 0
240
- else []
241
- )
242
-
243
- @callback
244
- def parse(self, map_buf: bytes) -> Dict[str, Any]:
245
- """Parse the map data."""
246
- if map_buf[0:2] == b"rr":
247
- return {
248
- "header_length": struct.unpack("<H", map_buf[2:4])[0],
249
- "data_length": struct.unpack("<H", map_buf[4:6])[0],
250
- "version": {
251
- "major": struct.unpack("<H", map_buf[8:10])[0],
252
- "minor": struct.unpack("<H", map_buf[10:12])[0],
253
- },
254
- "map_index": struct.unpack("<H", map_buf[12:14])[0],
255
- "map_sequence": struct.unpack("<H", map_buf[16:18])[0],
256
- }
257
- return {}
258
-
259
- @callback
260
- def parse_rrm_data(
261
- self, map_buf: bytes, pixels: bool = False
262
- ) -> Optional[Dict[str, Any]]:
263
- """Parse the complete map data."""
264
- if not self.parse(map_buf).get("map_index"):
265
- return None
266
-
267
- parsed_map_data = {}
268
- blocks = self.parse_block(map_buf, 0x14, None, pixels)
269
-
270
- self._parse_image_data(parsed_map_data, blocks)
271
- self._parse_charger_data(parsed_map_data, blocks)
272
- self._parse_robot_data(parsed_map_data, blocks)
273
- self._parse_zones_data(parsed_map_data, blocks)
274
- self._parse_virtual_walls_data(parsed_map_data, blocks)
275
- self._parse_misc_data(parsed_map_data, blocks)
276
-
277
- return parsed_map_data
278
-
279
- @staticmethod
280
- def _parse_image_data(parsed_map_data: Dict[str, Any], blocks: Dict[int, Any]):
281
- """Parse image-related data."""
282
- if RRMapParser.Types.IMAGE.value in blocks:
283
- parsed_map_data["image"] = blocks[RRMapParser.Types.IMAGE.value]
284
- for item in [
285
- {"type": RRMapParser.Types.PATH.value, "path": "path"},
286
- {
287
- "type": RRMapParser.Types.GOTO_PREDICTED_PATH.value,
288
- "path": "goto_predicted_path",
289
- },
290
- ]:
291
- if item["type"] in blocks:
292
- parsed_map_data[item["path"]] = blocks[item["type"]]
293
- parsed_map_data[item["path"]]["points"] = [
294
- [point[0], RRMapParser.Tools.DIMENSION_MM - point[1]]
295
- for point in parsed_map_data[item["path"]]["points"]
296
- ]
297
- if len(parsed_map_data[item["path"]]["points"]) >= 2:
298
- parsed_map_data[item["path"]]["current_angle"] = math.degrees(
299
- math.atan2(
300
- parsed_map_data[item["path"]]["points"][-1][1]
301
- - parsed_map_data[item["path"]]["points"][-2][1],
302
- parsed_map_data[item["path"]]["points"][-1][0]
303
- - parsed_map_data[item["path"]]["points"][-2][0],
304
- )
305
- )
306
-
307
- @staticmethod
308
- def _parse_charger_data(parsed_map_data: Dict[str, Any], blocks: Dict[int, Any]):
309
- """Parse charger location data."""
310
- if RRMapParser.Types.CHARGER_LOCATION.value in blocks:
311
- charger = blocks[RRMapParser.Types.CHARGER_LOCATION.value]["position"]
312
- parsed_map_data["charger"] = charger
313
-
314
- @staticmethod
315
- def _parse_robot_data(parsed_map_data: Dict[str, Any], blocks: Dict[int, Any]):
316
- """Parse robot position data."""
317
- if RRMapParser.Types.ROBOT_POSITION.value in blocks:
318
- robot = blocks[RRMapParser.Types.ROBOT_POSITION.value]["position"]
319
- rob_angle = blocks[RRMapParser.Types.ROBOT_POSITION.value]["angle"]
320
- parsed_map_data["robot"] = robot
321
- parsed_map_data["robot_angle"] = rob_angle
322
-
323
- @staticmethod
324
- def _parse_zones_data(parsed_map_data: Dict[str, Any], blocks: Dict[int, Any]):
325
- """Parse zones and forbidden zones data."""
326
- if RRMapParser.Types.CURRENTLY_CLEANED_ZONES.value in blocks:
327
- parsed_map_data["currently_cleaned_zones"] = [
328
- [
329
- zone[0],
330
- RRMapParser.Tools.DIMENSION_MM - zone[1],
331
- zone[2],
332
- RRMapParser.Tools.DIMENSION_MM - zone[3],
333
- ]
334
- for zone in blocks[RRMapParser.Types.CURRENTLY_CLEANED_ZONES.value]
335
- ]
336
-
337
- if RRMapParser.Types.FORBIDDEN_ZONES.value in blocks:
338
- parsed_map_data["forbidden_zones"] = [
339
- [
340
- zone[0],
341
- RRMapParser.Tools.DIMENSION_MM - zone[1],
342
- zone[2],
343
- RRMapParser.Tools.DIMENSION_MM - zone[3],
344
- zone[4],
345
- RRMapParser.Tools.DIMENSION_MM - zone[5],
346
- zone[6],
347
- RRMapParser.Tools.DIMENSION_MM - zone[7],
348
- ]
349
- for zone in blocks[RRMapParser.Types.FORBIDDEN_ZONES.value]
350
- ]
351
-
352
- @staticmethod
353
- def _parse_virtual_walls_data(
354
- parsed_map_data: Dict[str, Any], blocks: Dict[int, Any]
355
- ):
356
- """Parse virtual walls data."""
357
- if RRMapParser.Types.VIRTUAL_WALLS.value in blocks:
358
- parsed_map_data["virtual_walls"] = [
359
- [
360
- wall[0],
361
- RRMapParser.Tools.DIMENSION_MM - wall[1],
362
- wall[2],
363
- RRMapParser.Tools.DIMENSION_MM - wall[3],
364
- ]
365
- for wall in blocks[RRMapParser.Types.VIRTUAL_WALLS.value]
366
- ]
367
-
368
- @staticmethod
369
- def _parse_misc_data(parsed_map_data: Dict[str, Any], blocks: Dict[int, Any]):
370
- """Parse miscellaneous data like cleaned blocks and mop zones."""
371
- if RRMapParser.Types.CURRENTLY_CLEANED_BLOCKS.value in blocks:
372
- parsed_map_data["currently_cleaned_blocks"] = blocks[
373
- RRMapParser.Types.CURRENTLY_CLEANED_BLOCKS.value
374
- ]
375
-
376
- if RRMapParser.Types.FORBIDDEN_MOP_ZONES.value in blocks:
377
- parsed_map_data["forbidden_mop_zones"] = [
378
- [
379
- zone[0],
380
- RRMapParser.Tools.DIMENSION_MM - zone[1],
381
- zone[2],
382
- RRMapParser.Tools.DIMENSION_MM - zone[3],
383
- zone[4],
384
- RRMapParser.Tools.DIMENSION_MM - zone[5],
385
- zone[6],
386
- RRMapParser.Tools.DIMENSION_MM - zone[7],
387
- ]
388
- for zone in blocks[RRMapParser.Types.FORBIDDEN_MOP_ZONES.value]
389
- ]
390
-
391
- if RRMapParser.Types.GOTO_TARGET.value in blocks:
392
- parsed_map_data["goto_target"] = blocks[
393
- RRMapParser.Types.GOTO_TARGET.value
394
- ]["position"]
395
-
396
- def parse_data(
397
- self, payload: Optional[bytes] = None, pixels: bool = False
398
- ) -> Optional[Dict[str, Any]]:
399
- """Get the map data from MQTT and return the json."""
400
- if payload:
401
- self.map_data = self.parse(payload)
402
- self.map_data.update(self.parse_rrm_data(payload, pixels) or {})
403
- return self.map_data
404
-
405
- def get_image(self) -> Dict[str, Any]:
406
- """Get the image data from the map data."""
407
- return self.map_data.get("image", {})
408
-
409
- @staticmethod
410
- def get_int32(data: bytes, address: int) -> int:
411
- """Get a 32-bit integer from the data."""
412
- return struct.unpack_from("<i", data, address)[0]