Skip to content

Commit

Permalink
Pre-release last adjustments #293 from sca075/refactoring_camera
Browse files Browse the repository at this point in the history
Pre-release last adjustments
  • Loading branch information
sca075 authored Dec 3, 2024
2 parents a59996c + 3b52280 commit f79f589
Show file tree
Hide file tree
Showing 20 changed files with 203 additions and 78 deletions.
7 changes: 5 additions & 2 deletions custom_components/mqtt_vacuum_camera/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down
60 changes: 43 additions & 17 deletions custom_components/mqtt_vacuum_camera/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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"]
Expand All @@ -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):
"""
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down
9 changes: 7 additions & 2 deletions custom_components/mqtt_vacuum_camera/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
"""
Expand Down Expand Up @@ -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
Expand Down
8 changes: 5 additions & 3 deletions custom_components/mqtt_vacuum_camera/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions custom_components/mqtt_vacuum_camera/const.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
"""Constants for the mqtt_vacuum_camera integration."""

"""Version v2024.12.0"""

from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.components.vacuum import DOMAIN as VACUUM_DOMAIN

"""Required in Config_Flow"""
PLATFORMS = ["camera"]
DOMAIN = "mqtt_vacuum_camera"
DEFAULT_NAME = "mqtt vacuum camera"

Expand Down Expand Up @@ -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"

2 changes: 1 addition & 1 deletion custom_components/mqtt_vacuum_camera/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
14 changes: 7 additions & 7 deletions custom_components/mqtt_vacuum_camera/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
""" Logs and files colloection
"""Logs and files colloection
MQTT Vacuum Camera component for Home Assistant
Version: v2024.10.0"""

Expand Down
4 changes: 3 additions & 1 deletion custom_components/mqtt_vacuum_camera/utils/auto_crop.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -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,
)
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
ATTR_VACUUM_POSITION,
ATTR_VACUUM_STATUS,
ATTR_ZONES,
CameraModes,
CONF_ASPECT_RATIO,
CONF_AUTO_ZOOM,
CONF_OFFSET_BOTTOM,
Expand All @@ -33,6 +32,7 @@
CONF_VAC_STAT_SIZE,
CONF_ZOOM_LOCK_RATIO,
DEFAULT_VALUES,
CameraModes,
)
from custom_components.mqtt_vacuum_camera.types import Colors

Expand Down
4 changes: 3 additions & 1 deletion custom_components/mqtt_vacuum_camera/utils/drawable.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion custom_components/mqtt_vacuum_camera/utils/img_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading

0 comments on commit f79f589

Please sign in to comment.