diff --git a/.coderabbit.yaml b/.coderabbit.yaml index b03bd437..c174ba0d 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -1,5 +1,5 @@ language: en-US -tone_instructions: '' +tone_instructions: 'cool' early_access: false enable_free_tier: true reviews: @@ -22,7 +22,7 @@ reviews: path_instructions: [] abort_on_close: true auto_review: - enabled: false + enabled: true auto_incremental_review: true ignore_title_keywords: [] labels: [] diff --git a/custom_components/mqtt_vacuum_camera/camera.py b/custom_components/mqtt_vacuum_camera/camera.py index 592d65f0..556a9f84 100755 --- a/custom_components/mqtt_vacuum_camera/camera.py +++ b/custom_components/mqtt_vacuum_camera/camera.py @@ -1,6 +1,6 @@ """ Camera -Version: v2024.10.0 +Version: v2024.11.0 """ from __future__ import annotations @@ -84,7 +84,7 @@ def __init__(self, coordinator, device_info): self._attr_brand = "MQTT Vacuum Camera" self._attr_name = "Camera" self._attr_is_on = True - self._directory_path = self.hass.config.path() # get Home Assistant path + self._homeassistant_path = self.hass.config.path() # get Home Assistant path self._shared, self._file_name = coordinator.update_shared_data(device_info) self._start_up_logs() self._storage_path, self.snapshot_img, self.log_file = self._init_paths() @@ -135,9 +135,9 @@ def _init_clear_www_folder(self): """Remove PNG and ZIP's stored in HA config WWW""" # If enable_snapshots check if for png in www if not self._shared.enable_snapshots and os.path.isfile( - f"{self._directory_path}/www/snapshot_{self._file_name}.png" + f"{self._homeassistant_path}/www/snapshot_{self._file_name}.png" ): - os.remove(f"{self._directory_path}/www/snapshot_{self._file_name}.png") + os.remove(f"{self._homeassistant_path}/www/snapshot_{self._file_name}.png") # If there is a log zip in www remove it if os.path.isfile(self.log_file): os.remove(self.log_file) @@ -146,7 +146,7 @@ def _init_paths(self): """Initialize Camera Paths""" storage_path = f"{self.hass.config.path(STORAGE_DIR)}/{CAMERA_STORAGE}" if not os.path.exists(storage_path): - storage_path = f"{self._directory_path}/{STORAGE_DIR}" + storage_path = f"{self._homeassistant_path}/{STORAGE_DIR}" snapshot_img = f"{storage_path}/{self._file_name}.png" log_file = f"{storage_path}/{self._file_name}.zip" return storage_path, snapshot_img, log_file @@ -225,7 +225,7 @@ def extra_state_attributes(self) -> dict: return attributes async def handle_vacuum_start(self, event): - """Handle the vacuum.start event.""" + """Handle the event_vacuum_start event.""" _LOGGER.debug(f"Received event: {event.event_type}, Data: {event.data}") # Call the reset_trims service when vacuum.start event occurs diff --git a/custom_components/mqtt_vacuum_camera/const.py b/custom_components/mqtt_vacuum_camera/const.py index d957998f..0810ba0b 100755 --- a/custom_components/mqtt_vacuum_camera/const.py +++ b/custom_components/mqtt_vacuum_camera/const.py @@ -1,6 +1,6 @@ """Constants for the mqtt_vacuum_camera integration.""" -"""Version v2024.10.0""" +"""Version v2024.11.0""" """Required in Config_Flow""" PLATFORMS = ["camera"] @@ -53,12 +53,21 @@ "mainBrush": 0, "sideBrush": 0, "filter": 0, - "sensor": 0, "currentCleanTime": 0, "currentCleanArea": 0, "cleanTime": 0, "cleanArea": 0, "cleanCount": 0, + "battery": 0, + "state": 0, + "last_run_start": 0, + "last_run_end": 0, + "last_run_duration": 0, + "last_run_area": 0, + "last_bin_out": 0, + "last_bin_full": 0, + "last_loaded_map": "NoMap", + "robot_in_room": "Unsupported", } DEFAULT_PIXEL_SIZE = 5 @@ -270,6 +279,8 @@ DECODED_TOPICS = { "/MapData/segments", + "/maploader/map", + "/maploader/status", "/StatusStateAttribute/status", "/StatusStateAttribute/error_description", "/$state", diff --git a/custom_components/mqtt_vacuum_camera/coordinator.py b/custom_components/mqtt_vacuum_camera/coordinator.py index 6bff9149..6b7776e4 100644 --- a/custom_components/mqtt_vacuum_camera/coordinator.py +++ b/custom_components/mqtt_vacuum_camera/coordinator.py @@ -1,8 +1,9 @@ """ MQTT Vacuum Camera Coordinator. -Version: v2024.10.0 +Version: v2024.11.0 """ +import asyncio from datetime import timedelta import logging from typing import Optional @@ -10,6 +11,7 @@ import async_timeout from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.event import async_call_later from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -53,14 +55,24 @@ def __init__( # Initialize shared data and MQTT connector self.shared, self.file_name = self._init_shared_data(self.vacuum_topic) self.start_up_mqtt() + self.scheduled_refresh: asyncio.TimerHandle | None = None + + def schedule_refresh(self) -> None: + """Schedule coordinator refresh after 1 second.""" + if self.scheduled_refresh: + self.scheduled_refresh.cancel() + self.scheduled_refresh = async_call_later( + self.hass, 1, lambda: asyncio.create_task(self.async_refresh()) + ) async def _async_update_data(self): """ Fetch data from the MQTT topics for sensors. """ - if (self.sensor_data == SENSOR_NO_DATA) or ( - self.shared is not None and self.shared.vacuum_state != "docked" - ): + if ( + (self.sensor_data == SENSOR_NO_DATA) + or (self.shared is not None and self.shared.vacuum_state != "docked") + ) and self.connector: try: async with async_timeout.timeout(10): # Fetch and process sensor data from the MQTT connector @@ -73,7 +85,7 @@ async def _async_update_data(self): return self.sensor_data return self.sensor_data except Exception as err: - _LOGGER.error(f"Error fetching sensor data: {err}") + _LOGGER.error(f"Exception raised fetching sensor data: {err}") raise UpdateFailed(f"Error fetching sensor data: {err}") from err else: return self.sensor_data @@ -99,9 +111,7 @@ def start_up_mqtt(self) -> ValetudoConnector: """ Initialize the MQTT Connector. """ - self.connector = ValetudoConnector( - self.vacuum_topic, self.hass, self.shared - ) + self.connector = ValetudoConnector(self.vacuum_topic, self.hass, self.shared) return self.connector def update_shared_data(self, dev_info: DeviceInfo) -> tuple[CameraShared, str]: @@ -134,7 +144,11 @@ async def async_update_sensor_data(self, sensor_data): # Assume sensor_data is a dictionary or transform it into the expected format battery_level = await self.connector.get_battery_level() vacuum_state = await self.connector.get_vacuum_status() + vacuum_room = self.shared.current_room + if not vacuum_room: + vacuum_room = {"in_room": "Unsupported"} last_run_stats = sensor_data.get("last_run_stats", {}) + last_loaded_map = sensor_data.get("last_loaded_map", {}) formatted_data = { "mainBrush": sensor_data.get("mainBrush", 0), "sideBrush": sensor_data.get("sideBrush", 0), @@ -152,7 +166,8 @@ async def async_update_sensor_data(self, sensor_data): "last_run_area": last_run_stats.get("area", 0), "last_bin_out": sensor_data.get("last_bin_out", 0), "last_bin_full": sensor_data.get("last_bin_full", 0), - "last_loaded_map": sensor_data.get("last_loaded_map", "None"), + "last_loaded_map": last_loaded_map.get("name", "NoMap"), + "robot_in_room": vacuum_room.get("in_room", "Unsupported"), } return formatted_data return SENSOR_NO_DATA diff --git a/custom_components/mqtt_vacuum_camera/manifest.json b/custom_components/mqtt_vacuum_camera/manifest.json index b5139e5a..9fec4ea7 100755 --- a/custom_components/mqtt_vacuum_camera/manifest.json +++ b/custom_components/mqtt_vacuum_camera/manifest.json @@ -7,6 +7,6 @@ "documentation": "https://github.com/sca075/mqtt_vacuum_camera", "iot_class": "local_polling", "issue_tracker": "https://github.com/sca075/mqtt_vacuum_camera/issues", - "requirements": ["pillow>=10.3.0,<10.5.0", "numpy"], - "version": "2024.10.0" + "requirements": ["pillow>=10.3.0,<=11.0.0", "numpy"], + "version": "2024.11.0b0" } diff --git a/custom_components/mqtt_vacuum_camera/sensor.py b/custom_components/mqtt_vacuum_camera/sensor.py index 718648cb..c5b75752 100644 --- a/custom_components/mqtt_vacuum_camera/sensor.py +++ b/custom_components/mqtt_vacuum_camera/sensor.py @@ -155,9 +155,17 @@ class VacuumSensorDescription(SensorEntityDescription): key="last_loaded_map", icon="mdi:map", name="Last loaded map", + device_class=SensorDeviceClass.ENUM, entity_category=EntityCategory.DIAGNOSTIC, value=lambda v, _: v if isinstance(v, str) else "Unknown", ), + "robot_in_room": VacuumSensorDescription( + key="robot_in_room", + icon="mdi:location-enter", + name="Current Room", + entity_category=EntityCategory.DIAGNOSTIC, + value=lambda v, _: v if isinstance(v, str) else "Unsupported", + ), } @@ -242,6 +250,7 @@ async def _handle_coordinator_update(self): self.async_write_ha_state() + def convert_duration(seconds): """Convert seconds in days""" # Create a timedelta object from seconds diff --git a/custom_components/mqtt_vacuum_camera/valetudo/MQTT/connector.py b/custom_components/mqtt_vacuum_camera/valetudo/MQTT/connector.py index 436d4db8..57fcd9e3 100755 --- a/custom_components/mqtt_vacuum_camera/valetudo/MQTT/connector.py +++ b/custom_components/mqtt_vacuum_camera/valetudo/MQTT/connector.py @@ -1,7 +1,5 @@ """ -Version: v2024.10.0 -- Removed the PNG decode, the json is extracted from map-data instead of map-data-hass. -- Refactoring the subscribe method and decode payload method. +Version: v2024.11.0 """ import asyncio @@ -56,6 +54,8 @@ def __init__(self, mqtt_topic: str, hass: HomeAssistant, camera_shared): f"{self._mqtt_topic}/hass/{self._mqtt_topic.split('/')[-1]}_vacuum/command" ) self.rrm_command = f"{self._mqtt_topic}/command" # added for ValetudoRe + self._pkohelrs_maploader_map = None + self.pkohelrs_state = None async def update_data(self, process: bool = True): """ @@ -103,6 +103,12 @@ async def update_data(self, process: bool = True): self._is_rrm = False return None, data_type + async def async_get_pkohelrs_maploader_map(self) -> str: + """Return the Loaded Map of Dreame vacuums""" + if self._pkohelrs_maploader_map: + return self._pkohelrs_maploader_map + return "No Maps Loaded" + async def get_vacuum_status(self) -> str: """Return the vacuum status.""" if self._mqtt_vac_stat: @@ -142,6 +148,19 @@ async def get_rand256_attributes(self): return self.rrm_attributes return {} + async def handle_pkohelrs_maploader_map(self, msg) -> None: + """Handle Pkohelrs Maploader current map loaded payload""" + self._pkohelrs_maploader_map = await self.async_decode_mqtt_payload(msg) + _LOGGER.debug(f"{self._file_name}: Loaded Map {self._pkohelrs_maploader_map}.") + + async def handle_pkohelrs_maploader_state(self, msg) -> None: + """Get the pkohelrs state and handle camera restart""" + new_state = await self.async_decode_mqtt_payload(msg) + _LOGGER.debug(f"{self._file_name}: {self.pkohelrs_state} -> {new_state}") + if (self.pkohelrs_state == "loading_map") and (new_state == "idle"): + await self.async_fire_event_restart_camera(data=str(msg.payload)) + self.pkohelrs_state = new_state + async def hypfer_handle_image_data(self, msg) -> None: """ Handle new MQTT messages. @@ -156,7 +175,7 @@ async def hypfer_handle_image_data(self, msg) -> None: async def hypfer_handle_status_payload(self, state) -> None: """ Handle new MQTT messages. - /StatusStateAttribute/status" is for Hypfer. + /StatusStateAttribute/status is for Hypfer. """ if state: self._mqtt_vac_stat = state @@ -220,7 +239,9 @@ async def hypfer_handle_map_segments(self, msg): """ self._mqtt_segments = await self.async_decode_mqtt_payload(msg) # Store the decoded segments in RoomStore - await self._room_store.async_set_rooms_data(self._file_name, self._mqtt_segments) + await self._room_store.async_set_rooms_data( + self._file_name, self._mqtt_segments + ) async def rand256_handle_image_payload(self, msg): """ @@ -303,15 +324,25 @@ async def rrm_handle_active_segments(self, msg) -> None: self._shared.rand256_active_zone = rrm_active_segments + async def async_fire_event_restart_camera( + self, event_text: str = "event_vacuum_start", data: str = "" + ): + """Fire Event to reset the camera trims""" + self._hass.bus.async_fire( + event_text, + event_data={ + "device_id": f"mqtt_vacuum_{self._file_name}", + "type": "mqtt_payload", + "data": data, + }, + origin=EventOrigin.local, + ) + async def async_handle_start_command(self, msg): """fire event vacuum start""" if str(msg.payload).lower() == "start": # Fire the vacuum.start event when START command is detected - self._hass.bus.async_fire( - "event_vacuum_start", - event_data=str(msg.payload), - origin=EventOrigin.local, - ) + await self.async_fire_event_restart_camera(data=str(msg.payload)) @callback async def async_message_received(self, msg) -> None: @@ -354,6 +385,10 @@ async def async_message_received(self, msg) -> None: await self.async_handle_start_command(msg) elif self._rcv_topic == f"{self._mqtt_topic}/attributes": self.rrm_attributes = await self.async_decode_mqtt_payload(msg) + elif self._rcv_topic == f"{self._mqtt_topic}/maploader/map": + await self.handle_pkohelrs_maploader_map(msg) + elif self._rcv_topic == f"{self._mqtt_topic}/maploader/status": + await self.handle_pkohelrs_maploader_state(msg) async def async_subscribe_to_topics(self) -> None: """Subscribe to the MQTT topics for Hypfer and ValetudoRe.""" @@ -421,8 +456,8 @@ def parse_string_payload(string_payload: str) -> Any: else: return msg.payload except ValueError as e: - _LOGGER.warning(f"Value error during payload decoding: {e}") - raise + _LOGGER.warning(f"Value error during payload decoding: {e}") + raise except TypeError as e: _LOGGER.warning(f"Type error during payload decoding: {e}") raise