valetudo-map-parser 0.1.9b74__tar.gz → 0.1.10rc2__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.
- {valetudo_map_parser-0.1.9b74 → valetudo_map_parser-0.1.10rc2}/PKG-INFO +7 -4
- {valetudo_map_parser-0.1.9b74 → valetudo_map_parser-0.1.10rc2}/SCR/valetudo_map_parser/__init__.py +27 -5
- {valetudo_map_parser-0.1.9b74 → valetudo_map_parser-0.1.10rc2}/SCR/valetudo_map_parser/config/auto_crop.py +2 -27
- {valetudo_map_parser-0.1.9b74 → valetudo_map_parser-0.1.10rc2}/SCR/valetudo_map_parser/config/colors.py +2 -2
- {valetudo_map_parser-0.1.9b74 → valetudo_map_parser-0.1.10rc2}/SCR/valetudo_map_parser/config/drawable.py +69 -62
- valetudo_map_parser-0.1.10rc2/SCR/valetudo_map_parser/config/fonts/FiraSans.ttf +0 -0
- valetudo_map_parser-0.1.10rc2/SCR/valetudo_map_parser/config/fonts/Inter-VF.ttf +0 -0
- valetudo_map_parser-0.1.10rc2/SCR/valetudo_map_parser/config/fonts/Lato-Regular.ttf +0 -0
- valetudo_map_parser-0.1.10rc2/SCR/valetudo_map_parser/config/fonts/MPLUSRegular.ttf +0 -0
- valetudo_map_parser-0.1.10rc2/SCR/valetudo_map_parser/config/fonts/NotoKufiArabic-VF.ttf +0 -0
- valetudo_map_parser-0.1.10rc2/SCR/valetudo_map_parser/config/fonts/NotoSansCJKhk-VF.ttf +0 -0
- valetudo_map_parser-0.1.10rc2/SCR/valetudo_map_parser/config/fonts/NotoSansKhojki.ttf +0 -0
- {valetudo_map_parser-0.1.9b74 → valetudo_map_parser-0.1.10rc2}/SCR/valetudo_map_parser/config/shared.py +12 -11
- valetudo_map_parser-0.1.10rc2/SCR/valetudo_map_parser/config/status_text/status_text.py +95 -0
- valetudo_map_parser-0.1.10rc2/SCR/valetudo_map_parser/config/status_text/translations.py +280 -0
- {valetudo_map_parser-0.1.9b74 → valetudo_map_parser-0.1.10rc2}/SCR/valetudo_map_parser/config/types.py +8 -8
- {valetudo_map_parser-0.1.9b74 → valetudo_map_parser-0.1.10rc2}/SCR/valetudo_map_parser/config/utils.py +35 -61
- {valetudo_map_parser-0.1.9b74 → valetudo_map_parser-0.1.10rc2}/SCR/valetudo_map_parser/hypfer_draw.py +2 -90
- {valetudo_map_parser-0.1.9b74 → valetudo_map_parser-0.1.10rc2}/SCR/valetudo_map_parser/hypfer_handler.py +29 -47
- {valetudo_map_parser-0.1.9b74 → valetudo_map_parser-0.1.10rc2}/SCR/valetudo_map_parser/map_data.py +394 -81
- {valetudo_map_parser-0.1.9b74 → valetudo_map_parser-0.1.10rc2}/SCR/valetudo_map_parser/rand256_handler.py +6 -59
- {valetudo_map_parser-0.1.9b74 → valetudo_map_parser-0.1.10rc2}/SCR/valetudo_map_parser/reimg_draw.py +1 -6
- {valetudo_map_parser-0.1.9b74 → valetudo_map_parser-0.1.10rc2}/SCR/valetudo_map_parser/rooms_handler.py +4 -10
- {valetudo_map_parser-0.1.9b74 → valetudo_map_parser-0.1.10rc2}/pyproject.toml +3 -2
- {valetudo_map_parser-0.1.9b74 → valetudo_map_parser-0.1.10rc2}/LICENSE +0 -0
- {valetudo_map_parser-0.1.9b74 → valetudo_map_parser-0.1.10rc2}/NOTICE.txt +0 -0
- {valetudo_map_parser-0.1.9b74 → valetudo_map_parser-0.1.10rc2}/README.md +0 -0
- {valetudo_map_parser-0.1.9b74 → valetudo_map_parser-0.1.10rc2}/SCR/valetudo_map_parser/config/__init__.py +0 -0
- {valetudo_map_parser-0.1.9b74 → valetudo_map_parser-0.1.10rc2}/SCR/valetudo_map_parser/config/async_utils.py +0 -0
- {valetudo_map_parser-0.1.9b74 → valetudo_map_parser-0.1.10rc2}/SCR/valetudo_map_parser/config/color_utils.py +0 -0
- {valetudo_map_parser-0.1.9b74 → valetudo_map_parser-0.1.10rc2}/SCR/valetudo_map_parser/config/drawable_elements.py +0 -0
- {valetudo_map_parser-0.1.9b74 → valetudo_map_parser-0.1.10rc2}/SCR/valetudo_map_parser/config/enhanced_drawable.py +0 -0
- {valetudo_map_parser-0.1.9b74 → valetudo_map_parser-0.1.10rc2}/SCR/valetudo_map_parser/config/optimized_element_map.py +0 -0
- {valetudo_map_parser-0.1.9b74 → valetudo_map_parser-0.1.10rc2}/SCR/valetudo_map_parser/config/rand256_parser.py +0 -0
- {valetudo_map_parser-0.1.9b74 → valetudo_map_parser-0.1.10rc2}/SCR/valetudo_map_parser/hypfer_rooms_handler.py +0 -0
- {valetudo_map_parser-0.1.9b74 → valetudo_map_parser-0.1.10rc2}/SCR/valetudo_map_parser/py.typed +0 -0
@@ -1,16 +1,19 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: valetudo-map-parser
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.10rc2
|
4
4
|
Summary: A Python library to parse Valetudo map data returning a PIL Image object.
|
5
5
|
License: Apache-2.0
|
6
|
+
License-File: LICENSE
|
7
|
+
License-File: NOTICE.txt
|
6
8
|
Author: Sandro Cantarella
|
7
9
|
Author-email: gsca075@gmail.com
|
8
|
-
Requires-Python: >=3.
|
10
|
+
Requires-Python: >=3.13
|
9
11
|
Classifier: License :: OSI Approved :: Apache Software License
|
10
12
|
Classifier: Programming Language :: Python :: 3
|
11
|
-
Classifier: Programming Language :: Python :: 3.12
|
12
13
|
Classifier: Programming Language :: Python :: 3.13
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
13
15
|
Requires-Dist: Pillow (>=10.3.0)
|
16
|
+
Requires-Dist: mvcrender (>=0.0.2)
|
14
17
|
Requires-Dist: numpy (>=1.26.4)
|
15
18
|
Requires-Dist: scipy (>=1.12.0)
|
16
19
|
Project-URL: Bug Tracker, https://github.com/sca075/Python-package-valetudo-map-parser/issues
|
{valetudo_map_parser-0.1.9b74 → valetudo_map_parser-0.1.10rc2}/SCR/valetudo_map_parser/__init__.py
RENAMED
@@ -1,11 +1,12 @@
|
|
1
1
|
"""Valetudo map parser.
|
2
|
-
Version: 0.1.
|
2
|
+
Version: 0.1.10"""
|
3
|
+
|
4
|
+
from pathlib import Path
|
3
5
|
|
4
6
|
from .config.colors import ColorsManagement
|
5
7
|
from .config.drawable import Drawable
|
6
8
|
from .config.drawable_elements import DrawableElement, DrawingConfig
|
7
9
|
from .config.enhanced_drawable import EnhancedDrawable
|
8
|
-
from .config.utils import webp_bytes_to_pil
|
9
10
|
from .config.rand256_parser import RRMapParser
|
10
11
|
from .config.shared import CameraShared, CameraSharedManager
|
11
12
|
from .config.types import (
|
@@ -15,16 +16,32 @@ from .config.types import (
|
|
15
16
|
SnapshotStore,
|
16
17
|
TrimCropData,
|
17
18
|
UserLanguageStore,
|
18
|
-
|
19
|
+
JsonType,
|
20
|
+
PilPNG,
|
21
|
+
NumpyArray,
|
22
|
+
ImageSize,
|
19
23
|
)
|
24
|
+
from .config.status_text.status_text import StatusText
|
25
|
+
from .config.status_text.translations import translations as STATUS_TEXT_TRANSLATIONS
|
20
26
|
from .hypfer_handler import HypferMapImageHandler
|
21
27
|
from .rand256_handler import ReImageHandler
|
22
28
|
from .rooms_handler import RoomsHandler, RandRoomsHandler
|
29
|
+
from .map_data import HyperMapData
|
30
|
+
|
31
|
+
|
32
|
+
def get_default_font_path() -> str:
|
33
|
+
"""Return the absolute path to the bundled default font directory.
|
34
|
+
|
35
|
+
This returns the path to the fonts folder; the caller can join a specific font file
|
36
|
+
to avoid hard-coding a particular font here.
|
37
|
+
"""
|
38
|
+
return str((Path(__file__).resolve().parent / "config" / "fonts").resolve())
|
23
39
|
|
24
40
|
|
25
41
|
__all__ = [
|
26
42
|
"RoomsHandler",
|
27
43
|
"RandRoomsHandler",
|
44
|
+
"HyperMapData",
|
28
45
|
"HypferMapImageHandler",
|
29
46
|
"ReImageHandler",
|
30
47
|
"RRMapParser",
|
@@ -41,6 +58,11 @@ __all__ = [
|
|
41
58
|
"RoomsProperties",
|
42
59
|
"TrimCropData",
|
43
60
|
"CameraModes",
|
44
|
-
"
|
45
|
-
"
|
61
|
+
"JsonType",
|
62
|
+
"PilPNG",
|
63
|
+
"NumpyArray",
|
64
|
+
"ImageSize",
|
65
|
+
"StatusText",
|
66
|
+
"STATUS_TEXT_TRANSLATIONS",
|
67
|
+
"get_default_font_path",
|
46
68
|
]
|
@@ -6,10 +6,9 @@ from __future__ import annotations
|
|
6
6
|
import logging
|
7
7
|
|
8
8
|
import numpy as np
|
9
|
-
from numpy import rot90
|
10
9
|
from scipy import ndimage
|
11
10
|
|
12
|
-
from .async_utils import AsyncNumPy
|
11
|
+
from .async_utils import AsyncNumPy
|
13
12
|
from .types import Color, NumpyArray, TrimCropData, TrimsData
|
14
13
|
from .utils import BaseHandler
|
15
14
|
|
@@ -91,7 +90,6 @@ class AutoCrop:
|
|
91
90
|
|
92
91
|
async def _async_auto_crop_data(self, tdata: TrimsData): # , tdata=None
|
93
92
|
"""Load the auto crop data from the Camera config."""
|
94
|
-
_LOGGER.debug("Auto Crop init data: %s, %s", str(tdata), str(self.auto_crop))
|
95
93
|
if not self.auto_crop:
|
96
94
|
trims_data = TrimCropData.from_dict(dict(tdata.to_dict())).to_list()
|
97
95
|
(
|
@@ -100,7 +98,6 @@ class AutoCrop:
|
|
100
98
|
self.trim_right,
|
101
99
|
self.trim_down,
|
102
100
|
) = trims_data
|
103
|
-
_LOGGER.debug("Auto Crop trims data: %s", trims_data)
|
104
101
|
if trims_data != [0, 0, 0, 0]:
|
105
102
|
self._calculate_trimmed_dimensions()
|
106
103
|
else:
|
@@ -118,10 +115,6 @@ class AutoCrop:
|
|
118
115
|
|
119
116
|
async def _init_auto_crop(self):
|
120
117
|
"""Initialize the auto crop data."""
|
121
|
-
_LOGGER.debug("Auto Crop Init data: %s", str(self.auto_crop))
|
122
|
-
_LOGGER.debug(
|
123
|
-
"Auto Crop Init trims data: %r", self.handler.shared.trims.to_dict()
|
124
|
-
)
|
125
118
|
if not self.auto_crop: # and self.handler.shared.vacuum_state == "docked":
|
126
119
|
self.auto_crop = await self._async_auto_crop_data(self.handler.shared.trims)
|
127
120
|
if self.auto_crop:
|
@@ -131,7 +124,6 @@ class AutoCrop:
|
|
131
124
|
|
132
125
|
# Fallback: Ensure auto_crop is valid
|
133
126
|
if not self.auto_crop or any(v < 0 for v in self.auto_crop):
|
134
|
-
_LOGGER.debug("Auto-crop data unavailable. Scanning full image.")
|
135
127
|
self.auto_crop = None
|
136
128
|
|
137
129
|
return self.auto_crop
|
@@ -164,14 +156,6 @@ class AutoCrop:
|
|
164
156
|
min_y, max_y = y_slice.start, y_slice.stop - 1
|
165
157
|
min_x, max_x = x_slice.start, x_slice.stop - 1
|
166
158
|
|
167
|
-
_LOGGER.debug(
|
168
|
-
"%s: Found trims max and min values (y,x) (%s, %s) (%s, %s)...",
|
169
|
-
self.handler.file_name,
|
170
|
-
int(max_y),
|
171
|
-
int(max_x),
|
172
|
-
int(min_y),
|
173
|
-
int(min_x),
|
174
|
-
)
|
175
159
|
return min_y, min_x, max_x, max_y
|
176
160
|
|
177
161
|
async def async_get_room_bounding_box(
|
@@ -247,7 +231,7 @@ class AutoCrop:
|
|
247
231
|
return None
|
248
232
|
|
249
233
|
except Exception as e:
|
250
|
-
_LOGGER.
|
234
|
+
_LOGGER.warning(
|
251
235
|
"%s: Error calculating room bounding box for '%s': %s",
|
252
236
|
self.handler.file_name,
|
253
237
|
room_name,
|
@@ -403,7 +387,6 @@ class AutoCrop:
|
|
403
387
|
try:
|
404
388
|
self.auto_crop = await self._init_auto_crop()
|
405
389
|
if (self.auto_crop is None) or (self.auto_crop == [0, 0, 0, 0]):
|
406
|
-
_LOGGER.debug("%s: Calculating auto trim box", self.handler.file_name)
|
407
390
|
# Find the coordinates of the first occurrence of a non-background color
|
408
391
|
min_y, min_x, max_x, max_y = await self.async_image_margins(
|
409
392
|
image_array, detect_colour
|
@@ -456,15 +439,7 @@ class AutoCrop:
|
|
456
439
|
# Rotate the cropped image based on the given angle
|
457
440
|
rotated = await self.async_rotate_the_image(trimmed, rotate)
|
458
441
|
del trimmed # Free memory.
|
459
|
-
_LOGGER.debug(
|
460
|
-
"%s: Auto Trim Box data: %s", self.handler.file_name, self.crop_area
|
461
|
-
)
|
462
442
|
self.handler.crop_img_size = [rotated.shape[1], rotated.shape[0]]
|
463
|
-
_LOGGER.debug(
|
464
|
-
"%s: Auto Trimmed image size: %s",
|
465
|
-
self.handler.file_name,
|
466
|
-
self.handler.crop_img_size,
|
467
|
-
)
|
468
443
|
|
469
444
|
except RuntimeError as e:
|
470
445
|
_LOGGER.warning(
|
@@ -250,7 +250,7 @@ class ColorsManagement:
|
|
250
250
|
List[Tuple[int, int, int, int]]: List of RGBA colors with alpha channel added.
|
251
251
|
"""
|
252
252
|
if len(alpha_channels) != len(rgb_colors):
|
253
|
-
LOGGER.
|
253
|
+
LOGGER.warning("Input lists must have the same length.")
|
254
254
|
return []
|
255
255
|
|
256
256
|
# Fast path for empty lists
|
@@ -357,7 +357,7 @@ class ColorsManagement:
|
|
357
357
|
self.color_cache.clear()
|
358
358
|
|
359
359
|
except (ValueError, IndexError, UnboundLocalError) as e:
|
360
|
-
LOGGER.
|
360
|
+
LOGGER.warning("Error while populating colors: %s", e)
|
361
361
|
|
362
362
|
def initialize_user_colors(self, device_info: dict) -> List[Color]:
|
363
363
|
"""
|
@@ -11,7 +11,7 @@ Optimized with NumPy and SciPy for better performance.
|
|
11
11
|
from __future__ import annotations
|
12
12
|
|
13
13
|
import logging
|
14
|
-
import
|
14
|
+
from pathlib import Path
|
15
15
|
|
16
16
|
import numpy as np
|
17
17
|
from PIL import Image, ImageDraw, ImageFont
|
@@ -160,7 +160,7 @@ class Drawable:
|
|
160
160
|
|
161
161
|
# Get blended colors for flag and pole
|
162
162
|
flag_alpha = flag_color[3] if len(flag_color) == 4 else 255
|
163
|
-
pole_color_base =
|
163
|
+
pole_color_base = [0, 0, 255] # Blue for the pole
|
164
164
|
pole_alpha = 255
|
165
165
|
|
166
166
|
# Blend flag color if needed
|
@@ -170,7 +170,12 @@ class Drawable:
|
|
170
170
|
)
|
171
171
|
|
172
172
|
# Create pole color with alpha
|
173
|
-
pole_color: Color = (
|
173
|
+
pole_color: Color = (
|
174
|
+
pole_color_base[0],
|
175
|
+
pole_color_base[1],
|
176
|
+
pole_color_base[2],
|
177
|
+
pole_alpha,
|
178
|
+
)
|
174
179
|
|
175
180
|
# Blend pole color if needed
|
176
181
|
if pole_alpha < 255:
|
@@ -223,20 +228,18 @@ class Drawable:
|
|
223
228
|
|
224
229
|
@staticmethod
|
225
230
|
def point_inside(x: int, y: int, points: list[Tuple[int, int]]) -> bool:
|
226
|
-
"""
|
227
|
-
Check if a point (x, y) is inside a polygon defined by a list of points.
|
228
|
-
"""
|
231
|
+
"""Check if a point (x, y) is inside a polygon defined by a list of points."""
|
229
232
|
n = len(points)
|
230
233
|
inside = False
|
231
|
-
|
234
|
+
inters_x = 0.0
|
232
235
|
p1x, p1y = points[0]
|
233
236
|
for i in range(1, n + 1):
|
234
237
|
p2x, p2y = points[i % n]
|
235
238
|
if y > min(p1y, p2y):
|
236
239
|
if y <= max(p1y, p2y) and x <= max(p1x, p2x):
|
237
240
|
if p1y != p2y:
|
238
|
-
|
239
|
-
if p1x == p2x or x <=
|
241
|
+
inters_x = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x
|
242
|
+
if p1x == p2x or x <= inters_x:
|
240
243
|
inside = not inside
|
241
244
|
p1x, p1y = p2x, p2y
|
242
245
|
return inside
|
@@ -251,8 +254,7 @@ class Drawable:
|
|
251
254
|
color: Color,
|
252
255
|
width: int = 3,
|
253
256
|
) -> np.ndarray:
|
254
|
-
"""
|
255
|
-
Draw a line on a NumPy array (layer) from point A to B using Bresenham's algorithm.
|
257
|
+
"""Draw a line on a NumPy array (layer) from point A to B using Bresenham's algorithm.
|
256
258
|
|
257
259
|
Args:
|
258
260
|
layer: The numpy array to draw on (H, W, C)
|
@@ -283,11 +285,11 @@ class Drawable:
|
|
283
285
|
x_min = max(0, x1 - half_w)
|
284
286
|
x_max = min(w, x1 + half_w + 1)
|
285
287
|
|
286
|
-
|
288
|
+
sub_mask = mask[
|
287
289
|
(y_min - (y1 - half_w)) : (y_max - (y1 - half_w)),
|
288
290
|
(x_min - (x1 - half_w)) : (x_max - (x1 - half_w)),
|
289
291
|
]
|
290
|
-
layer[y_min:y_max, x_min:x_max][
|
292
|
+
layer[y_min:y_max, x_min:x_max][sub_mask] = blended_color
|
291
293
|
|
292
294
|
if x1 == x2 and y1 == y2:
|
293
295
|
break
|
@@ -317,12 +319,14 @@ class Drawable:
|
|
317
319
|
return layer
|
318
320
|
|
319
321
|
@staticmethod
|
320
|
-
async def lines(
|
322
|
+
async def lines(
|
323
|
+
arr: NumpyArray, coordinates, width: int, color: Color
|
324
|
+
) -> NumpyArray:
|
321
325
|
"""
|
322
326
|
Join the coordinates creating a continuous line (path).
|
323
327
|
Optimized with vectorized operations for better performance.
|
324
328
|
"""
|
325
|
-
for coord in
|
329
|
+
for coord in coordinates:
|
326
330
|
x0, y0 = coord[0]
|
327
331
|
try:
|
328
332
|
x1, y1 = coord[1]
|
@@ -466,9 +470,6 @@ class Drawable:
|
|
466
470
|
# Adjust points to the mask's coordinate system
|
467
471
|
adjusted_points = [(p[0] - min_x, p[1] - min_y) for p in points]
|
468
472
|
|
469
|
-
# Create a grid of coordinates and use it to test all points at once
|
470
|
-
y_indices, x_indices = np.mgrid[0 : mask.shape[0], 0 : mask.shape[1]]
|
471
|
-
|
472
473
|
# Test each point in the grid
|
473
474
|
for i in range(mask.shape[0]):
|
474
475
|
for j in range(mask.shape[1]):
|
@@ -545,68 +546,63 @@ class Drawable:
|
|
545
546
|
angle: float,
|
546
547
|
fill: Color,
|
547
548
|
robot_state: str | None = None,
|
549
|
+
radius: int = 25, # user-configurable
|
548
550
|
) -> NumpyArray:
|
549
551
|
"""
|
550
|
-
Draw the robot
|
551
|
-
Optimized with NumPy vectorized operations for better performance.
|
552
|
+
Draw the robot with configurable size. All elements scale with radius.
|
552
553
|
"""
|
553
|
-
#
|
554
|
+
# Minimum radius to keep things visible
|
555
|
+
radius = max(8, min(radius, 25))
|
556
|
+
|
554
557
|
height, width = layers.shape[:2]
|
555
558
|
if not (0 <= x < width and 0 <= y < height):
|
556
559
|
return layers
|
557
560
|
|
558
|
-
#
|
559
|
-
radius = 25
|
560
|
-
box_size = radius * 2 + 2 # Add a small margin
|
561
|
-
|
562
|
-
# Calculate the region to draw on
|
561
|
+
# Bounding box
|
563
562
|
top_left_x = max(0, x - radius - 1)
|
564
563
|
top_left_y = max(0, y - radius - 1)
|
565
564
|
bottom_right_x = min(width, x + radius + 1)
|
566
565
|
bottom_right_y = min(height, y + radius + 1)
|
567
566
|
|
568
|
-
# Skip if the robot is completely outside the image
|
569
567
|
if top_left_x >= bottom_right_x or top_left_y >= bottom_right_y:
|
570
568
|
return layers
|
571
569
|
|
572
|
-
# Create a temporary layer for the robot
|
573
570
|
tmp_width = bottom_right_x - top_left_x
|
574
571
|
tmp_height = bottom_right_y - top_left_y
|
575
572
|
tmp_layer = layers[top_left_y:bottom_right_y, top_left_x:bottom_right_x].copy()
|
576
573
|
|
577
|
-
# Calculate the robot center in the temporary layer
|
578
574
|
tmp_x = x - top_left_x
|
579
575
|
tmp_y = y - top_left_y
|
580
576
|
|
581
|
-
#
|
582
|
-
r_scaled = radius
|
583
|
-
r_cover = r_scaled *
|
577
|
+
# All geometry proportional to radius
|
578
|
+
r_scaled: float = max(1.0, radius / 11.0)
|
579
|
+
r_cover = int(r_scaled * 10)
|
580
|
+
r_lidar = max(1, int(r_scaled * 3))
|
581
|
+
r_button = max(1, int(r_scaled * 1))
|
582
|
+
lidar_offset = int(radius * 0.6) # was fixed 15
|
583
|
+
button_offset = int(radius * 0.8) # was fixed 20
|
584
|
+
|
584
585
|
lidar_angle = np.deg2rad(angle + 90)
|
585
|
-
r_lidar = r_scaled * 3
|
586
|
-
r_button = r_scaled * 1
|
587
586
|
|
588
|
-
# Set colors based on robot state
|
589
587
|
if robot_state == "error":
|
590
588
|
outline = Drawable.ERROR_OUTLINE
|
591
589
|
fill = Drawable.ERROR_COLOR
|
592
590
|
else:
|
593
591
|
outline = (fill[0] // 2, fill[1] // 2, fill[2] // 2, fill[3])
|
594
592
|
|
595
|
-
#
|
593
|
+
# Body
|
596
594
|
tmp_layer = Drawable._filled_circle(
|
597
595
|
tmp_layer, (tmp_y, tmp_x), radius, fill, outline, 1
|
598
596
|
)
|
599
597
|
|
600
|
-
#
|
598
|
+
# Direction wedge
|
601
599
|
angle -= 90
|
602
|
-
a1 = ((angle + 90) - 80)
|
603
|
-
a2 = ((angle + 90) + 80)
|
604
|
-
x1 = int(tmp_x - r_cover *
|
605
|
-
y1 = int(tmp_y + r_cover *
|
606
|
-
x2 = int(tmp_x - r_cover *
|
607
|
-
y2 = int(tmp_y + r_cover *
|
608
|
-
|
609
|
-
# Draw the direction line
|
600
|
+
a1 = np.deg2rad((angle + 90) - 80)
|
601
|
+
a2 = np.deg2rad((angle + 90) + 80)
|
602
|
+
x1 = int(tmp_x - r_cover * np.sin(a1))
|
603
|
+
y1 = int(tmp_y + r_cover * np.cos(a1))
|
604
|
+
x2 = int(tmp_x - r_cover * np.sin(a2))
|
605
|
+
y2 = int(tmp_y + r_cover * np.cos(a2))
|
610
606
|
if (
|
611
607
|
0 <= x1 < tmp_width
|
612
608
|
and 0 <= y1 < tmp_height
|
@@ -615,25 +611,23 @@ class Drawable:
|
|
615
611
|
):
|
616
612
|
tmp_layer = Drawable._line(tmp_layer, x1, y1, x2, y2, outline, width=1)
|
617
613
|
|
618
|
-
#
|
619
|
-
lidar_x = int(tmp_x +
|
620
|
-
lidar_y = int(tmp_y +
|
614
|
+
# Lidar
|
615
|
+
lidar_x = int(tmp_x + lidar_offset * np.cos(lidar_angle))
|
616
|
+
lidar_y = int(tmp_y + lidar_offset * np.sin(lidar_angle))
|
621
617
|
if 0 <= lidar_x < tmp_width and 0 <= lidar_y < tmp_height:
|
622
618
|
tmp_layer = Drawable._filled_circle(
|
623
619
|
tmp_layer, (lidar_y, lidar_x), r_lidar, outline
|
624
620
|
)
|
625
621
|
|
626
|
-
#
|
627
|
-
butt_x = int(tmp_x -
|
628
|
-
butt_y = int(tmp_y -
|
622
|
+
# Button
|
623
|
+
butt_x = int(tmp_x - button_offset * np.cos(lidar_angle))
|
624
|
+
butt_y = int(tmp_y - button_offset * np.sin(lidar_angle))
|
629
625
|
if 0 <= butt_x < tmp_width and 0 <= butt_y < tmp_height:
|
630
626
|
tmp_layer = Drawable._filled_circle(
|
631
627
|
tmp_layer, (butt_y, butt_x), r_button, outline
|
632
628
|
)
|
633
629
|
|
634
|
-
# Copy the robot layer back to the main layer
|
635
630
|
layers[top_left_y:bottom_right_y, top_left_x:bottom_right_x] = tmp_layer
|
636
|
-
|
637
631
|
return layers
|
638
632
|
|
639
633
|
@staticmethod
|
@@ -781,11 +775,11 @@ class Drawable:
|
|
781
775
|
continue
|
782
776
|
|
783
777
|
t = np.linspace(0, 1, length * 2)
|
784
|
-
|
785
|
-
|
778
|
+
x_coordinates = np.round(x1 * (1 - t) + x2 * t).astype(int)
|
779
|
+
y_coordinates = np.round(y1 * (1 - t) + y2 * t).astype(int)
|
786
780
|
|
787
781
|
# Add line points to mask
|
788
|
-
for x, y in zip(
|
782
|
+
for x, y in zip(x_coordinates, y_coordinates):
|
789
783
|
if width == 1:
|
790
784
|
mask[y, x] = True
|
791
785
|
else:
|
@@ -827,7 +821,6 @@ class Drawable:
|
|
827
821
|
|
828
822
|
# Precompute circular mask for radius
|
829
823
|
radius = 6
|
830
|
-
diameter = radius * 2 + 1
|
831
824
|
yy, xx = np.ogrid[-radius : radius + 1, -radius : radius + 1]
|
832
825
|
circle_mask = (xx**2 + yy**2) <= radius**2
|
833
826
|
|
@@ -882,11 +875,25 @@ class Drawable:
|
|
882
875
|
position: bool,
|
883
876
|
) -> None:
|
884
877
|
"""Draw the status text on the image."""
|
885
|
-
|
886
|
-
|
887
|
-
|
888
|
-
|
889
|
-
|
878
|
+
module_dir = Path(__file__).resolve().parent
|
879
|
+
default_font_path = module_dir / "fonts" / "FiraSans.ttf"
|
880
|
+
# Load default font with safety fallback to PIL's built-in if missing
|
881
|
+
try:
|
882
|
+
default_font = ImageFont.truetype(str(default_font_path), size)
|
883
|
+
except OSError:
|
884
|
+
_LOGGER.warning(
|
885
|
+
"Default font not found at %s; using PIL default font",
|
886
|
+
default_font_path,
|
887
|
+
)
|
888
|
+
default_font = ImageFont.load_default()
|
889
|
+
|
890
|
+
# Use provided font directly if available; else fall back to default
|
891
|
+
user_font = default_font
|
892
|
+
if path_font:
|
893
|
+
try:
|
894
|
+
user_font = ImageFont.truetype(str(path_font), size)
|
895
|
+
except OSError:
|
896
|
+
user_font = default_font
|
890
897
|
if position:
|
891
898
|
x, y = 10, 10
|
892
899
|
else:
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
@@ -1,7 +1,7 @@
|
|
1
1
|
"""
|
2
2
|
Class Camera Shared.
|
3
3
|
Keep the data between the modules.
|
4
|
-
Version: v0.1.
|
4
|
+
Version: v0.1.10
|
5
5
|
"""
|
6
6
|
|
7
7
|
import asyncio
|
@@ -58,6 +58,7 @@ class CameraShared:
|
|
58
58
|
self.frame_number: int = 0 # camera Frame number
|
59
59
|
self.destinations: list = [] # MQTT rand destinations
|
60
60
|
self.rand256_active_zone: list = [] # Active zone for rand256
|
61
|
+
self.rand256_zone_coordinates: list = [] # Active zone coordinates for rand256
|
61
62
|
self.is_rand: bool = False # MQTT rand data
|
62
63
|
self._new_mqtt_message = False # New MQTT message
|
63
64
|
# Initialize last_image with default gray image (250x150 minimum)
|
@@ -69,6 +70,7 @@ class CameraShared:
|
|
69
70
|
self.image_last_updated: float = 0.0 # Last image update time
|
70
71
|
self.image_format = "image/pil" # Image format
|
71
72
|
self.image_size = None # Image size
|
73
|
+
self.robot_size = None # Robot size
|
72
74
|
self.image_auto_zoom: bool = False # Auto zoom image
|
73
75
|
self.image_zoom_lock_ratio: bool = True # Zoom lock ratio
|
74
76
|
self.image_ref_height: int = 0 # Image reference height
|
@@ -81,8 +83,7 @@ class CameraShared:
|
|
81
83
|
self.user_colors = Colors # User base colors
|
82
84
|
self.rooms_colors = Colors # Rooms colors
|
83
85
|
self.vacuum_battery = 0 # Vacuum battery state
|
84
|
-
self.
|
85
|
-
self.vacuum_connection = None # Vacuum connection state
|
86
|
+
self.vacuum_connection = False # Vacuum connection state
|
86
87
|
self.vacuum_state = None # Vacuum state
|
87
88
|
self.charger_position = None # Vacuum Charger position
|
88
89
|
self.show_vacuum_state = None # Show vacuum state on the map
|
@@ -197,14 +198,13 @@ class CameraShared:
|
|
197
198
|
attrs = {
|
198
199
|
ATTR_CAMERA_MODE: self.camera_mode,
|
199
200
|
ATTR_VACUUM_BATTERY: f"{self.vacuum_battery}%",
|
200
|
-
ATTR_VACUUM_CHARGING: self.vacuum_bat_charged,
|
201
|
+
ATTR_VACUUM_CHARGING: self.vacuum_bat_charged(),
|
201
202
|
ATTR_VACUUM_POSITION: self.current_room,
|
202
203
|
ATTR_VACUUM_STATUS: self.vacuum_state,
|
203
204
|
ATTR_VACUUM_JSON_ID: self.vac_json_id,
|
204
205
|
ATTR_CALIBRATION_POINTS: self.attr_calibration_points,
|
205
206
|
}
|
206
207
|
if self.obstacles_pos and self.vacuum_ips:
|
207
|
-
_LOGGER.debug("Generating obstacle links from: %s", self.obstacles_pos)
|
208
208
|
self.obstacles_data = self._compose_obstacle_links(
|
209
209
|
self.vacuum_ips, self.obstacles_pos
|
210
210
|
)
|
@@ -302,19 +302,20 @@ class CameraSharedManager:
|
|
302
302
|
)
|
303
303
|
# Ensure trims are updated correctly
|
304
304
|
trim_data = device_info.get("trims_data", DEFAULT_VALUES["trims_data"])
|
305
|
-
_LOGGER.debug(
|
306
|
-
"%s: Updating shared trims with: %s", instance.file_name, trim_data
|
307
|
-
)
|
308
305
|
instance.trims = TrimsData.from_dict(trim_data)
|
306
|
+
# Robot size
|
307
|
+
instance.robot_size = device_info.get("robot_size", 25)
|
309
308
|
|
310
309
|
except TypeError as ex:
|
311
|
-
_LOGGER.
|
310
|
+
_LOGGER.warning(
|
311
|
+
"Shared data can't be initialized due to a TypeError! %s", ex
|
312
|
+
)
|
312
313
|
except AttributeError as ex:
|
313
|
-
_LOGGER.
|
314
|
+
_LOGGER.warning(
|
314
315
|
"Shared data can't be initialized due to an AttributeError! %s", ex
|
315
316
|
)
|
316
317
|
except RuntimeError as ex:
|
317
|
-
_LOGGER.
|
318
|
+
_LOGGER.warning(
|
318
319
|
"An unexpected error occurred while initializing shared data %s:", ex
|
319
320
|
)
|
320
321
|
|
@@ -0,0 +1,95 @@
|
|
1
|
+
"""
|
2
|
+
Version: 0.1.10
|
3
|
+
Status text of the vacuum cleaners.
|
4
|
+
Class to handle the status text of the vacuum cleaners.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from __future__ import annotations
|
8
|
+
|
9
|
+
from ..types import LOGGER, PilPNG
|
10
|
+
from .translations import translations
|
11
|
+
|
12
|
+
LOGGER.propagate = True
|
13
|
+
|
14
|
+
|
15
|
+
class StatusText:
|
16
|
+
"""
|
17
|
+
Status text of the vacuum cleaners.
|
18
|
+
"""
|
19
|
+
|
20
|
+
def __init__(self, camera_shared):
|
21
|
+
self._shared = camera_shared
|
22
|
+
self.file_name = self._shared.file_name
|
23
|
+
|
24
|
+
@staticmethod
|
25
|
+
async def get_vacuum_status_translation(
|
26
|
+
language: str = "en",
|
27
|
+
) -> dict[str, str] | None:
|
28
|
+
"""
|
29
|
+
Get the vacuum status translation.
|
30
|
+
@param language: Language code, default 'en'.
|
31
|
+
@return: Mapping for the given language or None.
|
32
|
+
"""
|
33
|
+
return translations.get((language or "en").lower())
|
34
|
+
|
35
|
+
async def translate_vacuum_status(self) -> str:
|
36
|
+
"""Return the translated status with EN fallback and safe default."""
|
37
|
+
status = self._shared.vacuum_state or "unknown"
|
38
|
+
language = (self._shared.user_language or "en").lower()
|
39
|
+
translation = await self.get_vacuum_status_translation(language)
|
40
|
+
if not translation:
|
41
|
+
translation = translations.get("en", {})
|
42
|
+
return translation.get(status, str(status).capitalize())
|
43
|
+
|
44
|
+
async def get_status_text(self, text_img: PilPNG) -> tuple[list[str], int]:
|
45
|
+
"""
|
46
|
+
Compose the image status text.
|
47
|
+
:param text_img: Image to draw the text on.
|
48
|
+
:return status_text, text_size: List of the status text and the text size.
|
49
|
+
"""
|
50
|
+
status_text = ["If you read me, something really went wrong.."] # default text
|
51
|
+
text_size_coverage = 1.5 # resize factor for the text
|
52
|
+
text_size = self._shared.vacuum_status_size # default text size
|
53
|
+
charge_level = "\u03de" # unicode Koppa symbol
|
54
|
+
charging = "\u2211" # unicode Charging symbol
|
55
|
+
vacuum_state = await self.translate_vacuum_status()
|
56
|
+
if self._shared.show_vacuum_state:
|
57
|
+
status_text = [f"{self.file_name}: {vacuum_state}"]
|
58
|
+
language = (self._shared.user_language or "en").lower()
|
59
|
+
lang_map = translations.get(language) or translations.get("en", {})
|
60
|
+
if not self._shared.vacuum_connection:
|
61
|
+
mqtt_disc = lang_map.get(
|
62
|
+
"mqtt_disconnected",
|
63
|
+
translations.get("en", {}).get(
|
64
|
+
"mqtt_disconnected", "Disconnected from MQTT?"
|
65
|
+
),
|
66
|
+
)
|
67
|
+
status_text = [f"{self.file_name}: {mqtt_disc}"]
|
68
|
+
else:
|
69
|
+
if self._shared.current_room:
|
70
|
+
in_room = self._shared.current_room.get("in_room")
|
71
|
+
if in_room:
|
72
|
+
status_text.append(f" ({in_room})")
|
73
|
+
if self._shared.vacuum_state == "docked":
|
74
|
+
if self._shared.vacuum_bat_charged():
|
75
|
+
status_text.append(" \u00b7 ")
|
76
|
+
status_text.append(f"{charging}{charge_level} ")
|
77
|
+
status_text.append(f"{self._shared.vacuum_battery}%")
|
78
|
+
else:
|
79
|
+
status_text.append(" \u00b7 ")
|
80
|
+
status_text.append(f"{charge_level} ")
|
81
|
+
ready_txt = lang_map.get(
|
82
|
+
"ready",
|
83
|
+
translations.get("en", {}).get("ready", "Ready."),
|
84
|
+
)
|
85
|
+
status_text.append(ready_txt)
|
86
|
+
else:
|
87
|
+
status_text.append(" \u00b7 ")
|
88
|
+
status_text.append(f"{charge_level}")
|
89
|
+
status_text.append(f" {self._shared.vacuum_battery}%")
|
90
|
+
if text_size >= 50 and getattr(text_img, "width", None):
|
91
|
+
text_pixels = max(1, sum(len(text) for text in status_text))
|
92
|
+
text_size = int(
|
93
|
+
(text_size_coverage * text_img.width) // text_pixels
|
94
|
+
)
|
95
|
+
return status_text, text_size
|