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

Version 2024.11.0 #260

Merged
merged 9 commits into from
Oct 16, 2024
4 changes: 2 additions & 2 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
language: en-US
tone_instructions: ''
tone_instructions: 'cool'
early_access: false
enable_free_tier: true
reviews:
Expand All @@ -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: []
Expand Down
12 changes: 6 additions & 6 deletions custom_components/mqtt_vacuum_camera/camera.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""
Camera
Version: v2024.10.0
Version: v2024.11.0
"""

from __future__ import annotations
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
15 changes: 13 additions & 2 deletions custom_components/mqtt_vacuum_camera/const.py
Original file line number Diff line number Diff line change
@@ -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"]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -270,6 +279,8 @@

DECODED_TOPICS = {
"/MapData/segments",
"/maploader/map",
"/maploader/status",
"/StatusStateAttribute/status",
"/StatusStateAttribute/error_description",
"/$state",
Expand Down
27 changes: 20 additions & 7 deletions custom_components/mqtt_vacuum_camera/coordinator.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -53,14 +54,23 @@ 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 = self.hass.loop.call_later(
1, lambda: asyncio.create_task(self.async_refresh())
)
sca075 marked this conversation as resolved.
Show resolved Hide resolved

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"
):
) and self.connector:
sca075 marked this conversation as resolved.
Show resolved Hide resolved
try:
async with async_timeout.timeout(10):
# Fetch and process sensor data from the MQTT connector
Expand All @@ -73,7 +83,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
Expand All @@ -99,9 +109,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]:
Expand Down Expand Up @@ -134,7 +142,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),
Expand All @@ -152,7 +164,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
4 changes: 2 additions & 2 deletions custom_components/mqtt_vacuum_camera/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
9 changes: 9 additions & 0 deletions custom_components/mqtt_vacuum_camera/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
),
}


Expand Down Expand Up @@ -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
Expand Down
57 changes: 47 additions & 10 deletions custom_components/mqtt_vacuum_camera/valetudo/MQTT/connector.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
Version: v2024.10.0
Version: v2024.11.0
sca075 marked this conversation as resolved.
Show resolved Hide resolved
- Removed the PNG decode, the json is extracted from map-data instead of map-data-hass.
- Refactoring the subscribe method and decode payload method.
"""
Expand Down Expand Up @@ -56,6 +56,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):
"""
Expand Down Expand Up @@ -103,6 +105,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"
sca075 marked this conversation as resolved.
Show resolved Hide resolved

async def get_vacuum_status(self) -> str:
"""Return the vacuum status."""
if self._mqtt_vac_stat:
Expand Down Expand Up @@ -142,6 +150,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}.")

sca075 marked this conversation as resolved.
Show resolved Hide resolved
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.
Expand All @@ -156,7 +177,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
Expand Down Expand Up @@ -220,7 +241,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):
"""
Expand Down Expand Up @@ -303,15 +326,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:
Expand Down Expand Up @@ -354,6 +387,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."""
Expand Down Expand Up @@ -421,8 +458,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
Expand Down
Loading