From 4845f286cc6574d5487a950f4bf7ed9d3ad471cb Mon Sep 17 00:00:00 2001 From: SCA075 <82227818+sca075@users.noreply.github.com> Date: Wed, 11 Dec 2024 00:11:32 +0100 Subject: [PATCH] Change logging to redact the IP address from the logs. Change to Map View improved to reload the immage from the vacuum. Added new Camera states Signed-off-by: 82227818+sca075@users.noreply.github.com <82227818+sca075@users.noreply.github.com> --- .../mqtt_vacuum_camera/camera.py | 88 +++++++++++++------ .../mqtt_vacuum_camera/common.py | 9 ++ custom_components/mqtt_vacuum_camera/const.py | 4 +- 3 files changed, 71 insertions(+), 30 deletions(-) diff --git a/custom_components/mqtt_vacuum_camera/camera.py b/custom_components/mqtt_vacuum_camera/camera.py index 53c1033d..f2dec5e3 100755 --- a/custom_components/mqtt_vacuum_camera/camera.py +++ b/custom_components/mqtt_vacuum_camera/camera.py @@ -26,12 +26,13 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from psutil_home_assistant import PsutilWrapper as ProcInsp -from .common import get_vacuum_unique_id_from_mqtt_topic +from .common import get_vacuum_unique_id_from_mqtt_topic, RedactIPFilter from .const import ( ATTR_FRIENDLY_NAME, ATTR_JSON_DATA, ATTR_OBSTACLES, ATTR_SNAPSHOT_PATH, + ATTR_CAMERA_MODE, ATTR_VACUUM_TOPIC, CAMERA_STORAGE, CONF_VACUUM_IDENTIFIERS, @@ -54,6 +55,7 @@ SCAN_INTERVAL = timedelta(seconds=3) _LOGGER = logging.getLogger(__name__) +_LOGGER.addFilter(RedactIPFilter()) async def async_setup_entry( @@ -124,10 +126,12 @@ def __init__(self, coordinator, device_info): self.processor = CameraProcessor(self.hass, self._shared) # Listen to the vacuum.start event - cancel = self.hass.bus.async_listen("event_vacuum_start", self.handle_vacuum_start) - cancel = self.hass.bus.async_listen( - "mqtt_vacuum_camera_obstacle_coordinates", self.handle_obstacle_view - ) + self.uns_event_vacuum_start = self.hass.bus.async_listen( + "event_vacuum_start", self.handle_vacuum_start + ) + self.uns_event_obstacle_coordinates = self.hass.bus.async_listen( + "mqtt_vacuum_camera_obstacle_coordinates", self.handle_obstacle_view + ) @staticmethod def _start_up_logs(): @@ -229,6 +233,7 @@ def extra_state_attributes(self) -> dict: ATTR_VACUUM_TOPIC: self._mqtt_listen_topic, ATTR_JSON_DATA: self._vac_json_available, ATTR_SNAPSHOT_PATH: f"/local/snapshot_{self._file_name}.png", + ATTR_CAMERA_MODE: self._shared.camera_mode, } if self._shared.obstacles_data: attributes.update({ATTR_OBSTACLES: self._shared.obstacles_data}) @@ -240,7 +245,10 @@ def extra_state_attributes(self) -> dict: @property def should_poll(self) -> bool: """ON/OFF Camera Polling Based on Camera Mode.""" - if self._shared.camera_mode == [CameraModes.OBSTACLE_DOWNLOAD]: + if self._shared.camera_mode in [ + CameraModes.OBSTACLE_DOWNLOAD, + CameraModes.OBSTACLE_SEARCH, + ]: self._should_poll = False elif isinstance(self._shared.camera_mode, bool): if self._shared.camera_mode: @@ -308,6 +316,12 @@ async def take_snapshot(self, json_data: Any, image_data: Image.Image) -> None: async def async_update(self): """Camera Frame Update.""" + # Obstacle View Processing + if self._shared.camera_mode == CameraModes.OBSTACLE_VIEW: + if self.Image is not None: + return self.camera_image(self._image_w, self._image_h) + + # Map View Processing if is_auth_updated(self): # Get the active user language self._shared.user_language = await async_get_active_user_language(self.hass) @@ -322,9 +336,6 @@ async def async_update(self): pid = os.getpid() # Start to log the CPU usage of this PID. proc = ProcInsp().psutil.Process(pid) # Get the process PID. process_data = await self._mqtt.is_data_available() - if self._shared.camera_mode == CameraModes.OBSTACLE_VIEW: - if self.Image is not None: - return self.camera_image(self._image_w, self._image_h) if process_data and self._shared.camera_mode == CameraModes.MAP_VIEW: # to calculate the cycle time for frame adjustment. start_time = time.perf_counter() @@ -482,13 +493,19 @@ def _update_frame_interval(self, start_time): processing_time = round((time.perf_counter() - start_time), 3) self._attr_frame_interval = max(0.1, processing_time) - async def async_pil_to_bytes(self, pil_img) -> Optional[bytes]: + async def async_pil_to_bytes( + self, pil_img, image_id: str = None + ) -> Optional[bytes]: """Convert PIL image to bytes""" - if pil_img: + if self._shared.camera_mode != CameraModes.MAP_VIEW: + self._image_bk = pil_img + _LOGGER.debug(f"{self._file_name}: Output Image: {image_id}.") + else: self._last_image = pil_img _LOGGER.debug( f"{self._file_name}: Image from Json: {self._shared.vac_json_id}." ) + if pil_img: if self._shared.show_vacuum_state: pil_img = await self.processor.run_async_draw_image_text( pil_img, self._shared.user_colors[8] @@ -508,17 +525,17 @@ async def async_pil_to_bytes(self, pil_img) -> Optional[bytes]: del buffered, pil_img return bytes_data - def process_pil_to_bytes(self, pil_img): + def process_pil_to_bytes(self, pil_img, image_id: str = None): """Async function to process the image data from the Vacuum Json data.""" loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) try: - result = loop.run_until_complete(self.async_pil_to_bytes(pil_img)) + result = loop.run_until_complete(self.async_pil_to_bytes(pil_img, image_id)) finally: loop.close() return result - async def run_async_pil_to_bytes(self, pil_img): + async def run_async_pil_to_bytes(self, pil_img, image_id: str = None): """Thread function to process the image data from the Vacuum Json data.""" num_processes = 1 pil_img_list = [pil_img for _ in range(num_processes)] @@ -531,7 +548,7 @@ async def run_async_pil_to_bytes(self, pil_img): loop.run_in_executor( executor, self.process_pil_to_bytes, - pil_img, + pil_img, image_id ) for pil_img in pil_img_list ] @@ -557,6 +574,16 @@ async def handle_obstacle_view(self, event): async def _set_map_view_mode(reason: str = None): """Set the camera mode to MAP_VIEW.""" self._shared.camera_mode = CameraModes.MAP_VIEW + self._shared.frame_number = 0 + self._shared.image_grab = True + _LOGGER.debug( + f"Camera Mode Change to {self._shared.camera_mode}" + f"{f': {reason}' if reason else ''}" + ) + + async def _set_camera_mode(mode_of_camera: CameraModes, reason: str = None): + """Set the camera mode.""" + self._shared.camera_mode = mode_of_camera _LOGGER.debug( f"Camera Mode Change to {self._shared.camera_mode}" f"{f': {reason}' if reason else ''}" @@ -567,7 +594,9 @@ async def _async_find_nearest_obstacle(x, y, all_obstacles): nearest_obstacles = None width = self._shared.image_ref_width height = self._shared.image_ref_height - min_distance = round(60 * (width / height)) # (60 * aspect ratio) pixels distance + min_distance = round( + 60 * (width / height) + ) # (60 * aspect ratio) pixels distance _LOGGER.debug( f"Finding in the nearest {min_distance} pixels obstacle to coordinates: {x}, {y}" ) @@ -597,14 +626,12 @@ async def _async_find_nearest_obstacle(x, y, all_obstacles): and self._shared.camera_mode == CameraModes.MAP_VIEW ): if event.data.get("entity_id") == self.entity_id: - self._shared.camera_mode = CameraModes.OBSTACLE_DOWNLOAD - _LOGGER.debug(f"Camera Mode Change to {self._shared.camera_mode}") + await _set_camera_mode(CameraModes.OBSTACLE_SEARCH) coordinates = event.data.get("coordinates") if coordinates: obstacles = self._shared.obstacles_data coordinates_x = coordinates.get("x") coordinates_y = coordinates.get("y") - # Find the nearest obstacle nearest_obstacle = await _async_find_nearest_obstacle( coordinates_x, coordinates_y, obstacles @@ -613,10 +640,10 @@ async def _async_find_nearest_obstacle(x, y, all_obstacles): if nearest_obstacle: _LOGGER.debug(f"Nearest obstacle found: {nearest_obstacle}") if nearest_obstacle["link"]: - _LOGGER.debug( - f"Downloading image: {nearest_obstacle['link']}" + await _set_camera_mode( + mode_of_camera=CameraModes.OBSTACLE_DOWNLOAD, + reason=f"Downloading image: {nearest_obstacle['link']}", ) - # You can now use nearest_obstacle["link"] to download the image try: temp_image = await asyncio.wait_for( fut=self.processor.download_image( @@ -638,11 +665,9 @@ async def _async_find_nearest_obstacle(x, y, all_obstacles): ) return # Return to Camera Mode if temp_image is not None: - self._shared.camera_mode = CameraModes.OBSTACLE_VIEW - _LOGGER.debug( - f"Camera Mode Change to {self._shared.camera_mode}" - ) + await _set_camera_mode(CameraModes.OBSTACLE_VIEW) try: + start_time = time.perf_counter() # Open the downloaded image with PIL pil_img = await self.hass.async_create_task( self.processor.async_open_image(temp_image) @@ -661,7 +686,13 @@ async def _async_find_nearest_obstacle(x, y, all_obstacles): # f"{self._file_name}: Image resized to: {width}, {height}" # ) self.Image = await self.hass.async_create_task( - self.run_async_pil_to_bytes(pil_img) + self.run_async_pil_to_bytes( + pil_img, image_id=nearest_obstacle["label"] + ) + ) + end_time = time.perf_counter() + _LOGGER.debug( + f"Image processing time: {end_time - start_time} seconds" ) return except Exception as e: @@ -673,8 +704,7 @@ async def _async_find_nearest_obstacle(x, y, all_obstacles): else: await _set_map_view_mode("No image downloaded.") else: - _LOGGER.debug("No nearby obstacle found.") - self._shared.camera_mode = CameraModes.MAP_VIEW + await _set_map_view_mode("No nearby obstacle found.") return # Return to Camera Mode else: await _set_map_view_mode("No obstacles data available.") diff --git a/custom_components/mqtt_vacuum_camera/common.py b/custom_components/mqtt_vacuum_camera/common.py index 5e89861c..81724dd0 100755 --- a/custom_components/mqtt_vacuum_camera/common.py +++ b/custom_components/mqtt_vacuum_camera/common.py @@ -255,3 +255,12 @@ def compose_obstacle_links(vacuum_host_ip: str, obstacles: list) -> list: _LOGGER.debug("Obstacle links: linked data complete.") return obstacle_links + +class RedactIPFilter(logging.Filter): + """ Remove from the logs IP address""" + def filter(self, record): + """Regex to match IP addresses""" + ip_pattern = r'\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b' + if record.msg: + record.msg = re.sub(ip_pattern, '[Redacted IP]', record.msg) + return True diff --git a/custom_components/mqtt_vacuum_camera/const.py b/custom_components/mqtt_vacuum_camera/const.py index 7260a1d2..39309215 100755 --- a/custom_components/mqtt_vacuum_camera/const.py +++ b/custom_components/mqtt_vacuum_camera/const.py @@ -399,6 +399,7 @@ ATTR_ZONES = "zones" ATTR_POINTS = "points" ATTR_OBSTACLES = "obstacles" +ATTR_CAMERA_MODE = "camera_mode" class CameraModes: @@ -406,7 +407,8 @@ class CameraModes: MAP_VIEW = "map_view" OBSTACLE_VIEW = "obstacle_view" - OBSTACLE_DOWNLOAD = "obstacle_download" + OBSTACLE_DOWNLOAD = "load_view" + OBSTACLE_SEARCH = "search_view" CAMERA_STANDBY = "camera_standby" CAMERA_OFF = False CAMERA_ON = True