From a27c4d22e7dd21cf5ae454886c743d28db130803 Mon Sep 17 00:00:00 2001 From: Luke Date: Sat, 9 Sep 2023 15:42:13 -0400 Subject: [PATCH 01/17] 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 67b92332..963fa351 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/17] chore: lint --- roborock/containers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/roborock/containers.py b/roborock/containers.py index 963fa351..42ec9e55 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/17] chore: lint --- roborock/containers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/roborock/containers.py b/roborock/containers.py index 50bdd57f..aa70cf9c 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/17] 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 aa70cf9c..3dd8e552 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/17] chore: lint --- roborock/containers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/roborock/containers.py b/roborock/containers.py index 3dd8e552..00821814 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/17] 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 747e3cb0..e5d45d7d 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/17] 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 e5d45d7d..3b396db3 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 fbcc5ff6..51e3e5e7 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 32b9239c..a6926596 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/17] 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 a6926596..73dcd3a0 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/17] 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 3b396db3..8e7b097f 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/17] 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 8e7b097f..f7d75910 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/17] 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 f7d75910..4dd3d9f9 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/17] 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 51e3e5e7..4806435a 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 73dcd3a0..fb3cef17 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 ) From 26a6d0bc270b16362d0e38b115e6cbfe5a0e094c Mon Sep 17 00:00:00 2001 From: Luke Date: Thu, 21 Sep 2023 09:29:57 -0400 Subject: [PATCH 13/17] fix: don't cache device cache --- roborock/api.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/roborock/api.py b/roborock/api.py index 4dd3d9f9..2232ad20 100644 --- a/roborock/api.py +++ b/roborock/api.py @@ -161,7 +161,6 @@ async def refresh_value(self): await self._async_value() -device_cache: dict[str, dict[CacheableAttribute, AttributeCache]] = {} class RoborockClient: @@ -176,13 +175,9 @@ def __init__(self, endpoint: str, device_info: DeviceData, queue_timeout: int = self.keep_alive = KEEPALIVE self._diagnostic_data: dict[str, dict[str, Any]] = {} self._logger = RoborockLoggerAdapter(device_info.device.name, _LOGGER) - cache = device_cache.get(device_info.device.duid) - if not cache: - cache = { - cacheable_attribute: AttributeCache(attr, self) for cacheable_attribute, attr in get_cache_map().items() - } - device_cache[device_info.device.duid] = cache - self.cache: dict[CacheableAttribute, AttributeCache] = cache + self.cache: dict[CacheableAttribute, AttributeCache] = { + cacheable_attribute: AttributeCache(attr, self) for cacheable_attribute, attr in get_cache_map().items() + } self._listeners: list[Callable[[str, CacheableAttribute, RoborockBase], None]] = [] self.is_available: bool = True self.queue_timeout = queue_timeout From a1d164e3620e5ff28252ed154d98c0d832216ab7 Mon Sep 17 00:00:00 2001 From: Luke Date: Thu, 21 Sep 2023 09:30:32 -0400 Subject: [PATCH 14/17] chore: lint --- roborock/api.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/roborock/api.py b/roborock/api.py index 2232ad20..05e2f91d 100644 --- a/roborock/api.py +++ b/roborock/api.py @@ -161,8 +161,6 @@ async def refresh_value(self): await self._async_value() - - class RoborockClient: def __init__(self, endpoint: str, device_info: DeviceData, queue_timeout: int = 4) -> None: self.event_loop = get_running_loop_or_create_one() From e31fcc8fbd6bb9470abc9b961aab215ea3b067de Mon Sep 17 00:00:00 2001 From: Luke Date: Thu, 21 Sep 2023 10:18:17 -0400 Subject: [PATCH 15/17] fix: double keepalive --- roborock/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roborock/api.py b/roborock/api.py index 05e2f91d..74875467 100644 --- a/roborock/api.py +++ b/roborock/api.py @@ -69,7 +69,7 @@ from .util import RepeatableTask, RoborockLoggerAdapter, get_running_loop_or_create_one, unpack_list _LOGGER = logging.getLogger(__name__) -KEEPALIVE = 60 +KEEPALIVE = 120 COMMANDS_SECURED = [ RoborockCommand.GET_MAP_V1, RoborockCommand.GET_MULTI_MAP, From e4c86d9b700f2ed46089a678cf8a93590e438c98 Mon Sep 17 00:00:00 2001 From: Luke Date: Thu, 21 Sep 2023 10:18:40 -0400 Subject: [PATCH 16/17] fix: don't continue calling unsupported functions --- roborock/api.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/roborock/api.py b/roborock/api.py index 74875467..563023f4 100644 --- a/roborock/api.py +++ b/roborock/api.py @@ -118,13 +118,21 @@ def __init__(self, attribute: RoborockAttribute, api: RoborockClient): self.task = RepeatableTask(self.api.event_loop, self._async_value, EVICT_TIME) self._value: Any = None self._mutex = asyncio.Lock() + self.unsupported: bool = False @property def value(self): return self._value async def _async_value(self): - self._value = await self.api._send_command(self.attribute.get_command) + if self.unsupported: + return None + try: + self._value = await self.api._send_command(self.attribute.get_command) + except UnknownMethodError as err: + # Limit the amount of times we call unsupported methods + self.unsupported = True + raise err return self._value async def async_value(self): From 8d951de2ae3b0b47afb2e0ac69c40a133b205947 Mon Sep 17 00:00:00 2001 From: Luke Date: Thu, 21 Sep 2023 14:10:26 -0400 Subject: [PATCH 17/17] fix: revert keepalive for now --- roborock/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roborock/api.py b/roborock/api.py index 563023f4..7022b6b3 100644 --- a/roborock/api.py +++ b/roborock/api.py @@ -69,7 +69,7 @@ from .util import RepeatableTask, RoborockLoggerAdapter, get_running_loop_or_create_one, unpack_list _LOGGER = logging.getLogger(__name__) -KEEPALIVE = 120 +KEEPALIVE = 60 COMMANDS_SECURED = [ RoborockCommand.GET_MAP_V1, RoborockCommand.GET_MULTI_MAP,