From a27c4d22e7dd21cf5ae454886c743d28db130803 Mon Sep 17 00:00:00 2001 From: Luke Date: Sat, 9 Sep 2023 15:42:13 -0400 Subject: [PATCH 01/12] feat: add datetime parsing in cleanrecord --- roborock/containers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/roborock/containers.py b/roborock/containers.py index 67b9233..963fa35 100644 --- a/roborock/containers.py +++ b/roborock/containers.py @@ -408,7 +408,9 @@ def __post_init__(self) -> None: @dataclass class CleanRecord(RoborockBase): begin: Optional[int] = None + begin_datetime: datetime.datetime | None = None end: Optional[int] = None + end_datetime: datetime.datetime | None = None duration: Optional[int] = None area: Optional[int] = None square_meter_area: Optional[float] = None @@ -424,6 +426,8 @@ class CleanRecord(RoborockBase): def __post_init__(self) -> None: self.square_meter_area = round(self.area / 1000000, 1) if self.area is not None else None + self.begin_datetime = datetime.datetime.fromtimestamp(self.begin).astimezone(datetime.UTC) if self.begin else None + self.end_datetime = datetime.datetime.fromtimestamp(self.end).astimezone(datetime.UTC) if self.end else None @dataclass From 97f1922433acabc97e6e6c9eb06b331f5cb2b0a6 Mon Sep 17 00:00:00 2001 From: Luke Date: Sat, 9 Sep 2023 16:42:57 -0400 Subject: [PATCH 02/12] chore: lint --- roborock/containers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/roborock/containers.py b/roborock/containers.py index 963fa35..42ec9e5 100644 --- a/roborock/containers.py +++ b/roborock/containers.py @@ -426,7 +426,9 @@ class CleanRecord(RoborockBase): def __post_init__(self) -> None: self.square_meter_area = round(self.area / 1000000, 1) if self.area is not None else None - self.begin_datetime = datetime.datetime.fromtimestamp(self.begin).astimezone(datetime.UTC) if self.begin else None + self.begin_datetime = ( + datetime.datetime.fromtimestamp(self.begin).astimezone(datetime.UTC) if self.begin else None + ) self.end_datetime = datetime.datetime.fromtimestamp(self.end).astimezone(datetime.UTC) if self.end else None From d296a90f59a3590d4cdbc52c34ba50009310a90f Mon Sep 17 00:00:00 2001 From: Luke Date: Sat, 9 Sep 2023 16:46:39 -0400 Subject: [PATCH 03/12] chore: lint --- roborock/containers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/roborock/containers.py b/roborock/containers.py index 50bdd57..aa70cf9 100644 --- a/roborock/containers.py +++ b/roborock/containers.py @@ -407,9 +407,9 @@ def __post_init__(self) -> None: @dataclass class CleanRecord(RoborockBase): - begin: Optional[int] = None + begin: int | None = None begin_datetime: datetime.datetime | None = None - end: Optional[int] = None + end: int | None = None end_datetime: datetime.datetime | None = None duration: int | None = None area: int | None = None From 4ab6dce0aeee0abaddc473459fcb6c2bc85a971c Mon Sep 17 00:00:00 2001 From: Luke Date: Sat, 9 Sep 2023 16:51:02 -0400 Subject: [PATCH 04/12] fix: timezone for non-3.11 --- roborock/containers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/roborock/containers.py b/roborock/containers.py index aa70cf9..3dd8e55 100644 --- a/roborock/containers.py +++ b/roborock/containers.py @@ -6,7 +6,7 @@ from dataclasses import asdict, dataclass from enum import Enum from typing import Any, NamedTuple - +from datetime import timezone from dacite import Config, from_dict from .code_mappings import ( @@ -427,9 +427,9 @@ class CleanRecord(RoborockBase): def __post_init__(self) -> None: self.square_meter_area = round(self.area / 1000000, 1) if self.area is not None else None self.begin_datetime = ( - datetime.datetime.fromtimestamp(self.begin).astimezone(datetime.UTC) if self.begin else None + datetime.datetime.fromtimestamp(self.begin).astimezone(timezone.utc) if self.begin else None ) - self.end_datetime = datetime.datetime.fromtimestamp(self.end).astimezone(datetime.UTC) if self.end else None + self.end_datetime = datetime.datetime.fromtimestamp(self.end).astimezone(timezone.utc) if self.end else None @dataclass From 8e5a63d7551982e586e53c8764944b097a56f641 Mon Sep 17 00:00:00 2001 From: Luke Date: Sat, 9 Sep 2023 16:51:22 -0400 Subject: [PATCH 05/12] chore: lint --- roborock/containers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/roborock/containers.py b/roborock/containers.py index 3dd8e55..0082181 100644 --- a/roborock/containers.py +++ b/roborock/containers.py @@ -4,9 +4,10 @@ import logging import re from dataclasses import asdict, dataclass +from datetime import timezone from enum import Enum from typing import Any, NamedTuple -from datetime import timezone + from dacite import Config, from_dict from .code_mappings import ( From 1f7d46c5e63f7dc7b553c33ddf2f6ea8cdcc0750 Mon Sep 17 00:00:00 2001 From: Luke Date: Sat, 9 Sep 2023 19:28:44 -0400 Subject: [PATCH 06/12] feat: add is_available for ha and here in future --- roborock/api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/roborock/api.py b/roborock/api.py index 747e3cb..e5d45d7 100644 --- a/roborock/api.py +++ b/roborock/api.py @@ -185,6 +185,7 @@ def __init__(self, endpoint: str, device_info: DeviceData) -> None: device_cache[device_info.device.duid] = cache self.cache: dict[CacheableAttribute, AttributeCache] = cache self._listeners: list[Callable[[str, CacheableAttribute, RoborockBase], None]] = [] + self.is_available: bool = False def __del__(self) -> None: self.release() From c3ce248ca0743cfd635692cf57b4b0e8475e1023 Mon Sep 17 00:00:00 2001 From: Luke Date: Mon, 11 Sep 2023 15:30:36 -0400 Subject: [PATCH 07/12] fix: add timeout as a variable and set a longer default timeout for cloud --- roborock/api.py | 8 ++++---- roborock/cloud_api.py | 4 ++-- roborock/local_api.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/roborock/api.py b/roborock/api.py index e5d45d7..3b396db 100644 --- a/roborock/api.py +++ b/roborock/api.py @@ -70,7 +70,6 @@ _LOGGER = logging.getLogger(__name__) KEEPALIVE = 60 -QUEUE_TIMEOUT = 4 COMMANDS_SECURED = [ RoborockCommand.GET_MAP_V1, RoborockCommand.GET_MULTI_MAP, @@ -166,7 +165,7 @@ async def refresh_value(self): class RoborockClient: - def __init__(self, endpoint: str, device_info: DeviceData) -> None: + def __init__(self, endpoint: str, device_info: DeviceData, queue_timeout: int = 4) -> None: self.event_loop = get_running_loop_or_create_one() self.device_info = device_info self._endpoint = endpoint @@ -186,6 +185,7 @@ def __init__(self, endpoint: str, device_info: DeviceData) -> None: self.cache: dict[CacheableAttribute, AttributeCache] = cache self._listeners: list[Callable[[str, CacheableAttribute, RoborockBase], None]] = [] self.is_available: bool = False + self.queue_timeout = queue_timeout def __del__(self) -> None: self.release() @@ -323,12 +323,12 @@ async def validate_connection(self) -> None: async def _wait_response(self, request_id: int, queue: RoborockFuture) -> tuple[Any, VacuumError | None]: try: - (response, err) = await queue.async_get(QUEUE_TIMEOUT) + (response, err) = await queue.async_get(self.queue_timeout) if response == "unknown_method": raise UnknownMethodError("Unknown method") return response, err except (asyncio.TimeoutError, asyncio.CancelledError): - raise RoborockTimeout(f"id={request_id} Timeout after {QUEUE_TIMEOUT} seconds") from None + raise RoborockTimeout(f"id={request_id} Timeout after {self.queue_timeout} seconds") from None finally: self._waiting_queue.pop(request_id, None) diff --git a/roborock/cloud_api.py b/roborock/cloud_api.py index fbcc5ff..51e3e5e 100644 --- a/roborock/cloud_api.py +++ b/roborock/cloud_api.py @@ -29,12 +29,12 @@ class RoborockMqttClient(RoborockClient, mqtt.Client): _thread: threading.Thread _client_id: str - def __init__(self, user_data: UserData, device_info: DeviceData) -> None: + def __init__(self, user_data: UserData, device_info: DeviceData, queue_timeout: int = 10) -> None: rriot = user_data.rriot if rriot is None: raise RoborockException("Got no rriot data from user_data") endpoint = base64.b64encode(Utils.md5(rriot.k.encode())[8:14]).decode() - RoborockClient.__init__(self, endpoint, device_info) + RoborockClient.__init__(self, endpoint, device_info, queue_timeout) mqtt.Client.__init__(self, protocol=mqtt.MQTTv5) self._logger = RoborockLoggerAdapter(device_info.device.name, _LOGGER) self._mqtt_user = rriot.u diff --git a/roborock/local_api.py b/roborock/local_api.py index 32b9239..a692659 100644 --- a/roborock/local_api.py +++ b/roborock/local_api.py @@ -18,10 +18,10 @@ class RoborockLocalClient(RoborockClient, asyncio.Protocol): - def __init__(self, device_data: DeviceData): + def __init__(self, device_data: DeviceData, queue_timeout: int = 4): if device_data.host is None: raise RoborockException("Host is required") - super().__init__("abc", device_data) + super().__init__("abc", device_data, queue_timeout) self.host = device_data.host self._batch_structs: list[RoborockMessage] = [] self._executing = False From f4e82a320a52f5d4c57fffe45f2aadded3296b80 Mon Sep 17 00:00:00 2001 From: Luke Date: Mon, 11 Sep 2023 15:33:04 -0400 Subject: [PATCH 08/12] chore: lint --- roborock/local_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/roborock/local_api.py b/roborock/local_api.py index a692659..73dcd3a 100644 --- a/roborock/local_api.py +++ b/roborock/local_api.py @@ -7,7 +7,7 @@ import async_timeout from . import DeviceData -from .api import COMMANDS_SECURED, QUEUE_TIMEOUT, RoborockClient +from .api import COMMANDS_SECURED, RoborockClient from .exceptions import CommandVacuumError, RoborockConnectionException, RoborockException from .protocol import MessageParser from .roborock_message import MessageRetry, RoborockMessage, RoborockMessageProtocol @@ -58,7 +58,7 @@ async def async_connect(self) -> None: try: if not self.is_connected(): self.sync_disconnect() - async with async_timeout.timeout(QUEUE_TIMEOUT): + async with async_timeout.timeout(self.queue_timeout): self._logger.info(f"Connecting to {self.host}") self.transport, _ = await self.event_loop.create_connection( # type: ignore lambda: self, self.host, 58867 From 2aaf68d2412f432f60b42d9bea00bd49a62807f4 Mon Sep 17 00:00:00 2001 From: Luke Date: Thu, 14 Sep 2023 13:06:41 -0400 Subject: [PATCH 09/12] fix: is_available true by default --- roborock/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roborock/api.py b/roborock/api.py index 3b396db..8e7b097 100644 --- a/roborock/api.py +++ b/roborock/api.py @@ -184,7 +184,7 @@ def __init__(self, endpoint: str, device_info: DeviceData, queue_timeout: int = device_cache[device_info.device.duid] = cache self.cache: dict[CacheableAttribute, AttributeCache] = cache self._listeners: list[Callable[[str, CacheableAttribute, RoborockBase], None]] = [] - self.is_available: bool = False + self.is_available: bool = True self.queue_timeout = queue_timeout def __del__(self) -> None: From eb0bd946da557442bf1efd2d0e9a69171d5811ba Mon Sep 17 00:00:00 2001 From: Luke Date: Thu, 14 Sep 2023 13:07:06 -0400 Subject: [PATCH 10/12] fix: status type as class variable --- roborock/api.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/roborock/api.py b/roborock/api.py index 8e7b097..f7d7591 100644 --- a/roborock/api.py +++ b/roborock/api.py @@ -186,10 +186,18 @@ def __init__(self, endpoint: str, device_info: DeviceData, queue_timeout: int = self._listeners: list[Callable[[str, CacheableAttribute, RoborockBase], None]] = [] self.is_available: bool = True self.queue_timeout = queue_timeout + self._status_type: type[Status] = ModelStatus.get( + self.device_info.model, S7MaxVStatus + ) def __del__(self) -> None: self.release() + @property + def status_type(self) -> type[Status]: + """Gets the status type for this device""" + return self._status_type + def release(self): self.sync_disconnect() [item.stop() for item in self.cache.values()] @@ -253,9 +261,6 @@ def on_message_received(self, messages: list[RoborockMessage]) -> None: data_protocol = RoborockDataProtocol(int(data_point_number)) self._logger.debug(f"Got device update for {data_protocol.name}: {data_point}") if data_protocol in ROBOROCK_DATA_STATUS_PROTOCOL: - _cls: type[Status] = ModelStatus.get( - self.device_info.model, S7MaxVStatus - ) # Default to S7 MAXV if we don't have the data if self.cache[CacheableAttribute.status].value is None: self._logger.debug( f"Got status update({data_protocol.name}) before get_status was called." @@ -263,7 +268,7 @@ def on_message_received(self, messages: list[RoborockMessage]) -> None: self.cache[CacheableAttribute.status]._value = {} value = self.cache[CacheableAttribute.status].value value[data_protocol.name] = data_point - status = _cls.from_dict(value) + status = self._status_type.from_dict(value) for listener in self._listeners: listener(self.device_info.device.duid, CacheableAttribute.status, status) elif data_protocol in ROBOROCK_DATA_CONSUMABLE_PROTOCOL: @@ -406,10 +411,7 @@ async def send_command( return response async def get_status(self) -> Status | None: - _cls: type[Status] = ModelStatus.get( - self.device_info.model, S7MaxVStatus - ) # Default to S7 MAXV if we don't have the data - return _cls.from_dict(await self.cache[CacheableAttribute.status].async_value()) + return self._status_type.from_dict(await self.cache[CacheableAttribute.status].async_value()) async def get_dnd_timer(self) -> DnDTimer | None: return DnDTimer.from_dict(await self.cache[CacheableAttribute.dnd_timer].async_value()) From 2d9c629eae06c8d77891b2f6d7f3fc01aa1a40d3 Mon Sep 17 00:00:00 2001 From: Luke Date: Thu, 14 Sep 2023 13:08:02 -0400 Subject: [PATCH 11/12] fix: don't update status when it was none before listener --- roborock/api.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/roborock/api.py b/roborock/api.py index f7d7591..4dd3d9f 100644 --- a/roborock/api.py +++ b/roborock/api.py @@ -186,9 +186,7 @@ def __init__(self, endpoint: str, device_info: DeviceData, queue_timeout: int = self._listeners: list[Callable[[str, CacheableAttribute, RoborockBase], None]] = [] self.is_available: bool = True self.queue_timeout = queue_timeout - self._status_type: type[Status] = ModelStatus.get( - self.device_info.model, S7MaxVStatus - ) + self._status_type: type[Status] = ModelStatus.get(self.device_info.model, S7MaxVStatus) def __del__(self) -> None: self.release() @@ -265,7 +263,7 @@ def on_message_received(self, messages: list[RoborockMessage]) -> None: self._logger.debug( f"Got status update({data_protocol.name}) before get_status was called." ) - self.cache[CacheableAttribute.status]._value = {} + return value = self.cache[CacheableAttribute.status].value value[data_protocol.name] = data_point status = self._status_type.from_dict(value) @@ -277,7 +275,7 @@ def on_message_received(self, messages: list[RoborockMessage]) -> None: f"Got consumable update({data_protocol.name})" + "before get_consumable was called." ) - self.cache[CacheableAttribute.consumable]._value = {} + return value = self.cache[CacheableAttribute.consumable].value value[data_protocol.name] = data_point consumable = Consumable.from_dict(value) From aa3e00c4d574994794fa97b9c3891bfec383adc4 Mon Sep 17 00:00:00 2001 From: Luke Date: Thu, 14 Sep 2023 15:02:51 -0400 Subject: [PATCH 12/12] fix: reduce info logs --- roborock/cloud_api.py | 2 +- roborock/local_api.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/roborock/cloud_api.py b/roborock/cloud_api.py index 51e3e5e..4806435 100644 --- a/roborock/cloud_api.py +++ b/roborock/cloud_api.py @@ -138,7 +138,7 @@ def sync_connect(self) -> tuple[bool, Task[tuple[Any, VacuumError | None]] | Non if self._mqtt_port is None or self._mqtt_host is None: raise RoborockException("Mqtt information was not entered. Cannot connect.") - self._logger.info("Connecting to mqtt") + self._logger.debug("Connecting to mqtt") connected_future = asyncio.ensure_future(self._async_response(CONNECT_REQUEST_ID)) super().connect(host=self._mqtt_host, port=self._mqtt_port, keepalive=KEEPALIVE) diff --git a/roborock/local_api.py b/roborock/local_api.py index 73dcd3a..fb3cef1 100644 --- a/roborock/local_api.py +++ b/roborock/local_api.py @@ -59,7 +59,7 @@ async def async_connect(self) -> None: if not self.is_connected(): self.sync_disconnect() async with async_timeout.timeout(self.queue_timeout): - self._logger.info(f"Connecting to {self.host}") + self._logger.debug(f"Connecting to {self.host}") self.transport, _ = await self.event_loop.create_connection( # type: ignore lambda: self, self.host, 58867 )