Skip to content

Commit

Permalink
__init__.py added migration for v1.5.0
Browse files Browse the repository at this point in the history
camera.py it will not process MQTT data until the frame is complete. Added memory and cpu logging.
common.py added margins to update_options.
connector.py moved in MQTT folder.
const.py added margins constant.
cropping_trimming.md updated auto trimming and cropping sections.
en.json modification of the menus.
strings.json modification of the menus.
image_handler.py auto_crop_trim function.
image_handler.py auto_crop_trim function
install.md updated method to setup the camera.
manifest.json version 1.5.0

Signed-off-by: SCA075 <[email protected]>
  • Loading branch information
sca075 committed Dec 10, 2023
1 parent f8bcdcb commit 74e86a8
Show file tree
Hide file tree
Showing 15 changed files with 792 additions and 767 deletions.
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ internal_variables:
topic: valetudo/your_topic
```

We did work also a little to help the author of the card, we guess soon a new version of the card will be released.
Those settings will be automatically setup in the card as soon the vacuum and camera will be setup.

### Known Supported Vacuums:
***<details><summary> We here list, thanks to our users, the known working vacuums. </summary>***
Expand All @@ -56,7 +58,7 @@ internal_variables:


### How to install:
Via [HACS](https://hacs.xyz//setup/download) please follow the instructions in [here](./docs/install.md). This detailed guide will help to set up the camera.
Via [HACS](https://hacs.xyz//setup/download) please follow. The instructions in [here](./docs/install.md) show detailed steps and will help to set up the camera also without HACS (manual setup).

### Features:
<details><summary> We here List what this camera offers as futures. </summary>
Expand All @@ -69,16 +71,16 @@ Via [HACS](https://hacs.xyz//setup/download) please follow the instructions in [
- [*Cropping function*](./docs/croping_trimming.md) (default is 50% of the standard Valetudo size 5210x5210 = 2605x2605).
- Base colors are the **colors for robot, charger, walls, background, zones etc**.
- **Rooms colors**, Room 1 is acrually also the Floor color (for vacuum that do not supports rooms).
- From v1.3.2 is possible to [**Trim the images**](./docs/croping_trimming.md) as desidered.
- From v1.5.0 the camera [**Trim automatically the images**](./docs/croping_trimming.md). From the first image you will get the images already without the need to trim them.
- It is possible to **display on the image the vacuum staus**.
- We also added the **[transparency level custom setup](./docs/transparency.md) for all elements and rooms** from v1.4.2.
5) This integration make possible to **integrate multiple vacuums** as per each camera will be named with the vacuum name (example: vacuum.robot1 = camera.robot1.. vacuum.robotx = camera.robotx)
5) This integration make possible to **integrate multiple vacuums** as per each camera will be named with the vacuum name (example: vacuum.robot1 = camera.robot1_camera.. vacuum.robotx = camera.robotx_camera)
6) The camera as all cameras in HA **supports the ON/OFF service**, it is possible to *suspend and resume the camera streem as desired*.
</details>

## In implementation plan:
- Reconfigure the camera more easily, and pre-crop and trim automatically the images.
- Add the SVG export function with some options to split the images.
- Create the scripts to use Assist (not sure if this is necessary).

## Notes:
- This integration is developed and tested using a PI4 with Home Assistant OS fully updated [to the last version](https://www.home-assistant.io/faq/release/), this allows us to confirm that the component is working properly with Home Assistant. Tested also on Docker Supervised "production" enviroment (fully setup home installation).
Expand Down
63 changes: 19 additions & 44 deletions custom_components/valetudo_vacuum_camera/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
get_device_info,
get_vacuum_mqtt_topic,
get_vacuum_unique_id_from_mqtt_topic,
update_options,
)

_LOGGER = logging.getLogger(__name__)
Expand All @@ -35,53 +36,10 @@ async def options_update_listener(

async def async_migrate_entry(hass, config_entry: config_entries.ConfigEntry):
mqtt_topic_base = ""

"""Migrate old entry."""
_LOGGER.debug("Migrating config entry from version %s", config_entry.version)

if config_entry.version == 1.2:
new_data = {**config_entry.data}
_LOGGER.debug(new_data)
new_data.update({"trim_top": "0"})
new_data.update({"trim_bottom": "0"})
new_data.update({"trim_left": "0"})
new_data.update({"trim_right": "0"})
new_data.update({"show_vac_status": False})
new_data.update({"color_text": [255, 255, 255]})
_LOGGER.debug(new_data)
new_options = {**config_entry.options}
_LOGGER.debug(new_options)
if new_options or len(new_options) > 0:
new_options.update({"trim_top": "0"})
new_options.update({"trim_bottom": "0"})
new_options.update({"trim_left": "0"})
new_options.update({"trim_right": "0"})
new_options.update({"show_vac_status": False})
new_options.update({"color_text": [255, 255, 255]})
else:
new_options = new_data
_LOGGER.debug(new_options)

config_entry.version = 1.3
hass.config_entries.async_update_entry(config_entry, data=new_data)
hass.config_entries.async_update_entry(config_entry, options=new_options)

if config_entry.version == 1.3:
new_data = {**config_entry.data}
_LOGGER.debug(new_data)
new_data.update({"broker_host": "core-mosquitto"})
_LOGGER.debug(new_data)
new_options = {**config_entry.options}
_LOGGER.debug(new_options)
if new_options or len(new_options) > 0:
new_options.update({"broker_host": "core-mosquitto"})
else:
new_options = new_data
_LOGGER.debug(new_options)

config_entry.version = 1.4
hass.config_entries.async_update_entry(config_entry, data=new_data)
hass.config_entries.async_update_entry(config_entry, options=new_options)

if config_entry.version <= 2.0:
new_data = {**config_entry.data}
if config_entry.version < 2.0:
Expand Down Expand Up @@ -201,6 +159,23 @@ async def async_migrate_entry(hass, config_entry: config_entries.ConfigEntry):
config_entry, data=new_data, options=new_options
)

if config_entry.version == 2.1:
old_data = {**config_entry.data}
new_data = {'vacuum_config_entry': old_data['vacuum_config_entry']}
_LOGGER.debug(dict(new_data))
old_options = {**config_entry.options}
if len(old_options) is not 0:
tmp_option = {"margins": "150"}
new_options = await update_options(old_options, tmp_option)
_LOGGER.debug(dict(new_options))
config_entry.version = 2.2
hass.config_entries.async_update_entry(
config_entry, data=new_data, options=new_options
)
else:
_LOGGER.error("Please REMOVE and SETUP the Camera again. Error in migration process!!")
return False

_LOGGER.info(
"Migration to config entry version %s successful", config_entry.version
)
Expand Down
88 changes: 50 additions & 38 deletions custom_components/valetudo_vacuum_camera/camera.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
"""Camera Version 1.4.9
"""Camera Version 1.5.0
Valetudo Re Test image.
"""

from __future__ import annotations

import logging
import os
import psutil_home_assistant as proc_insp
import json
from io import BytesIO
from datetime import datetime
Expand All @@ -24,7 +26,7 @@
HomeAssistantType,
)
from homeassistant.util import Throttle
from custom_components.valetudo_vacuum_camera.valetudo.connector import (
from custom_components.valetudo_vacuum_camera.valetudo.MQTT.connector import (
ValetudoConnector,
)
from .valetudo.image_handler import (
Expand All @@ -49,6 +51,7 @@
PLATFORMS,
ATTR_ROTATE,
ATTR_CROP,
ATTR_MARGINS,
ATTR_TRIM_TOP,
ATTR_TRIM_BOTTOM,
ATTR_TRIM_LEFT,
Expand Down Expand Up @@ -170,12 +173,15 @@ def __init__(self, hass, device_info):
self._mqtt = ValetudoConnector(self._mqtt_listen_topic, self.hass)
self._identifiers = device_info.get(CONF_VACUUM_IDENTIFIERS)
self._image = None
self._processing = False
self._image_w = None
self._image_h = None
self._should_poll = False
self._map_handler = MapImageHandler()
self._re_handler = ReImageHandler()
self._map_rooms = None
self._map_pred_zones = None
self._map_pred_points = None
self._vacuum_shared = Vacuum()
self._vacuum_state = None
self._frame_interval = 1
Expand All @@ -187,6 +193,7 @@ def __init__(self, hass, device_info):
self._current = None
self._image_rotate = int(device_info.get(ATTR_ROTATE, 0))
self._image_crop = int(device_info.get(ATTR_CROP, 0))
self._margins = int(device_info.get(ATTR_MARGINS, 150))
self._trim_up = int(device_info.get(ATTR_TRIM_TOP, 0))
self._trim_down = int(device_info.get(ATTR_TRIM_BOTTOM, 0))
self._trim_left = int(device_info.get(ATTR_TRIM_LEFT, 0))
Expand All @@ -206,7 +213,7 @@ def __init__(self, hass, device_info):
self._image_grab = True
self._frame_nuber = 0
self._rrm_data = False # Temp. check for rrm data
self.throttled_camera_image = Throttle(timedelta(seconds=4))(self.camera_image)
self.throttled_camera_image = Throttle(timedelta(seconds=6))(self.camera_image)
try:
self.user_colors = [
device_info.get(COLOR_WALL),
Expand Down Expand Up @@ -333,6 +340,10 @@ def extra_state_attributes(self):
attrs["snapshot"] = False
if (self._map_rooms is not None) and (self._map_rooms != {}):
attrs["rooms"] = self._map_rooms
if (self._map_pred_zones is not None) and (self._map_pred_zones != {}):
attrs["zones"] = self._map_pred_zones
if (self._map_pred_points is not None) and (self._map_pred_points != {}):
attrs["points"] = self._map_pred_points
return attrs

@property
Expand Down Expand Up @@ -391,7 +402,7 @@ async def take_snapshot(self, json_data, image_data):
_LOGGER.warning(
"Error Saving"
+ self.file_name
+ ": Snapshot, no snapshot available till restart."
+ ": Snapshot, will not be available till restart."
)
else:
_LOGGER.debug(
Expand All @@ -416,11 +427,12 @@ async def async_update(self):
"""Camera Frame Update"""
# check and update the vacuum reported state
if not self._mqtt:
return
return self.empty_if_no_data()
# If we have data from MQTT, we process the image
self._vacuum_state = await self._mqtt.get_vacuum_status()
process_data = await self._mqtt.is_data_available()
process_data = await self._mqtt.is_data_available(self._processing)
if process_data:
self._processing = True
# if the vacuum is working, or it is the first image.
if (
self._vacuum_state == "cleaning"
Expand All @@ -434,8 +446,7 @@ async def async_update(self):
# take the snapshot.
self._snapshot_taken = False
_LOGGER.info(
self.file_name + ": Camera image data update available: %s",
process_data,
f"{self.file_name}: Camera image data update available: {process_data}"
)
# calculate the cycle time for frame adjustment
start_time = datetime.now()
Expand All @@ -458,17 +469,13 @@ async def async_update(self):
else:
# Just in case, let's check that the data is available
if parsed_json is not None:
pil_img = None
if self._rrm_data:
pil_img = await self._re_handler.get_image_from_rrm(
m_json=self._rrm_data,
robot_state=self._vacuum_state,
crop=self._image_crop,
img_rotation=self._image_rotate,
trim_u=self._trim_up,
trim_b=self._trim_down,
trim_r=self._trim_right,
trim_l=self._trim_left,
margins=self._margins,
user_colors=self._vacuum_shared.get_user_colors(),
rooms_colors=self._vacuum_shared.get_rooms_colors(),
file_name=self.file_name,
Expand All @@ -479,10 +486,7 @@ async def async_update(self):
robot_state=self._vacuum_state,
crop=self._image_crop,
img_rotation=self._image_rotate,
trim_u=self._trim_up,
trim_b=self._trim_down,
trim_r=self._trim_right,
trim_l=self._trim_left,
margins=self._margins,
user_colors=self._vacuum_shared.get_user_colors(),
rooms_colors=self._vacuum_shared.get_rooms_colors(),
file_name=self.file_name,
Expand All @@ -491,17 +495,18 @@ async def async_update(self):
if self._map_rooms is None:
if self._rrm_data is None:
self._map_rooms = await self._map_handler.get_rooms_attributes()
else:
elif (self._map_rooms is None) and (self._rrm_data is not None):
destinations = await self._mqtt.get_destinations()
if destinations is not None:
self._map_rooms = await self._re_handler.get_rooms_attributes(destinations)
if self._map_rooms:
_LOGGER.debug(
"State attributes rooms update: %s",
self._map_rooms)
self._map_rooms, self._map_pred_zones, self._map_pred_points = \
await self._re_handler.get_rooms_attributes(destinations)
if self._map_rooms:
_LOGGER.debug(
f"State attributes rooms update: {self._map_rooms}"
)
_LOGGER.debug(
"Applied " + self.file_name + " image rotation: %s",
{self._image_rotate},
f"Applied {self.file_name} "
f"image rotation: {self._image_rotate}"
)
if self._show_vacuum_state:
self._map_handler.draw.status_text(
Expand All @@ -523,18 +528,14 @@ async def async_update(self):
):
self._image_grab = False
_LOGGER.info(
"Suspended the camera data processing for: "
+ self.file_name
+ "."
f"Suspended the camera data processing for: {self.file_name}."
)
# take a snapshot
await self.take_snapshot(parsed_json, pil_img)
else:
self._image_grab = False
_LOGGER.info(
"Suspended the camera data processing for: "
+ self.file_name
+ "."
f"Suspended the camera data processing for: {self.file_name}."
)
# take a snapshot
await self.take_snapshot(self._rrm_data, pil_img)
Expand Down Expand Up @@ -564,9 +565,15 @@ async def async_update(self):
# On Py4 HA OS is not possible to install the openCV library.
buffered = BytesIO()
# backup the image
self._last_image = pil_img
self._image_w = pil_img.width
self._image_h = pil_img.height
if pil_img:
self._last_image = pil_img
self._image_w = pil_img.width
self._image_h = pil_img.height
else:
pil_img = self.empty_if_no_data()
self._last_image = None
self._image_w = pil_img.width
self._image_h = pil_img.height
pil_img.save(buffered, format="PNG")
bytes_data = buffered.getvalue()
self._image = bytes_data
Expand All @@ -575,15 +582,20 @@ async def async_update(self):
_LOGGER.info(self.file_name + ": Image update complete")
processing_time = (datetime.now() - start_time).total_seconds()
self._frame_interval = max(0.1, processing_time)
_LOGGER.debug(
"Adjusted " + self.file_name + ": Frame interval: %s",
self._frame_interval,
)
_LOGGER.debug(f"Adjusted {self.file_name}: Frame interval: {self._frame_interval}")

else:
_LOGGER.info(
self.file_name
+ ": Image not processed. Returning not updated image."
)
self._frame_interval = 0.1
self.camera_image(self._image_w, self._image_h)
# HA supervised memory and CUP usage report.
_LOGGER.debug(
f"{self.file_name} Camera CPU usage %: "
f"{proc_insp.PsutilWrapper().psutil.cpu_percent(interval=self.frame_interval)}")
_LOGGER.debug(f"{self.file_name} Camera Virtual Memory usage %: "
f"{proc_insp.PsutilWrapper().psutil.virtual_memory().percent}")
self._processing = False
return self._image
3 changes: 1 addition & 2 deletions custom_components/valetudo_vacuum_camera/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ def get_device_info(config_entry_id: str, hass: HomeAssistant) -> tuple[str, Dev
entity registry and device registry.
"""
vacuum_entity_id = er.async_resolve_entity_id(er.async_get(hass), config_entry_id)

if not vacuum_entity_id:
_LOGGER.error("Unable to lookup vacuum's entity ID. Was it removed?")
return None
Expand Down Expand Up @@ -86,7 +85,7 @@ async def update_options(bk_options, new_options):
# Initialize updated_options as an empty dictionary
updated_options = {}

keys_to_update = ['rotate_image', 'crop_image', 'trim_top', 'trim_bottom', 'trim_left', 'trim_right',
keys_to_update = ['rotate_image', 'crop_image', 'margins', 'trim_top', 'trim_bottom', 'trim_left', 'trim_right',
'show_vac_status', 'enable_www_snapshots', 'color_charger', 'color_move', 'color_wall',
'color_robot', 'color_go_to', 'color_no_go', 'color_zone_clean', 'color_background',
'color_text', 'alpha_charger', 'alpha_move', 'alpha_wall', 'alpha_robot', 'alpha_go_to',
Expand Down
Loading

0 comments on commit 74e86a8

Please sign in to comment.