valetudo-map-parser 0.1.11b1__py3-none-any.whl → 0.1.12b0__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.
@@ -110,82 +110,17 @@ class BaseHandler:
110
110
  """
111
111
  try:
112
112
  # Backup current image to last_image before processing new one
113
- if hasattr(self.shared, "new_image") and self.shared.new_image is not None:
114
- # Close old last_image to free memory before replacing it
115
- if hasattr(self.shared, "last_image") and self.shared.last_image is not None:
116
- try:
117
- self.shared.last_image.close()
118
- except Exception:
119
- pass # Ignore errors if image is already closed
120
- self.shared.last_image = self.shared.new_image
113
+ self._backup_last_image()
121
114
 
122
115
  # Call the appropriate handler method based on handler type
123
- if hasattr(self, "get_image_from_rrm"):
124
- # This is a Rand256 handler
125
- new_image = await self.get_image_from_rrm(
126
- m_json=m_json,
127
- destinations=destinations,
128
- )
116
+ new_image = await self._generate_new_image(m_json, destinations)
117
+ if new_image is None:
118
+ return self._handle_failed_image_generation()
129
119
 
130
- elif hasattr(self, "async_get_image_from_json"):
131
- # This is a Hypfer handler
132
- self.json_data = await HyperMapData.async_from_valetudo_json(m_json)
133
- new_image = await self.async_get_image_from_json(
134
- m_json=m_json,
135
- )
136
- else:
137
- LOGGER.warning(
138
- "%s: Handler type not recognized for async_get_image",
139
- self.file_name,
140
- )
141
- return (
142
- self.shared.last_image
143
- if hasattr(self.shared, "last_image")
144
- else None
145
- )
120
+ # Process and store the new image
121
+ return await self._process_new_image(new_image, destinations, bytes_format)
146
122
 
147
- # Store the new image in shared data
148
- if new_image is not None:
149
- # Update shared data
150
- await self._async_update_shared_data(destinations)
151
- self.shared.new_image = new_image
152
- # Add text to the image
153
- if self.shared.show_vacuum_state:
154
- text_editor = StatusText(self.shared)
155
- img_text = await text_editor.get_status_text(new_image)
156
- Drawable.status_text(
157
- new_image,
158
- img_text[1],
159
- self.shared.user_colors[8],
160
- img_text[0],
161
- self.shared.vacuum_status_font,
162
- self.shared.vacuum_status_position,
163
- )
164
- # Convert to binary (PNG bytes) if requested
165
- if bytes_format:
166
- self.shared.binary_image = pil_to_png_bytes(new_image)
167
- else:
168
- self.shared.binary_image = pil_to_png_bytes(self.shared.last_image)
169
- # Update the timestamp with current datetime
170
- self.shared.image_last_updated = datetime.datetime.fromtimestamp(time())
171
- LOGGER.debug("%s: Frame Completed.", self.file_name)
172
- data = {}
173
- if bytes_format:
174
- data = self.shared.to_dict()
175
- return new_image, data
176
- else:
177
- LOGGER.warning(
178
- "%s: Failed to generate image from JSON data", self.file_name
179
- )
180
- if bytes_format and hasattr(self.shared, "last_image"):
181
- return pil_to_png_bytes(self.shared.last_image), {}
182
- return (
183
- self.shared.last_image
184
- if hasattr(self.shared, "last_image")
185
- else None
186
- ), {}
187
-
188
- except Exception as e:
123
+ except (ValueError, TypeError, AttributeError, KeyError, RuntimeError) as e:
189
124
  LOGGER.warning(
190
125
  "%s: Error in async_get_image: %s",
191
126
  self.file_name,
@@ -193,24 +128,111 @@ class BaseHandler:
193
128
  exc_info=True,
194
129
  )
195
130
  return (
196
- self.shared.last_image if hasattr(self.shared, "last_image") else None
131
+ self.shared.last_image if hasattr(self.shared, "last_image") else None,
132
+ {},
197
133
  )
198
134
 
135
+ def _backup_last_image(self):
136
+ """Backup current image to last_image before processing new one."""
137
+ if hasattr(self.shared, "new_image") and self.shared.new_image is not None:
138
+ # Close old last_image to free memory before replacing it
139
+ if (
140
+ hasattr(self.shared, "last_image")
141
+ and self.shared.last_image is not None
142
+ ):
143
+ try:
144
+ self.shared.last_image.close()
145
+ except (OSError, AttributeError, RuntimeError):
146
+ pass # Ignore errors if image is already closed
147
+ self.shared.last_image = self.shared.new_image
148
+
149
+ async def _generate_new_image(
150
+ self, m_json: dict | None, destinations: Destinations | None
151
+ ) -> PilPNG | None:
152
+ """Generate new image based on handler type."""
153
+ if hasattr(self, "get_image_from_rrm"):
154
+ # This is a Rand256 handler
155
+ return await self.get_image_from_rrm(
156
+ m_json=m_json,
157
+ destinations=destinations,
158
+ )
159
+
160
+ if hasattr(self, "async_get_image_from_json"):
161
+ # This is a Hypfer handler
162
+ self.json_data = await HyperMapData.async_from_valetudo_json(m_json)
163
+ return await self.async_get_image_from_json(m_json=m_json)
164
+
165
+ LOGGER.warning(
166
+ "%s: Handler type not recognized for async_get_image",
167
+ self.file_name,
168
+ )
169
+ return None
170
+
171
+ def _handle_failed_image_generation(self) -> Tuple[PilPNG | None, dict]:
172
+ """Handle case when image generation fails."""
173
+ LOGGER.warning("%s: Failed to generate image from JSON data", self.file_name)
174
+ return (
175
+ self.shared.last_image if hasattr(self.shared, "last_image") else None
176
+ ), self.shared.to_dict()
177
+
178
+ async def _process_new_image(
179
+ self, new_image: PilPNG, destinations: Destinations | None, bytes_format: bool
180
+ ) -> Tuple[PilPNG, dict]:
181
+ """Process and store the new image with text and binary conversion."""
182
+ # Update shared data
183
+ await self._async_update_shared_data(destinations)
184
+ self.shared.new_image = new_image
185
+
186
+ # Add text to the image
187
+ if self.shared.show_vacuum_state:
188
+ await self._add_status_text(new_image)
189
+
190
+ # Convert to binary (PNG bytes) if requested
191
+ self._convert_to_binary(new_image, bytes_format)
192
+
193
+ # Update the timestamp with current datetime
194
+ self.shared.image_last_updated = datetime.datetime.fromtimestamp(time())
195
+ LOGGER.debug("%s: Frame Completed.", self.file_name)
196
+
197
+ data = self.shared.to_dict() if bytes_format else {}
198
+ return new_image, data
199
+
200
+ async def _add_status_text(self, new_image: PilPNG):
201
+ """Add status text to the image."""
202
+ text_editor = StatusText(self.shared)
203
+ img_text = await text_editor.get_status_text(new_image)
204
+ Drawable.status_text(
205
+ new_image,
206
+ img_text[1],
207
+ self.shared.user_colors[8],
208
+ img_text[0],
209
+ self.shared.vacuum_status_font,
210
+ self.shared.vacuum_status_position,
211
+ )
212
+
213
+ def _convert_to_binary(self, new_image: PilPNG, bytes_format: bool):
214
+ """Convert image to binary PNG bytes."""
215
+ if bytes_format:
216
+ self.shared.binary_image = pil_to_png_bytes(new_image)
217
+ else:
218
+ self.shared.binary_image = pil_to_png_bytes(self.shared.last_image)
219
+
199
220
  async def _async_update_shared_data(self, destinations: Destinations | None = None):
200
221
  """Update the shared data with the latest information."""
201
222
 
202
223
  if hasattr(self, "get_rooms_attributes") and (
203
224
  self.shared.map_rooms is None and destinations is not None
204
225
  ):
226
+ # pylint: disable=no-member
205
227
  self.shared.map_rooms = await self.get_rooms_attributes(destinations)
206
228
  if self.shared.map_rooms:
207
229
  LOGGER.debug("%s: Rand256 attributes rooms updated", self.file_name)
208
230
 
209
-
210
231
  if hasattr(self, "async_get_rooms_attributes") and (
211
232
  self.shared.map_rooms is None
212
233
  ):
213
234
  if self.shared.map_rooms is None:
235
+ # pylint: disable=no-member
214
236
  self.shared.map_rooms = await self.async_get_rooms_attributes()
215
237
  if self.shared.map_rooms:
216
238
  LOGGER.debug("%s: Hyper attributes rooms updated", self.file_name)
@@ -219,6 +241,7 @@ class BaseHandler:
219
241
  hasattr(self, "get_calibration_data")
220
242
  and self.shared.attr_calibration_points is None
221
243
  ):
244
+ # pylint: disable=no-member
222
245
  self.shared.attr_calibration_points = self.get_calibration_data(
223
246
  self.shared.image_rotate
224
247
  )
@@ -474,7 +497,8 @@ class BaseHandler:
474
497
  return hashlib.sha256(data_json.encode()).hexdigest()
475
498
  return None
476
499
 
477
- async def async_copy_array(self, original_array: NumpyArray) -> NumpyArray:
500
+ @staticmethod
501
+ async def async_copy_array(original_array: NumpyArray) -> NumpyArray:
478
502
  """Copy the array using AsyncNumPy to yield control to the event loop."""
479
503
  return await AsyncNumPy.async_copy(original_array)
480
504
 
@@ -0,0 +1,288 @@
1
+ """Constants for the Valetudo Map Parser library."""
2
+
3
+ CAMERA_STORAGE = "valetudo_camera"
4
+ ATTR_IMAGE_LAST_UPDATED = "image_last_updated"
5
+ ATTR_ROTATE = "rotate_image"
6
+ ATTR_CROP = "crop_image"
7
+ ATTR_MARGINS = "margins"
8
+ ATTR_CONTENT_TYPE = "content_type"
9
+ CONF_OFFSET_TOP = "offset_top"
10
+ CONF_OFFSET_BOTTOM = "offset_bottom"
11
+ CONF_OFFSET_LEFT = "offset_left"
12
+ CONF_OFFSET_RIGHT = "offset_right"
13
+ CONF_ASPECT_RATIO = "aspect_ratio"
14
+ CONF_VAC_STAT = "show_vac_status"
15
+ CONF_VAC_STAT_SIZE = "vac_status_size"
16
+ CONF_VAC_STAT_POS = "vac_status_position"
17
+ CONF_VAC_STAT_FONT = "vac_status_font"
18
+ CONF_VACUUM_CONNECTION_STRING = "vacuum_map"
19
+ CONF_VACUUM_ENTITY_ID = "vacuum_entity"
20
+ CONF_VACUUM_CONFIG_ENTRY_ID = "vacuum_config_entry"
21
+ CONF_VACUUM_IDENTIFIERS = "vacuum_identifiers"
22
+ CONF_SNAPSHOTS_ENABLE = "enable_www_snapshots"
23
+ CONF_EXPORT_SVG = "get_svg_file"
24
+ CONF_AUTO_ZOOM = "auto_zoom"
25
+ CONF_ZOOM_LOCK_RATIO = "zoom_lock_ratio"
26
+ CONF_TRIMS_SAVE = "save_trims"
27
+ ICON = "mdi:camera"
28
+ NAME = "MQTT Vacuum Camera"
29
+
30
+ DEFAULT_IMAGE_SIZE = {
31
+ "x": 5120,
32
+ "y": 5120,
33
+ "centre": [(5120 // 2), (5120 // 2)],
34
+ }
35
+
36
+ COLORS = [
37
+ "wall",
38
+ "zone_clean",
39
+ "robot",
40
+ "background",
41
+ "move",
42
+ "charger",
43
+ "no_go",
44
+ "go_to",
45
+ ]
46
+
47
+ SENSOR_NO_DATA = {
48
+ "mainBrush": 0,
49
+ "sideBrush": 0,
50
+ "filter": 0,
51
+ "currentCleanTime": 0,
52
+ "currentCleanArea": 0,
53
+ "cleanTime": 0,
54
+ "cleanArea": 0,
55
+ "cleanCount": 0,
56
+ "battery": 0,
57
+ "state": 0,
58
+ "last_run_start": 0,
59
+ "last_run_end": 0,
60
+ "last_run_duration": 0,
61
+ "last_run_area": 0,
62
+ "last_bin_out": 0,
63
+ "last_bin_full": 0,
64
+ "last_loaded_map": "NoMap",
65
+ "robot_in_room": "Unsupported",
66
+ }
67
+
68
+ DEFAULT_PIXEL_SIZE = 5
69
+
70
+ DEFAULT_VALUES = {
71
+ "rotate_image": "0",
72
+ "margins": "100",
73
+ "aspect_ratio": "None",
74
+ "offset_top": 0,
75
+ "offset_bottom": 0,
76
+ "offset_left": 0,
77
+ "offset_right": 0,
78
+ "auto_zoom": False,
79
+ "zoom_lock_ratio": True,
80
+ "show_vac_status": False,
81
+ "vac_status_font": "SCR/valetudo_map_parser/config/fonts/FiraSans.ttf",
82
+ "vac_status_size": 50,
83
+ "vac_status_position": True,
84
+ "get_svg_file": False,
85
+ "save_trims": True,
86
+ "trims_data": {"trim_left": 0, "trim_up": 0, "trim_right": 0, "trim_down": 0},
87
+ "enable_www_snapshots": False,
88
+ "color_charger": [255, 128, 0],
89
+ "color_move": [238, 247, 255],
90
+ "color_wall": [255, 255, 0],
91
+ "color_robot": [255, 255, 204],
92
+ "color_go_to": [0, 255, 0],
93
+ "color_no_go": [255, 0, 0],
94
+ "color_zone_clean": [255, 255, 255],
95
+ "color_background": [0, 125, 255],
96
+ "color_text": [255, 255, 255],
97
+ "alpha_charger": 255.0,
98
+ "alpha_move": 255.0,
99
+ "alpha_wall": 255.0,
100
+ "alpha_robot": 255.0,
101
+ "alpha_go_to": 255.0,
102
+ "alpha_no_go": 125.0,
103
+ "alpha_zone_clean": 125.0,
104
+ "alpha_background": 255.0,
105
+ "alpha_text": 255.0,
106
+ "color_room_0": [135, 206, 250],
107
+ "color_room_1": [176, 226, 255],
108
+ "color_room_2": [165, 105, 18],
109
+ "color_room_3": [164, 211, 238],
110
+ "color_room_4": [141, 182, 205],
111
+ "color_room_5": [96, 123, 139],
112
+ "color_room_6": [224, 255, 255],
113
+ "color_room_7": [209, 238, 238],
114
+ "color_room_8": [180, 205, 205],
115
+ "color_room_9": [122, 139, 139],
116
+ "color_room_10": [175, 238, 238],
117
+ "color_room_11": [84, 153, 199],
118
+ "color_room_12": [133, 193, 233],
119
+ "color_room_13": [245, 176, 65],
120
+ "color_room_14": [82, 190, 128],
121
+ "color_room_15": [72, 201, 176],
122
+ "alpha_room_0": 255.0,
123
+ "alpha_room_1": 255.0,
124
+ "alpha_room_2": 255.0,
125
+ "alpha_room_3": 255.0,
126
+ "alpha_room_4": 255.0,
127
+ "alpha_room_5": 255.0,
128
+ "alpha_room_6": 255.0,
129
+ "alpha_room_7": 255.0,
130
+ "alpha_room_8": 255.0,
131
+ "alpha_room_9": 255.0,
132
+ "alpha_room_10": 255.0,
133
+ "alpha_room_11": 255.0,
134
+ "alpha_room_12": 255.0,
135
+ "alpha_room_13": 255.0,
136
+ "alpha_room_14": 255.0,
137
+ "alpha_room_15": 255.0,
138
+ }
139
+
140
+ FONTS_AVAILABLE = [
141
+ {
142
+ "label": "Fira Sans",
143
+ "value": "config/fonts/FiraSans.ttf",
144
+ },
145
+ {
146
+ "label": "Inter",
147
+ "value": "config/fonts/Inter-VF.ttf",
148
+ },
149
+ {
150
+ "label": "M Plus Regular",
151
+ "value": "config/fonts/MPLUSRegular.ttf",
152
+ },
153
+ {
154
+ "label": "Noto Sans CJKhk",
155
+ "value": "config/fonts/NotoSansCJKhk-VF.ttf",
156
+ },
157
+ {
158
+ "label": "Noto Kufi Arabic",
159
+ "value": "config/fonts/NotoKufiArabic-VF.ttf",
160
+ },
161
+ {
162
+ "label": "Noto Sans Khojki",
163
+ "value": "config/fonts/NotoSansKhojki.ttf",
164
+ },
165
+ {
166
+ "label": "Lato Regular",
167
+ "value": "config/fonts/Lato-Regular.ttf",
168
+ },
169
+ ]
170
+
171
+ NOT_STREAMING_STATES = {
172
+ "idle",
173
+ "paused",
174
+ "charging",
175
+ "error",
176
+ "docked",
177
+ }
178
+
179
+ DECODED_TOPICS = {
180
+ "/MapData/segments",
181
+ "/maploader/map",
182
+ "/maploader/status",
183
+ "/StatusStateAttribute/status",
184
+ "/StatusStateAttribute/error_description",
185
+ "/$state",
186
+ "/BatteryStateAttribute/level",
187
+ "/WifiConfigurationCapability/ips",
188
+ "/state", # Rand256
189
+ "/destinations", # Rand256
190
+ "/command", # Rand256
191
+ "/custom_command", # Rand256
192
+ "/attributes", # Rand256
193
+ }
194
+
195
+
196
+ # self.command_topic need to be added to this dictionary after init.
197
+ NON_DECODED_TOPICS = {
198
+ "/MapData/map-data",
199
+ "/map_data",
200
+ }
201
+
202
+ """App Constants. Not in use, and dummy values"""
203
+ IDLE_SCAN_INTERVAL = 120
204
+ CLEANING_SCAN_INTERVAL = 5
205
+ IS_ALPHA = "add_base_alpha"
206
+ IS_ALPHA_R1 = "add_room_1_alpha"
207
+ IS_ALPHA_R2 = "add_room_2_alpha"
208
+ IS_OFFSET = "add_offset"
209
+
210
+ """Base Colours RGB"""
211
+ COLOR_CHARGER = "color_charger"
212
+ COLOR_MOVE = "color_move"
213
+ COLOR_ROBOT = "color_robot"
214
+ COLOR_NO_GO = "color_no_go"
215
+ COLOR_GO_TO = "color_go_to"
216
+ COLOR_BACKGROUND = "color_background"
217
+ COLOR_ZONE_CLEAN = "color_zone_clean"
218
+ COLOR_WALL = "color_wall"
219
+ COLOR_TEXT = "color_text"
220
+
221
+ """Rooms Colours RGB"""
222
+ COLOR_ROOM_0 = "color_room_0"
223
+ COLOR_ROOM_1 = "color_room_1"
224
+ COLOR_ROOM_2 = "color_room_2"
225
+ COLOR_ROOM_3 = "color_room_3"
226
+ COLOR_ROOM_4 = "color_room_4"
227
+ COLOR_ROOM_5 = "color_room_5"
228
+ COLOR_ROOM_6 = "color_room_6"
229
+ COLOR_ROOM_7 = "color_room_7"
230
+ COLOR_ROOM_8 = "color_room_8"
231
+ COLOR_ROOM_9 = "color_room_9"
232
+ COLOR_ROOM_10 = "color_room_10"
233
+ COLOR_ROOM_11 = "color_room_11"
234
+ COLOR_ROOM_12 = "color_room_12"
235
+ COLOR_ROOM_13 = "color_room_13"
236
+ COLOR_ROOM_14 = "color_room_14"
237
+ COLOR_ROOM_15 = "color_room_15"
238
+
239
+ """Alpha for RGBA Colours"""
240
+ ALPHA_CHARGER = "alpha_charger"
241
+ ALPHA_MOVE = "alpha_move"
242
+ ALPHA_ROBOT = "alpha_robot"
243
+ ALPHA_NO_GO = "alpha_no_go"
244
+ ALPHA_GO_TO = "alpha_go_to"
245
+ ALPHA_BACKGROUND = "alpha_background"
246
+ ALPHA_ZONE_CLEAN = "alpha_zone_clean"
247
+ ALPHA_WALL = "alpha_wall"
248
+ ALPHA_TEXT = "alpha_text"
249
+ ALPHA_ROOM_0 = "alpha_room_0"
250
+ ALPHA_ROOM_1 = "alpha_room_1"
251
+ ALPHA_ROOM_2 = "alpha_room_2"
252
+ ALPHA_ROOM_3 = "alpha_room_3"
253
+ ALPHA_ROOM_4 = "alpha_room_4"
254
+ ALPHA_ROOM_5 = "alpha_room_5"
255
+ ALPHA_ROOM_6 = "alpha_room_6"
256
+ ALPHA_ROOM_7 = "alpha_room_7"
257
+ ALPHA_ROOM_8 = "alpha_room_8"
258
+ ALPHA_ROOM_9 = "alpha_room_9"
259
+ ALPHA_ROOM_10 = "alpha_room_10"
260
+ ALPHA_ROOM_11 = "alpha_room_11"
261
+ ALPHA_ROOM_12 = "alpha_room_12"
262
+ ALPHA_ROOM_13 = "alpha_room_13"
263
+ ALPHA_ROOM_14 = "alpha_room_14"
264
+ ALPHA_ROOM_15 = "alpha_room_15"
265
+
266
+ """ Constants for the attribute keys """
267
+ ATTR_FRIENDLY_NAME = "friendly_name"
268
+ ATTR_VACUUM_BATTERY = "battery"
269
+ ATTR_VACUUM_CHARGING = "charging"
270
+ ATTR_VACUUM_POSITION = "vacuum_position"
271
+ ATTR_VACUUM_TOPIC = "vacuum_topic"
272
+ ATTR_VACUUM_STATUS = "vacuum_status"
273
+ ATTR_JSON_DATA = "json_data"
274
+ ATTR_VACUUM_JSON_ID = "vacuum_json_id"
275
+ ATTR_CALIBRATION_POINTS = "calibration_points"
276
+ ATTR_SNAPSHOT = "snapshot"
277
+ ATTR_SNAPSHOT_PATH = "snapshot_path"
278
+ ATTR_ROOMS = "rooms"
279
+ ATTR_ZONES = "zones"
280
+ ATTR_POINTS = "points"
281
+ ATTR_OBSTACLES = "obstacles"
282
+ ATTR_CAMERA_MODE = "camera_mode"
283
+
284
+ # Status text cost
285
+ charge_level = "\u03de" # unicode Koppa symbol
286
+ charging = "\u2211" # unicode Charging symbol
287
+ dot = " \u00b7 " # unicode middle dot
288
+ text_size_coverage = 1.5 # resize factor for the text