valetudo-map-parser 0.1.8__py3-none-any.whl → 0.1.9a2__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 +19 -12
- valetudo_map_parser/config/auto_crop.py +174 -116
- valetudo_map_parser/config/color_utils.py +105 -0
- valetudo_map_parser/config/colors.py +662 -13
- valetudo_map_parser/config/drawable.py +624 -279
- valetudo_map_parser/config/drawable_elements.py +292 -0
- valetudo_map_parser/config/enhanced_drawable.py +324 -0
- valetudo_map_parser/config/optimized_element_map.py +406 -0
- valetudo_map_parser/config/rand25_parser.py +42 -28
- valetudo_map_parser/config/room_outline.py +148 -0
- valetudo_map_parser/config/shared.py +73 -6
- valetudo_map_parser/config/types.py +102 -51
- valetudo_map_parser/config/utils.py +841 -0
- valetudo_map_parser/hypfer_draw.py +398 -132
- valetudo_map_parser/hypfer_handler.py +259 -241
- valetudo_map_parser/hypfer_rooms_handler.py +599 -0
- valetudo_map_parser/map_data.py +45 -64
- valetudo_map_parser/rand25_handler.py +429 -310
- valetudo_map_parser/reimg_draw.py +55 -74
- valetudo_map_parser/rooms_handler.py +470 -0
- valetudo_map_parser-0.1.9a2.dist-info/METADATA +93 -0
- valetudo_map_parser-0.1.9a2.dist-info/RECORD +27 -0
- {valetudo_map_parser-0.1.8.dist-info → valetudo_map_parser-0.1.9a2.dist-info}/WHEEL +1 -1
- valetudo_map_parser/images_utils.py +0 -398
- valetudo_map_parser-0.1.8.dist-info/METADATA +0 -23
- valetudo_map_parser-0.1.8.dist-info/RECORD +0 -20
- {valetudo_map_parser-0.1.8.dist-info → valetudo_map_parser-0.1.9a2.dist-info}/LICENSE +0 -0
- {valetudo_map_parser-0.1.8.dist-info → valetudo_map_parser-0.1.9a2.dist-info}/NOTICE.txt +0 -0
@@ -0,0 +1,148 @@
|
|
1
|
+
"""
|
2
|
+
Room Outline Extraction Utilities.
|
3
|
+
Uses scipy for efficient room outline extraction.
|
4
|
+
Version: 0.1.9
|
5
|
+
"""
|
6
|
+
|
7
|
+
from __future__ import annotations
|
8
|
+
|
9
|
+
import numpy as np
|
10
|
+
from scipy import ndimage
|
11
|
+
|
12
|
+
from .types import LOGGER
|
13
|
+
|
14
|
+
|
15
|
+
async def extract_room_outline_with_scipy(
|
16
|
+
room_mask, min_x, min_y, max_x, max_y, file_name=None, room_id=None
|
17
|
+
):
|
18
|
+
"""Extract a room outline using scipy for contour finding.
|
19
|
+
|
20
|
+
Args:
|
21
|
+
room_mask: Binary mask of the room (1 for room, 0 for non-room)
|
22
|
+
min_x, min_y, max_x, max_y: Bounding box coordinates
|
23
|
+
file_name: Optional file name for logging
|
24
|
+
room_id: Optional room ID for logging
|
25
|
+
|
26
|
+
Returns:
|
27
|
+
List of points forming the outline of the room
|
28
|
+
"""
|
29
|
+
# If the mask is empty, return a rectangular outline
|
30
|
+
if np.sum(room_mask) == 0:
|
31
|
+
LOGGER.warning(
|
32
|
+
"%s: Empty room mask for room %s, using rectangular outline",
|
33
|
+
file_name or "RoomOutline",
|
34
|
+
str(room_id) if room_id is not None else "unknown",
|
35
|
+
)
|
36
|
+
return [(min_x, min_y), (max_x, min_y), (max_x, max_y), (min_x, max_y)]
|
37
|
+
|
38
|
+
# Use scipy to clean up the mask (remove noise, fill holes)
|
39
|
+
# Fill small holes
|
40
|
+
room_mask = ndimage.binary_fill_holes(room_mask).astype(np.uint8)
|
41
|
+
|
42
|
+
# Remove small objects
|
43
|
+
labeled_array, num_features = ndimage.label(room_mask)
|
44
|
+
if num_features > 1:
|
45
|
+
# Find the largest connected component
|
46
|
+
component_sizes = np.bincount(labeled_array.ravel())[1:]
|
47
|
+
largest_component = np.argmax(component_sizes) + 1
|
48
|
+
room_mask = (labeled_array == largest_component).astype(np.uint8)
|
49
|
+
|
50
|
+
# Find the boundary points by tracing the perimeter
|
51
|
+
boundary_points = []
|
52
|
+
height, width = room_mask.shape
|
53
|
+
|
54
|
+
# Scan horizontally (top and bottom edges)
|
55
|
+
for x in range(width):
|
56
|
+
# Top edge
|
57
|
+
for y in range(height):
|
58
|
+
if room_mask[y, x] == 1:
|
59
|
+
boundary_points.append((x + min_x, y + min_y))
|
60
|
+
break
|
61
|
+
|
62
|
+
# Bottom edge
|
63
|
+
for y in range(height - 1, -1, -1):
|
64
|
+
if room_mask[y, x] == 1:
|
65
|
+
boundary_points.append((x + min_x, y + min_y))
|
66
|
+
break
|
67
|
+
|
68
|
+
# Scan vertically (left and right edges)
|
69
|
+
for y in range(height):
|
70
|
+
# Left edge
|
71
|
+
for x in range(width):
|
72
|
+
if room_mask[y, x] == 1:
|
73
|
+
boundary_points.append((x + min_x, y + min_y))
|
74
|
+
break
|
75
|
+
|
76
|
+
# Right edge
|
77
|
+
for x in range(width - 1, -1, -1):
|
78
|
+
if room_mask[y, x] == 1:
|
79
|
+
boundary_points.append((x + min_x, y + min_y))
|
80
|
+
break
|
81
|
+
|
82
|
+
# Remove duplicates while preserving order
|
83
|
+
unique_points = []
|
84
|
+
for point in boundary_points:
|
85
|
+
if point not in unique_points:
|
86
|
+
unique_points.append(point)
|
87
|
+
|
88
|
+
# If we have too few points, return a simple rectangle
|
89
|
+
if len(unique_points) < 4:
|
90
|
+
LOGGER.warning(
|
91
|
+
"%s: Too few boundary points for room %s, using rectangular outline",
|
92
|
+
file_name or "RoomOutline",
|
93
|
+
str(room_id) if room_id is not None else "unknown",
|
94
|
+
)
|
95
|
+
return [(min_x, min_y), (max_x, min_y), (max_x, max_y), (min_x, max_y)]
|
96
|
+
|
97
|
+
# Simplify the outline by keeping only significant points
|
98
|
+
simplified = simplify_outline(unique_points, tolerance=5)
|
99
|
+
|
100
|
+
LOGGER.debug(
|
101
|
+
"%s: Extracted outline for room %s with %d points",
|
102
|
+
file_name or "RoomOutline",
|
103
|
+
str(room_id) if room_id is not None else "unknown",
|
104
|
+
len(simplified),
|
105
|
+
)
|
106
|
+
|
107
|
+
return simplified
|
108
|
+
|
109
|
+
|
110
|
+
def simplify_outline(points, tolerance=5):
|
111
|
+
"""Simplify an outline by removing points that don't contribute much to the shape."""
|
112
|
+
if len(points) <= 4:
|
113
|
+
return points
|
114
|
+
|
115
|
+
# Start with the first point
|
116
|
+
simplified = [points[0]]
|
117
|
+
|
118
|
+
# Process remaining points
|
119
|
+
for i in range(1, len(points) - 1):
|
120
|
+
# Get previous and next points
|
121
|
+
prev = simplified[-1]
|
122
|
+
current = points[i]
|
123
|
+
next_point = points[i + 1]
|
124
|
+
|
125
|
+
# Calculate vectors
|
126
|
+
v1 = (current[0] - prev[0], current[1] - prev[1])
|
127
|
+
v2 = (next_point[0] - current[0], next_point[1] - current[1])
|
128
|
+
|
129
|
+
# Calculate change in direction
|
130
|
+
dot_product = v1[0] * v2[0] + v1[1] * v2[1]
|
131
|
+
len_v1 = (v1[0] ** 2 + v1[1] ** 2) ** 0.5
|
132
|
+
len_v2 = (v2[0] ** 2 + v2[1] ** 2) ** 0.5
|
133
|
+
|
134
|
+
# Avoid division by zero
|
135
|
+
if len_v1 == 0 or len_v2 == 0:
|
136
|
+
continue
|
137
|
+
|
138
|
+
# Calculate cosine of angle between vectors
|
139
|
+
cos_angle = dot_product / (len_v1 * len_v2)
|
140
|
+
|
141
|
+
# If angle is significant or distance is large, keep the point
|
142
|
+
if abs(cos_angle) < 0.95 or len_v1 > tolerance or len_v2 > tolerance:
|
143
|
+
simplified.append(current)
|
144
|
+
|
145
|
+
# Add the last point
|
146
|
+
simplified.append(points[-1])
|
147
|
+
|
148
|
+
return simplified
|
@@ -6,10 +6,13 @@ Version: v2024.12.0
|
|
6
6
|
|
7
7
|
import asyncio
|
8
8
|
import logging
|
9
|
+
from typing import List
|
9
10
|
|
10
11
|
from .types import (
|
11
12
|
ATTR_CALIBRATION_POINTS,
|
13
|
+
ATTR_CAMERA_MODE,
|
12
14
|
ATTR_MARGINS,
|
15
|
+
ATTR_OBSTACLES,
|
13
16
|
ATTR_POINTS,
|
14
17
|
ATTR_ROOMS,
|
15
18
|
ATTR_ROTATE,
|
@@ -19,8 +22,6 @@ from .types import (
|
|
19
22
|
ATTR_VACUUM_POSITION,
|
20
23
|
ATTR_VACUUM_STATUS,
|
21
24
|
ATTR_ZONES,
|
22
|
-
ATTR_CAMERA_MODE,
|
23
|
-
ATTR_OBSTACLES,
|
24
25
|
CONF_ASPECT_RATIO,
|
25
26
|
CONF_AUTO_ZOOM,
|
26
27
|
CONF_OFFSET_BOTTOM,
|
@@ -35,8 +36,10 @@ from .types import (
|
|
35
36
|
CONF_ZOOM_LOCK_RATIO,
|
36
37
|
DEFAULT_VALUES,
|
37
38
|
CameraModes,
|
39
|
+
Colors,
|
40
|
+
TrimsData,
|
38
41
|
)
|
39
|
-
|
42
|
+
|
40
43
|
|
41
44
|
_LOGGER = logging.getLogger(__name__)
|
42
45
|
|
@@ -85,6 +88,7 @@ class CameraShared:
|
|
85
88
|
self.vac_json_id = None # Vacuum json id
|
86
89
|
self.margins = "100" # Image margins
|
87
90
|
self.obstacles_data = None # Obstacles data
|
91
|
+
self.obstacles_pos = None # Obstacles position
|
88
92
|
self.offset_top = 0 # Image offset top
|
89
93
|
self.offset_down = 0 # Image offset down
|
90
94
|
self.offset_left = 0 # Image offset left
|
@@ -99,8 +103,50 @@ class CameraShared:
|
|
99
103
|
self.map_pred_points = None # Predefined points data
|
100
104
|
self.map_new_path = None # New path data
|
101
105
|
self.map_old_path = None # Old path data
|
102
|
-
self.trim_crop_data = None
|
103
106
|
self.user_language = None # User language
|
107
|
+
self.trim_crop_data = None
|
108
|
+
self.trims = TrimsData.from_dict(DEFAULT_VALUES["trims_data"]) # Trims data
|
109
|
+
self.skip_room_ids: List[str] = []
|
110
|
+
self.device_info = None # Store the device_info
|
111
|
+
|
112
|
+
@staticmethod
|
113
|
+
def _compose_obstacle_links(vacuum_host_ip: str, obstacles: list) -> list | None:
|
114
|
+
"""
|
115
|
+
Compose JSON with obstacle details including the image link.
|
116
|
+
"""
|
117
|
+
obstacle_links = []
|
118
|
+
if not obstacles or not vacuum_host_ip:
|
119
|
+
return None
|
120
|
+
|
121
|
+
for obstacle in obstacles:
|
122
|
+
# Extract obstacle details
|
123
|
+
label = obstacle.get("label", "")
|
124
|
+
points = obstacle.get("points", {})
|
125
|
+
image_id = obstacle.get("id", "None")
|
126
|
+
|
127
|
+
if label and points and image_id and vacuum_host_ip:
|
128
|
+
# Append formatted obstacle data
|
129
|
+
if image_id != "None":
|
130
|
+
# Compose the link
|
131
|
+
image_link = (
|
132
|
+
f"http://{vacuum_host_ip}"
|
133
|
+
f"/api/v2/robot/capabilities/ObstacleImagesCapability/img/{image_id}"
|
134
|
+
)
|
135
|
+
obstacle_links.append(
|
136
|
+
{
|
137
|
+
"point": points,
|
138
|
+
"label": label,
|
139
|
+
"link": image_link,
|
140
|
+
}
|
141
|
+
)
|
142
|
+
else:
|
143
|
+
obstacle_links.append(
|
144
|
+
{
|
145
|
+
"point": points,
|
146
|
+
"label": label,
|
147
|
+
}
|
148
|
+
)
|
149
|
+
return obstacle_links
|
104
150
|
|
105
151
|
def update_user_colors(self, user_colors):
|
106
152
|
"""Update the user colors."""
|
@@ -118,6 +164,11 @@ class CameraShared:
|
|
118
164
|
"""Get the rooms colors."""
|
119
165
|
return self.rooms_colors
|
120
166
|
|
167
|
+
def reset_trims(self) -> dict:
|
168
|
+
"""Reset the trims."""
|
169
|
+
self.trims = TrimsData.from_dict(DEFAULT_VALUES["trims_data"])
|
170
|
+
return self.trims
|
171
|
+
|
121
172
|
async def batch_update(self, **kwargs):
|
122
173
|
"""Batch update multiple attributes."""
|
123
174
|
for key, value in kwargs.items():
|
@@ -137,7 +188,11 @@ class CameraShared:
|
|
137
188
|
ATTR_VACUUM_JSON_ID: self.vac_json_id,
|
138
189
|
ATTR_CALIBRATION_POINTS: self.attr_calibration_points,
|
139
190
|
}
|
140
|
-
if self.
|
191
|
+
if self.obstacles_pos and self.vacuum_ips:
|
192
|
+
_LOGGER.debug("Generating obstacle links from: %s", self.obstacles_pos)
|
193
|
+
self.obstacles_data = self._compose_obstacle_links(
|
194
|
+
self.vacuum_ips, self.obstacles_pos
|
195
|
+
)
|
141
196
|
attrs[ATTR_OBSTACLES] = self.obstacles_data
|
142
197
|
|
143
198
|
if self.enable_snapshots:
|
@@ -167,6 +222,7 @@ class CameraSharedManager:
|
|
167
222
|
self._lock = asyncio.Lock()
|
168
223
|
self.file_name = file_name
|
169
224
|
self.device_info = device_info
|
225
|
+
self.update_shared_data(device_info)
|
170
226
|
|
171
227
|
# Automatically initialize shared data for the instance
|
172
228
|
# self._init_shared_data(device_info)
|
@@ -176,6 +232,12 @@ class CameraSharedManager:
|
|
176
232
|
instance = self.get_instance() # Retrieve the correct instance
|
177
233
|
|
178
234
|
try:
|
235
|
+
# Store the device_info in the instance
|
236
|
+
instance.device_info = device_info
|
237
|
+
_LOGGER.info(
|
238
|
+
"%s: Stored device_info in shared instance", instance.file_name
|
239
|
+
)
|
240
|
+
|
179
241
|
instance.attr_calibration_points = None
|
180
242
|
|
181
243
|
# Initialize shared data with defaults from DEFAULT_VALUES
|
@@ -218,11 +280,16 @@ class CameraSharedManager:
|
|
218
280
|
instance.vacuum_status_position = device_info.get(
|
219
281
|
CONF_VAC_STAT_POS, DEFAULT_VALUES["vac_status_position"]
|
220
282
|
)
|
221
|
-
|
222
283
|
# If enable_snapshots, check for png in www.
|
223
284
|
instance.enable_snapshots = device_info.get(
|
224
285
|
CONF_SNAPSHOTS_ENABLE, DEFAULT_VALUES["enable_www_snapshots"]
|
225
286
|
)
|
287
|
+
# Ensure trims are updated correctly
|
288
|
+
trim_data = device_info.get("trims_data", DEFAULT_VALUES["trims_data"])
|
289
|
+
_LOGGER.debug(
|
290
|
+
"%s: Updating shared trims with: %s", instance.file_name, trim_data
|
291
|
+
)
|
292
|
+
instance.trims = TrimsData.from_dict(trim_data)
|
226
293
|
|
227
294
|
except TypeError as ex:
|
228
295
|
_LOGGER.error("Shared data can't be initialized due to a TypeError! %s", ex)
|
@@ -4,29 +4,27 @@ Version 0.0.1
|
|
4
4
|
"""
|
5
5
|
|
6
6
|
import asyncio
|
7
|
-
from dataclasses import dataclass
|
8
7
|
import json
|
9
8
|
import logging
|
10
|
-
|
9
|
+
import threading
|
10
|
+
from dataclasses import asdict, dataclass
|
11
|
+
from typing import Any, Dict, Optional, Tuple, TypedDict, Union
|
11
12
|
|
12
|
-
from PIL import Image
|
13
13
|
import numpy as np
|
14
|
+
from PIL import Image
|
15
|
+
|
14
16
|
|
15
17
|
DEFAULT_ROOMS = 1
|
16
18
|
|
17
|
-
|
19
|
+
LOGGER = logging.getLogger(__package__)
|
18
20
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
JsonType = Any # json.loads() return type is Any
|
27
|
-
PilPNG = Image.Image
|
28
|
-
NumpyArray = np.ndarray
|
29
|
-
Point = Tuple[int, int]
|
21
|
+
|
22
|
+
class RoomProperty(TypedDict):
|
23
|
+
number: int
|
24
|
+
outline: list[tuple[int, int]]
|
25
|
+
name: str
|
26
|
+
x: int
|
27
|
+
y: int
|
30
28
|
|
31
29
|
|
32
30
|
# pylint: disable=no-member
|
@@ -73,43 +71,37 @@ class TrimCropData:
|
|
73
71
|
)
|
74
72
|
|
75
73
|
|
76
|
-
# pylint: disable=no-member
|
77
74
|
class RoomStore:
|
78
|
-
""
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
75
|
+
_instances: Dict[str, "RoomStore"] = {}
|
76
|
+
_lock = threading.Lock()
|
77
|
+
|
78
|
+
def __new__(cls, vacuum_id: str, rooms_data: Optional[dict] = None) -> "RoomStore":
|
79
|
+
with cls._lock:
|
80
|
+
if vacuum_id not in cls._instances:
|
81
|
+
instance = super(RoomStore, cls).__new__(cls)
|
82
|
+
instance.vacuum_id = vacuum_id
|
83
|
+
instance.vacuums_data = rooms_data or {}
|
84
|
+
cls._instances[vacuum_id] = instance
|
85
|
+
else:
|
86
|
+
if rooms_data is not None:
|
87
|
+
cls._instances[vacuum_id].vacuums_data = rooms_data
|
88
|
+
return cls._instances[vacuum_id]
|
89
|
+
|
90
|
+
def get_rooms(self) -> dict:
|
91
|
+
return self.vacuums_data
|
92
|
+
|
93
|
+
def set_rooms(self, rooms_data: dict) -> None:
|
94
|
+
self.vacuums_data = rooms_data
|
95
|
+
|
96
|
+
def get_rooms_count(self) -> int:
|
97
|
+
if isinstance(self.vacuums_data, dict):
|
98
|
+
count = len(self.vacuums_data)
|
99
|
+
return count if count > 0 else DEFAULT_ROOMS
|
100
|
+
return DEFAULT_ROOMS
|
96
101
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
data = self.vacuums_data.get(vacuum_id, {})
|
101
|
-
if isinstance(data, str):
|
102
|
-
json_data = json.loads(data)
|
103
|
-
return json_data
|
104
|
-
return data
|
105
|
-
|
106
|
-
async def async_get_rooms_count(self, vacuum_id: str) -> int:
|
107
|
-
"""Count the number of rooms for a vacuum."""
|
108
|
-
async with self._lock:
|
109
|
-
count = len(self.vacuums_data.get(vacuum_id, {}))
|
110
|
-
if count == 0:
|
111
|
-
return DEFAULT_ROOMS
|
112
|
-
return count
|
102
|
+
@classmethod
|
103
|
+
def get_all_instances(cls) -> Dict[str, "RoomStore"]:
|
104
|
+
return cls._instances
|
113
105
|
|
114
106
|
|
115
107
|
# pylint: disable=no-member
|
@@ -202,8 +194,19 @@ class SnapshotStore:
|
|
202
194
|
self.vacuum_json_data[vacuum_id] = json_data
|
203
195
|
|
204
196
|
|
197
|
+
Color = Union[Tuple[int, int, int], Tuple[int, int, int, int]]
|
198
|
+
Colors = Dict[str, Color]
|
199
|
+
CalibrationPoints = list[dict[str, Any]]
|
200
|
+
RobotPosition = dict[str, int | float]
|
201
|
+
ChargerPosition = dict[str, Any]
|
202
|
+
RoomsProperties = dict[str, RoomProperty]
|
203
|
+
ImageSize = dict[str, int | list[int]]
|
204
|
+
JsonType = Any # json.loads() return type is Any
|
205
|
+
PilPNG = Image.Image
|
206
|
+
NumpyArray = np.ndarray
|
207
|
+
Point = Tuple[int, int]
|
208
|
+
|
205
209
|
CAMERA_STORAGE = "valetudo_camera"
|
206
|
-
DEFAULT_ROOMS = 1 # 15 is the maximum number of rooms.
|
207
210
|
ATTR_ROTATE = "rotate_image"
|
208
211
|
ATTR_CROP = "crop_image"
|
209
212
|
ATTR_MARGINS = "margins"
|
@@ -284,6 +287,7 @@ DEFAULT_VALUES = {
|
|
284
287
|
"vac_status_position": True,
|
285
288
|
"get_svg_file": False,
|
286
289
|
"save_trims": True,
|
290
|
+
"trims_data": {"trim_left": 0, "trim_up": 0, "trim_right": 0, "trim_down": 0},
|
287
291
|
"enable_www_snapshots": False,
|
288
292
|
"color_charger": [255, 128, 0],
|
289
293
|
"color_move": [238, 247, 255],
|
@@ -345,6 +349,7 @@ KEYS_TO_UPDATE = [
|
|
345
349
|
"offset_bottom",
|
346
350
|
"offset_left",
|
347
351
|
"offset_right",
|
352
|
+
"trims_data",
|
348
353
|
"auto_zoom",
|
349
354
|
"zoom_lock_ratio",
|
350
355
|
"show_vac_status",
|
@@ -588,3 +593,49 @@ class CameraModes:
|
|
588
593
|
CAMERA_STANDBY = "camera_standby"
|
589
594
|
CAMERA_OFF = False
|
590
595
|
CAMERA_ON = True
|
596
|
+
|
597
|
+
|
598
|
+
# noinspection PyTypeChecker
|
599
|
+
@dataclass
|
600
|
+
class TrimsData:
|
601
|
+
"""Dataclass to store and manage trims data."""
|
602
|
+
|
603
|
+
floor: str = ""
|
604
|
+
trim_up: int = 0
|
605
|
+
trim_left: int = 0
|
606
|
+
trim_down: int = 0
|
607
|
+
trim_right: int = 0
|
608
|
+
|
609
|
+
@classmethod
|
610
|
+
def from_json(cls, json_data: str):
|
611
|
+
"""Create a TrimsConfig instance from a JSON string."""
|
612
|
+
data = json.loads(json_data)
|
613
|
+
return cls(
|
614
|
+
floor=data.get("floor", ""),
|
615
|
+
trim_up=data.get("trim_up", 0),
|
616
|
+
trim_left=data.get("trim_left", 0),
|
617
|
+
trim_down=data.get("trim_down", 0),
|
618
|
+
trim_right=data.get("trim_right", 0),
|
619
|
+
)
|
620
|
+
|
621
|
+
def to_json(self) -> str:
|
622
|
+
"""Convert TrimsConfig instance to a JSON string."""
|
623
|
+
return json.dumps(asdict(self))
|
624
|
+
|
625
|
+
@classmethod
|
626
|
+
def from_dict(cls, data: dict):
|
627
|
+
"""Initialize TrimData from a dictionary."""
|
628
|
+
return cls(**data)
|
629
|
+
|
630
|
+
def to_dict(self) -> dict:
|
631
|
+
"""Convert TrimData to a dictionary."""
|
632
|
+
return asdict(self)
|
633
|
+
|
634
|
+
def clear(self) -> dict:
|
635
|
+
"""Clear all the trims."""
|
636
|
+
self.floor = ""
|
637
|
+
self.trim_up = 0
|
638
|
+
self.trim_left = 0
|
639
|
+
self.trim_down = 0
|
640
|
+
self.trim_right = 0
|
641
|
+
return asdict(self)
|