diff --git a/custom_components/valetudo_vacuum_camera/camera.py b/custom_components/valetudo_vacuum_camera/camera.py index be37b21a..2d404822 100644 --- a/custom_components/valetudo_vacuum_camera/camera.py +++ b/custom_components/valetudo_vacuum_camera/camera.py @@ -1,4 +1,4 @@ -"""Camera Version 1.2.0""" +"""Camera Version 1.2.1""" from __future__ import annotations import logging import os @@ -41,8 +41,8 @@ DEFAULT_NAME, DOMAIN, PLATFORMS, - ATT_ROTATE, - ATT_CROP, + ATTR_ROTATE, + ATTR_CROP, COLOR_WALL, COLOR_ZONE_CLEAN, COLOR_ROBOT, @@ -75,8 +75,8 @@ vol.Required(CONF_VACUUM_ENTITY_ID): cv.string, vol.Required(CONF_MQTT_USER): cv.string, vol.Required(CONF_MQTT_PASS): cv.string, - vol.Required(ATT_ROTATE, default="0"): cv.string, - vol.Required(ATT_CROP, default="50"): cv.string, + vol.Required(ATTR_ROTATE, default="0"): cv.string, + vol.Required(ATTR_CROP, default="50"): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.entity_id, } ) @@ -109,17 +109,19 @@ async def async_setup_platform( class ValetudoCamera(Camera, Entity): + _attr_has_entity_name = True + def __init__(self, hass, device_info): super().__init__() self.hass = hass - self._name = device_info.get(CONF_NAME) - self._attr_unique_id = "_" # uses the config name for unique id self._vacuum_entity = device_info.get(CONF_VACUUM_ENTITY_ID) self._mqtt_listen_topic = device_info.get(CONF_VACUUM_CONNECTION_STRING) if self._mqtt_listen_topic: self._mqtt_listen_topic = str(self._mqtt_listen_topic) file_name = self._mqtt_listen_topic.split("/") self.snapshot_img = "/config/www/snapshot_" + file_name[1].lower() + ".png" + self._attr_name = file_name[1] + " Camera" + self._attr_unique_id = file_name[1].lower() + "_camera" self.file_name = file_name[1].lower() self._mqtt_user = device_info.get(CONF_MQTT_USER) self._mqtt_pass = device_info.get(CONF_MQTT_PASS) @@ -138,14 +140,14 @@ def __init__(self, hass, device_info): self._base = None self._current = None self._temp_dir = "config/tmp" - self._image_rotate = device_info.get(ATT_ROTATE) + self._image_rotate = device_info.get(ATTR_ROTATE) if self._image_rotate: - self._image_rotate = int(device_info.get(ATT_ROTATE)) + self._image_rotate = int(device_info.get(ATTR_ROTATE)) else: self._image_rotate = 0 - self._image_crop = device_info.get(ATT_CROP) + self._image_crop = device_info.get(ATTR_CROP) if self._image_crop: - self._image_crop = int(device_info.get(ATT_CROP)) + self._image_crop = int(device_info.get(ATTR_CROP)) else: self._image_crop = 0 self._image = self.update() @@ -214,7 +216,7 @@ def camera_image( @property def name(self) -> str: - return self._name + return self._attr_name def turn_on(self): self._mqtt.client_start() @@ -263,14 +265,14 @@ def empty_if_no_data(self): if os.path.isfile(self.snapshot_img) and (self._last_image is None): # Load the snapshot image self._last_image = Image.open(self.snapshot_img) - _LOGGER.info("Snapshot image loaded") + _LOGGER.info(self.file_name + ": Snapshot image loaded") return self._last_image elif self._last_image is not None: return self._last_image else: # Create an empty image with a gray background empty_img = Image.new("RGB", (800, 600), "gray") - _LOGGER.info("Staring up ...") + _LOGGER.info(self.file_name + ": Staring up ...") return empty_img def take_snapshot(self, json_data, image_data): @@ -292,15 +294,16 @@ def take_snapshot(self, json_data, image_data): image_data.save( self.snapshot_img ) - _LOGGER.info("Camera Snapshot Taken.") + _LOGGER.info(self.file_name + ": Camera Snapshot Taken.") except IOError: self._snapshot_taken = None _LOGGER.warning( - "Error Saving Image Snapshot, no snapshot available till restart." + "Error Saving" + self.file_name + ": Snapshot, no snapshot available till restart." ) else: _LOGGER.debug( - "Snapshot acquired during %s", + self.file_name + + ": Snapshot acquired during %s", {self._vacuum_state}, " Vacuum State.", ) @@ -325,7 +328,7 @@ def update(self): # take the snapshot. self._snapshot_taken = False # Starting the image processing. - _LOGGER.info("Camera image data update available: %s", process_data) + _LOGGER.info(self.file_name + ": Camera image data update available: %s", process_data) start_time = datetime.now() try: # bypassed code is for debug purpose only @@ -350,11 +353,13 @@ def update(self): self._image_crop, self._vacuum_shared.get_user_colors(), self._vacuum_shared.get_rooms_colors(), + self.file_name, ) if pil_img is not None: pil_img = pil_img.rotate(self._image_rotate) _LOGGER.debug( - "Applied image rotation: %s", {self._image_rotate} + "Applied " + self.file_name + " image rotation: %s", + {self._image_rotate} ) if not self._snapshot_taken and ( self._vacuum_state == "idle" @@ -367,7 +372,8 @@ def update(self): is not self._map_handler.get_frame_number() ): self._image_grab = False - _LOGGER.info("Suspended the camera data processing.") + _LOGGER.info("Suspended the camera data processing for: " + + self.file_name + ".") # take a snapshot self.take_snapshot(parsed_json, pil_img) self._vac_json_id = self._map_handler.get_json_id() @@ -391,13 +397,13 @@ def update(self): self._image = bytes_data # clean up del buffered, pil_img, bytes_data - _LOGGER.info("Camera image update complete") + _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 frame interval: %s", self._frame_interval) + _LOGGER.debug("Adjusted " + self.file_name + ": Frame interval: %s", self._frame_interval) else: _LOGGER.info( - "Camera image not processed. Returning not updated image." + self.file_name + ": Image not processed. Returning not updated image." ) self._frame_interval = 0.1 return self._image diff --git a/custom_components/valetudo_vacuum_camera/config_flow.py b/custom_components/valetudo_vacuum_camera/config_flow.py index 940e1081..5750eb55 100644 --- a/custom_components/valetudo_vacuum_camera/config_flow.py +++ b/custom_components/valetudo_vacuum_camera/config_flow.py @@ -1,11 +1,10 @@ -"""config_flow ver.1.1.9""" +"""config_flow ver.1.2.1""" import voluptuous as vol import logging from typing import Any, Dict, Optional from homeassistant import config_entries -# from homeassistant.const import CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.selector import EntitySelector, ColorRGBSelector @@ -15,9 +14,8 @@ CONF_VACUUM_CONNECTION_STRING, CONF_MQTT_USER, CONF_MQTT_PASS, - DEFAULT_NAME, - ATT_ROTATE, - ATT_CROP, + ATTR_ROTATE, + ATTR_CROP, COLOR_MOVE, COLOR_ROBOT, COLOR_WALL, @@ -63,8 +61,8 @@ IMG_SCHEMA = vol.Schema( { - vol.Required(ATT_ROTATE, default="0"): vol.In(["0", "90", "180", "270"]), - vol.Required(ATT_CROP, default="50"): cv.string, + vol.Required(ATTR_ROTATE, default="0"): vol.In(["0", "90", "180", "270"]), + vol.Required(ATTR_CROP, default="50"): cv.string, } ) @@ -106,6 +104,9 @@ class ValetudoCameraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1.2 + def __init__(self): + self.data = None + async def async_step_user(self, user_input: Optional[Dict[str, Any]] = None): if user_input is not None: return await self.async_step_mqtt() @@ -123,8 +124,8 @@ async def async_step_options_1(self, user_input: Optional[Dict[str, Any]] = None if user_input is not None: self.data.update( { - "rotate_image": user_input.get(ATT_ROTATE), - "crop_image": user_input.get(ATT_CROP), + "rotate_image": user_input.get(ATTR_ROTATE), + "crop_image": user_input.get(ATTR_CROP), } ) @@ -192,16 +193,20 @@ async def async_step_options_3(self, user_input: Optional[Dict[str, Any]] = None } ) + tmp_name = self.data["vacuum_map"] + tmp_name = tmp_name.split("/") + default_name = tmp_name[1] + " Camera" + return self.async_create_entry( - title=DEFAULT_NAME, + title=default_name, data=self.data, ) return self.async_show_form( - step_id="options_3", - data_schema=ROOMS_COLOR_SCHEMA, - description_placeholders=self.data, - ) + step_id="options_3", + data_schema=ROOMS_COLOR_SCHEMA, + description_placeholders=self.data, + ) @staticmethod @callback @@ -222,10 +227,10 @@ def __init__(self, config_entry: config_entries.ConfigEntry): self.IMG_SCHEMA = vol.Schema( { vol.Required( - ATT_ROTATE, default=config_entry.options.get("rotate_image") + ATTR_ROTATE, default=config_entry.options.get("rotate_image") ): vol.In(["0", "90", "180", "270"]), vol.Required( - ATT_CROP, default=config_entry.options.get("crop_image") + ATTR_CROP, default=config_entry.options.get("crop_image") ): cv.string, } ) @@ -315,10 +320,10 @@ def __init__(self, config_entry: config_entries.ConfigEntry): self.IMG_SCHEMA = vol.Schema( { vol.Required( - ATT_ROTATE, default=config_entry.data.get("rotate_image") + ATTR_ROTATE, default=config_entry.data.get("rotate_image") ): vol.In(["0", "90", "180", "270"]), vol.Required( - ATT_CROP, default=config_entry.data.get("crop_image") + ATTR_CROP, default=config_entry.data.get("crop_image") ): cv.string, } ) @@ -352,7 +357,7 @@ def __init__(self, config_entry: config_entries.ConfigEntry): ): ColorRGBSelector(), } ) - self.COLOR_2_SCHEMA =vol.Schema( + self.COLOR_2_SCHEMA = vol.Schema( { vol.Optional( COLOR_ROOM_0, default=config_entry.data.get("color_room_0") @@ -412,8 +417,8 @@ async def async_step_init(self, user_input: Optional[Dict[str, Any]] = None): if user_input is not None: self.data.update( { - "rotate_image": user_input.get(ATT_ROTATE), - "crop_image": user_input.get(ATT_CROP), + "rotate_image": user_input.get(ATTR_ROTATE), + "crop_image": user_input.get(ATTR_CROP), } ) return await self.async_step_init_2() diff --git a/custom_components/valetudo_vacuum_camera/const.py b/custom_components/valetudo_vacuum_camera/const.py index bf799fd3..f9a3cb21 100644 --- a/custom_components/valetudo_vacuum_camera/const.py +++ b/custom_components/valetudo_vacuum_camera/const.py @@ -1,12 +1,12 @@ """Constants for the valetudo_vacuum_camera integration.""" -"""Version 1.1.8""" +"""Version 1.2.1""" """Required in Config_Flow""" PLATFORMS = ["camera"] DOMAIN = "valetudo_vacuum_camera" DEFAULT_NAME = "valetudo vacuum camera" -ATT_ROTATE = "rotate_image" -ATT_CROP = "crop_image" +ATTR_ROTATE = "rotate_image" +ATTR_CROP = "crop_image" CONF_MQTT_PASS = "broker_password" CONF_MQTT_USER = "broker_user" CONF_VACUUM_CONNECTION_STRING = "vacuum_map" diff --git a/custom_components/valetudo_vacuum_camera/manifest.json b/custom_components/valetudo_vacuum_camera/manifest.json index 2e01d6bf..3cfb2b46 100644 --- a/custom_components/valetudo_vacuum_camera/manifest.json +++ b/custom_components/valetudo_vacuum_camera/manifest.json @@ -13,5 +13,5 @@ "pillow", "numpy" ], - "version": "v1.2.0" + "version": "v1.3.0-beta.1" } diff --git a/custom_components/valetudo_vacuum_camera/utils/valetudo_jdata.py b/custom_components/valetudo_vacuum_camera/utils/valetudo_jdata.py index 0bd52d6d..6cf56ad1 100644 --- a/custom_components/valetudo_vacuum_camera/utils/valetudo_jdata.py +++ b/custom_components/valetudo_vacuum_camera/utils/valetudo_jdata.py @@ -1,4 +1,4 @@ -"""Version 1.1.5""" +"""Version 1.2.1""" import logging import struct import zlib @@ -99,24 +99,24 @@ def extract_png_chunks(self, data): _LOGGER.debug("Valetudo Json data grabbed") return self._jdata - def camera_message_received(self, payload): + def camera_message_received(self, payload, source: "" = None): # Process the camera data here - _LOGGER.debug("Decoding PNG to JSON") + _LOGGER.debug("Decoding, " + source + " PNG to JSON") if payload is not None: try: extract_data = self.extract_png_chunks(payload) except Warning as warning: - _LOGGER.warning("MQTT message format error:", {warning}) + _LOGGER.warning(source + ": MQTT message format error:", {warning}) return None else: if self._jdata or extract_data is not None: - _LOGGER.debug("Extracting JSON") + _LOGGER.debug(source + ": Extracting JSON") dec_data = zlib.decompress(self._jdata).decode("utf-8") json_data = dec_data response = json.loads(json_data) - _LOGGER.debug("Extracting JSON Complete") + _LOGGER.debug(source + ": Extracting JSON Complete") del json_data return response else: - _LOGGER.debug("No data to process") + _LOGGER.debug(source + ": No data to process") return None diff --git a/custom_components/valetudo_vacuum_camera/valetudo/connector.py b/custom_components/valetudo_vacuum_camera/valetudo/connector.py index 7668a4f5..5b950090 100644 --- a/custom_components/valetudo_vacuum_camera/valetudo/connector.py +++ b/custom_components/valetudo_vacuum_camera/valetudo/connector.py @@ -1,4 +1,4 @@ -"""Version 1.2.0""" +"""Version 1.2.1""" import logging import time import paho.mqtt.client as client @@ -37,12 +37,12 @@ def __init__(self, mqttusr, mqttpass, mqtt_topic, hass): def update_data(self, process: bool = True): if self._img_payload: if process: - _LOGGER.debug("Processing data from MQTT") - result = self._img_decoder.camera_message_received(self._img_payload) + _LOGGER.debug("Processing " + self._mqtt_topic + " data from MQTT") + result = self._img_decoder.camera_message_received(self._img_payload, self._mqtt_topic) self._data_in = False return result else: - _LOGGER.debug("No data from MQTT or vacuum docked") + _LOGGER.debug("No data from " + self._mqtt_topic + " or vacuum docked") self._data_in = False return None @@ -62,49 +62,49 @@ def save_payload(self, file_name): "custom_components/valetudo_vacuum_camera/snapshots/mqtt_" + file_name + ".raw", "wb" ) as file: file.write(self._img_payload) - _LOGGER.info("Saved image data from MQTT in mqtt_data.raw!") + _LOGGER.info("Saved image data from MQTT in mqtt_" + file_name + ".raw!") def on_message_callback(self, client, userdata, msg): self._rcv_topic = msg.topic if self._rcv_topic == (self._mqtt_topic + "/MapData/map-data-hass"): - _LOGGER.debug("Received image data from MQTT") + _LOGGER.debug("Received " + self._mqtt_topic +" image data from MQTT") self._img_payload = msg.payload self._data_in = True elif self._rcv_topic == (self._mqtt_topic + "/StatusStateAttribute/status"): - _LOGGER.debug("Received vacuum status data from MQTT") + _LOGGER.debug(self._mqtt_topic + ": Received vacuum status data from MQTT") self._payload = msg.payload if self._payload: self._mqtt_vac_stat = bytes.decode(msg.payload, "utf-8") elif self._rcv_topic == ( self._mqtt_topic + "/StatusStateAttribute/error_description" ): - _LOGGER.debug("Received vacuum error data from MQTT") + _LOGGER.debug(self._mqtt_topic + ": Received vacuum error data from MQTT") self._payload = msg.payload self._mqtt_vac_err = bytes.decode(msg.payload, "utf-8") def on_connect_callback(self, client, userdata, flags, rc): self.subscribe(self._mqtt_subscribe) - _LOGGER.debug("Connected to MQTT broker.") + _LOGGER.debug("Subscribed to MQTT broker with topic: " + self._mqtt_topic) def stop_and_disconnect(self): self.loop_stop(force=False) # Stop the MQTT loop gracefully self.disconnect() # Disconnect from the broker - _LOGGER.debug("Stopped and disconnected from MQTT broker.") + _LOGGER.debug(self._mqtt_topic + ": Stopped and disconnected from MQTT broker.") def connect_broker(self): self.connect_async(host=self._broker, port=1883) self.enable_bridge_mode() self.loop_start() - _LOGGER.debug("Connect MQTT broker.") + _LOGGER.debug(self._mqtt_topic + ": Connect MQTT broker.") def client_start(self): self.loop_start() - _LOGGER.debug("Started MQTT loop") + _LOGGER.debug(self._mqtt_topic + ": Started MQTT loop") def client_stop(self): self.loop_stop() self._mqtt_run = False - _LOGGER.debug("Stopped MQTT loop") + _LOGGER.debug(self._mqtt_topic + ": Stopped MQTT loop") def is_client_check_mode(self, check_topic): test_topic = "valetudo/myTopic" diff --git a/custom_components/valetudo_vacuum_camera/valetudo/image_handler.py b/custom_components/valetudo_vacuum_camera/valetudo/image_handler.py index bd84390d..26603e12 100644 --- a/custom_components/valetudo_vacuum_camera/valetudo/image_handler.py +++ b/custom_components/valetudo_vacuum_camera/valetudo/image_handler.py @@ -446,7 +446,8 @@ def get_image_from_json( robot_state, crop: int = 50, user_colors: Colors = None, - rooms_colors: Color = None + rooms_colors: Color = None, + file_name: "" = None ): color_wall: Color = user_colors[0] color_no_go: Color = user_colors[6] @@ -458,8 +459,10 @@ def get_image_from_json( color_zone_clean: Color = user_colors[1] try: if m_json is not None: - _LOGGER.info("Composing the image for the camera.") + _LOGGER.info(file_name + ":Composing the image for the camera.") self.room_propriety = self.extract_room_properties(m_json) + if self.room_propriety: + _LOGGER.info(file_name + ": Supporting Rooms Cleaning!") size_x = int(m_json["size"]["x"]) size_y = int(m_json["size"]["y"]) self.img_size = { @@ -479,7 +482,7 @@ def get_image_from_json( predicted_path = paths_data.get("predicted_path", []) path_pixels = paths_data.get("path", []) except KeyError as e: - _LOGGER.info("Error extracting paths data: %s", str(e)) + _LOGGER.info(file_name + ": Error extracting paths data: %s", str(e)) if predicted_path: predicted_path = predicted_path[0]["points"] @@ -494,18 +497,18 @@ def get_image_from_json( try: zone_clean = self.find_zone_entities(m_json, None) except (ValueError, KeyError) as e: - _LOGGER.info("No zone clean: %s", str(e)) + _LOGGER.info(file_name + ": No zone clean: %s", str(e)) zone_clean = None else: - _LOGGER.debug("Got zone clean: %s", zone_clean) + _LOGGER.debug(file_name + ": Got zone clean: %s", zone_clean) try: entity_dict = self.find_points_entities(m_json, None) except (ValueError, KeyError) as e: - _LOGGER.warning("No points in json data: %s", str(e)) + _LOGGER.warning(file_name + ": No points in json data: %s", str(e)) entity_dict = None else: - _LOGGER.debug("Got the points in the json: %s", entity_dict) + _LOGGER.debug(file_name + ": Got the points in the json: %s", entity_dict) robot_position_angle = None robot_position = None @@ -539,10 +542,10 @@ def get_image_from_json( go_to = entity_dict.get("go_to_target") pixel_size = int(m_json["pixelSize"]) layers = self.find_layers(m_json["layers"]) - _LOGGER.debug("Layers to draw: %s", layers.keys()) - _LOGGER.info("Empty image with background color") + _LOGGER.debug(file_name + ": Layers to draw: %s", layers.keys()) + _LOGGER.info(file_name + ": Empty image with background color") img_np_array = self.create_empty_image(size_x, size_y, color_background) - _LOGGER.info("Overlapping Layers") + _LOGGER.info(file_name + ": Overlapping Layers") for layer_type, compressed_pixels_list in layers.items(): room_id = 0 for compressed_pixels in compressed_pixels_list: @@ -559,15 +562,15 @@ def get_image_from_json( elif layer_type == "wall": if zone_clean: try: - zones_clean = zone_clean.get("active_zone") + zones_clean = zone_clean.get(file_name + ": active_zone") except KeyError: zones_clean = None - _LOGGER.debug("No Zone Clean.") + _LOGGER.debug(file_name + ": No Zone Clean.") try: - no_go_zones = zone_clean.get("no_go_area") + no_go_zones = zone_clean.get(file_name + ": no_go_area") except KeyError: no_go_zones = None - _LOGGER.debug("No No Go area found.") + _LOGGER.debug(file_name + ": No Go area not found.") if zones_clean: img_np_array = self.draw_zone( img_np_array, zones_clean, color_zone_clean @@ -580,10 +583,10 @@ def get_image_from_json( img_np_array = self.from_json_to_image( img_np_array, pixels, pixel_size, color_wall ) - _LOGGER.info("Completed base Layers") + _LOGGER.info(file_name + ": Completed base Layers") if self.frame_number == 0: - _LOGGER.debug("Drawing image background") + _LOGGER.debug(file_name + ": Drawing image background") img_np_array = self.draw_battery_charger( img_np_array, charger_pos[0], charger_pos[1], color_charger ) @@ -592,7 +595,7 @@ def get_image_from_json( else: img_np_array = self.img_base_layer # If there is a zone clean we draw it now. - _LOGGER.debug("Frame number %s", self.frame_number) + _LOGGER.debug(file_name + ": Frame number %s", self.frame_number) self.frame_number += 1 if self.frame_number > 5: self.frame_number = 0 @@ -626,7 +629,7 @@ def get_image_from_json( del img_np_array return pil_img except Exception as e: - _LOGGER.error("Error in get_image_from_json: %s", str(e)) + _LOGGER.error(file_name + ": Error in get_image_from_json: %s", str(e)) return None def get_frame_number(self):