valetudo-map-parser 0.1.9b100__py3-none-any.whl → 0.1.10__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.
- valetudo_map_parser/__init__.py +24 -8
- valetudo_map_parser/config/auto_crop.py +2 -27
- valetudo_map_parser/config/color_utils.py +3 -4
- valetudo_map_parser/config/colors.py +2 -2
- valetudo_map_parser/config/drawable.py +102 -153
- valetudo_map_parser/config/drawable_elements.py +0 -2
- valetudo_map_parser/config/fonts/FiraSans.ttf +0 -0
- valetudo_map_parser/config/fonts/Inter-VF.ttf +0 -0
- valetudo_map_parser/config/fonts/Lato-Regular.ttf +0 -0
- valetudo_map_parser/config/fonts/MPLUSRegular.ttf +0 -0
- valetudo_map_parser/config/fonts/NotoKufiArabic-VF.ttf +0 -0
- valetudo_map_parser/config/fonts/NotoSansCJKhk-VF.ttf +0 -0
- valetudo_map_parser/config/fonts/NotoSansKhojki.ttf +0 -0
- valetudo_map_parser/config/rand256_parser.py +169 -44
- valetudo_map_parser/config/shared.py +103 -101
- valetudo_map_parser/config/status_text/status_text.py +96 -0
- valetudo_map_parser/config/status_text/translations.py +280 -0
- valetudo_map_parser/config/types.py +42 -13
- valetudo_map_parser/config/utils.py +221 -181
- valetudo_map_parser/hypfer_draw.py +6 -169
- valetudo_map_parser/hypfer_handler.py +40 -130
- valetudo_map_parser/map_data.py +403 -84
- valetudo_map_parser/rand256_handler.py +53 -197
- valetudo_map_parser/reimg_draw.py +14 -24
- valetudo_map_parser/rooms_handler.py +3 -18
- {valetudo_map_parser-0.1.9b100.dist-info → valetudo_map_parser-0.1.10.dist-info}/METADATA +7 -4
- valetudo_map_parser-0.1.10.dist-info/RECORD +34 -0
- {valetudo_map_parser-0.1.9b100.dist-info → valetudo_map_parser-0.1.10.dist-info}/WHEEL +1 -1
- valetudo_map_parser/config/enhanced_drawable.py +0 -324
- valetudo_map_parser/hypfer_rooms_handler.py +0 -599
- valetudo_map_parser-0.1.9b100.dist-info/RECORD +0 -27
- {valetudo_map_parser-0.1.9b100.dist-info → valetudo_map_parser-0.1.10.dist-info/licenses}/LICENSE +0 -0
- {valetudo_map_parser-0.1.9b100.dist-info → valetudo_map_parser-0.1.10.dist-info/licenses}/NOTICE.txt +0 -0
valetudo_map_parser/map_data.py
CHANGED
@@ -3,26 +3,134 @@ Collections of Json and List routines
|
|
3
3
|
ImageData is part of the Image_Handler
|
4
4
|
used functions to search data in the json
|
5
5
|
provided for the creation of the new camera frame
|
6
|
-
Version: v0.1.
|
6
|
+
Version: v0.1.10
|
7
7
|
"""
|
8
8
|
|
9
9
|
from __future__ import annotations
|
10
10
|
|
11
|
+
from dataclasses import asdict, dataclass, field
|
12
|
+
from typing import (
|
13
|
+
Any,
|
14
|
+
Literal,
|
15
|
+
NotRequired,
|
16
|
+
Optional,
|
17
|
+
Sequence,
|
18
|
+
TypedDict,
|
19
|
+
TypeVar,
|
20
|
+
)
|
21
|
+
|
11
22
|
import numpy as np
|
12
23
|
|
13
|
-
from
|
24
|
+
from .config.types import ImageSize, JsonType
|
25
|
+
|
26
|
+
|
27
|
+
T = TypeVar("T")
|
28
|
+
|
29
|
+
# --- Common Nested Structures ---
|
30
|
+
|
31
|
+
|
32
|
+
class RangeStats(TypedDict):
|
33
|
+
min: int
|
34
|
+
max: int
|
35
|
+
mid: int
|
36
|
+
avg: int
|
37
|
+
|
38
|
+
|
39
|
+
class Dimensions(TypedDict):
|
40
|
+
x: RangeStats
|
41
|
+
y: RangeStats
|
42
|
+
pixelCount: int
|
43
|
+
|
44
|
+
|
45
|
+
# --- Layer Types ---
|
46
|
+
|
47
|
+
|
48
|
+
class FloorWallMeta(TypedDict, total=False):
|
49
|
+
area: int
|
50
|
+
|
51
|
+
|
52
|
+
class SegmentMeta(TypedDict, total=False):
|
53
|
+
segmentId: str
|
54
|
+
active: bool
|
55
|
+
source: str
|
56
|
+
area: int
|
57
|
+
|
58
|
+
|
59
|
+
class MapLayerBase(TypedDict):
|
60
|
+
__class__: Literal["MapLayer"]
|
61
|
+
type: str
|
62
|
+
pixels: list[int]
|
63
|
+
compressedPixels: list[int]
|
64
|
+
dimensions: Dimensions
|
65
|
+
|
66
|
+
|
67
|
+
class FloorWallLayer(MapLayerBase):
|
68
|
+
metaData: FloorWallMeta
|
69
|
+
type: Literal["floor", "wall"]
|
70
|
+
|
71
|
+
|
72
|
+
class SegmentLayer(MapLayerBase):
|
73
|
+
metaData: SegmentMeta
|
74
|
+
type: Literal["segment"]
|
75
|
+
|
76
|
+
|
77
|
+
# --- Entity Types ---
|
78
|
+
|
79
|
+
|
80
|
+
class PointMeta(TypedDict, total=False):
|
81
|
+
angle: float
|
82
|
+
label: str
|
83
|
+
id: str
|
84
|
+
|
85
|
+
|
86
|
+
class PointMapEntity(TypedDict):
|
87
|
+
__class__: Literal["PointMapEntity"]
|
88
|
+
type: str
|
89
|
+
points: list[int]
|
90
|
+
metaData: NotRequired[PointMeta]
|
91
|
+
|
92
|
+
|
93
|
+
class PathMapEntity(TypedDict):
|
94
|
+
__class__: Literal["PathMapEntity"]
|
95
|
+
type: str
|
96
|
+
points: list[int]
|
97
|
+
metaData: dict[str, object] # flexible for now
|
98
|
+
|
99
|
+
|
100
|
+
Entity = PointMapEntity | PathMapEntity
|
101
|
+
|
102
|
+
# --- Top-level Map ---
|
103
|
+
|
104
|
+
|
105
|
+
class MapMeta(TypedDict, total=False):
|
106
|
+
version: int
|
107
|
+
totalLayerArea: int
|
108
|
+
|
109
|
+
|
110
|
+
class Size(TypedDict):
|
111
|
+
x: int
|
112
|
+
y: int
|
113
|
+
|
114
|
+
|
115
|
+
class ValetudoMap(TypedDict):
|
116
|
+
__class__: Literal["ValetudoMap"]
|
117
|
+
metaData: MapMeta
|
118
|
+
size: Size
|
119
|
+
pixelSize: int
|
120
|
+
layers: list[FloorWallLayer | SegmentLayer]
|
121
|
+
entities: list[Entity]
|
14
122
|
|
15
123
|
|
16
124
|
class ImageData:
|
17
125
|
"""Class to handle the image data."""
|
18
126
|
|
19
127
|
@staticmethod
|
20
|
-
def sublist(lst, n):
|
128
|
+
def sublist(lst: Sequence[T], n: int) -> list[Sequence[T]]:
|
21
129
|
"""Sub lists of specific n number of elements"""
|
22
130
|
return [lst[i : i + n] for i in range(0, len(lst), n)]
|
23
131
|
|
24
132
|
@staticmethod
|
25
|
-
def sublist_join(lst, n):
|
133
|
+
def sublist_join(lst: Sequence[T], n: int) -> list[list[T]]:
|
26
134
|
"""Join the lists in a unique list of n elements"""
|
27
135
|
arr = np.array(lst)
|
28
136
|
num_windows = len(lst) - n + 1
|
@@ -35,57 +143,130 @@ class ImageData:
|
|
35
143
|
# Vacuums Json in parallel.
|
36
144
|
|
37
145
|
@staticmethod
|
38
|
-
def
|
39
|
-
"""Get the
|
146
|
+
def get_image_size(json_data: JsonType) -> dict[str, int | list[int]]:
|
147
|
+
"""Get the image size from the json."""
|
148
|
+
if json_data:
|
149
|
+
size_x = int(json_data["size"]["x"])
|
150
|
+
size_y = int(json_data["size"]["y"])
|
151
|
+
return {
|
152
|
+
"x": size_x,
|
153
|
+
"y": size_y,
|
154
|
+
"centre": [(size_x // 2), (size_y // 2)],
|
155
|
+
}
|
156
|
+
return {"x": 0, "y": 0, "centre": [0, 0]}
|
157
|
+
|
158
|
+
@staticmethod
|
159
|
+
def get_json_id(json_data: JsonType) -> str | None:
|
160
|
+
"""Get the json id from the json."""
|
40
161
|
try:
|
41
|
-
|
42
|
-
except KeyError:
|
162
|
+
json_id = json_data["metaData"]["nonce"]
|
163
|
+
except (ValueError, KeyError):
|
164
|
+
json_id = None
|
165
|
+
return json_id
|
166
|
+
|
167
|
+
@staticmethod
|
168
|
+
def get_obstacles(
|
169
|
+
entity_dict: dict[str, list[PointMapEntity]],
|
170
|
+
) -> list[dict[str, str | int | None]]:
|
171
|
+
"""
|
172
|
+
Extract obstacle positions from Valetudo entity data.
|
173
|
+
|
174
|
+
Args:
|
175
|
+
entity_dict: Parsed JSON-like dict containing obstacle data.
|
176
|
+
|
177
|
+
Returns:
|
178
|
+
A list of obstacle dicts with keys:
|
179
|
+
- 'label': obstacle label string
|
180
|
+
- 'points': dict with 'x' and 'y' coordinates
|
181
|
+
- 'id': obstacle image/metadata ID (if any)
|
182
|
+
Returns an empty list if no valid obstacles found.
|
183
|
+
"""
|
184
|
+
obstacle_data = entity_dict.get("obstacle") # .get() won't raise KeyError
|
185
|
+
if not obstacle_data:
|
43
186
|
return []
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
187
|
+
|
188
|
+
obstacle_positions: list[dict[str, Any]] = []
|
189
|
+
|
190
|
+
for obstacle in obstacle_data:
|
191
|
+
meta = obstacle.get("metaData", {}) or {}
|
192
|
+
label = meta.get("label")
|
193
|
+
image_id = meta.get("id")
|
194
|
+
points = obstacle.get("points") or []
|
195
|
+
|
196
|
+
# Expecting at least two coordinates for a valid obstacle
|
197
|
+
if label and len(points) >= 2:
|
198
|
+
obstacle_positions.append(
|
199
|
+
{
|
53
200
|
"label": label,
|
54
201
|
"points": {"x": points[0], "y": points[1]},
|
55
202
|
"id": image_id,
|
56
203
|
}
|
57
|
-
|
58
|
-
|
59
|
-
return
|
204
|
+
)
|
205
|
+
|
206
|
+
return obstacle_positions
|
60
207
|
|
61
208
|
@staticmethod
|
62
209
|
def find_layers(
|
63
|
-
json_obj: JsonType,
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
210
|
+
json_obj: JsonType,
|
211
|
+
layer_dict: dict[str, list[Any]] | None,
|
212
|
+
active_list: list[int] | None,
|
213
|
+
) -> tuple[dict[str, list[Any]], list[int]]:
|
214
|
+
"""
|
215
|
+
Recursively traverse a JSON-like structure to find MapLayer entries.
|
216
|
+
|
217
|
+
Args:
|
218
|
+
json_obj: The JSON-like object (dicts/lists) to search.
|
219
|
+
layer_dict: Optional mapping of layer_type to a list of compressed pixel data.
|
220
|
+
active_list: Optional list of active segment flags.
|
221
|
+
|
222
|
+
Returns:
|
223
|
+
A tuple:
|
224
|
+
- dict mapping layer types to their compressed pixel arrays.
|
225
|
+
- list of integers marking active segment layers.
|
226
|
+
"""
|
227
|
+
if layer_dict is None:
|
228
|
+
layer_dict = {}
|
229
|
+
active_list = []
|
230
|
+
|
68
231
|
if isinstance(json_obj, dict):
|
69
|
-
if
|
232
|
+
if json_obj.get("__class") == "MapLayer":
|
70
233
|
layer_type = json_obj.get("type")
|
71
|
-
|
234
|
+
meta_data = json_obj.get("metaData") or {}
|
72
235
|
if layer_type:
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
236
|
+
layer_dict.setdefault(layer_type, []).append(
|
237
|
+
json_obj.get("compressedPixels", [])
|
238
|
+
)
|
239
|
+
# Safely extract "active" flag if present and convertible to int
|
240
|
+
if layer_type == "segment":
|
241
|
+
try:
|
242
|
+
active_list.append(int(meta_data.get("active", 0)))
|
243
|
+
except (ValueError, TypeError):
|
244
|
+
pass # skip invalid/missing 'active' values
|
245
|
+
|
246
|
+
# json_obj.items() yields (key, value), so we only want the values
|
247
|
+
for _, value in json_obj.items():
|
80
248
|
ImageData.find_layers(value, layer_dict, active_list)
|
249
|
+
|
81
250
|
elif isinstance(json_obj, list):
|
82
251
|
for item in json_obj:
|
83
252
|
ImageData.find_layers(item, layer_dict, active_list)
|
253
|
+
|
84
254
|
return layer_dict, active_list
|
85
255
|
|
86
256
|
@staticmethod
|
87
|
-
def find_points_entities(
|
88
|
-
|
257
|
+
def find_points_entities(
|
258
|
+
json_obj: ValetudoMap, entity_dict: dict = None
|
259
|
+
) -> dict[str, list[PointMapEntity]]:
|
260
|
+
"""
|
261
|
+
Traverse a ValetudoMap and collect PointMapEntity objects by their `type`.
|
262
|
+
|
263
|
+
Args:
|
264
|
+
json_obj: The full parsed JSON structure of a ValetudoMap.
|
265
|
+
entity_dict: Optional starting dict to append into.
|
266
|
+
|
267
|
+
Returns:
|
268
|
+
A dict mapping entity type strings to lists of PointMapEntitys.
|
269
|
+
"""
|
89
270
|
if entity_dict is None:
|
90
271
|
entity_dict = {}
|
91
272
|
if isinstance(json_obj, dict):
|
@@ -101,7 +282,9 @@ class ImageData:
|
|
101
282
|
return entity_dict
|
102
283
|
|
103
284
|
@staticmethod
|
104
|
-
def find_paths_entities(
|
285
|
+
def find_paths_entities(
|
286
|
+
json_obj: JsonType, entity_dict: dict[str, list[Entity]] | None = None
|
287
|
+
) -> dict[str, list[Entity]]:
|
105
288
|
"""Find the paths entities in the json object."""
|
106
289
|
|
107
290
|
if entity_dict is None:
|
@@ -119,7 +302,9 @@ class ImageData:
|
|
119
302
|
return entity_dict
|
120
303
|
|
121
304
|
@staticmethod
|
122
|
-
def find_zone_entities(
|
305
|
+
def find_zone_entities(
|
306
|
+
json_obj: JsonType, entity_dict: dict[str, list[Entity]] | None = None
|
307
|
+
) -> dict[str, list[Entity]]:
|
123
308
|
"""Find the zone entities in the json object."""
|
124
309
|
if entity_dict is None:
|
125
310
|
entity_dict = {}
|
@@ -136,61 +321,85 @@ class ImageData:
|
|
136
321
|
return entity_dict
|
137
322
|
|
138
323
|
@staticmethod
|
139
|
-
def find_virtual_walls(json_obj: JsonType) -> list:
|
140
|
-
"""
|
141
|
-
|
324
|
+
def find_virtual_walls(json_obj: JsonType) -> list[list[tuple[float, float]]]:
|
325
|
+
"""
|
326
|
+
Recursively search a JSON-like structure for virtual wall line entities.
|
142
327
|
|
143
|
-
|
144
|
-
|
328
|
+
Args:
|
329
|
+
json_obj: The JSON-like data (dicts/lists) to search.
|
330
|
+
|
331
|
+
Returns:
|
332
|
+
A list of point lists, where each point list belongs to a virtual wall.
|
333
|
+
"""
|
334
|
+
virtual_walls: list[list[tuple[float, float]]] = []
|
335
|
+
|
336
|
+
def _recurse(obj: Any) -> None:
|
145
337
|
if isinstance(obj, dict):
|
146
|
-
if
|
147
|
-
|
148
|
-
|
149
|
-
|
338
|
+
if (
|
339
|
+
obj.get("__class") == "LineMapEntity"
|
340
|
+
and obj.get("type") == "virtual_wall"
|
341
|
+
):
|
342
|
+
points = obj.get("points")
|
343
|
+
if isinstance(points, list):
|
344
|
+
virtual_walls.append(
|
345
|
+
points
|
346
|
+
) # Type checkers may refine further here
|
347
|
+
|
150
348
|
for value in obj.values():
|
151
|
-
|
349
|
+
_recurse(value)
|
350
|
+
|
152
351
|
elif isinstance(obj, list):
|
153
352
|
for item in obj:
|
154
|
-
|
353
|
+
_recurse(item)
|
155
354
|
|
156
|
-
|
355
|
+
_recurse(json_obj)
|
157
356
|
return virtual_walls
|
158
357
|
|
159
358
|
@staticmethod
|
160
359
|
async def async_get_rooms_coordinates(
|
161
|
-
pixels:
|
162
|
-
) -> tuple:
|
360
|
+
pixels: Sequence[tuple[int, int, int]], pixel_size: int = 5, rand: bool = False
|
361
|
+
) -> tuple[int, int, int, int] | tuple[tuple[int, int], tuple[int, int]]:
|
163
362
|
"""
|
164
|
-
Extract the room coordinates from
|
165
|
-
|
166
|
-
|
167
|
-
|
363
|
+
Extract the room bounding box coordinates from vacuum pixel data.
|
364
|
+
|
365
|
+
Args:
|
366
|
+
pixels: Sequence of (x, y, z) values representing pixels.
|
367
|
+
pixel_size: Size of each pixel in mm. Defaults to 5.
|
368
|
+
rand: If True, return coordinates in rand256 format.
|
369
|
+
|
370
|
+
Returns:
|
371
|
+
If rand is True:
|
372
|
+
((max_x_mm, max_y_mm), (min_x_mm, min_y_mm))
|
373
|
+
Else:
|
374
|
+
(min_x_mm, min_y_mm, max_x_mm, max_y_mm)
|
168
375
|
"""
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
376
|
+
|
377
|
+
def to_mm(coord):
|
378
|
+
"""Convert pixel coordinates to millimeters."""
|
379
|
+
return round(coord * pixel_size * 10)
|
380
|
+
|
381
|
+
if not pixels:
|
382
|
+
raise ValueError("Pixels list cannot be empty.")
|
383
|
+
|
384
|
+
# Initialise min/max using the first pixel
|
385
|
+
first_x, first_y, _ = pixels[0]
|
386
|
+
min_x = max_x = first_x
|
387
|
+
min_y = max_y = first_y
|
388
|
+
|
389
|
+
for x, y, z in pixels:
|
174
390
|
if rand:
|
175
|
-
|
176
|
-
|
177
|
-
max_y = max(max_y, y + pixel_size) # Update max y coordinate
|
178
|
-
min_x = min(min_x, x) # Update min x coordinate
|
179
|
-
min_y = min(min_y, y) # Update min y coordinate
|
391
|
+
max_x = max(max_x, x)
|
392
|
+
max_y = max(max_y, y + pixel_size)
|
180
393
|
else:
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
394
|
+
max_x = max(max_x, x + z)
|
395
|
+
max_y = max(max_y, y + pixel_size)
|
396
|
+
|
397
|
+
min_x = min(min_x, x)
|
398
|
+
min_y = min(min_y, y)
|
399
|
+
|
186
400
|
if rand:
|
187
|
-
return (
|
188
|
-
|
189
|
-
(
|
190
|
-
((min_x * pixel_size) * 10),
|
191
|
-
((min_y * pixel_size) * 10),
|
192
|
-
),
|
193
|
-
)
|
401
|
+
return (to_mm(max_x), to_mm(max_y)), (to_mm(min_x), to_mm(min_y))
|
402
|
+
|
194
403
|
return (
|
195
404
|
min_x * pixel_size,
|
196
405
|
min_y * pixel_size,
|
@@ -279,7 +488,7 @@ class RandImageData:
|
|
279
488
|
return json_data.get("path", {})
|
280
489
|
|
281
490
|
@staticmethod
|
282
|
-
def get_rrm_goto_predicted_path(json_data: JsonType) -> list
|
491
|
+
def get_rrm_goto_predicted_path(json_data: JsonType) -> Optional[list]:
|
283
492
|
"""Get the predicted path data from the json."""
|
284
493
|
try:
|
285
494
|
predicted_path = json_data.get("goto_predicted_path", {})
|
@@ -321,7 +530,7 @@ class RandImageData:
|
|
321
530
|
return angle, json_data.get("robot_angle", 0)
|
322
531
|
|
323
532
|
@staticmethod
|
324
|
-
def get_rrm_goto_target(json_data: JsonType) ->
|
533
|
+
def get_rrm_goto_target(json_data: JsonType) -> Any:
|
325
534
|
"""Get the goto target from the json."""
|
326
535
|
try:
|
327
536
|
path_data = json_data.get("goto_target", {})
|
@@ -334,21 +543,23 @@ class RandImageData:
|
|
334
543
|
return None
|
335
544
|
|
336
545
|
@staticmethod
|
337
|
-
def get_rrm_currently_cleaned_zones(json_data: JsonType) -> dict:
|
546
|
+
def get_rrm_currently_cleaned_zones(json_data: JsonType) -> list[dict[str, Any]]:
|
338
547
|
"""Get the currently cleaned zones from the json."""
|
339
548
|
re_zones = json_data.get("currently_cleaned_zones", [])
|
340
549
|
formatted_zones = RandImageData._rrm_valetudo_format_zone(re_zones)
|
341
550
|
return formatted_zones
|
342
551
|
|
343
552
|
@staticmethod
|
344
|
-
def get_rrm_forbidden_zones(json_data: JsonType) -> dict:
|
553
|
+
def get_rrm_forbidden_zones(json_data: JsonType) -> list[dict[str, Any]]:
|
345
554
|
"""Get the forbidden zones from the json."""
|
346
|
-
re_zones = json_data.get("forbidden_zones", [])
|
555
|
+
re_zones = json_data.get("forbidden_zones", []) + json_data.get(
|
556
|
+
"forbidden_mop_zones", []
|
557
|
+
)
|
347
558
|
formatted_zones = RandImageData._rrm_valetudo_format_zone(re_zones)
|
348
559
|
return formatted_zones
|
349
560
|
|
350
561
|
@staticmethod
|
351
|
-
def _rrm_valetudo_format_zone(coordinates: list) ->
|
562
|
+
def _rrm_valetudo_format_zone(coordinates: list) -> list[dict[str, Any]]:
|
352
563
|
"""Format the zones from RRM to Valetudo."""
|
353
564
|
formatted_zones = []
|
354
565
|
for zone_data in coordinates:
|
@@ -497,3 +708,111 @@ class RandImageData:
|
|
497
708
|
except KeyError:
|
498
709
|
return None
|
499
710
|
return seg_ids
|
711
|
+
|
712
|
+
|
713
|
+
@dataclass
|
714
|
+
class HyperMapData:
|
715
|
+
"""Class to handle the map data snapshots."""
|
716
|
+
|
717
|
+
json_data: Any = None
|
718
|
+
json_id: Optional[str] = None
|
719
|
+
obstacles: dict[str, list[Any]] = field(default_factory=dict)
|
720
|
+
paths: dict[str, list[Any]] = field(default_factory=dict)
|
721
|
+
image_size: dict[str, int | list[int]] = field(default_factory=dict)
|
722
|
+
areas: dict[str, list[Any]] = field(default_factory=dict)
|
723
|
+
pixel_size: int = 0
|
724
|
+
entity_dict: dict[str, list[Any]] = field(default_factory=dict)
|
725
|
+
layers: dict[str, list[Any]] = field(default_factory=dict)
|
726
|
+
active_zones: list[int] = field(default_factory=list)
|
727
|
+
virtual_walls: list[list[tuple[float, float]]] = field(default_factory=list)
|
728
|
+
|
729
|
+
@classmethod
|
730
|
+
async def async_from_valetudo_json(cls, json_data: Any) -> "HyperMapData":
|
731
|
+
"""
|
732
|
+
Build a fully-populated MapSnapshot from raw Valetudo JSON
|
733
|
+
using ImageData's helper functions.
|
734
|
+
"""
|
735
|
+
|
736
|
+
# Call into your refactored static/class methods
|
737
|
+
json_id = ImageData.get_json_id(json_data)
|
738
|
+
paths = ImageData.find_paths_entities(json_data)
|
739
|
+
image_size = ImageData.get_image_size(json_data)
|
740
|
+
areas = ImageData.find_zone_entities(json_data)
|
741
|
+
layers = {}
|
742
|
+
active_zones = []
|
743
|
+
# Hypothetical obstacles finder, if you have one
|
744
|
+
obstacles = getattr(ImageData, "find_obstacles_entities", lambda *_: {})(
|
745
|
+
json_data
|
746
|
+
)
|
747
|
+
virtual_walls = ImageData.find_virtual_walls(json_data)
|
748
|
+
pixel_size = int(json_data["pixelSize"])
|
749
|
+
layers, active_zones = ImageData.find_layers(
|
750
|
+
json_data["layers"], layers, active_zones
|
751
|
+
)
|
752
|
+
entity_dict = ImageData.find_points_entities(json_data)
|
753
|
+
|
754
|
+
return cls(
|
755
|
+
json_data=json_data,
|
756
|
+
json_id=json_id,
|
757
|
+
image_size=image_size,
|
758
|
+
obstacles=obstacles,
|
759
|
+
paths=paths,
|
760
|
+
areas=areas,
|
761
|
+
virtual_walls=virtual_walls,
|
762
|
+
entity_dict=entity_dict,
|
763
|
+
pixel_size=pixel_size,
|
764
|
+
layers=layers,
|
765
|
+
active_zones=active_zones,
|
766
|
+
)
|
767
|
+
|
768
|
+
def to_dict(self) -> dict[str, Any]:
|
769
|
+
"""Return a dictionary representation of this dataclass."""
|
770
|
+
return asdict(self)
|
771
|
+
|
772
|
+
@classmethod
|
773
|
+
def from_dict(cls, data: dict[str, Any]) -> "HyperMapData":
|
774
|
+
"""Construct a HyperMapData from a plain dictionary.
|
775
|
+
Unknown keys are ignored; missing keys use safe defaults.
|
776
|
+
"""
|
777
|
+
return cls(
|
778
|
+
json_data=data.get("json_data"),
|
779
|
+
json_id=data.get("json_id") or None,
|
780
|
+
obstacles=data.get("obstacles", {}),
|
781
|
+
paths=data.get("paths", {}),
|
782
|
+
image_size=data.get("image_size", {}),
|
783
|
+
areas=data.get("areas", {}),
|
784
|
+
pixel_size=int(data.get("pixel_size", 0) or 0),
|
785
|
+
entity_dict=data.get("entity_dict", {}),
|
786
|
+
layers=data.get("layers", {}),
|
787
|
+
active_zones=data.get("active_zones", []),
|
788
|
+
virtual_walls=data.get("virtual_walls", []),
|
789
|
+
)
|
790
|
+
|
791
|
+
def update_from_dict(self, updates: dict[str, Any]) -> None:
|
792
|
+
"""Update one or more fields in place, preserving the rest.
|
793
|
+
Unknown keys are ignored; pixel_size is coerced to int.
|
794
|
+
"""
|
795
|
+
if not updates:
|
796
|
+
return
|
797
|
+
allowed = {
|
798
|
+
"json_data",
|
799
|
+
"json_id",
|
800
|
+
"obstacles",
|
801
|
+
"paths",
|
802
|
+
"image_size",
|
803
|
+
"areas",
|
804
|
+
"pixel_size",
|
805
|
+
"entity_dict",
|
806
|
+
"layers",
|
807
|
+
"active_zones",
|
808
|
+
"virtual_walls",
|
809
|
+
}
|
810
|
+
for key, value in updates.items():
|
811
|
+
if key not in allowed:
|
812
|
+
continue
|
813
|
+
if key == "pixel_size":
|
814
|
+
try:
|
815
|
+
value = int(value)
|
816
|
+
except (TypeError, ValueError):
|
817
|
+
continue
|
818
|
+
setattr(self, key, value)
|