diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2dfe62a..0d819b9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,24 @@
+## v2.7.2 (2024-11-08)
+
+### Fix
+
+* Add some new roborock codes ([#233](https://github.com/humbertogontijo/python-roborock/issues/233)) ([`59546dd`](https://github.com/humbertogontijo/python-roborock/commit/59546dd68f7b40ad368d58fd502680ff9c03c81b))
+
+## v2.7.1 (2024-10-28)
+
+### Fix
+
+* Check that clean area is not a str ([#230](https://github.com/humbertogontijo/python-roborock/issues/230)) ([`e66a91e`](https://github.com/humbertogontijo/python-roborock/commit/e66a91edaf6fedf5d4b2ab9117b7759295add492))
+
+## v2.7.0 (2024-10-28)
+
+### Feature
+
+* Remove dacite ([#227](https://github.com/humbertogontijo/python-roborock/issues/227)) ([`86878a7`](https://github.com/humbertogontijo/python-roborock/commit/86878a71d82c2cc707daa16dec109fc07360e3f6))
+
## v2.6.1 (2024-10-22)
### Fix
diff --git a/docs/source/_templates/footer.html b/docs/source/_templates/footer.html
index d2276f4..768ee7a 100644
--- a/docs/source/_templates/footer.html
+++ b/docs/source/_templates/footer.html
@@ -1,5 +1,5 @@
{% extends "!footer.html" %}
{%- block contentinfo %}
-{{ super }}
+{{ super() }}
We are looking for contributors to help with our documentation, if you are interested please contribute here.
{% endblock %}
diff --git a/pyproject.toml b/pyproject.toml
index 17a799b..c77301e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "python-roborock"
-version = "2.6.1"
+version = "2.7.2"
description = "A package to control Roborock vacuums."
authors = ["humbertogontijo "]
license = "GPL-3.0-only"
diff --git a/roborock/api.py b/roborock/api.py
index 9b0c86f..633bddc 100644
--- a/roborock/api.py
+++ b/roborock/api.py
@@ -23,7 +23,7 @@
RoborockMessage,
)
from .roborock_typing import RoborockCommand
-from .util import RoborockLoggerAdapter, get_running_loop_or_create_one
+from .util import RoborockLoggerAdapter, get_next_int, get_running_loop_or_create_one
_LOGGER = logging.getLogger(__name__)
KEEPALIVE = 60
@@ -113,6 +113,13 @@ def _async_response(
self, request_id: int, protocol_id: int = 0
) -> Coroutine[Any, Any, tuple[Any, VacuumError | None]]:
queue = RoborockFuture(protocol_id)
+ if request_id in self._waiting_queue:
+ new_id = get_next_int(10000, 32767)
+ _LOGGER.warning(
+ f"Attempting to create a future with an existing request_id... New id is {new_id}. "
+ f"Code may not function properly."
+ )
+ request_id = new_id
self._waiting_queue[request_id] = queue
return self._wait_response(request_id, queue)
diff --git a/roborock/code_mappings.py b/roborock/code_mappings.py
index bf82379..8bd58b4 100644
--- a/roborock/code_mappings.py
+++ b/roborock/code_mappings.py
@@ -297,6 +297,7 @@ class RoborockMopModeS8ProUltra(RoborockMopModeCode):
class RoborockMopModeS8MaxVUltra(RoborockMopModeCode):
standard = 300
deep = 301
+ custom = 302
deep_plus = 303
fast = 304
deep_plus_pearl = 305
@@ -382,6 +383,16 @@ class RoborockMopIntensityS6MaxV(RoborockMopIntensityCode):
custom_water_flow = 207
+class RoborockMopIntensityQ7Max(RoborockMopIntensityCode):
+ """Describes the mop intensity of the vacuum cleaner."""
+
+ off = 200
+ low = 201
+ medium = 202
+ high = 203
+ custom_water_flow = 207
+
+
class RoborockDockErrorCode(RoborockEnum):
"""Describes the error code of the dock."""
diff --git a/roborock/containers.py b/roborock/containers.py
index 555733e..579dee4 100644
--- a/roborock/containers.py
+++ b/roborock/containers.py
@@ -7,9 +7,7 @@
from dataclasses import asdict, dataclass, field
from datetime import timezone
from enum import Enum
-from typing import Any, NamedTuple
-
-from dacite import Config, from_dict
+from typing import Any, NamedTuple, get_args, get_origin
from .code_mappings import (
RoborockCategory,
@@ -32,11 +30,11 @@
RoborockMopIntensityCode,
RoborockMopIntensityP10,
RoborockMopIntensityQRevoMaster,
+ RoborockMopIntensityQ7Max,
RoborockMopIntensityS5Max,
RoborockMopIntensityS6MaxV,
RoborockMopIntensityS7,
RoborockMopIntensityS8MaxVUltra,
- RoborockMopIntensityV2,
RoborockMopModeCode,
RoborockMopModeS7,
RoborockMopModeS8MaxVUltra,
@@ -105,14 +103,70 @@ class RoborockBase:
_ignore_keys = [] # type: ignore
is_cached = False
+ @staticmethod
+ def convert_to_class_obj(type, value):
+ try:
+ class_type = eval(type)
+ if get_origin(class_type) is list:
+ return_list = []
+ cls_type = get_args(class_type)[0]
+ for obj in value:
+ if issubclass(cls_type, RoborockBase):
+ return_list.append(cls_type.from_dict(obj))
+ elif cls_type in {str, int, float}:
+ return_list.append(cls_type(obj))
+ else:
+ return_list.append(cls_type(**obj))
+ return return_list
+ if issubclass(class_type, RoborockBase):
+ converted_value = class_type.from_dict(value)
+ else:
+ converted_value = class_type(value)
+ return converted_value
+ except NameError as err:
+ _LOGGER.exception(err)
+ except ValueError as err:
+ _LOGGER.exception(err)
+ except Exception as err:
+ _LOGGER.exception(err)
+ raise Exception("Fail")
+
@classmethod
def from_dict(cls, data: dict[str, Any]):
if isinstance(data, dict):
ignore_keys = cls._ignore_keys
- try:
- return from_dict(cls, decamelize_obj(data, ignore_keys), config=Config(cast=[Enum]))
- except AttributeError as err:
- raise RoborockException("It seems like you have an outdated version of dacite.") from err
+ data = decamelize_obj(data, ignore_keys)
+ cls_annotations: dict[str, str] = {}
+ for base in reversed(cls.__mro__):
+ cls_annotations.update(getattr(base, "__annotations__", {}))
+ remove_keys = []
+ for key, value in data.items():
+ if value == "None" or value is None:
+ data[key] = None
+ continue
+ if key not in cls_annotations:
+ remove_keys.append(key)
+ continue
+ field_type: str = cls_annotations[key]
+ if "|" in field_type:
+ # It's a union
+ types = field_type.split("|")
+ for type in types:
+ if "None" in type or "Any" in type:
+ continue
+ try:
+ data[key] = RoborockBase.convert_to_class_obj(type, value)
+ break
+ except Exception:
+ ...
+ else:
+ try:
+ data[key] = RoborockBase.convert_to_class_obj(field_type, value)
+ except Exception:
+ ...
+ for key in remove_keys:
+ del data[key]
+ return cls(**data)
def as_dict(self) -> dict:
return asdict(
@@ -188,6 +242,7 @@ class HomeDataProductSchema(RoborockBase):
mode: Any | None = None
type: Any | None = None
product_property: Any | None = None
+ property: Any | None = None
desc: Any | None = None
@@ -198,7 +253,7 @@ class HomeDataProduct(RoborockBase):
model: str
category: RoborockCategory
code: str | None = None
- iconurl: str | None = None
+ icon_url: str | None = None
attribute: Any | None = None
capability: int | None = None
schema: list[HomeDataProductSchema] | None = None
@@ -515,7 +570,7 @@ class S5MaxStatus(Status):
@dataclass
class Q7MaxStatus(Status):
fan_power: RoborockFanSpeedQ7Max | None = None
- water_box_mode: RoborockMopIntensityV2 | None = None
+ water_box_mode: RoborockMopIntensityQ7Max | None = None
@dataclass
@@ -622,7 +677,7 @@ class CleanSummary(RoborockBase):
last_clean_t: int | None = None
def __post_init__(self) -> None:
- if isinstance(self.clean_area, list):
+ if isinstance(self.clean_area, list | str):
_LOGGER.warning(f"Clean area is a unexpected type! Please give the following in a issue: {self.clean_area}")
else:
self.square_meter_clean_area = round(self.clean_area / 1000000, 1) if self.clean_area is not None else None
diff --git a/roborock/roborock_message.py b/roborock/roborock_message.py
index 7536770..df03108 100644
--- a/roborock/roborock_message.py
+++ b/roborock/roborock_message.py
@@ -4,9 +4,9 @@
import math
import time
from dataclasses import dataclass
-from random import randint
from roborock import RoborockEnum
+from roborock.util import get_next_int
class RoborockMessageProtocol(RoborockEnum):
@@ -155,9 +155,9 @@ class MessageRetry:
class RoborockMessage:
protocol: RoborockMessageProtocol
payload: bytes | None = None
- seq: int = randint(100000, 999999)
+ seq: int = get_next_int(100000, 999999)
version: bytes = b"1.0"
- random: int = randint(10000, 99999)
+ random: int = get_next_int(10000, 99999)
timestamp: int = math.floor(time.time())
message_retry: MessageRetry | None = None
diff --git a/roborock/util.py b/roborock/util.py
index d1d8c4d..c6013d2 100644
--- a/roborock/util.py
+++ b/roborock/util.py
@@ -108,3 +108,15 @@ def __init__(self, prefix: str, logger: logging.Logger) -> None:
def process(self, msg: str, kwargs: MutableMapping[str, Any]) -> tuple[str, MutableMapping[str, Any]]:
return f"[{self.prefix}] {msg}", kwargs
+
+
+counter_map: dict[tuple[int, int], int] = {}
+
+
+def get_next_int(min_val: int, max_val: int):
+ """Gets a random int in the range, precached to help keep it fast."""
+ if (min_val, max_val) not in counter_map:
+ # If we have never seen this range, or if the cache is getting low, make a bunch of preshuffled values.
+ counter_map[(min_val, max_val)] = min_val
+ counter_map[(min_val, max_val)] += 1
+ return counter_map[(min_val, max_val)] % max_val + min_val
diff --git a/roborock/version_1_apis/roborock_client_v1.py b/roborock/version_1_apis/roborock_client_v1.py
index 95f4de3..279721e 100644
--- a/roborock/version_1_apis/roborock_client_v1.py
+++ b/roborock/version_1_apis/roborock_client_v1.py
@@ -5,7 +5,6 @@
import struct
import time
from collections.abc import Callable, Coroutine
-from random import randint
from typing import Any, TypeVar, final
from roborock import (
@@ -54,7 +53,7 @@
RoborockMessage,
RoborockMessageProtocol,
)
-from roborock.util import RepeatableTask, unpack_list
+from roborock.util import RepeatableTask, get_next_int, unpack_list
COMMANDS_SECURED = {
RoborockCommand.GET_MAP_V1,
@@ -338,7 +337,7 @@ def _get_payload(
secured=False,
):
timestamp = math.floor(time.time())
- request_id = randint(10000, 32767)
+ request_id = get_next_int(10000, 32767)
inner = {
"id": request_id,
"method": method,
diff --git a/tests/test_containers.py b/tests/test_containers.py
index b565646..3ce72c0 100644
--- a/tests/test_containers.py
+++ b/tests/test_containers.py
@@ -47,7 +47,7 @@ def test_home_data():
assert product.name == "Roborock S7 MaxV"
assert product.code == "a27"
assert product.model == "roborock.vacuum.a27"
- assert product.iconurl is None
+ assert product.icon_url is None
assert product.attribute is None
assert product.capability == 0
assert product.category == RoborockCategory.VACUUM