valetudo-map-parser 0.1.11b2__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,80 +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
- )
129
-
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
- )
116
+ new_image = await self._generate_new_image(m_json, destinations)
117
+ if new_image is None:
118
+ return self._handle_failed_image_generation()
146
119
 
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
- return (
181
- self.shared.last_image
182
- if hasattr(self.shared, "last_image")
183
- else None
184
- ), self.shared.to_dict()
120
+ # Process and store the new image
121
+ return await self._process_new_image(new_image, destinations, bytes_format)
185
122
 
186
- except Exception as e:
123
+ except (ValueError, TypeError, AttributeError, KeyError, RuntimeError) as e:
187
124
  LOGGER.warning(
188
125
  "%s: Error in async_get_image: %s",
189
126
  self.file_name,
@@ -191,24 +128,111 @@ class BaseHandler:
191
128
  exc_info=True,
192
129
  )
193
130
  return (
194
- 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
+ {},
133
+ )
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,
195
158
  )
196
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
+
197
220
  async def _async_update_shared_data(self, destinations: Destinations | None = None):
198
221
  """Update the shared data with the latest information."""
199
222
 
200
223
  if hasattr(self, "get_rooms_attributes") and (
201
224
  self.shared.map_rooms is None and destinations is not None
202
225
  ):
226
+ # pylint: disable=no-member
203
227
  self.shared.map_rooms = await self.get_rooms_attributes(destinations)
204
228
  if self.shared.map_rooms:
205
229
  LOGGER.debug("%s: Rand256 attributes rooms updated", self.file_name)
206
230
 
207
-
208
231
  if hasattr(self, "async_get_rooms_attributes") and (
209
232
  self.shared.map_rooms is None
210
233
  ):
211
234
  if self.shared.map_rooms is None:
235
+ # pylint: disable=no-member
212
236
  self.shared.map_rooms = await self.async_get_rooms_attributes()
213
237
  if self.shared.map_rooms:
214
238
  LOGGER.debug("%s: Hyper attributes rooms updated", self.file_name)
@@ -217,6 +241,7 @@ class BaseHandler:
217
241
  hasattr(self, "get_calibration_data")
218
242
  and self.shared.attr_calibration_points is None
219
243
  ):
244
+ # pylint: disable=no-member
220
245
  self.shared.attr_calibration_points = self.get_calibration_data(
221
246
  self.shared.image_rotate
222
247
  )
@@ -472,7 +497,8 @@ class BaseHandler:
472
497
  return hashlib.sha256(data_json.encode()).hexdigest()
473
498
  return None
474
499
 
475
- async def async_copy_array(self, original_array: NumpyArray) -> NumpyArray:
500
+ @staticmethod
501
+ async def async_copy_array(original_array: NumpyArray) -> NumpyArray:
476
502
  """Copy the array using AsyncNumPy to yield control to the event loop."""
477
503
  return await AsyncNumPy.async_copy(original_array)
478
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