Skip to content

Commit

Permalink
Change logging to redact the IP address from the logs.
Browse files Browse the repository at this point in the history
Change to Map View improved to reload the immage from the vacuum.
Added new Camera states

Signed-off-by: [email protected] <[email protected]>
  • Loading branch information
sca075 committed Dec 10, 2024
1 parent 7a6ea03 commit 4845f28
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 30 deletions.
88 changes: 59 additions & 29 deletions custom_components/mqtt_vacuum_camera/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -54,6 +55,7 @@

SCAN_INTERVAL = timedelta(seconds=3)
_LOGGER = logging.getLogger(__name__)
_LOGGER.addFilter(RedactIPFilter())


async def async_setup_entry(
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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})
Expand All @@ -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:
Expand Down Expand Up @@ -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)
Expand All @@ -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()
Expand Down Expand Up @@ -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]
Expand All @@ -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)]
Expand All @@ -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
]
Expand All @@ -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 ''}"
Expand All @@ -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}"
)
Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -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)
Expand All @@ -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:
Expand All @@ -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.")
9 changes: 9 additions & 0 deletions custom_components/mqtt_vacuum_camera/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 3 additions & 1 deletion custom_components/mqtt_vacuum_camera/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -399,14 +399,16 @@
ATTR_ZONES = "zones"
ATTR_POINTS = "points"
ATTR_OBSTACLES = "obstacles"
ATTR_CAMERA_MODE = "camera_mode"


class CameraModes:
"""Constants for the camera modes"""

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

0 comments on commit 4845f28

Please sign in to comment.