From 47d845772ccb1cc63c2167767dc4cad2816b1c30 Mon Sep 17 00:00:00 2001 From: SCA075 <82227818+sca075@users.noreply.github.com> Date: Tue, 3 Dec 2024 19:38:26 +0100 Subject: [PATCH 1/3] Clean-up and formatting Signed-off-by: 82227818+sca075@users.noreply.github.com <82227818+sca075@users.noreply.github.com> --- .../mqtt_vacuum_camera/__init__.py | 7 ++- .../mqtt_vacuum_camera/camera.py | 60 +++++++++++++------ .../mqtt_vacuum_camera/common.py | 9 ++- .../mqtt_vacuum_camera/config_flow.py | 8 ++- custom_components/mqtt_vacuum_camera/const.py | 7 ++- .../mqtt_vacuum_camera/services.yaml | 14 ++--- .../mqtt_vacuum_camera/snapshots/log_files.py | 2 +- .../mqtt_vacuum_camera/utils/auto_crop.py | 4 +- .../utils/camera/camera_services.py | 13 ++-- .../utils/camera/camera_shared.py | 2 +- .../mqtt_vacuum_camera/utils/drawable.py | 4 +- .../utils/files_operations.py | 6 +- .../mqtt_vacuum_camera/utils/img_data.py | 2 +- .../mqtt_vacuum_camera/utils/status_text.py | 8 +-- .../valetudo/MQTT/connector.py | 5 +- .../valetudo/hypfer/image_handler.py | 26 ++++---- .../valetudo/rand256/image_handler.py | 26 ++++---- .../valetudo/rand256/reimg_draw.py | 9 +-- 18 files changed, 135 insertions(+), 77 deletions(-) diff --git a/custom_components/mqtt_vacuum_camera/__init__.py b/custom_components/mqtt_vacuum_camera/__init__.py index 6aaa5868..47d333ef 100755 --- a/custom_components/mqtt_vacuum_camera/__init__.py +++ b/custom_components/mqtt_vacuum_camera/__init__.py @@ -27,9 +27,12 @@ DOMAIN, ) from .coordinator import MQTTVacuumCoordinator -from .utils.camera.camera_services import reload_camera_config, reset_trims, obstacle_view +from .utils.camera.camera_services import ( + obstacle_view, + reload_camera_config, + reset_trims, +) from .utils.files_operations import ( - async_clean_up_all_auto_crop_files, async_get_translations_vacuum_id, async_rename_room_description, ) diff --git a/custom_components/mqtt_vacuum_camera/camera.py b/custom_components/mqtt_vacuum_camera/camera.py index e7139a4a..23adcebf 100755 --- a/custom_components/mqtt_vacuum_camera/camera.py +++ b/custom_components/mqtt_vacuum_camera/camera.py @@ -6,7 +6,6 @@ from __future__ import annotations import asyncio -import aiohttp from asyncio import gather, get_event_loop import concurrent.futures from concurrent.futures import ThreadPoolExecutor @@ -20,6 +19,7 @@ from typing import Any, Optional from PIL import Image +import aiohttp from homeassistant import config_entries, core from homeassistant.components.camera import Camera, CameraEntityFeature from homeassistant.const import CONF_UNIQUE_ID, MATCH_ALL @@ -125,7 +125,9 @@ def __init__(self, coordinator, device_info): # Listen to the vacuum.start event self.hass.bus.async_listen("event_vacuum_start", self.handle_vacuum_start) - self.hass.bus.async_listen("mqtt_vacuum_camera_obstacle_coordinates", self.handle_obstacle_view) + self.hass.bus.async_listen( + "mqtt_vacuum_camera_obstacle_coordinates", self.handle_obstacle_view + ) @staticmethod def _start_up_logs(): @@ -249,7 +251,10 @@ async def handle_obstacle_view(self, event): self._should_poll = True return - if self._shared.obstacles_data and self._shared.camera_mode == CameraModes.MAP_VIEW: + if ( + self._shared.obstacles_data + and self._shared.camera_mode == CameraModes.MAP_VIEW + ): _LOGGER.debug(f"Received event: {event.event_type}, Data: {event.data}") if event.data.get("entity_id") == self.entity_id: self._shared.camera_mode = CameraModes.OBSTACLE_DOWNLOAD @@ -268,12 +273,19 @@ async def handle_obstacle_view(self, event): if nearest_obstacle: _LOGGER.debug(f"Nearest obstacle found: {nearest_obstacle}") if nearest_obstacle["link"]: - _LOGGER.debug(f"Downloading image: {nearest_obstacle['link']}") + _LOGGER.debug( + f"Downloading image: {nearest_obstacle['link']}" + ) # You can now use nearest_obstacle["link"] to download the image - temp_image = await self.download_image(nearest_obstacle['link'], - self._storage_path, "obstacle.jpg") + temp_image = await self.download_image( + nearest_obstacle["link"], + self._storage_path, + "obstacle.jpg", + ) else: - _LOGGER.info("No link found for the obstacle image. Skipping download.") + _LOGGER.info( + "No link found for the obstacle image. Skipping download." + ) self._should_poll = True # Turn on polling self._shared.camera_mode = CameraModes.MAP_VIEW return None @@ -288,12 +300,16 @@ async def handle_obstacle_view(self, event): f"{self._file_name}: Image resized to: {self._image_w}, {self._image_h}" ) except Exception as e: - _LOGGER.warning(f"{self._file_name}: Error processing image: {e}") + _LOGGER.warning( + f"{self._file_name}: Error processing image: {e}" + ) self._shared.camera_mode = CameraModes.MAP_VIEW self._should_poll = True # Turn on polling return None - self.Image = await self.hass.async_create_task(self.run_async_pil_to_bytes(pil_img)) + self.Image = await self.hass.async_create_task( + self.run_async_pil_to_bytes(pil_img) + ) self._shared.camera_mode = CameraModes.OBSTACLE_VIEW else: self._shared.camera_mode = CameraModes.MAP_VIEW @@ -310,8 +326,10 @@ async def handle_obstacle_view(self, event): async def _async_find_nearest_obstacle(x, y, obstacles): """Find the nearest obstacle to the given coordinates.""" nearest_obstacle = None - min_distance = float('inf') # Start with a very large distance - _LOGGER.debug(f"Finding the nearest {min_distance} obstacle to coordinates: {x}, {y}") + min_distance = float("inf") # Start with a very large distance + _LOGGER.debug( + f"Finding the nearest {min_distance} obstacle to coordinates: {x}, {y}" + ) for obstacle in obstacles: obstacle_point = obstacle["point"] @@ -327,7 +345,6 @@ async def _async_find_nearest_obstacle(x, y, obstacles): return nearest_obstacle - @staticmethod async def download_image(url: str, storage_path: str, filename: str): """ @@ -354,20 +371,25 @@ async def blocking_download(): if response.status == 200: with open(obstacle_file, "wb") as f: f.write(await response.read()) - _LOGGER.debug(f"Image downloaded successfully: {obstacle_file}") + _LOGGER.debug( + f"Image downloaded successfully: {obstacle_file}" + ) return obstacle_file else: - _LOGGER.warning(f"Failed to download image: {response.status}") + _LOGGER.warning( + f"Failed to download image: {response.status}" + ) return None except Exception as e: _LOGGER.error(f"Error downloading image: {e}") return None - executor = ThreadPoolExecutor(max_workers=3) # Limit to 3 workers # Run the blocking I/O in a thread - return await asyncio.get_running_loop().run_in_executor(executor, asyncio.run, blocking_download()) + return await asyncio.get_running_loop().run_in_executor( + executor, asyncio.run, blocking_download() + ) @property def should_poll(self) -> bool: @@ -552,7 +574,11 @@ async def _process_parsed_json(self, test_mode: bool = False): self.Image = await self.hass.async_create_task( self.run_async_pil_to_bytes(self.empty_if_no_data()) ) - raise ValueError + self.camera_image(self._image_w, self._image_h) + _LOGGER.warning( + f"{self._file_name}: No JSON data available. Camera Suspended." + ) + self._should_pull = False if parsed_json[1] == "Rand256": self._shared.is_rand = True diff --git a/custom_components/mqtt_vacuum_camera/common.py b/custom_components/mqtt_vacuum_camera/common.py index 0d45e468..9c644c35 100755 --- a/custom_components/mqtt_vacuum_camera/common.py +++ b/custom_components/mqtt_vacuum_camera/common.py @@ -162,7 +162,9 @@ def build_full_topic_set( return full_topics -def from_device_ids_to_entity_ids(device_ids: str, hass: HomeAssistant, domain: str = "vacuum") -> str: +def from_device_ids_to_entity_ids( + device_ids: str, hass: HomeAssistant, domain: str = "vacuum" +) -> str: """ Convert a device_id to an entity_id. """ @@ -196,7 +198,10 @@ def get_device_info_from_entity_id(entity_id: str, hass) -> DeviceEntry: def get_entity_id( - entity_id: str | None, device_id: str | None, hass: HomeAssistant, domain: str = "vacuum" + entity_id: str | None, + device_id: str | None, + hass: HomeAssistant, + domain: str = "vacuum", ) -> str | None: """Resolve the Entity ID""" vacuum_entity_id = entity_id # Default to entity_id diff --git a/custom_components/mqtt_vacuum_camera/config_flow.py b/custom_components/mqtt_vacuum_camera/config_flow.py index 4d9cf9a2..65b3081a 100755 --- a/custom_components/mqtt_vacuum_camera/config_flow.py +++ b/custom_components/mqtt_vacuum_camera/config_flow.py @@ -5,14 +5,14 @@ sting.json and en.json please. """ +from copy import deepcopy import logging import os -from copy import deepcopy from typing import Any, Dict, Optional from homeassistant import config_entries -from homeassistant.config_entries import OptionsFlow, ConfigEntry from homeassistant.components.vacuum import DOMAIN as ZONE_VACUUM +from homeassistant.config_entries import ConfigEntry, OptionsFlow from homeassistant.const import CONF_UNIQUE_ID from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryError @@ -868,7 +868,9 @@ async def async_step_opt_save(self): """ Save the options in a sorted way. It stores all the options. """ - _LOGGER.info(f"Storing Updated Camera ({self.camera_config.unique_id}) Options.") + _LOGGER.info( + f"Storing Updated Camera ({self.camera_config.unique_id}) Options." + ) try: _, vacuum_device = get_vacuum_device_info( self.camera_config.data.get(CONF_VACUUM_CONFIG_ENTRY_ID), self.hass diff --git a/custom_components/mqtt_vacuum_camera/const.py b/custom_components/mqtt_vacuum_camera/const.py index 396ee970..35f14096 100755 --- a/custom_components/mqtt_vacuum_camera/const.py +++ b/custom_components/mqtt_vacuum_camera/const.py @@ -1,4 +1,5 @@ """Constants for the mqtt_vacuum_camera integration.""" + """Version v2024.12.0""" from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN @@ -6,7 +7,6 @@ from homeassistant.components.vacuum import DOMAIN as VACUUM_DOMAIN """Required in Config_Flow""" -PLATFORMS = ["camera"] DOMAIN = "mqtt_vacuum_camera" DEFAULT_NAME = "mqtt vacuum camera" @@ -400,9 +400,10 @@ ATTR_POINTS = "points" ATTR_OBSTACLES = "obstacles" + class CameraModes: - """ Constants for the camera modes """ + """Constants for the camera modes""" + MAP_VIEW = "map_view" OBSTACLE_VIEW = "obstacle_view" OBSTACLE_DOWNLOAD = "obstacle_download" - diff --git a/custom_components/mqtt_vacuum_camera/services.yaml b/custom_components/mqtt_vacuum_camera/services.yaml index 17bf9c62..06ee1135 100755 --- a/custom_components/mqtt_vacuum_camera/services.yaml +++ b/custom_components/mqtt_vacuum_camera/services.yaml @@ -42,13 +42,13 @@ obstacle_view: min: 0 max: 90000 coordinates_y: - name: y - description: Coordinate y for the obstacle view. - required: true - selector: - number: - min: 0 - max: 90000 + name: y + description: Coordinate y for the obstacle view. + required: true + selector: + number: + min: 0 + max: 90000 vacuum_go_to: name: Vacuum go to diff --git a/custom_components/mqtt_vacuum_camera/snapshots/log_files.py b/custom_components/mqtt_vacuum_camera/snapshots/log_files.py index 69bf9ee5..601874d4 100644 --- a/custom_components/mqtt_vacuum_camera/snapshots/log_files.py +++ b/custom_components/mqtt_vacuum_camera/snapshots/log_files.py @@ -1,4 +1,4 @@ -""" Logs and files colloection +"""Logs and files colloection MQTT Vacuum Camera component for Home Assistant Version: v2024.10.0""" diff --git a/custom_components/mqtt_vacuum_camera/utils/auto_crop.py b/custom_components/mqtt_vacuum_camera/utils/auto_crop.py index 740dd486..ef3d2714 100644 --- a/custom_components/mqtt_vacuum_camera/utils/auto_crop.py +++ b/custom_components/mqtt_vacuum_camera/utils/auto_crop.py @@ -263,7 +263,9 @@ async def async_auto_trim_and_zoom_image( self.imh.trim_down, ).to_list() if self.imh.shared.vacuum_state == "docked": - await self._async_save_auto_crop_data() # Save the crop data to the disk + await ( + self._async_save_auto_crop_data() + ) # Save the crop data to the disk self.auto_crop_offset() # If it is needed to zoom the image. trimmed = await self.async_check_if_zoom_is_on( diff --git a/custom_components/mqtt_vacuum_camera/utils/camera/camera_services.py b/custom_components/mqtt_vacuum_camera/utils/camera/camera_services.py index 60e82143..78ffe6eb 100644 --- a/custom_components/mqtt_vacuum_camera/utils/camera/camera_services.py +++ b/custom_components/mqtt_vacuum_camera/utils/camera/camera_services.py @@ -8,8 +8,8 @@ from homeassistant.const import SERVICE_RELOAD from homeassistant.core import HomeAssistant, ServiceCall -from ...const import DOMAIN from ...common import get_entity_id +from ...const import DOMAIN from ...utils.files_operations import async_clean_up_all_auto_crop_files _LOGGER = logging.getLogger(__name__) @@ -59,24 +59,27 @@ async def reload_camera_config(call: ServiceCall, hass: HomeAssistant) -> None: context=call.context, ) + async def obstacle_view(call: ServiceCall, hass: HomeAssistant) -> None: """Action to download and show the obstacles in the maps.""" coordinates_x = call.data.get("coordinates_x") coordinates_y = call.data.get("coordinates_y") - #attempt to get the entity_id or device. + # attempt to get the entity_id or device. entity_id = call.data.get("entity_id") device_id = call.data.get("device_id") - #resolve the entity_id if not provided. + # resolve the entity_id if not provided. camera_entity_id = get_entity_id(entity_id, device_id, hass, "camera")[0] _LOGGER.debug(f"Obstacle view for {camera_entity_id}") - _LOGGER.debug(f"Firing event for search and obstacle view at coordinates {coordinates_x}, {coordinates_y}") + _LOGGER.debug( + f"Firing event for search and obstacle view at coordinates {coordinates_x}, {coordinates_y}" + ) hass.bus.async_fire( event_type=f"{DOMAIN}_obstacle_coordinates", event_data={ "entity_id": camera_entity_id, - "coordinates": {"x": coordinates_x, "y": coordinates_y} + "coordinates": {"x": coordinates_x, "y": coordinates_y}, }, context=call.context, ) diff --git a/custom_components/mqtt_vacuum_camera/utils/camera/camera_shared.py b/custom_components/mqtt_vacuum_camera/utils/camera/camera_shared.py index 8a28cea9..a871e224 100755 --- a/custom_components/mqtt_vacuum_camera/utils/camera/camera_shared.py +++ b/custom_components/mqtt_vacuum_camera/utils/camera/camera_shared.py @@ -19,7 +19,6 @@ ATTR_VACUUM_POSITION, ATTR_VACUUM_STATUS, ATTR_ZONES, - CameraModes, CONF_ASPECT_RATIO, CONF_AUTO_ZOOM, CONF_OFFSET_BOTTOM, @@ -33,6 +32,7 @@ CONF_VAC_STAT_SIZE, CONF_ZOOM_LOCK_RATIO, DEFAULT_VALUES, + CameraModes, ) from custom_components.mqtt_vacuum_camera.types import Colors diff --git a/custom_components/mqtt_vacuum_camera/utils/drawable.py b/custom_components/mqtt_vacuum_camera/utils/drawable.py index ff3430a6..24adc540 100755 --- a/custom_components/mqtt_vacuum_camera/utils/drawable.py +++ b/custom_components/mqtt_vacuum_camera/utils/drawable.py @@ -22,8 +22,10 @@ class Drawable: This class contains static methods to draw various elements on the Numpy Arrays (images). We cant use openCV because it is not supported by the Home Assistant OS. """ + ERROR_OUTLINE = (0, 0, 0, 255) # Red color for error messages ERROR_COLOR = (255, 0, 0, 191) # Red color with lower opacity for error outlines + @staticmethod async def create_empty_image( width: int, height: int, background_color: Color @@ -542,7 +544,7 @@ def status_text( draw = ImageDraw.Draw(image) # Draw the text for text in status: - if "\u2211" in text or "\u03DE" in text: + if "\u2211" in text or "\u03de" in text: font = default_font width = None else: diff --git a/custom_components/mqtt_vacuum_camera/utils/files_operations.py b/custom_components/mqtt_vacuum_camera/utils/files_operations.py index c220625f..b71f7896 100755 --- a/custom_components/mqtt_vacuum_camera/utils/files_operations.py +++ b/custom_components/mqtt_vacuum_camera/utils/files_operations.py @@ -311,9 +311,9 @@ async def async_rename_room_description(hass: HomeAssistant, vacuum_id: str) -> for j in range(start_index, end_index): if j < len(room_data): room_id, room_name = list(room_data.items())[j] - data["options"]["step"][alpha_key]["data"][ - f"alpha_room_{j}" - ] = f"RoomID {room_id} {room_name}" + data["options"]["step"][alpha_key]["data"][f"alpha_room_{j}"] = ( + f"RoomID {room_id} {room_name}" + ) # Write the modified data back to the JSON files for idx, data in enumerate(data_list): diff --git a/custom_components/mqtt_vacuum_camera/utils/img_data.py b/custom_components/mqtt_vacuum_camera/utils/img_data.py index 688932b2..0b815aaa 100755 --- a/custom_components/mqtt_vacuum_camera/utils/img_data.py +++ b/custom_components/mqtt_vacuum_camera/utils/img_data.py @@ -332,7 +332,7 @@ def get_rrm_goto_target(json_data: JsonType) -> list or None: except KeyError: return None else: - if path_data is not []: + if path_data != []: path_data = ImageData.rrm_coordinates_to_valetudo(path_data) return path_data else: diff --git a/custom_components/mqtt_vacuum_camera/utils/status_text.py b/custom_components/mqtt_vacuum_camera/utils/status_text.py index 2a062f56..e0b4ed05 100755 --- a/custom_components/mqtt_vacuum_camera/utils/status_text.py +++ b/custom_components/mqtt_vacuum_camera/utils/status_text.py @@ -88,7 +88,7 @@ def get_status_text(self, text_img: PilPNG) -> tuple[list[str], int]: status_text = ["If you read me, something really went wrong.."] # default text text_size_coverage = 1.5 # resize factor for the text text_size = self._shared.vacuum_status_size # default text size - charge_level = "\u03DE" # unicode Koppa symbol + charge_level = "\u03de" # unicode Koppa symbol charging = "\u2211" # unicode Charging symbol vacuum_state = self.translate_vacuum_status() if self._shared.show_vacuum_state: @@ -106,17 +106,17 @@ def get_status_text(self, text_img: PilPNG) -> tuple[list[str], int]: status_text.append(f" ({in_room})") if self._shared.vacuum_state == "docked": if int(self._shared.vacuum_battery) <= 99: - status_text.append(" \u00B7 ") + status_text.append(" \u00b7 ") status_text.append(f"{charging}{charge_level} ") status_text.append(f"{self._shared.vacuum_battery}%") self._shared.vacuum_bat_charged = False else: - status_text.append(" \u00B7 ") + status_text.append(" \u00b7 ") status_text.append(f"{charge_level} ") status_text.append("Ready.") self._shared.vacuum_bat_charged = True else: - status_text.append(" \u00B7 ") + status_text.append(" \u00b7 ") status_text.append(f"{charge_level}") status_text.append(f" {self._shared.vacuum_battery}%") if text_size >= 50: diff --git a/custom_components/mqtt_vacuum_camera/valetudo/MQTT/connector.py b/custom_components/mqtt_vacuum_camera/valetudo/MQTT/connector.py index a28a0900..f569b750 100755 --- a/custom_components/mqtt_vacuum_camera/valetudo/MQTT/connector.py +++ b/custom_components/mqtt_vacuum_camera/valetudo/MQTT/connector.py @@ -406,7 +406,10 @@ async def async_message_received(self, msg) -> None: ) _LOGGER.debug(f"Vacuum API URL: {self._shared.vacuum_api}") elif self._rcv_topic == f"{self._mqtt_topic}/WifiConfigurationCapability/ips": - self._shared.vacuum_ips = await self.async_decode_mqtt_payload(msg) + vacuum_host_ip = await self.async_decode_mqtt_payload(msg) + # When IPV4 and IPV6 are available, use IPV4 + if vacuum_host_ip.split(",").__len__() > 1: + self._shared.vacuum_ips = vacuum_host_ip.split(",")[0] _LOGGER.debug(f"Vacuum IPs: {self._shared.vacuum_ips}") async def async_subscribe_to_topics(self) -> None: diff --git a/custom_components/mqtt_vacuum_camera/valetudo/hypfer/image_handler.py b/custom_components/mqtt_vacuum_camera/valetudo/hypfer/image_handler.py index 6cc2a835..0a5e2a71 100755 --- a/custom_components/mqtt_vacuum_camera/valetudo/hypfer/image_handler.py +++ b/custom_components/mqtt_vacuum_camera/valetudo/hypfer/image_handler.py @@ -94,9 +94,12 @@ async def async_extract_room_properties(self, json_data): compressed_pixels = layer.get("compressedPixels", []) pixels = self.data.sublist(compressed_pixels, 3) # Calculate x and y min/max from compressed pixels - x_min, y_min, x_max, y_max = ( - await self.data.async_get_rooms_coordinates(pixels, pixel_size) - ) + ( + x_min, + y_min, + x_max, + y_max, + ) = await self.data.async_get_rooms_coordinates(pixels, pixel_size) corners = [ (x_min, y_min), (x_max, y_min), @@ -156,9 +159,11 @@ async def async_get_image_from_json( # Check entity data. entity_dict = await self.imd.async_get_entity_data(m_json) # Update the Robot position. - robot_pos, robot_position, robot_position_angle = ( - await self.imd.async_get_robot_position(entity_dict) - ) + ( + robot_pos, + robot_position, + robot_position_angle, + ) = await self.imd.async_get_robot_position(entity_dict) # Get the pixels size and layers from the JSON data pixel_size = int(m_json["pixelSize"]) @@ -284,10 +289,11 @@ async def async_get_image_from_json( new_width = pil_img.width new_height = int(pil_img.width / new_aspect_ratio) resized = ImageOps.pad(pil_img, (new_width, new_height)) - self.crop_img_size[0], self.crop_img_size[1] = ( - await self.async_map_coordinates_offset( - wsf, hsf, new_width, new_height - ) + ( + self.crop_img_size[0], + self.crop_img_size[1], + ) = await self.async_map_coordinates_offset( + wsf, hsf, new_width, new_height ) _LOGGER.debug( f"{self.file_name}: Image Aspect Ratio ({wsf}, {hsf}): {new_width}x{new_height}" diff --git a/custom_components/mqtt_vacuum_camera/valetudo/rand256/image_handler.py b/custom_components/mqtt_vacuum_camera/valetudo/rand256/image_handler.py index edbded22..eabc3b67 100755 --- a/custom_components/mqtt_vacuum_camera/valetudo/rand256/image_handler.py +++ b/custom_components/mqtt_vacuum_camera/valetudo/rand256/image_handler.py @@ -79,10 +79,11 @@ async def extract_room_properties( top, left = ImageData.get_rrm_image_position(json_data) try: if not self.segment_data or not self.outlines: - self.segment_data, self.outlines = ( - await ImageData.async_get_rrm_segments( - json_data, size_x, size_y, top, left, True - ) + ( + self.segment_data, + self.outlines, + ) = await ImageData.async_get_rrm_segments( + json_data, size_x, size_y, top, left, True ) dest_json = destinations room_data = dict(dest_json).get("rooms", []) @@ -172,9 +173,11 @@ async def get_image_from_rrm( self.json_id = str(uuid.uuid4()) # image id _LOGGER.info(f"Vacuum Data ID: {self.json_id}") # get the robot position - robot_pos, robot_position, robot_position_angle = ( - await self.imd.async_get_robot_position(m_json) - ) + ( + robot_pos, + robot_position, + robot_position_angle, + ) = await self.imd.async_get_robot_position(m_json) if self.frame_number == 0: room_id, img_np_array = await self.imd.async_draw_base_layer( m_json, @@ -270,10 +273,11 @@ async def get_image_from_rrm( new_height = int(pil_img.width / new_aspect_ratio) resized = ImageOps.pad(pil_img, (new_width, new_height)) - self.crop_img_size[0], self.crop_img_size[1] = ( - await self.async_map_coordinates_offset( - wsf, hsf, new_width, new_height - ) + ( + self.crop_img_size[0], + self.crop_img_size[1], + ) = await self.async_map_coordinates_offset( + wsf, hsf, new_width, new_height ) _LOGGER.debug( f"{self.file_name}: Image Aspect Ratio ({wsf}, {hsf}): {new_width}x{new_height}" diff --git a/custom_components/mqtt_vacuum_camera/valetudo/rand256/reimg_draw.py b/custom_components/mqtt_vacuum_camera/valetudo/rand256/reimg_draw.py index f7a2e731..7da6f464 100644 --- a/custom_components/mqtt_vacuum_camera/valetudo/rand256/reimg_draw.py +++ b/custom_components/mqtt_vacuum_camera/valetudo/rand256/reimg_draw.py @@ -62,10 +62,11 @@ async def async_segment_data( """Get the segments data from the JSON data.""" try: if not self.img_h.segment_data: - self.img_h.segment_data, self.img_h.outlines = ( - await self.data.async_get_rrm_segments( - m_json, size_x, size_y, pos_top, pos_left, True - ) + ( + self.img_h.segment_data, + self.img_h.outlines, + ) = await self.data.async_get_rrm_segments( + m_json, size_x, size_y, pos_top, pos_left, True ) except ValueError as e: self.img_h.segment_data = None From abc9edbfbe70112e4bf50cd6d79d0a1896b0ddb7 Mon Sep 17 00:00:00 2001 From: SCA075 <82227818+sca075@users.noreply.github.com> Date: Tue, 3 Dec 2024 23:56:49 +0100 Subject: [PATCH 2/3] adding docs Signed-off-by: 82227818+sca075@users.noreply.github.com <82227818+sca075@users.noreply.github.com> --- docs/obstacles_detection.md | 67 +++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 docs/obstacles_detection.md diff --git a/docs/obstacles_detection.md b/docs/obstacles_detection.md new file mode 100644 index 00000000..bfe65f23 --- /dev/null +++ b/docs/obstacles_detection.md @@ -0,0 +1,67 @@ +# Obstacle Detection and Image Processing + +## Overview +The Obstacle Detection and Image Processing feature allows users to visualize obstacles detected by their vacuum directly in Home Assistant. This feature is designed for vacuums supporting the `ObstacleImagesCapability` and enables a dynamic experience by switching between map and obstacle views. + +## Key Features +- **Dynamic Obstacle Interaction**: + - Click on detected obstacles in the map view to display their corresponding images. + - Switch back to the map view by clicking anywhere on the obstacle image. + +- **Seamless View Switching**: + - Switch between map and obstacle views in near real-time is possible thanks to the [Xaiomi Vacuum Map Card](https://github.com/PiotrMachowski/lovelace-xiaomi-vacuum-map-card). + - In order to select the obstacle that is highlight in a red dot drawn on the map, it is necessary to add this code to the card configuration: +```yaml +map_modes: + - other map modes + ... + - name: Obstacles View + icon: mdi:map-marker + run_immediately: false + coordinates_rounding: true + coordinates_to_meters_divider: 100 + selection_type: MANUAL_POINT + max_selections: 999 + repeats_type: NONE + max_repeats: 1 + service_call_schema: + service: mqtt_vacuum_camera.obstacle_view + service_data: + coordinates_x: "[[point_x]]" + coordinates_y: "[[point_y]]" + target: + entity_id: camera.YOUR_MQTT_CAMERA_camera + variables: {} +``` + +## How It Works +1. **Triggering the Event**: + - When a user clicks on the map, the frontend card will trigger the action "obstacle_view": + - `entity_id`: The camera entity to handle the request. + - `coordinates`: The map coordinates of the clicked point. + - The below video demonstrates the feature in action: + + https://github.com/user-attachments/assets/0815fa06-4e19-47a1-9fdc-e12d22449acc + +2. **Finding the Nearest Obstacle**: + - The system locates the nearest obstacle to the given coordinates it isn't necessary to point directly on it. + - +3. **Image Download and Processing**: + - If an obstacle is found, the integration: + 1. Downloads the image from the vacuum. + 2. Resizes it to fit the UI (this will be later improved). + 3. Displays it in the camera view. + +4. **Switching Views**: + - Clicking on an obstacle switches the camera to `Obstacle View`. + - Clicking any ware obstacle image switches back to `Map View`. + +## Configuration +1. Ensure your vacuum supports `ObstacleImagesCapability` and is integrated into Home Assistant. +2. Use a compatible frontend card that allows interaction with map coordinates such the [Xaiomi Vacuum Map Card](https://github.com/PiotrMachowski/lovelace-xiaomi-vacuum-map-card). + +## Notes +### Supported Vacuums +If the vacuum do not support the `ObstacleImagesCapability`, the Camera will simply display the obstacles with a Red Dot on the map, when the Vacuum support Obstacle Detections and has no oboard camera. +If the vacuum supports the capability, the user can interact with the map and view obstacles in near real-time. +This feature was tested on Supervised HA-OS on Pi4 with 8GB RAM and 64GB disk. From 3b52280476bd94944a83dbeeb24ef90543c0b5c7 Mon Sep 17 00:00:00 2001 From: SCA075 <82227818+sca075@users.noreply.github.com> Date: Wed, 4 Dec 2024 00:00:56 +0100 Subject: [PATCH 3/3] updated manifest.json Signed-off-by: 82227818+sca075@users.noreply.github.com <82227818+sca075@users.noreply.github.com> --- custom_components/mqtt_vacuum_camera/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/mqtt_vacuum_camera/manifest.json b/custom_components/mqtt_vacuum_camera/manifest.json index 2acdbc75..cf1f3f25 100755 --- a/custom_components/mqtt_vacuum_camera/manifest.json +++ b/custom_components/mqtt_vacuum_camera/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/sca075/mqtt_vacuum_camera/issues", "requirements": ["pillow>=10.3.0,<=11.0.0", "numpy"], - "version": "2024.12.0b1" + "version": "2024.12.0" }