Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
21 changes: 16 additions & 5 deletions miio/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,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
self._actions: Optional[Dict[str, ActionDescriptor]] = None
timeout = timeout if timeout is not None else self.timeout
self._protocol = MiIOProtocol(
Expand Down Expand Up @@ -250,6 +252,14 @@ 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 actions(self) -> Dict[str, ActionDescriptor]:
"""Return device actions."""
if self._actions is None:
Expand All @@ -263,8 +273,10 @@ def actions(self) -> Dict[str, ActionDescriptor]:
return self._actions

def settings(self) -> Dict[str, SettingDescriptor]:
"""Return device settings."""
settings = self.status().settings()
"""Return list of 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 Down Expand Up @@ -292,9 +304,8 @@ def settings(self) -> Dict[str, SettingDescriptor]:
return settings

def sensors(self) -> Dict[str, SensorDescriptor]:
"""Return device sensors."""
# TODO: the latest status should be cached and re-used by all meta information getters
sensors = self.status().sensors()
"""Return sensors."""
sensors = self.cached_status().sensors()
return sensors

def __repr__(self):
Expand Down
12 changes: 1 addition & 11 deletions miio/devicestatus.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,7 @@
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 (
ActionDescriptor,
Expand Down Expand Up @@ -173,7 +164,6 @@ def _sensor_type_for_return_type(func):
def setting(
name: str,
*,
setter: Optional[Callable] = None,
setter_name: Optional[str] = None,
unit: Optional[str] = None,
min_value: Optional[int] = None,
Expand Down
5 changes: 5 additions & 0 deletions miio/integrations/vacuum/roborock/tests/test_vacuum.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@ def __init__(self, *args, **kwargs):
"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
78 changes: 70 additions & 8 deletions miio/integrations/vacuum/roborock/vacuum.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import os
import pathlib
import time
from typing import List, Optional, Type, Union
from typing import Dict, List, Optional, Tuple, Type, Union

import click
import pytz
Expand Down Expand Up @@ -47,6 +47,7 @@
CleaningSummary,
ConsumableStatus,
DNDStatus,
FloorCleanDetails,
MapList,
SoundInstallStatus,
SoundStatus,
Expand Down Expand Up @@ -137,6 +138,9 @@ def __init__(
ip, token, start_id, debug, lazy_discover, timeout, model=model
)
self.manual_seqnum = -1
self._floor_clean_details: Dict[str, Optional[CleaningDetails]] = {}
self._last_clean_details: Optional[CleaningDetails] = None
self._searched_clean_id: Optional[int] = None
self._maps: Optional[MapList] = None
self._map_enum_cache = None

Expand Down Expand Up @@ -335,14 +339,20 @@ def status(self) -> VacuumStatus:
"""Return status of the vacuum."""
status = self.vacuum_status()
status.embed(self.consumable_status())
status.embed(self.clean_history())
clean_history = self.clean_history()
status.embed(clean_history)
(details_floors, details_last) = self.last_clean_all_floor(
history=clean_history
)
status.embed(details_last)
status.embed(details_floors)
status.embed(self.dnd_status())
return status

@command()
def vacuum_status(self) -> VacuumStatus:
"""Return only status of the vacuum."""
return VacuumStatus(self.send("get_status")[0])
return VacuumStatus(self.send("get_status")[0], self.get_multi_maps())

def enable_log_upload(self):
raise NotImplementedError("unknown parameters")
Expand Down Expand Up @@ -489,17 +499,69 @@ def clean_history(self) -> CleaningSummary:
return CleaningSummary(self.send("get_clean_summary"))

@command()
def last_clean_details(self) -> Optional[CleaningDetails]:
def last_clean_details(
self, history: Optional[CleaningSummary] = None
) -> Optional[CleaningDetails]:
"""Return details from the last cleaning.

Returns None if there has been no cleanups.
"""
history = self.clean_history()
if history is None:
history = self.clean_history()
if not history.ids:
return None

last_clean_id = history.ids.pop(0)
return self.clean_details(last_clean_id)
last_clean_id = history.ids[0]
if last_clean_id == self._searched_clean_id:
return self._last_clean_details

self._last_clean_details = self.clean_details(last_clean_id)
return self._last_clean_details

@command()
def last_clean_all_floor(
self, history: Optional[CleaningSummary] = None
) -> Tuple[FloorCleanDetails, Optional[CleaningDetails]]:
"""Return details from the last cleaning and for each floor.

Returns None if there has been no cleanups for that floor.
"""
if history is None:
history = self.clean_history()

map_ids = self.get_multi_maps().map_id_list

# if cache empty, fill with None
if not self._floor_clean_details:
for id in map_ids:
self._floor_clean_details[str(id)] = None

if not history.ids:
return (
FloorCleanDetails(self._floor_clean_details),
self._last_clean_details,
)

last_clean_id = history.ids[0]
for id in history.ids:
# already searched this record
if id == self._searched_clean_id:
break

clean_detail = self.clean_details(id)
if clean_detail.multi_map_id in map_ids:
self._floor_clean_details[str(clean_detail.multi_map_id)] = clean_detail
map_ids.remove(clean_detail.multi_map_id)

if id == last_clean_id:
self._last_clean_details = clean_detail

# all floors found
if not map_ids:
break

self._searched_clean_id = last_clean_id
return (FloorCleanDetails(self._floor_clean_details), self._last_clean_details)

@command(
click.argument("id_", type=int, metavar="ID"),
Expand All @@ -514,7 +576,7 @@ def clean_details(
_LOGGER.warning("No cleaning record found for id %s", id_)
return None

res = CleaningDetails(details.pop())
res = CleaningDetails(details.pop(), self.get_multi_maps())
return res

@command()
Expand Down
82 changes: 80 additions & 2 deletions miio/integrations/vacuum/roborock/vacuumcontainers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from croniter import croniter
from pytz import BaseTzInfo

from miio.descriptors import SensorDescriptor
from miio.device import DeviceStatus
from miio.devicestatus import sensor, setting
from miio.interfaces.vacuuminterface import VacuumDeviceStatus, VacuumState
Expand Down Expand Up @@ -136,7 +137,9 @@ def map_name_dict(self) -> Dict[str, int]:
class VacuumStatus(VacuumDeviceStatus):
"""Container for status reports from the vacuum."""

def __init__(self, data: Dict[str, Any]) -> None:
def __init__(
self, data: Dict[str, Any], multi_maps: Optional[MultiMapList] = None
) -> None:
# {'result': [{'state': 8, 'dnd_enabled': 1, 'clean_time': 0,
# 'msg_ver': 4, 'map_present': 1, 'error_code': 0, 'in_cleaning': 0,
# 'clean_area': 0, 'battery': 100, 'fan_power': 20, 'msg_seq': 320}],
Expand Down Expand Up @@ -177,6 +180,7 @@ def __init__(self, data: Dict[str, Any]) -> None:
# 'water_shortage_status': 0, 'dock_type': 0, 'dust_collection_status': 0,
# 'auto_dust_collection': 1, 'mop_mode': 300, 'debug_mode': 0}]
self.data = data
self._multi_maps = multi_maps
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should avoid doing this, and do it another way around (i.e., the multimap container could expose whatever is necessary based on the status container). Does that sound reasonable to you?

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 do not get what you mean.
The multi_map information is nessesary to map the current multi_map_id (e.g. 1) where the Roborock is now to a actual floor name (e.g. "Upstairs").

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So basically I don't like the idea of injecting the information to the main status container but I'm currently too tired to think for a proper solution.. Let's remove the multi map parts from this PR–as it's a very large PR already–and figure a nicer solution on a separate discussion.


@property
@sensor("State code", entity_category="diagnostic", enabled_default=False)
Expand Down Expand Up @@ -337,6 +341,14 @@ def current_map_id(self) -> int:
"""
return int((self.data["map_status"] + 1) / 4 - 1)

@property
def map_name(self) -> str:
"""The name of the current map with regards to the multi map feature."""
if self._multi_maps is None:
return str(self.multi_map_id)

return self._multi_maps.map_list[self.multi_map_id]["name"]

@property
def in_zone_cleaning(self) -> bool:
"""Return True if the vacuum is in zone cleaning mode."""
Expand Down Expand Up @@ -494,10 +506,15 @@ def dust_collection_count(self) -> Optional[int]:
class CleaningDetails(DeviceStatus):
"""Contains details about a specific cleaning run."""

def __init__(self, data: Union[List[Any], Dict[str, Any]]) -> None:
def __init__(
self,
data: Union[List[Any], Dict[str, Any]],
multi_maps: Optional[MultiMapList] = None,
) -> None:
# start, end, duration, area, unk, complete
# { "result": [ [ 1488347071, 1488347123, 16, 0, 0, 0 ] ], "id": 1 }
# newer models return a dict
self._multi_maps = multi_maps
if isinstance(data, list):
self.data = {
"begin": data[0],
Expand Down Expand Up @@ -560,6 +577,15 @@ def map_id(self) -> int:
"""Map id used (multi map feature) during the cleaning run."""
return self.data.get("map_flag", 0)

@property
@sensor("Last clean map name", icon="mdi:floor-plan", entity_category="diagnostic")
def map_name(self) -> str:
"""The name of the map used (multi map feature) during the cleaning run."""
if self._multi_maps is None:
return str(self.multi_map_id)

return self._multi_maps.map_list[self.multi_map_id]["name"]

@property
def error_code(self) -> int:
"""Error code."""
Expand All @@ -579,6 +605,58 @@ def complete(self) -> bool:
return self.data["complete"] == 1


class FloorCleanDetails(DeviceStatus):
"""Contains details about a last cleaning run per floor."""

def __init__(self, data: Dict[str, Any]) -> None:
self.data = data

for map_id in self.data:
if self.data[map_id] is None:
setattr(self, f"CleanDetails_{map_id}", None)
setattr(self, f"start_{map_id}", None)
continue
setattr(self, f"CleanDetails_{map_id}", self.data[map_id])
setattr(self, f"start_{map_id}", self.data[map_id].start)
Comment on lines +614 to +620
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks rather messy and hard to follow through the setattr "magic", I think we can need to find a better solution for doing this. See my comment above wrt multiroom support.


def __repr__(self):

s = f"<{self.__class__.__name__}"
Comment on lines +622 to +624
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shouldn't have a need for extra magic, but a proper solution that leverages the existing repr.

for map_id in self.data:
name = f"CleanDetails_{map_id}"
value = getattr(self, name)
s += f" {name}={value}"

name = f"start_{map_id}"
value = getattr(self, name)
s += f" {name}={value}"

for name, embedded in self._embedded.items():
s += f" {name}={repr(embedded)}"

s += ">"
return s

def sensors(self) -> Dict[str, SensorDescriptor]:
"""Return the dict of sensors exposed by the status container."""
self._sensors = {} # type: ignore[attr-defined]

for map_id in self.data:
self._sensors[f"start_{map_id}"] = SensorDescriptor(
id=f"FloorCleanDetails.start_{map_id}",
property=f"start_{map_id}",
name=f"Floor {map_id} clean start",
type="sensor",
extras={
"icon": "mdi:clock-time-twelve",
"device_class": "timestamp",
"entity_category": "diagnostic",
},
)

return self._sensors


class ConsumableStatus(DeviceStatus):
"""Container for consumable status information, including information about brushes
and duration until they should be changed. The methods returning time left are based
Expand Down