Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pre-release last adjustments #293

Merged
merged 3 commits into from
Dec 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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:
Comment on lines +165 to +167
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix early return in device ID processing loop.

The current implementation only processes the first device ID and ignores the rest. This could lead to missing entities when multiple device IDs are provided.

Here's the fix:

 def from_device_ids_to_entity_ids(
     device_ids: str, hass: HomeAssistant, domain: str = "vacuum"
 ) -> str:
     dev_reg = dr.async_get(hass)
     entity_reg = er.async_get(hass)
     resolved_entity_ids = []
     
     for device_id in device_ids:
         device = dev_reg.async_get(device_id)
         if device:
             for entry in entity_reg.entities.values():
                 if entry.device_id == device_id and entry.domain == domain:
                     resolved_entity_ids.append(entry.entity_id)
-            return resolved_entity_ids
+    return resolved_entity_ids

Committable suggestion skipped: line range outside the PR's diff.

"""
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
Loading