Skip to content
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

Closed
wants to merge 67 commits into from
Closed
Show file tree
Hide file tree
Changes from 58 commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
c808adf
Remove Fanspeed
starkillerOG Sep 30, 2022
fc8c7b4
Fix unit
starkillerOG Sep 30, 2022
a63270a
capital letters
starkillerOG Sep 30, 2022
b040a7c
disable battery by default
starkillerOG Sep 30, 2022
bc09260
Add sensors
starkillerOG Sep 30, 2022
c766026
fix typo
starkillerOG Sep 30, 2022
383de8b
adjust sensors
starkillerOG Sep 30, 2022
92dae6e
update icons
starkillerOG Sep 30, 2022
444f8ed
Add last_clean_details
starkillerOG Sep 30, 2022
2c3b4b3
Add DnD status
starkillerOG Sep 30, 2022
f7cbb95
use qualified_name for id
starkillerOG Sep 30, 2022
43e7a3e
add multi map id
starkillerOG Sep 30, 2022
33faf82
Update vacuumcontainers.py
starkillerOG Sep 30, 2022
c32b24a
Add mullti_map_id to clean history
starkillerOG Sep 30, 2022
7488ea9
fix type
starkillerOG Sep 30, 2022
32d0028
Add retrieving multi_maps_list
starkillerOG Oct 1, 2022
73fa561
add dust_collection_work_times
starkillerOG Oct 1, 2022
60e6d70
add load_multi_map
starkillerOG Oct 1, 2022
d995d26
add Multi map selector
starkillerOG Oct 1, 2022
a4e6dc4
fix typo
starkillerOG Oct 1, 2022
5cc7664
make changing multi map work in homeassistant
starkillerOG Oct 1, 2022
b3ecd75
Add Mop scrub intensity and mop route
starkillerOG Oct 1, 2022
a164846
fix circiler import
starkillerOG Oct 1, 2022
0c0a617
update icons
starkillerOG Oct 1, 2022
68a4fb4
Add auto_dust_collection
starkillerOG Oct 1, 2022
00dfae9
Last clean per floor
starkillerOG Oct 2, 2022
4d9ca0e
fixes
starkillerOG Oct 2, 2022
6c8bb0e
add entity_categories
starkillerOG Oct 2, 2022
e490cd4
add device_classes
starkillerOG Oct 2, 2022
298a485
add state_class
starkillerOG Oct 2, 2022
5fb7f9e
fix styling
starkillerOG Oct 3, 2022
f80a13e
docformatter
starkillerOG Oct 3, 2022
39d9781
docformatter correct lenght
starkillerOG Oct 3, 2022
fd49e94
fix mypy
starkillerOG Oct 3, 2022
142dfb0
fix flake8
starkillerOG Oct 3, 2022
4485c79
fix python compatibility
starkillerOG Oct 3, 2022
6f21e31
fix vacuum test
starkillerOG Oct 3, 2022
8c3eae4
fix tests
starkillerOG Oct 3, 2022
1b009d5
Change new fan mode Mopping to Off
starkillerOG Oct 7, 2022
bef9197
Merge branch 'master' into roborock_sensors
starkillerOG Oct 7, 2022
a86d7cc
Reduce device calls by caching the status
starkillerOG Oct 7, 2022
cd86f24
split out vacuum_status
starkillerOG Oct 7, 2022
46c2b80
simplify return
starkillerOG Oct 7, 2022
f53edc3
notes
starkillerOG Oct 7, 2022
28f3340
return FloorCleanDetails from last_clean_all_floor
starkillerOG Oct 8, 2022
65037f9
Use MultiMapList container
starkillerOG Oct 8, 2022
edc42d4
dynamically create FloorCleanDetail settings
starkillerOG Oct 8, 2022
6068336
fix __repr__
starkillerOG Oct 8, 2022
e47a9de
Embed CleanDetails for each floor in FloorCleanDetails container
starkillerOG Oct 8, 2022
cdc140e
Add button support
starkillerOG Oct 8, 2022
c051028
add start_dust_collection button
starkillerOG Oct 8, 2022
6ad0e45
Add stop dust collection button
starkillerOG Oct 8, 2022
14a7d6e
bind button method
starkillerOG Oct 8, 2022
2f5f3b3
fix
starkillerOG Oct 8, 2022
ee90954
fix styling
starkillerOG Oct 8, 2022
758e185
fix imports
starkillerOG Oct 8, 2022
252c1b6
fix mypy issues
starkillerOG Oct 8, 2022
de9dac9
fix styling
starkillerOG Oct 8, 2022
180dd5c
Add dock_error and dock_error_code
starkillerOG Nov 9, 2022
cc7d4ea
Merge branch 'master' into roborock_sensors
starkillerOG Nov 9, 2022
83e7e90
Update to latest miio
starkillerOG Nov 9, 2022
ac58bf3
Update to latest miio
starkillerOG Nov 9, 2022
f2fed51
Merge branch 'master' into roborock_sensors
starkillerOG Nov 9, 2022
1c69cc5
Merge branch 'master' into roborock_sensors
starkillerOG Nov 14, 2022
f1f3a62
Merge branch 'master' into roborock_sensors
starkillerOG Nov 14, 2022
998ec16
Merge branch 'master' into roborock_sensors
starkillerOG Nov 15, 2022
a5a0e3c
Merge branch 'master' into roborock_sensors
starkillerOG Dec 5, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 34 additions & 6 deletions miio/device.py
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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -238,13 +242,32 @@ def status(self) -> DeviceStatus:
"""Return device status."""
raise NotImplementedError()

