Skip to content

Commit 8230bd4

Browse files
authored
Various fixes to roborockvacuum (#1916)
* Include descriptors from `status()` response. This performs I/O during the initialization to find out which information is available. Alternative would be changing `updatehelper.py` to collect the descriptors from all embedded updates, unsure what's the best approach. * Fix exposing vacuum state. * Expose fan speed presets.
1 parent 4083283 commit 8230bd4

File tree

7 files changed

+76
-11
lines changed

7 files changed

+76
-11
lines changed

miio/descriptorcollection.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logging
22
from collections import UserDict
3+
from enum import Enum
34
from inspect import getmembers
45
from typing import TYPE_CHECKING, Generic, TypeVar, cast
56

@@ -12,6 +13,7 @@
1213
PropertyDescriptor,
1314
RangeDescriptor,
1415
)
16+
from .exceptions import DeviceException
1517

1618
_LOGGER = logging.getLogger(__name__)
1719

@@ -42,10 +44,11 @@ def descriptors_from_object(self, obj):
4244
2. Going through all members and looking if they have a '_descriptor' attribute set by a decorator
4345
"""
4446
_LOGGER.debug("Adding descriptors from %s", obj)
47+
descriptors_to_add = []
4548
# 1. Check for existence of _descriptors as DeviceStatus' metaclass collects them already
4649
if descriptors := getattr(obj, "_descriptors"): # noqa: B009
4750
for _name, desc in descriptors.items():
48-
self.add_descriptor(desc)
51+
descriptors_to_add.append(desc)
4952

5053
# 2. Check if object members have descriptors
5154
for _name, method in getmembers(obj, lambda o: hasattr(o, "_descriptor")):
@@ -55,7 +58,10 @@ def descriptors_from_object(self, obj):
5558
continue
5659

5760
prop_desc.method = method
58-
self.add_descriptor(prop_desc)
61+
descriptors_to_add.append(prop_desc)
62+
63+
for desc in descriptors_to_add:
64+
self.add_descriptor(desc)
5965

6066
def add_descriptor(self, descriptor: Descriptor):
6167
"""Add a descriptor to the collection.
@@ -102,7 +108,11 @@ def _handle_property_descriptor(self, prop: PropertyDescriptor) -> None:
102108
if prop.access & AccessFlags.Write and prop.setter is None:
103109
raise ValueError(f"Neither setter or setter_name was defined for {prop}")
104110

105-
self._handle_constraints(prop)
111+
# TODO: temporary hack as this should not cause I/O nor fail
112+
try:
113+
self._handle_constraints(prop)
114+
except DeviceException as ex:
115+
_LOGGER.error("Adding constraints failed: %s", ex)
106116

107117
def _handle_constraints(self, prop: PropertyDescriptor) -> None:
108118
"""Set attribute-based constraints for the descriptor."""
@@ -112,7 +122,11 @@ def _handle_constraints(self, prop: PropertyDescriptor) -> None:
112122
retrieve_choices_function = getattr(
113123
self._device, prop.choices_attribute
114124
)
115-
prop.choices = retrieve_choices_function()
125+
choices = retrieve_choices_function()
126+
if isinstance(choices, dict):
127+
prop.choices = Enum(f"GENERATED_ENUM_{prop.name}", choices)
128+
else:
129+
prop.choices = choices
116130

117131
if prop.choices is None:
118132
raise ValueError(

miio/device.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,9 @@ def _initialize_descriptors(self) -> None:
153153
This can be overridden to add additional descriptors to the device.
154154
If you do so, do not forget to call this method.
155155
"""
156+
if self._initialized:
157+
return
158+
156159
self._descriptors.descriptors_from_object(self)
157160

158161
# Read descriptors from the status class

miio/integrations/roborock/vacuum/vacuum.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1061,6 +1061,18 @@ def firmware_features(self) -> List[int]:
10611061
"""
10621062
return self.send("get_fw_features")
10631063

1064+
def _initialize_descriptors(self) -> None:
1065+
"""Initialize device descriptors.
1066+
1067+
Overridden to collect descriptors also from the update helper.
1068+
"""
1069+
if self._initialized:
1070+
return
1071+
1072+
super()._initialize_descriptors()
1073+
res = self.status()
1074+
self._descriptors.descriptors_from_object(res)
1075+
10641076
@classmethod
10651077
def get_device_group(cls):
10661078
@click.pass_context

miio/integrations/roborock/vacuum/vacuumcontainers.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ def state(self) -> str:
192192
self.state_code, f"Unknown state (code: {self.state_code})"
193193
)
194194

