-
-
Notifications
You must be signed in to change notification settings - Fork 571
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
Add additional sensors and settings to Roborock vacuums #1543
Changes from 58 commits
c808adf
fc8c7b4
a63270a
b040a7c
bc09260
c766026
383de8b
92dae6e
444f8ed
2c3b4b3
f7cbb95
43e7a3e
33faf82
c32b24a
7488ea9
32d0028
73fa561
60e6d70
d995d26
a4e6dc4
5cc7664
b3ecd75
a164846
0c0a617
68a4fb4
00dfae9
4d9ca0e
6c8bb0e
e490cd4
298a485
5fb7f9e
f80a13e
39d9781
fd49e94
142dfb0
4485c79
6f21e31
8c3eae4
1b009d5
bef9197
a86d7cc
cd86f24
46c2b80
f53edc3
28f3340
65037f9
edc42d4
6068336
e47a9de
cdc140e
c051028
6ad0e45
14a7d6e
2f5f3b3
ee90954
758e185
252c1b6
de9dac9
180dd5c
cc7d4ea
83e7e90
ac58bf3
f2fed51
1c69cc5
f1f3a62
998ec16
a5a0e3c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,14 @@ | ||
import logging | ||
from enum import Enum | ||
from inspect import getmembers | ||
from typing import Any, Dict, List, Optional, Union # noqa: F401 | ||
|
||
import click | ||
|
||
from .click_common import DeviceGroupMeta, LiteralParamType, command, format_output | ||
from .descriptors import ( | ||
ButtonDescriptor, | ||
EnumSettingDescriptor, | ||
SensorDescriptor, | ||
SettingDescriptor, | ||
SwitchDescriptor, | ||
|
@@ -54,6 +56,8 @@ def __init__( | |
self.token: Optional[str] = token | ||
self._model: Optional[str] = model | ||
self._info: Optional[DeviceInfo] = None | ||
self._status: Optional[DeviceStatus] = None | ||
self._buttons: Optional[List[ButtonDescriptor]] = None | ||
timeout = timeout if timeout is not None else self.timeout | ||
self._protocol = MiIOProtocol( | ||
ip, token, start_id, debug, lazy_discover, timeout | ||
|
@@ -238,13 +242,32 @@ def status(self) -> DeviceStatus: | |
"""Return device status.""" | ||
raise NotImplementedError() | ||
|
||
def cached_status(self) -> DeviceStatus: | ||
"""Return device status from cache.""" | ||
if self._status is None: | ||
self._status = self.status() | ||
|
||
return self._status | ||
|
||
def buttons(self) -> List[ButtonDescriptor]: | ||
"""Return a list of button-like, clickable actions of the device.""" | ||
return [] | ||
if self._buttons is None: | ||
self._buttons = [] | ||
for button_tuple in getmembers(self, lambda o: hasattr(o, "_button")): | ||
method_name, method = button_tuple | ||
button = method._button | ||
button.method = method # bind the method | ||
self._buttons.append(button) | ||
|
||
def settings(self) -> Dict[str, SettingDescriptor]: | ||
return self._buttons | ||
|
||
def settings( | ||
self, | ||
) -> Dict[str, SettingDescriptor]: | ||
"""Return list of settings.""" | ||
settings = self.status().settings() | ||
settings = ( | ||
self.cached_status().settings() | ||
) # NOTE that this already does IO so schould be run in executer job in HA | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See my note about below about doing the binding during the init. I suppose the best place to hook this is to happen is in the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can move all the binding/IO (status retrieving, choices list retrieving etc) to the Do you want me to move it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or we could raise an error when sensors/buttons/settings is accesed when cache is still empty, maybe that is nicer since you then know that the sensors/buttons/settings will not do IO, and therefore do not have to be run in a executor job... |
||
for setting in settings.values(): | ||
# TODO: Bind setter methods, this should probably done only once during init. | ||
if setting.setter is None: | ||
|
@@ -255,18 +278,23 @@ def settings(self) -> Dict[str, SettingDescriptor]: | |
) | ||
|
||
setting.setter = getattr(self, setting.setter_name) | ||
if ( | ||
isinstance(setting, EnumSettingDescriptor) | ||
and setting.choices_attribute is not None | ||
): | ||
retrieve_choices_function = getattr(self, setting.choices_attribute) | ||
setting.choices = retrieve_choices_function() # This can do IO | ||
|
||
return settings | ||
|
||
def sensors(self) -> Dict[str, SensorDescriptor]: | ||
"""Return sensors.""" | ||
# TODO: the latest status should be cached and re-used by all meta information getters | ||
sensors = self.status().sensors() | ||
sensors = self.cached_status().sensors() | ||
return sensors | ||
|
||
def switches(self) -> Dict[str, SwitchDescriptor]: | ||
"""Return toggleable switches.""" | ||
switches = self.status().switches() | ||
switches = self.cached_status().switches() | ||
for switch in switches.values(): | ||
# TODO: Bind setter methods, this should probably done only once during init. | ||
if switch.setter is None: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,18 +2,10 @@ | |
import logging | ||
import warnings | ||
from enum import Enum | ||
from typing import ( | ||
Callable, | ||
Dict, | ||
Optional, | ||
Type, | ||
Union, | ||
get_args, | ||
get_origin, | ||
get_type_hints, | ||
) | ||
from typing import Dict, Optional, Type, Union, get_args, get_origin, get_type_hints | ||
|
||
from .descriptors import ( | ||
ButtonDescriptor, | ||
EnumSettingDescriptor, | ||
NumberSettingDescriptor, | ||
SensorDescriptor, | ||
|
@@ -100,7 +92,9 @@ def switches(self) -> Dict[str, SwitchDescriptor]: | |
""" | ||
return self._switches # type: ignore[attr-defined] | ||
|
||
def settings(self) -> Dict[str, SettingDescriptor]: | ||
def settings( | ||
self, | ||
) -> Dict[str, SettingDescriptor]: | ||
"""Return the dict of settings exposed by the status container. | ||
|
||
You can use @setting decorator to define sensors inside your status class. | ||
|
@@ -143,7 +137,7 @@ def __getattribute__(self, item): | |
return getattr(self._embedded[embed], prop) | ||
|
||
|
||
def sensor(name: str, *, unit: str = "", **kwargs): | ||
def sensor(name: str, *, unit: Optional[str] = None, **kwargs): | ||
"""Syntactic sugar to create SensorDescriptor objects. | ||
|
||
The information can be used by users of the library to programmatically find out what | ||
|
@@ -155,7 +149,8 @@ def sensor(name: str, *, unit: str = "", **kwargs): | |
""" | ||
|
||
def decorator_sensor(func): | ||
property_name = func.__name__ | ||
property_name = str(func.__name__) | ||
qualified_name = str(func.__qualname__) | ||
|
||
def _sensor_type_for_return_type(func): | ||
rtype = get_type_hints(func).get("return") | ||
|
@@ -169,8 +164,8 @@ def _sensor_type_for_return_type(func): | |
|
||
sensor_type = _sensor_type_for_return_type(func) | ||
descriptor = SensorDescriptor( | ||
id=str(property_name), | ||
property=str(property_name), | ||
id=qualified_name, | ||
property=property_name, | ||
name=name, | ||
unit=unit, | ||
type=sensor_type, | ||
|
@@ -194,12 +189,13 @@ def switch(name: str, *, setter_name: str, **kwargs): | |
and can be interpreted downstream users as they wish. | ||
""" | ||
|
||
def decorator_sensor(func): | ||
property_name = func.__name__ | ||
def decorator_switch(func): | ||
property_name = str(func.__name__) | ||
qualified_name = str(func.__qualname__) | ||
|
||
descriptor = SwitchDescriptor( | ||
id=str(property_name), | ||
property=str(property_name), | ||
id=qualified_name, | ||
property=property_name, | ||
name=name, | ||
setter_name=setter_name, | ||
extras=kwargs, | ||
|
@@ -208,13 +204,12 @@ def decorator_sensor(func): | |
|
||
return func | ||
|
||
return decorator_sensor | ||
return decorator_switch | ||
|
||
|
||
def setting( | ||
name: str, | ||
*, | ||
setter: Optional[Callable] = None, | ||
setter_name: Optional[str] = None, | ||
unit: Optional[str] = None, | ||
min_value: Optional[int] = None, | ||
|
@@ -235,35 +230,32 @@ def setting( | |
""" | ||
|
||
def decorator_setting(func): | ||
property_name = func.__name__ | ||
property_name = str(func.__name__) | ||
qualified_name = str(func.__qualname__) | ||
|
||
if setter is None and setter_name is None: | ||
raise Exception("Either setter or setter_name needs to be defined") | ||
if setter_name is None: | ||
raise Exception("Setter_name needs to be defined") | ||
|
||
if min_value or max_value: | ||
descriptor = NumberSettingDescriptor( | ||
id=str(property_name), | ||
property=str(property_name), | ||
id=qualified_name, | ||
property=property_name, | ||
name=name, | ||
unit=unit, | ||
setter=setter, | ||
setter=None, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It feels like this changes the behavior and breaks cases where the |
||
setter_name=setter_name, | ||
min_value=min_value or 0, | ||
max_value=max_value, | ||
step=step or 1, | ||
extras=kwargs, | ||
) | ||
elif choices or choices_attribute: | ||
if choices_attribute is not None: | ||
# TODO: adding choices from attribute is a bit more complex, as it requires a way to | ||
# construct enums pointed by the attribute | ||
raise NotImplementedError("choices_attribute is not yet implemented") | ||
descriptor = EnumSettingDescriptor( | ||
id=str(property_name), | ||
property=str(property_name), | ||
id=qualified_name, | ||
property=property_name, | ||
name=name, | ||
unit=unit, | ||
setter=setter, | ||
setter=None, | ||
setter_name=setter_name, | ||
choices=choices, | ||
choices_attribute=choices_attribute, | ||
|
@@ -279,3 +271,32 @@ def decorator_setting(func): | |
return func | ||
|
||
return decorator_setting | ||
|
||
|
||
def button(name: str, **kwargs): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please move the button decorator feature into a separate PR to keep this one simpler, you can introduce the buttons for roborock as an example like I did in other "introduce introspectable X" PRs in #1495 |
||
"""Syntactic sugar to create ButtonDescriptor objects. | ||
|
||
The information can be used by users of the library to programmatically find out what | ||
types of sensors are available for the device. | ||
|
||
The interface is kept minimal, but you can pass any extra keyword arguments. | ||
These extras are made accessible over :attr:`~miio.descriptors.ButtonDescriptor.extras`, | ||
and can be interpreted downstream users as they wish. | ||
""" | ||
|
||
def decorator_button(func): | ||
property_name = str(func.__name__) | ||
qualified_name = str(func.__qualname__) | ||
|
||
descriptor = ButtonDescriptor( | ||
id=qualified_name, | ||
name=name, | ||
method_name=property_name, | ||
method=None, | ||
extras=kwargs, | ||
) | ||
func._button = descriptor | ||
|
||
return func | ||
|
||
return decorator_button |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's not add caching on this PR as it isn't directly relevant to the improvements for the roborock vacuums, and it will require some more investigation on what is the best approach to implement the functionality you are aiming to fix with this.