def cached_status(self) -> DeviceStatus:
Copy link
Owner

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.

"""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
Copy link
Owner

Choose a reason for hiding this comment

The 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 info() call, but I'll think on it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 info() call, cache it there and then return the list of sensors/buttons/settings from the cache.
If sensors/buttons/settings is accesed when cache is still empty I can call info() there.

Do you want me to move it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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:
Expand All @@ -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:
Expand Down
89 changes: 55 additions & 34 deletions miio/devicestatus.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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")
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Copy link
Owner

Choose a reason for hiding this comment

The 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 is used (like upcoming generic miot integration I'm working on).

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,
Expand All @@ -279,3 +271,32 @@ def decorator_setting(func):
return func

return decorator_setting


def button(name: str, **kwargs):
Copy link
Owner

Choose a reason for hiding this comment

The 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
47 changes: 47 additions & 0 deletions miio/integrations/vacuum/roborock/tests/test_vacuum.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,45 @@ def __init__(self, *args, **kwargs):
1487548800,
],
]
self.dummies["dnd_timer"] = [
{
"enabled": 1,
"start_minute": 0,
"end_minute": 0,
"start_hour": 22,
"end_hour": 8,
}
]
self.dummies["multi_maps"] = [
{
"max_multi_map": 4,
"max_bak_map": 1,
"multi_map_count": 3,
"map_info": [
{
"mapFlag": 0,
"add_time": 1664448893,
"length": 10,
"name": "Downstairs",
"bak_maps": [{"mapFlag": 4, "add_time": 1663577737}],
},
{
"mapFlag": 1,
"add_time": 1663580330,
"length": 8,
"name": "Upstairs",
"bak_maps": [{"mapFlag": 5, "add_time": 1663577752}],
},
{
"mapFlag": 2,
"add_time": 1663580384,
"length": 5,
"name": "Attic",
"bak_maps": [{"mapFlag": 6, "add_time": 1663577765}],
},
],
}
]

self.return_values = {
"get_status": lambda x: [self.state],
Expand All @@ -81,8 +120,16 @@ def __init__(self, *args, **kwargs):
"app_zoned_clean": lambda x: self.change_mode("zoned clean"),
"app_charge": lambda x: self.change_mode("charge"),
"miIO.info": "dummy info",
"get_clean_record": lambda x: [[1488347071, 1488347123, 16, 0, 0, 0]],
"get_dnd_timer": lambda x: self.dummies["dnd_timer"],
"get_multi_maps_list": lambda x: self.dummies["multi_maps"],
}

self._multi_maps = None

self._floor_clean_details = {}
self._searched_clean_id = None

super().__init__(args, kwargs)

def change_mode(self, new_mode):
Expand Down
Loading