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