195+
@property
195196
@sensor("Vacuum state", id=VacuumId.State)
196197
def vacuum_state(self) -> VacuumState:
197198
"""Return vacuum state."""
@@ -276,6 +277,17 @@ def fanspeed(self) -> Optional[int]:
276277
return None
277278
return fan_power
278279

280+
@property
281+
@setting(
282+
"Fanspeed preset",
283+
choices_attribute="fan_speed_presets",
284+
setter_name="set_fan_speed_preset",
285+
icon="mdi:fan",
286+
id=VacuumId.FanSpeedPreset,
287+
)
288+
def fan_speed_preset(self):
289+
return self.data["fan_power"]
290+
279291
@property
280292
@setting(
281293
"Mop scrub intensity",

miio/tests/dummies.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from miio import DeviceError
1+
from miio import DescriptorCollection, DeviceError
22

33

44
class DummyMiIOProtocol:
@@ -46,6 +46,8 @@ def __init__(self, *args, **kwargs):
4646
self._settings = {}
4747
self._sensors = {}
4848
self._actions = {}
49+
self._initialized = False
50+
self._descriptors = DescriptorCollection(device=self)
4951
# TODO: ugly hack to check for pre-existing _model
5052
if getattr(self, "_model", None) is None:
5153
self._model = "dummy.model"

miio/tests/test_descriptorcollection.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from enum import Enum
2+
13
import pytest
24

35
from miio import (
@@ -119,22 +121,39 @@ def test_handle_enum_constraints(dummy_device, mocker):
119121
"status_attribute": "attr",
120122
}
121123

122-
mocker.patch.object(dummy_device, "choices_attr", create=True)
123-
124124
# Check that error is raised if choices are missing
125125
invalid = EnumDescriptor(id="missing", **data)
126126
with pytest.raises(
127127
ValueError, match="Neither choices nor choices_attribute was defined"
128128
):
129129
coll.add_descriptor(invalid)
130130

131-
# Check that binding works
131+
# Check that enum binding works
132+
mocker.patch.object(
133+
dummy_device,
134+
"choices_attr",
135+
create=True,
136+
return_value=Enum("test enum", {"foo": 1}),
137+
)
132138
choices_attribute = EnumDescriptor(
133139
id="with_choices_attr", choices_attribute="choices_attr", **data
134140
)
135141
coll.add_descriptor(choices_attribute)
136142
assert len(coll) == 1
137-
assert coll["with_choices_attr"].choices is not None
143+
144+
assert issubclass(coll["with_choices_attr"].choices, Enum)
145+
146+
# Check that dict binding works
147+
mocker.patch.object(
148+
dummy_device, "choices_attr_dict", create=True, return_value={"test": "dict"}
149+
)
150+
choices_attribute_dict = EnumDescriptor(
151+
id="with_choices_attr_dict", choices_attribute="choices_attr_dict", **data
152+
)
153+
coll.add_descriptor(choices_attribute_dict)
154+
assert len(coll) == 2
155+
156+
assert issubclass(coll["with_choices_attr_dict"].choices, Enum)
138157

139158

140159
def test_handle_range_constraints(dummy_device, mocker):

miio/tests/test_device.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,10 @@ def test_init_signature(cls, mocker):
143143
"""Make sure that __init__ of every device-inheriting class accepts the expected
144144
parameters."""
145145
mocker.patch("miio.Device.send")
146+
mocker.patch("miio.Device.send_handshake")
146147
parent_init = mocker.spy(Device, "__init__")
147148
kwargs = {
148-
"ip": "IP",
149+
"ip": "127.123.123.123",
149150
"token": None,
150151
"start_id": 0,
151152
"debug": False,
@@ -181,7 +182,9 @@ def test_supports_miot(mocker):
181182
assert d.supports_miot() is True
182183

183184

184-
@pytest.mark.parametrize("getter_name", ["actions", "settings", "sensors"])
185+
@pytest.mark.parametrize(
186+
"getter_name", ["actions", "settings", "sensors", "descriptors"]
187+
)
185188
def test_cached_descriptors(getter_name, mocker, caplog):
186189
d = Device("127.0.0.1", "68ffffffffffffffffffffffffffffff")
187190
getter = getattr(d, getter_name)

0 commit comments

Comments
 (0)