Skip to content

Commit

Permalink
Merge pull request #139 from sca075/2024.05.2
Browse files Browse the repository at this point in the history
Image Handler Refactoring.
  • Loading branch information
sca075 authored May 10, 2024
2 parents d9e083f + a2be69e commit ae25662
Show file tree
Hide file tree
Showing 31 changed files with 1,087 additions and 749 deletions.
12 changes: 5 additions & 7 deletions custom_components/valetudo_vacuum_camera/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@


async def options_update_listener(
hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry
hass: core.HomeAssistant, config_entry: config_entries.ConfigEntry
):
"""Handle options update."""
await hass.config_entries.async_reload(config_entry.entry_id)
Expand Down Expand Up @@ -69,7 +69,7 @@ async def async_migrate_entry(hass, config_entry: config_entries.ConfigEntry):
"Unable to migrate to config entry version 2.0. Could not find a device for %s."
+ " Please recreate this entry.",
mqtt_topic_base,
)
)
return False
new_data.update(
{
Expand Down Expand Up @@ -215,14 +215,12 @@ async def async_migrate_entry(hass, config_entry: config_entries.ConfigEntry):
)
return False

_LOGGER.info(
f"Migration to config entry version successful {config_entry.version}"
)
_LOGGER.info(f"Migration to config entry version successful {config_entry.version}")
return True


async def async_setup_entry(
hass: core.HomeAssistant, entry: config_entries.ConfigEntry
hass: core.HomeAssistant, entry: config_entries.ConfigEntry
) -> bool:
"""Set up platform from a ConfigEntry."""
hass.data.setdefault(DOMAIN, {})
Expand Down Expand Up @@ -263,7 +261,7 @@ async def async_setup_entry(


async def async_unload_entry(
hass: core.HomeAssistant, entry: config_entries.ConfigEntry
hass: core.HomeAssistant, entry: config_entries.ConfigEntry
) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
Expand Down
48 changes: 24 additions & 24 deletions custom_components/valetudo_vacuum_camera/camera.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""
Camera
Version: v2024.05
Version: v2024.05.2
Image Processing Threading implemented on Version 1.5.7.
"""

Expand Down Expand Up @@ -122,7 +122,8 @@ def __init__(self, hass, device_info):
if self._mqtt_listen_topic:
self._mqtt_listen_topic = str(self._mqtt_listen_topic)
self._shared.file_name = self._mqtt_listen_topic.split("/")[1].lower()
_LOGGER.debug(f"Camera {self._shared.file_name} Starting up..")
self._file_name = self._shared.file_name
_LOGGER.debug(f"Camera {self._file_name} Starting up..")
_LOGGER.info(f"System Release: {platform.node()}, {platform.release()}")
_LOGGER.info(f"System Version: {platform.version()}")
_LOGGER.info(f"System Machine: {platform.machine()}")
Expand All @@ -135,8 +136,8 @@ def __init__(self, hass, device_info):
self._storage_path = f"{self.hass.config.path(STORAGE_DIR)}/valetudo_camera"
if not os.path.exists(self._storage_path):
self._storage_path = f"{self._directory_path}/{STORAGE_DIR}"
self.snapshot_img = f"{self._storage_path}/{self._shared.file_name}.png"
self.log_file = f"{self._storage_path}/{self._shared.file_name}.zip"
self.snapshot_img = f"{self._storage_path}/{self._file_name}.png"
self.log_file = f"{self._storage_path}/{self._file_name}.zip"
self._attr_unique_id = device_info.get(
CONF_UNIQUE_ID,
get_vacuum_unique_id_from_mqtt_topic(self._mqtt_listen_topic),
Expand Down Expand Up @@ -175,11 +176,9 @@ def __init__(self, hass, device_info):
self._shared.enable_snapshots = True
# If snapshots are disabled, delete www data
if not self._shared.enable_snapshots and os.path.isfile(
f"{self._directory_path}/www/snapshot_{self._shared.file_name}.png"
f"{self._directory_path}/www/snapshot_{self._file_name}.png"
):
os.remove(
f"{self._directory_path}/www/snapshot_{self._shared.file_name}.png"
)
os.remove(f"{self._directory_path}/www/snapshot_{self._file_name}.png")
# If there is a log zip in www remove it
if os.path.isfile(self.log_file):
os.remove(self.log_file)
Expand Down Expand Up @@ -254,7 +253,7 @@ def extra_state_attributes(self) -> dict:
}
if self._shared.enable_snapshots:
attrs["snapshot"] = self._shared.snapshot_take
attrs["snapshot_path"] = f"/local/snapshot_{self._shared.file_name}.png"
attrs["snapshot_path"] = f"/local/snapshot_{self._file_name}.png"
else:
attrs["snapshot"] = False
if (self._shared.map_rooms is not None) and (self._shared.map_rooms != {}):
Expand Down Expand Up @@ -303,20 +302,20 @@ def empty_if_no_data(self) -> Image.Image:
an empty image if there are no data.
"""
if self._last_image:
_LOGGER.debug(f"{self._shared.file_name}: Returning Last image.")
_LOGGER.debug(f"{self._file_name}: Returning Last image.")
return self._last_image
elif self._last_image is None:
# Check if the snapshot file exists
_LOGGER.info(f"Searching for {self.snapshot_img}.")
if os.path.isfile(self.snapshot_img):
# Load the snapshot image
self._last_image = Image.open(self.snapshot_img)
_LOGGER.debug(f"{self._shared.file_name}: Returning Snapshot image.")
_LOGGER.debug(f"{self._file_name}: Returning Snapshot image.")
return self._last_image
else:
# Create an empty image with a gray background
empty_img = Image.new("RGB", (800, 600), "gray")
_LOGGER.info(f"{self._shared.file_name}: Returning Empty image.")
_LOGGER.info(f"{self._file_name}: Returning Empty image.")
return empty_img

async def take_snapshot(self, json_data: Any, image_data: Image.Image) -> None:
Expand All @@ -342,7 +341,7 @@ async def async_update(self):
self._shared.user_language = await async_get_active_user_id(self.hass)
# check and update the vacuum reported state
if not self._mqtt:
_LOGGER.debug(f"{self._shared.file_name}: No MQTT data available.")
_LOGGER.debug(f"{self._file_name}: No MQTT data available.")
# return last/empty image if no MQTT or CPU usage too high.
pil_img = self.empty_if_no_data()
self.Image = await self.hass.async_create_task(
Expand Down Expand Up @@ -386,7 +385,7 @@ async def async_update(self):
# do not take the automatic snapshot.
self._shared.snapshot_take = False
_LOGGER.info(
f"{self._shared.file_name}: Camera image data update available: {process_data}"
f"{self._file_name}: Camera image data update available: {process_data}"
)
try:
parsed_json = await self._mqtt.update_data(self._shared.image_grab)
Expand Down Expand Up @@ -445,17 +444,18 @@ async def async_update(self):
else:
await self.take_snapshot(parsed_json, pil_img)
# clean up
del (pil_img,)
_LOGGER.debug(f"{self._shared.file_name}: Image update complete")
del pil_img
_LOGGER.debug(f"{self._file_name}: Image update complete")
processing_time = round((time.perf_counter() - start_time), 3)
# Adjust the frame interval to the processing time.
self._attr_frame_interval = max(0.1, processing_time)
_LOGGER.debug(
f"Adjusted {self._shared.file_name}: Frame interval: {self._attr_frame_interval}"
f"{self._file_name}: Frame {self._shared.frame_number} interval"
f" is {self._attr_frame_interval}"
)
else:
_LOGGER.info(
f"{self._shared.file_name}: Image not processed. Returning not updated image."
f"{self._file_name}: Image not processed. Returning not updated image."
)
self._attr_frame_interval = 0.1
self.camera_image(self._image_w, self._image_h)
Expand All @@ -472,10 +472,10 @@ async def async_update(self):
((proc.cpu_percent() / int(ProcInsp().psutil.cpu_count())) / 10), 1
)
_LOGGER.debug(
f"{self._shared.file_name} System CPU usage stat: {self._cpu_percent}%"
f"{self._file_name} System CPU usage stat: {self._cpu_percent}%"
)
_LOGGER.debug(
f"{self._shared.file_name} Camera Memory usage in GB: "
f"{self._file_name} Camera Memory usage in GB: "
f"{round(proc.memory_info()[0] / 2. ** 30, 2)}, "
f"{memory_percent}% of Total."
)
Expand All @@ -487,18 +487,18 @@ async def async_pil_to_bytes(self, pil_img) -> Optional[bytes]:
if pil_img:
self._last_image = pil_img
_LOGGER.debug(
f"{self._shared.file_name}: Image from Json: {self._shared.vac_json_id}."
f"{self._file_name}: Image from Json: {self._shared.vac_json_id}."
)
if self._shared.show_vacuum_state:
pil_img = await self.processor.run_async_draw_image_text(
pil_img, self._shared.user_colors[8]
)
else:
if self._last_image is not None:
_LOGGER.debug(f"{self._shared.file_name}: Output Last Image.")
_LOGGER.debug(f"{self._file_name}: Output Last Image.")
pil_img = self._last_image
else:
_LOGGER.debug(f"{self._shared.file_name}: Output Gray Image.")
_LOGGER.debug(f"{self._file_name}: Output Gray Image.")
pil_img = self.empty_if_no_data()
self._image_w = pil_img.width
self._image_h = pil_img.height
Expand All @@ -525,7 +525,7 @@ async def run_async_pil_to_bytes(self, pil_img):
loop = get_event_loop()

with concurrent.futures.ThreadPoolExecutor(
max_workers=1, thread_name_prefix=f"{self._shared.file_name}_camera"
max_workers=1, thread_name_prefix=f"{self._file_name}_camera"
) as executor:
tasks = [
loop.run_in_executor(
Expand Down
23 changes: 15 additions & 8 deletions custom_components/valetudo_vacuum_camera/camera_processing.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""
Multiprocessing module
Version: v2024.05
Version: v2024.05.2
This module provide the image multiprocessing in order to
avoid the overload of the main_thread of Home Assistant.
"""
Expand Down Expand Up @@ -32,6 +32,7 @@ def __init__(self, hass, camera_shared):
self._map_handler = MapImageHandler(camera_shared)
self._re_handler = ReImageHandler(camera_shared)
self._shared = camera_shared
self._file_name = self._shared.file_name
self._translations_path = self.hass.config.path(
"custom_components/valetudo_vacuum_camera/translations/"
)
Expand All @@ -57,7 +58,9 @@ async def async_process_valetudo_data(self, parsed_json: JsonType) -> PilPNG | N
await self._map_handler.async_get_rooms_attributes()
)
if self._shared.map_rooms:
_LOGGER.debug("State attributes rooms updated")
_LOGGER.debug(
f"{self._file_name}: State attributes rooms updated"
)

if self._shared.attr_calibration_points is None:
self._shared.attr_calibration_points = (
Expand All @@ -72,6 +75,7 @@ async def async_process_valetudo_data(self, parsed_json: JsonType) -> PilPNG | N
)

self._shared.current_room = self._map_handler.get_robot_position()
self._shared.map_rooms = self._map_handler.room_propriety

if not self._shared.image_size:
self._shared.image_size = self._map_handler.get_img_size()
Expand All @@ -88,7 +92,7 @@ async def async_process_valetudo_data(self, parsed_json: JsonType) -> PilPNG | N
):
self._shared.image_grab = False
_LOGGER.info(
f"Suspended the camera data processing for: {self._shared.file_name}."
f"Suspended the camera data processing for: {self._file_name}."
)
# take a snapshot
self._shared.snapshot_take = True
Expand Down Expand Up @@ -118,7 +122,9 @@ async def async_process_rand256_data(self, parsed_json: JsonType) -> PilPNG | No
self._shared.map_pred_points,
) = await self._re_handler.get_rooms_attributes(destinations)
if self._shared.map_rooms:
_LOGGER.debug("State attributes rooms updated")
_LOGGER.debug(
f"{self._file_name}: State attributes rooms updated"
)

if self._shared.attr_calibration_points is None:
self._shared.attr_calibration_points = (
Expand All @@ -142,7 +148,7 @@ async def async_process_rand256_data(self, parsed_json: JsonType) -> PilPNG | No
):
# suspend image processing if we are at the next frame.
_LOGGER.info(
f"Suspended the camera data processing for: {self._shared.file_name}."
f"Suspended the camera data processing for: {self._file_name}."
)
# take a snapshot
self._shared.snapshot_take = True
Expand Down Expand Up @@ -176,7 +182,7 @@ async def run_async_process_valetudo_data(
loop = get_event_loop()

with concurrent.futures.ThreadPoolExecutor(
max_workers=1, thread_name_prefix=f"{self._shared.file_name}_camera"
max_workers=1, thread_name_prefix=f"{self._file_name}_camera"
) as executor:
tasks = [
loop.run_in_executor(executor, self.process_valetudo_data, parsed_json)
Expand All @@ -185,7 +191,7 @@ async def run_async_process_valetudo_data(
images = await gather(*tasks)

if isinstance(images, list) and len(images) > 0:
_LOGGER.debug(f"{self._shared.file_name}: Camera frame processed.")
_LOGGER.debug(f"{self._file_name}: Camera frame processed.")
result = images[0]
else:
result = None
Expand All @@ -204,6 +210,7 @@ async def async_draw_image_text(
self, pil_img: PilPNG, color: Color, font: str, img_top: bool = True
) -> PilPNG:
"""Draw text on the image."""

if pil_img is not None:
text, size = self._status_text.get_status_text(pil_img)
Draw.status_text(
Expand Down Expand Up @@ -237,7 +244,7 @@ async def run_async_draw_image_text(self, pil_img: PilPNG, color: Color) -> PilP
loop = get_event_loop()

with concurrent.futures.ThreadPoolExecutor(
max_workers=1, thread_name_prefix=f"{self._shared.file_name}_camera_text"
max_workers=1, thread_name_prefix=f"{self._file_name}_camera_text"
) as executor:
tasks = [
loop.run_in_executor(
Expand Down
2 changes: 1 addition & 1 deletion custom_components/valetudo_vacuum_camera/camera_shared.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""
Class Camera Shared.
Keep the data between the modules.
Version: v2024.05
Version: v2024.05.2
"""

import logging
Expand Down
Loading

0 comments on commit ae25662

Please sign in to comment.