From c808adf43f4730533fcb71cc7fd0195572cc74a0 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 30 Sep 2022 16:09:03 +0200 Subject: [PATCH 01/60] Remove Fanspeed Fanspeed values for the Roborock S7 MaxV are 101, 102, 103, 104, 108 which are already coverd by the vacuum entity select. Percentage based values from 0 to 100 are not supported --- miio/integrations/vacuum/roborock/vacuumcontainers.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index 953516bc2..f15c12fbe 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -149,15 +149,6 @@ def battery(self) -> int: return int(self.data["battery"]) @property - @setting( - "Fanspeed", - unit="%", - setter_name="set_fan_speed", - min_value=0, - max_value=100, - step=1, - icon="mdi:fan", - ) def fanspeed(self) -> int: """Current fan speed.""" return int(self.data["fan_power"]) From fc8c7b4ab78f2e3bb6c19c8c94432fbf52dd21fe Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 30 Sep 2022 16:15:39 +0200 Subject: [PATCH 02/60] Fix unit --- miio/integrations/vacuum/roborock/vacuumcontainers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index f15c12fbe..992e3a441 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -134,7 +134,7 @@ def error_code(self) -> int: return int(self.data["error_code"]) @property - @sensor("Error", icon="mdi:alert") + @sensor("Error String", icon="mdi:alert") def error(self) -> str: """Human readable error description, see also :func:`error_code`.""" try: @@ -160,7 +160,7 @@ def clean_time(self) -> timedelta: return pretty_seconds(self.data["clean_time"]) @property - @sensor("Cleaned Area", unit="m2", icon="mdi:texture-box") + @sensor("Cleaned Area", unit="m²", icon="mdi:texture-box") def clean_area(self) -> float: """Cleaned area in m2.""" return pretty_area(self.data["clean_area"]) From a63270af2ac0bec5125af981e8685c91f9707d14 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 30 Sep 2022 16:21:09 +0200 Subject: [PATCH 03/60] capital letters --- .../vacuum/roborock/vacuumcontainers.py | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index 992e3a441..2a7d2e867 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -88,7 +88,7 @@ def __init__(self, data: Dict[str, Any]) -> None: self.data = data @property - @sensor("State Code") + @sensor("State code") def state_code(self) -> int: """State code as returned by the device.""" return int(self.data["state"]) @@ -128,13 +128,13 @@ def state(self) -> str: return "Definition missing for state %s" % self.state_code @property - @sensor("Error Code", icon="mdi:alert") + @sensor("Error code", icon="mdi:alert") def error_code(self) -> int: """Error code as returned by the device.""" return int(self.data["error_code"]) @property - @sensor("Error String", icon="mdi:alert") + @sensor("Error string", icon="mdi:alert") def error(self) -> str: """Human readable error description, see also :func:`error_code`.""" try: @@ -154,13 +154,13 @@ def fanspeed(self) -> int: return int(self.data["fan_power"]) @property - @sensor("Clean Duration", unit="s", icon="mdi:timer-sand") + @sensor("Clean duration", unit="s", icon="mdi:timer-sand") def clean_time(self) -> timedelta: """Time used for cleaning (if finished, shows how long it took).""" return pretty_seconds(self.data["clean_time"]) @property - @sensor("Cleaned Area", unit="m²", icon="mdi:texture-box") + @sensor("Cleaned area", unit="m²", icon="mdi:texture-box") def clean_area(self) -> float: """Cleaned area in m2.""" return pretty_area(self.data["clean_area"]) @@ -197,7 +197,7 @@ def is_on(self) -> bool: ) @property - @sensor("Water Box Attached") + @sensor("Water box attached") def is_water_box_attached(self) -> Optional[bool]: """Return True is water box is installed.""" if "water_box_status" in self.data: @@ -205,7 +205,7 @@ def is_water_box_attached(self) -> Optional[bool]: return None @property - @sensor("Mop Attached") + @sensor("Mop attached") def is_water_box_carriage_attached(self) -> Optional[bool]: """Return True if water box carriage (mop) is installed, None if sensor not present.""" @@ -214,7 +214,7 @@ def is_water_box_carriage_attached(self) -> Optional[bool]: return None @property - @sensor("Water Level Low", icon="mdi:alert") + @sensor("Water level low", icon="mdi:alert") def is_water_shortage(self) -> Optional[bool]: """Returns True if water is low in the tank, None if sensor not present.""" if "water_shortage_status" in self.data: @@ -254,19 +254,19 @@ def __init__(self, data: Union[List[Any], Dict[str, Any]]) -> None: self.data["records"] = [] @property - @sensor("Total Cleaning Time", icon="mdi:timer-sand") + @sensor("Total cleaning time", icon="mdi:timer-sand") def total_duration(self) -> timedelta: """Total cleaning duration.""" return pretty_seconds(self.data["clean_time"]) @property - @sensor("Total Cleaning Area", icon="mdi:texture-box") + @sensor("Total cleaning area", unit="m²", icon="mdi:texture-box") def total_area(self) -> float: """Total cleaned area.""" return pretty_area(self.data["clean_area"]) @property - @sensor("Total Clean Count") + @sensor("Total clean count") def count(self) -> int: """Number of cleaning runs.""" return int(self.data["clean_count"]) @@ -277,7 +277,7 @@ def ids(self) -> List[int]: return list(self.data["records"]) @property - @sensor("Dust Collection Count") + @sensor("Dust collection count") def dust_collection_count(self) -> Optional[int]: """Total number of dust collections.""" if "dust_collection_count" in self.data: @@ -368,13 +368,13 @@ def __init__(self, data: Dict[str, Any]) -> None: self.sensor_dirty_total = timedelta(hours=30) @property - @sensor("Main Brush Usage", unit="s") + @sensor("Main brush usage", unit="s") def main_brush(self) -> timedelta: """Main brush usage time.""" return pretty_seconds(self.data["main_brush_work_time"]) @property - @sensor("Main Brush Remaining", unit="s") + @sensor("Main brush remaining", unit="s") def main_brush_left(self) -> timedelta: """How long until the main brush should be changed.""" return self.main_brush_total - self.main_brush @@ -418,7 +418,7 @@ def __init__(self, data: Dict[str, Any]): self.data = data @property - @sensor("Do Not Disturb") + @sensor("Do not disturb (DnD)") def enabled(self) -> bool: """True if DnD is enabled.""" return bool(self.data["enabled"]) @@ -587,7 +587,7 @@ def __init__(self, data): self.data = data @property - @sensor("Carpet Mode") + @sensor("Carpet mode") def enabled(self) -> bool: """True if carpet mode is enabled.""" return self.data["enable"] == 1 From b040a7cccbce98d1abe6e1da49fe25b11b293824 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 30 Sep 2022 16:33:57 +0200 Subject: [PATCH 04/60] disable battery by default Since it is already included in the vacuum entity --- miio/integrations/vacuum/roborock/vacuumcontainers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index 2a7d2e867..cd7f6857d 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -143,7 +143,7 @@ def error(self) -> str: return "Definition missing for error %s" % self.error_code @property - @sensor("Battery", unit="%", device_class="battery") + @sensor("Battery", unit="%", device_class="battery", enabled_default=False}) def battery(self) -> int: """Remaining battery in percentage.""" return int(self.data["battery"]) From bc09260dc1838d01c6051e36923b2f201ffe3bb4 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 30 Sep 2022 17:02:35 +0200 Subject: [PATCH 05/60] Add sensors --- .../vacuum/roborock/vacuumcontainers.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index cd7f6857d..1f572bf16 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -154,13 +154,13 @@ def fanspeed(self) -> int: return int(self.data["fan_power"]) @property - @sensor("Clean duration", unit="s", icon="mdi:timer-sand") + @sensor("Current clean duration", unit="s", icon="mdi:timer-sand") def clean_time(self) -> timedelta: """Time used for cleaning (if finished, shows how long it took).""" return pretty_seconds(self.data["clean_time"]) @property - @sensor("Cleaned area", unit="m²", icon="mdi:texture-box") + @sensor("Current clean area", unit="m²", icon="mdi:texture-box") def clean_area(self) -> float: """Cleaned area in m2.""" return pretty_area(self.data["clean_area"]) @@ -368,43 +368,49 @@ def __init__(self, data: Dict[str, Any]) -> None: self.sensor_dirty_total = timedelta(hours=30) @property - @sensor("Main brush usage", unit="s") + @sensor("Main brush used", unit="s", icon="mdi:brush", enabled_default=False) def main_brush(self) -> timedelta: """Main brush usage time.""" return pretty_seconds(self.data["main_brush_work_time"]) @property - @sensor("Main brush remaining", unit="s") + @sensor("Main brush left", unit="s", icon="mdi:brush") def main_brush_left(self) -> timedelta: """How long until the main brush should be changed.""" return self.main_brush_total - self.main_brush @property + @sensor("Side brush used", unit="s", icon="mdi:brush", enabled_default=False) def side_brush(self) -> timedelta: """Side brush usage time.""" return pretty_seconds(self.data["side_brush_work_time"]) @property + @sensor("Side brush left", unit="s", icon="mdi:brush") def side_brush_left(self) -> timedelta: """How long until the side brush should be changed.""" return self.side_brush_total - self.side_brush @property + @sensor("Filter used", unit="s", icon="mdi:air-filter", enabled_default=False) def filter(self) -> timedelta: """Filter usage time.""" return pretty_seconds(self.data["filter_work_time"]) @property + @sensor("Filter left", unit="s", icon="mdi:air-filter") def filter_left(self) -> timedelta: """How long until the filter should be changed.""" return self.filter_total - self.filter @property + @sensor("Sensor dirty used", unit="s", icon="mdi:eye-outline", enabled_default=False) def sensor_dirty(self) -> timedelta: """Return ``sensor_dirty_time``""" return pretty_seconds(self.data["sensor_dirty_time"]) @property + @sensor("Sensor dirty left", unit="s", icon="mdi:eye-outline") def sensor_dirty_left(self) -> timedelta: return self.sensor_dirty_total - self.sensor_dirty From c766026ddecd08209df02c7685cca1968e78a303 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 30 Sep 2022 17:04:46 +0200 Subject: [PATCH 06/60] fix typo --- miio/integrations/vacuum/roborock/vacuumcontainers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index 1f572bf16..9aefe2f31 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -143,7 +143,7 @@ def error(self) -> str: return "Definition missing for error %s" % self.error_code @property - @sensor("Battery", unit="%", device_class="battery", enabled_default=False}) + @sensor("Battery", unit="%", device_class="battery", enabled_default=False) def battery(self) -> int: """Remaining battery in percentage.""" return int(self.data["battery"]) From 383de8bc478ed31cf15f9aef540424b1e0602273 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 30 Sep 2022 17:23:22 +0200 Subject: [PATCH 07/60] adjust sensors --- .../vacuum/roborock/vacuumcontainers.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index 9aefe2f31..a8d89a970 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -128,13 +128,13 @@ def state(self) -> str: return "Definition missing for state %s" % self.state_code @property - @sensor("Error code", icon="mdi:alert") + @sensor("Error code", icon="mdi:alert", enabled_default=False) def error_code(self) -> int: """Error code as returned by the device.""" return int(self.data["error_code"]) @property - @sensor("Error string", icon="mdi:alert") + @sensor("Error string", icon="mdi:alert", enabled_default=False) def error(self) -> str: """Human readable error description, see also :func:`error_code`.""" try: @@ -222,7 +222,7 @@ def is_water_shortage(self) -> Optional[bool]: return None @property - @sensor("Error", icon="mdi:alert") + @sensor("Error", icon="mdi:alert", enabled_default=False) def got_error(self) -> bool: """True if an error has occurred.""" return self.error_code != 0 @@ -254,19 +254,19 @@ def __init__(self, data: Union[List[Any], Dict[str, Any]]) -> None: self.data["records"] = [] @property - @sensor("Total cleaning time", icon="mdi:timer-sand") + @sensor("Total clean duration", icon="mdi:timer-sand") def total_duration(self) -> timedelta: """Total cleaning duration.""" return pretty_seconds(self.data["clean_time"]) @property - @sensor("Total cleaning area", unit="m²", icon="mdi:texture-box") + @sensor("Total clean area", unit="m²", icon="mdi:texture-box") def total_area(self) -> float: """Total cleaned area.""" return pretty_area(self.data["clean_area"]) @property - @sensor("Total clean count") + @sensor("Total clean count", icon="mdi:counter") def count(self) -> int: """Number of cleaning runs.""" return int(self.data["clean_count"]) @@ -277,7 +277,7 @@ def ids(self) -> List[int]: return list(self.data["records"]) @property - @sensor("Dust collection count") + @sensor("Total_dust collection count", icon="mdi:counter") def dust_collection_count(self) -> Optional[int]: """Total number of dust collections.""" if "dust_collection_count" in self.data: From 92dae6e689eb4da9ed2a9e4c87159d49d8f4903b Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 30 Sep 2022 17:52:51 +0200 Subject: [PATCH 08/60] update icons --- miio/integrations/vacuum/roborock/vacuumcontainers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index a8d89a970..84dfd3de8 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -88,7 +88,7 @@ def __init__(self, data: Dict[str, Any]) -> None: self.data = data @property - @sensor("State code") + @sensor("State code", enabled_default=False) def state_code(self) -> int: """State code as returned by the device.""" return int(self.data["state"]) @@ -197,7 +197,7 @@ def is_on(self) -> bool: ) @property - @sensor("Water box attached") + @sensor("Water box attached", icon="mdi:cup-water") def is_water_box_attached(self) -> Optional[bool]: """Return True is water box is installed.""" if "water_box_status" in self.data: @@ -214,7 +214,7 @@ def is_water_box_carriage_attached(self) -> Optional[bool]: return None @property - @sensor("Water level low", icon="mdi:alert") + @sensor("Water level low", icon="mdi:water-alert-outline") def is_water_shortage(self) -> Optional[bool]: """Returns True if water is low in the tank, None if sensor not present.""" if "water_shortage_status" in self.data: From 444f8ed215c288565f188f198c7043affd3719af Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 30 Sep 2022 18:14:03 +0200 Subject: [PATCH 09/60] Add last_clean_details --- miio/integrations/vacuum/roborock/vacuum.py | 11 +++++++---- miio/integrations/vacuum/roborock/vacuumcontainers.py | 6 +++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/miio/integrations/vacuum/roborock/vacuum.py b/miio/integrations/vacuum/roborock/vacuum.py index 78abe51bb..715c7e9bf 100644 --- a/miio/integrations/vacuum/roborock/vacuum.py +++ b/miio/integrations/vacuum/roborock/vacuum.py @@ -405,7 +405,9 @@ def status(self) -> VacuumStatus: """Return status of the vacuum.""" status = VacuumStatus(self.send("get_status")[0]) status.embed(self.consumable_status()) - status.embed(self.clean_history()) + clean_history = self.clean_history() + status.embed(clean_history) + status.embed(self.last_clean_details(history=clean_history)) return status def enable_log_upload(self): @@ -519,16 +521,17 @@ 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) + last_clean_id = history.ids[0] return self.clean_details(last_clean_id) @command( diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index 84dfd3de8..8baa03180 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -254,7 +254,7 @@ def __init__(self, data: Union[List[Any], Dict[str, Any]]) -> None: self.data["records"] = [] @property - @sensor("Total clean duration", icon="mdi:timer-sand") + @sensor("Total clean duration", unit="s", icon="mdi:timer-sand") def total_duration(self) -> timedelta: """Total cleaning duration.""" return pretty_seconds(self.data["clean_time"]) @@ -306,21 +306,25 @@ def __init__(self, data: Union[List[Any], Dict[str, Any]]) -> None: self.data = data @property + @sensor("Last clean start", icon="mdi:clock-time-twelve") def start(self) -> datetime: """When cleaning was started.""" return pretty_time(self.data["begin"]) @property + @sensor("Last clean end", icon="mdi:clock-time-twelve") def end(self) -> datetime: """When cleaning was finished.""" return pretty_time(self.data["end"]) @property + @sensor("Last clean duration", unit="s", icon="mdi:timer-sand") def duration(self) -> timedelta: """Total duration of the cleaning run.""" return pretty_seconds(self.data["duration"]) @property + @sensor("Last clean area", unit="m²", icon="mdi:texture-box") def area(self) -> float: """Total cleaned area.""" return pretty_area(self.data["area"]) From 2c3b4b3b3f331ee0108e45f55ee69381cf7664d6 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 30 Sep 2022 20:43:40 +0200 Subject: [PATCH 10/60] Add DnD status Change id to qualified name. Otherwise DNDStatus.start and CleaningDetails:start get the same unique ID, causing problems in HomeAssistant --- miio/devicestatus.py | 7 ++++--- miio/integrations/vacuum/roborock/vacuum.py | 1 + miio/integrations/vacuum/roborock/vacuumcontainers.py | 4 +++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/miio/devicestatus.py b/miio/devicestatus.py index bef394dd5..60fcebff9 100644 --- a/miio/devicestatus.py +++ b/miio/devicestatus.py @@ -155,7 +155,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 +170,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, diff --git a/miio/integrations/vacuum/roborock/vacuum.py b/miio/integrations/vacuum/roborock/vacuum.py index 715c7e9bf..10ed4d462 100644 --- a/miio/integrations/vacuum/roborock/vacuum.py +++ b/miio/integrations/vacuum/roborock/vacuum.py @@ -408,6 +408,7 @@ def status(self) -> VacuumStatus: clean_history = self.clean_history() status.embed(clean_history) status.embed(self.last_clean_details(history=clean_history)) + status.embed(self.dnd_status()) return status def enable_log_upload(self): diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index 8baa03180..6b196a7a7 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -428,17 +428,19 @@ def __init__(self, data: Dict[str, Any]): self.data = data @property - @sensor("Do not disturb (DnD)") + @sensor("Do not disturb", icon="mdi:minus-circle-off") def enabled(self) -> bool: """True if DnD is enabled.""" return bool(self.data["enabled"]) @property + @sensor("Do not disturb start", icon="mdi:minus-circle-off") def start(self) -> time: """Start time of DnD.""" return time(hour=self.data["start_hour"], minute=self.data["start_minute"]) @property + @sensor("Do not disturb end", icon="mdi:minus-circle-off") def end(self) -> time: """End time of DnD.""" return time(hour=self.data["end_hour"], minute=self.data["end_minute"]) From f7cbb95e9a1dd4702f14b740ddd6a370db54bc5b Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 30 Sep 2022 21:15:45 +0200 Subject: [PATCH 11/60] use qualified_name for id --- miio/devicestatus.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/miio/devicestatus.py b/miio/devicestatus.py index 60fcebff9..642d203e2 100644 --- a/miio/devicestatus.py +++ b/miio/devicestatus.py @@ -195,12 +195,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, @@ -209,7 +210,7 @@ def decorator_sensor(func): return func - return decorator_sensor + return decorator_switch def setting( @@ -236,15 +237,16 @@ 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 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, @@ -260,8 +262,8 @@ def decorator_setting(func): # 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, From 43e7a3ef023855c7a7dc8f2f54d814e387c63731 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 30 Sep 2022 21:48:46 +0200 Subject: [PATCH 12/60] add multi map id --- miio/integrations/vacuum/roborock/vacuumcontainers.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index 6b196a7a7..c7aed9c08 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -170,6 +170,12 @@ def map(self) -> bool: """Map token.""" return bool(self.data["map_present"]) + @property + @sensor("Multi map id", icon="mdi:floor-plan") + def multi_map_id(self) -> int: + """The id of the current map with regards to the multi map feature, [3,7,11,15] -> [0,1,2,3].""" + return (self.data["map_status"]+1)/4 - 1 + @property def in_zone_cleaning(self) -> bool: """Return True if the vacuum is in zone cleaning mode.""" From 33faf822953d5a0fae87c5c50a3304fd96b1440d Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 30 Sep 2022 21:49:13 +0200 Subject: [PATCH 13/60] Update vacuumcontainers.py --- miio/integrations/vacuum/roborock/vacuumcontainers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index c7aed9c08..15edbe361 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -440,13 +440,13 @@ def enabled(self) -> bool: return bool(self.data["enabled"]) @property - @sensor("Do not disturb start", icon="mdi:minus-circle-off") + @sensor("Do not disturb start", icon="mdi:minus-circle-off", enabled_default=False) def start(self) -> time: """Start time of DnD.""" return time(hour=self.data["start_hour"], minute=self.data["start_minute"]) @property - @sensor("Do not disturb end", icon="mdi:minus-circle-off") + @sensor("Do not disturb end", icon="mdi:minus-circle-off", enabled_default=False) def end(self) -> time: """End time of DnD.""" return time(hour=self.data["end_hour"], minute=self.data["end_minute"]) From c32b24a2c86d2101e6fe543fa89c972f042369cd Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 30 Sep 2022 21:55:43 +0200 Subject: [PATCH 14/60] Add mullti_map_id to clean history --- miio/integrations/vacuum/roborock/vacuumcontainers.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index 15edbe361..227d907c6 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -335,6 +335,12 @@ def area(self) -> float: """Total cleaned area.""" return pretty_area(self.data["area"]) + @property + @sensor("Last clean map id", icon="mdi:floor-plan") + def multi_map_id(self) -> int: + """Map id used (multi map feature) during the cleaning run.""" + return self.data["map_flag"] + @property def error_code(self) -> int: """Error code.""" From 7488ea927e08b2d4ec250a1ceb7533efefefa1f4 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 30 Sep 2022 22:01:56 +0200 Subject: [PATCH 15/60] fix type --- miio/integrations/vacuum/roborock/vacuumcontainers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index 227d907c6..b4c2bf5a9 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -174,7 +174,7 @@ def map(self) -> bool: @sensor("Multi map id", icon="mdi:floor-plan") def multi_map_id(self) -> int: """The id of the current map with regards to the multi map feature, [3,7,11,15] -> [0,1,2,3].""" - return (self.data["map_status"]+1)/4 - 1 + return int((self.data["map_status"]+1)/4 - 1) @property def in_zone_cleaning(self) -> bool: From 32d002810c2bf8ac366df32f12e0f824fb9f5b19 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 1 Oct 2022 11:27:40 +0200 Subject: [PATCH 16/60] Add retrieving multi_maps_list --- miio/integrations/vacuum/roborock/vacuum.py | 19 ++++++++++++-- .../vacuum/roborock/vacuumcontainers.py | 26 ++++++++++++++++--- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/miio/integrations/vacuum/roborock/vacuum.py b/miio/integrations/vacuum/roborock/vacuum.py index 10ed4d462..8fb6d08db 100644 --- a/miio/integrations/vacuum/roborock/vacuum.py +++ b/miio/integrations/vacuum/roborock/vacuum.py @@ -214,6 +214,7 @@ def __init__( ): super().__init__(ip, token, start_id, debug, model=model) self.manual_seqnum = -1 + self._multi_maps = None @command() def start(self): @@ -403,7 +404,7 @@ def manual_control( @command() def status(self) -> VacuumStatus: """Return status of the vacuum.""" - status = VacuumStatus(self.send("get_status")[0]) + status = VacuumStatus(self.send("get_status")[0], self.get_multi_maps()) status.embed(self.consumable_status()) clean_history = self.clean_history() status.embed(clean_history) @@ -436,6 +437,20 @@ def map(self): # returns ['retry'] without internet return self.send("get_map_v1") + @command() + def get_multi_maps(self, skip_cache=False): + """Return list of 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}]} + # ]} + if self._multi_maps is not None and not skip_cache: + return self._multi_maps + + self._multi_maps = self.send("get_multi_maps_list")[0] + return self._multi_maps + @command(click.argument("start", type=bool)) def edit_map(self, start): """Start map editing?""" @@ -548,7 +563,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() diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index b4c2bf5a9..8afe6c6da 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -45,7 +45,7 @@ def pretty_area(x: float) -> float: class VacuumStatus(DeviceStatus): """Container for status reports from the vacuum.""" - def __init__(self, data: Dict[str, Any]) -> None: + def __init__(self, data: Dict[str, Any], multi_maps=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}], @@ -86,6 +86,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 @property @sensor("State code", enabled_default=False) @@ -171,11 +172,19 @@ def map(self) -> bool: return bool(self.data["map_present"]) @property - @sensor("Multi map id", icon="mdi:floor-plan") def multi_map_id(self) -> int: """The id of the current map with regards to the multi map feature, [3,7,11,15] -> [0,1,2,3].""" return int((self.data["map_status"]+1)/4 - 1) + @property + @sensor("Multi map name", icon="mdi:floor-plan") + def multi_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_info"][self.multi_map_id]["name"] + @property def in_zone_cleaning(self) -> bool: """Return True if the vacuum is in zone cleaning mode.""" @@ -295,10 +304,11 @@ 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=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], @@ -336,11 +346,19 @@ def area(self) -> float: return pretty_area(self.data["area"]) @property - @sensor("Last clean map id", icon="mdi:floor-plan") def multi_map_id(self) -> int: """Map id used (multi map feature) during the cleaning run.""" return self.data["map_flag"] + @property + @sensor("Last clean map name", icon="mdi:floor-plan") + def multi_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_info"][self.multi_map_id]["name"] + @property def error_code(self) -> int: """Error code.""" From 73fa5610a5086fd06877b617a4a8e94f2c8f62b1 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 1 Oct 2022 11:52:32 +0200 Subject: [PATCH 17/60] add dust_collection_work_times --- miio/integrations/vacuum/roborock/vacuumcontainers.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index 8afe6c6da..5f5dcf6b9 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -448,6 +448,12 @@ def sensor_dirty(self) -> timedelta: def sensor_dirty_left(self) -> timedelta: return self.sensor_dirty_total - self.sensor_dirty + @property + @sensor("Dustbin times auto-empty used", icon="mdi:delete", enabled_default=False) + def dustbin_auto_empty_used(self) -> int: + """Return ``dust_collection_work_times``""" + return self.data["dust_collection_work_times"] + class DNDStatus(DeviceStatus): """A container for the do-not-disturb status.""" From 60e6d70601c66a80a3153cd9130af37c2e3c4794 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 1 Oct 2022 11:57:44 +0200 Subject: [PATCH 18/60] add load_multi_map --- miio/integrations/vacuum/roborock/vacuum.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/miio/integrations/vacuum/roborock/vacuum.py b/miio/integrations/vacuum/roborock/vacuum.py index 8fb6d08db..9a524fa29 100644 --- a/miio/integrations/vacuum/roborock/vacuum.py +++ b/miio/integrations/vacuum/roborock/vacuum.py @@ -451,6 +451,11 @@ def get_multi_maps(self, skip_cache=False): self._multi_maps = self.send("get_multi_maps_list")[0] return self._multi_maps + @command(click.argument("multi_map_id", type=int)) + def load_multi_map(self, multi_map_id: int): + """Change the current map used.""" + return self.send("load_multi_map", [multi_map_id])[0] == "ok" + @command(click.argument("start", type=bool)) def edit_map(self, start): """Start map editing?""" From d995d26de7cabf2a3dc569909e4d55f51ed5acd8 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 1 Oct 2022 12:16:04 +0200 Subject: [PATCH 19/60] add Multi map selector --- miio/integrations/vacuum/roborock/vacuum.py | 21 ++++++++++++++++++- .../vacuum/roborock/vacuumcontainers.py | 5 ++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/miio/integrations/vacuum/roborock/vacuum.py b/miio/integrations/vacuum/roborock/vacuum.py index 9a524fa29..6df92c2b9 100644 --- a/miio/integrations/vacuum/roborock/vacuum.py +++ b/miio/integrations/vacuum/roborock/vacuum.py @@ -448,7 +448,12 @@ def get_multi_maps(self, skip_cache=False): if self._multi_maps is not None and not skip_cache: return self._multi_maps - self._multi_maps = self.send("get_multi_maps_list")[0] + multi_maps = self.send("get_multi_maps_list")[0] + multi_maps['map_names'] = [] + for map in multi_maps["map_info"] + multi_maps['map_names'].append(map["name"]) + + self._multi_maps = multi_maps return self._multi_maps @command(click.argument("multi_map_id", type=int)) @@ -456,6 +461,20 @@ def load_multi_map(self, multi_map_id: int): """Change the current map used.""" return self.send("load_multi_map", [multi_map_id])[0] == "ok" + @command(click.argument("multi_map_name", type=str)) + def load_multi_map_by_name(self, multi_map_name: str): + """Change the current map used by name.""" + multi_map_id = None + for map in self.get_multi_maps()["map_info"]: + if map["name"] == multi_map_name: + multi_map_id = map["mapFlag"] + break + + if multi_map_id is None: + return False + + return self.load_multi_map(multi_map_id) + @command(click.argument("start", type=bool)) def edit_map(self, start): """Start map editing?""" diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index 5f5dcf6b9..c846be455 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -87,6 +87,9 @@ def __init__(self, data: Dict[str, Any], multi_maps=None) -> None: # 'auto_dust_collection': 1, 'mop_mode': 300, 'debug_mode': 0}] self.data = data self._multi_maps = multi_maps + self._map_names = [] + if multi_maps is not None: + self._map_names = multi_maps["map_names"] @property @sensor("State code", enabled_default=False) @@ -177,7 +180,7 @@ def multi_map_id(self) -> int: return int((self.data["map_status"]+1)/4 - 1) @property - @sensor("Multi map name", icon="mdi:floor-plan") + @setting("Multi map name", choices=self._map_names, setter_name="load_multi_map_by_name", icon="mdi:floor-plan") def multi_map_name(self) -> str: """The name of the current map with regards to the multi map feature.""" if self._multi_maps is None: From a4e6dc47e0862ec48bbb801bd7796b300bb9e0ba Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 1 Oct 2022 12:19:51 +0200 Subject: [PATCH 20/60] fix typo --- miio/integrations/vacuum/roborock/vacuum.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miio/integrations/vacuum/roborock/vacuum.py b/miio/integrations/vacuum/roborock/vacuum.py index 6df92c2b9..a8dcf5508 100644 --- a/miio/integrations/vacuum/roborock/vacuum.py +++ b/miio/integrations/vacuum/roborock/vacuum.py @@ -450,7 +450,7 @@ def get_multi_maps(self, skip_cache=False): multi_maps = self.send("get_multi_maps_list")[0] multi_maps['map_names'] = [] - for map in multi_maps["map_info"] + for map in multi_maps["map_info"]: multi_maps['map_names'].append(map["name"]) self._multi_maps = multi_maps From 5cc7664d0eb55a708130f462b0997e1831219250 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 1 Oct 2022 13:52:09 +0200 Subject: [PATCH 21/60] make changing multi map work in homeassistant --- miio/device.py | 5 ++- miio/devicestatus.py | 4 +- miio/integrations/vacuum/roborock/vacuum.py | 37 ++++++++++--------- .../vacuum/roborock/vacuumcontainers.py | 5 +-- 4 files changed, 26 insertions(+), 25 deletions(-) diff --git a/miio/device.py b/miio/device.py index c857885f7..503fe7578 100644 --- a/miio/device.py +++ b/miio/device.py @@ -244,7 +244,7 @@ def buttons(self) -> List[ButtonDescriptor]: def settings(self) -> Dict[str, SettingDescriptor]: """Return list of settings.""" - settings = self.status().settings() + settings = self.status().settings() #NOTE that this already does IO so schould be run in executer job in HA for setting in settings.values(): # TODO: Bind setter methods, this should probably done only once during init. if setting.setter is None: @@ -255,6 +255,9 @@ def settings(self) -> Dict[str, SettingDescriptor]: ) setting.setter = getattr(self, setting.setter_name) + if setting.choices_attribute is not None: + retrieve_choices_function = getattr(self, setting.choices_attribute) + setting.choices = retrieve_choices_function() return settings diff --git a/miio/devicestatus.py b/miio/devicestatus.py index 642d203e2..0076bba18 100644 --- a/miio/devicestatus.py +++ b/miio/devicestatus.py @@ -257,10 +257,10 @@ def decorator_setting(func): extras=kwargs, ) elif choices or choices_attribute: - if choices_attribute is not None: + #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") + #raise NotImplementedError("choices_attribute is not yet implemented") descriptor = EnumSettingDescriptor( id=qualified_name, property=property_name, diff --git a/miio/integrations/vacuum/roborock/vacuum.py b/miio/integrations/vacuum/roborock/vacuum.py index a8dcf5508..38ad652b3 100644 --- a/miio/integrations/vacuum/roborock/vacuum.py +++ b/miio/integrations/vacuum/roborock/vacuum.py @@ -215,6 +215,7 @@ def __init__( super().__init__(ip, token, start_id, debug, model=model) self.manual_seqnum = -1 self._multi_maps = None + self._multi_map_enum = None @command() def start(self): @@ -448,32 +449,32 @@ def get_multi_maps(self, skip_cache=False): if self._multi_maps is not None and not skip_cache: return self._multi_maps - multi_maps = self.send("get_multi_maps_list")[0] - multi_maps['map_names'] = [] + self._multi_maps = self.send("get_multi_maps_list")[0] + return self._multi_maps + + @command() + def multi_map_enum(self, skip_cache=False) -> enum.Enum: + """Enum of the available map names.""" + if self._multi_map_enum is not None and not skip_cache: + return self._multi_map_enum + + multi_maps = self.get_multi_maps() + maps_dict = {} for map in multi_maps["map_info"]: - multi_maps['map_names'].append(map["name"]) + maps_dict[map["name"]] = map["mapFlag"] - self._multi_maps = multi_maps - return self._multi_maps + self._multi_map_enum = enum.Enum("multi_map_enum", maps_dict) + return self._multi_map_enum @command(click.argument("multi_map_id", type=int)) def load_multi_map(self, multi_map_id: int): """Change the current map used.""" return self.send("load_multi_map", [multi_map_id])[0] == "ok" - @command(click.argument("multi_map_name", type=str)) - def load_multi_map_by_name(self, multi_map_name: str): - """Change the current map used by name.""" - multi_map_id = None - for map in self.get_multi_maps()["map_info"]: - if map["name"] == multi_map_name: - multi_map_id = map["mapFlag"] - break - - if multi_map_id is None: - return False - - return self.load_multi_map(multi_map_id) + @command() + def load_multi_map_by_enum(self, multi_map_enum): + """Change the current map used by enum.""" + return self.load_multi_map(multi_map_enum.value) @command(click.argument("start", type=bool)) def edit_map(self, start): diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index c846be455..c5063d781 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -87,9 +87,6 @@ def __init__(self, data: Dict[str, Any], multi_maps=None) -> None: # 'auto_dust_collection': 1, 'mop_mode': 300, 'debug_mode': 0}] self.data = data self._multi_maps = multi_maps - self._map_names = [] - if multi_maps is not None: - self._map_names = multi_maps["map_names"] @property @sensor("State code", enabled_default=False) @@ -175,12 +172,12 @@ def map(self) -> bool: return bool(self.data["map_present"]) @property + @setting("Multi map", choices_attribute="multi_map_enum", setter_name="load_multi_map_by_enum", icon="mdi:floor-plan") def multi_map_id(self) -> int: """The id of the current map with regards to the multi map feature, [3,7,11,15] -> [0,1,2,3].""" return int((self.data["map_status"]+1)/4 - 1) @property - @setting("Multi map name", choices=self._map_names, setter_name="load_multi_map_by_name", icon="mdi:floor-plan") def multi_map_name(self) -> str: """The name of the current map with regards to the multi map feature.""" if self._multi_maps is None: From b3ecd75a2c9ac77f8890de0bc8e1f4b5c0d6fd51 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 1 Oct 2022 17:27:17 +0200 Subject: [PATCH 22/60] Add Mop scrub intensity and mop route --- miio/integrations/vacuum/roborock/vacuum.py | 10 ++++++---- .../vacuum/roborock/vacuumcontainers.py | 13 +++++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/miio/integrations/vacuum/roborock/vacuum.py b/miio/integrations/vacuum/roborock/vacuum.py index 38ad652b3..0c091be7c 100644 --- a/miio/integrations/vacuum/roborock/vacuum.py +++ b/miio/integrations/vacuum/roborock/vacuum.py @@ -99,6 +99,7 @@ class FanspeedS7(FanspeedEnum): class FanspeedS7_Maxv(FanspeedEnum): + # Original names from the app: Quiet, Balanced, Turbo, Max, Max+ Silent = 101 Standard = 102 Medium = 103 @@ -116,16 +117,17 @@ class WaterFlow(enum.Enum): class MopMode(enum.Enum): - """Mop routing on S7.""" + """Mop routing on S7 + S7MAXV.""" Standard = 300 Deep = 301 + DeepPlus = 303 class MopIntensity(enum.Enum): """Mop scrub intensity on S7 + S7MAXV.""" - Close = 200 + Off = 200 Mild = 201 Moderate = 202 Intense = 203 @@ -1001,7 +1003,7 @@ def set_mop_mode(self, mop_mode: MopMode): @command() def mop_intensity(self) -> MopIntensity: """Get mop scrub intensity setting.""" - if self.model != ROCKROBO_S7: + if self.model not in [ROCKROBO_S7, ROCKROBO_S7_MAXV]: raise VacuumException("Mop scrub intensity not supported by %s", self.model) return MopIntensity(self.send("get_water_box_custom_mode")[0]) @@ -1009,7 +1011,7 @@ def mop_intensity(self) -> MopIntensity: @command(click.argument("mop_intensity", type=EnumType(MopIntensity))) def set_mop_intensity(self, mop_intensity: MopIntensity): """Set mop scrub intensity setting.""" - if self.model != ROCKROBO_S7: + if self.model not in [ROCKROBO_S7, ROCKROBO_S7_MAXV]: raise VacuumException("Mop scrub intensity not supported by %s", self.model) return self.send("set_water_box_custom_mode", [mop_intensity.value]) diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index c5063d781..888716cf5 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -9,6 +9,7 @@ from miio.devicestatus import sensor, setting from miio.utils import pretty_seconds, pretty_time +from .vacuum import MopIntensity, MopMode def pretty_area(x: float) -> float: return int(x) / 1000000 @@ -154,6 +155,18 @@ def fanspeed(self) -> int: """Current fan speed.""" return int(self.data["fan_power"]) + @property + @setting("Mop scrub intensity", choices=MopIntensity, setter_name="set_mop_intensity", icon="mdi:floor-plan") + def mop_intensity(self) -> int: + """Current mop intensity.""" + return int(self.data["water_box_mode"]) + + @property + @setting("Mop route", choices=MopMode, setter_name="set_mop_mode", icon="mdi:floor-plan") + def mop_route(self) -> int: + """Current mop route.""" + return int(self.data["mop_mode"]) + @property @sensor("Current clean duration", unit="s", icon="mdi:timer-sand") def clean_time(self) -> timedelta: From a1648466c00069a8de1f7e9d7d17a1082e290255 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 1 Oct 2022 17:39:04 +0200 Subject: [PATCH 23/60] fix circiler import --- miio/integrations/vacuum/roborock/vacuum.py | 126 +++--------------- .../vacuum/roborock/vacuum_enums.py | 110 +++++++++++++++ .../vacuum/roborock/vacuumcontainers.py | 2 +- 3 files changed, 128 insertions(+), 110 deletions(-) create mode 100644 miio/integrations/vacuum/roborock/vacuum_enums.py diff --git a/miio/integrations/vacuum/roborock/vacuum.py b/miio/integrations/vacuum/roborock/vacuum.py index 0c091be7c..b79d62e69 100644 --- a/miio/integrations/vacuum/roborock/vacuum.py +++ b/miio/integrations/vacuum/roborock/vacuum.py @@ -36,121 +36,29 @@ VacuumStatus, ) +from .vacuum_enums import ( + TimerState, + Consumable, + FanspeedEnum, + FanspeedV1, + FanspeedV2, + FanspeedV3, + FanspeedE2, + FanspeedS7, + FanspeedS7_Maxv, + WaterFlow, + MopMode, + MopIntensity, + CarpetCleaningMode, + DustCollectionMode, +) + _LOGGER = logging.getLogger(__name__) class VacuumException(DeviceException): pass - -class TimerState(enum.Enum): - On = "on" - Off = "off" - - -class Consumable(enum.Enum): - MainBrush = "main_brush_work_time" - SideBrush = "side_brush_work_time" - Filter = "filter_work_time" - SensorDirty = "sensor_dirty_time" - - -class FanspeedEnum(enum.Enum): - pass - - -class FanspeedV1(FanspeedEnum): - Silent = 38 - Standard = 60 - Medium = 77 - Turbo = 90 - - -class FanspeedV2(FanspeedEnum): - Silent = 101 - Standard = 102 - Medium = 103 - Turbo = 104 - Gentle = 105 - Auto = 106 - - -class FanspeedV3(FanspeedEnum): - Silent = 38 - Standard = 60 - Medium = 75 - Turbo = 100 - - -class FanspeedE2(FanspeedEnum): - # Original names from the app: Gentle, Silent, Standard, Strong, Max - Gentle = 41 - Silent = 50 - Standard = 68 - Medium = 79 - Turbo = 100 - - -class FanspeedS7(FanspeedEnum): - Silent = 101 - Standard = 102 - Medium = 103 - Turbo = 104 - - -class FanspeedS7_Maxv(FanspeedEnum): - # Original names from the app: Quiet, Balanced, Turbo, Max, Max+ - Silent = 101 - Standard = 102 - Medium = 103 - Turbo = 104 - Max = 108 - - -class WaterFlow(enum.Enum): - """Water flow strength on s5 max.""" - - Minimum = 200 - Low = 201 - High = 202 - Maximum = 203 - - -class MopMode(enum.Enum): - """Mop routing on S7 + S7MAXV.""" - - Standard = 300 - Deep = 301 - DeepPlus = 303 - - -class MopIntensity(enum.Enum): - """Mop scrub intensity on S7 + S7MAXV.""" - - Off = 200 - Mild = 201 - Moderate = 202 - Intense = 203 - - -class CarpetCleaningMode(enum.Enum): - """Type of carpet cleaning/avoidance.""" - - Avoid = 0 - Rise = 1 - Ignore = 2 - - -class DustCollectionMode(enum.Enum): - """Auto emptying mode (S7 + S7MAXV only)""" - - Smart = 0 - Quick = 1 - Daily = 2 - Strong = 3 - Max = 4 - - ROCKROBO_V1 = "rockrobo.vacuum.v1" ROCKROBO_S4 = "roborock.vacuum.s4" ROCKROBO_S4_MAX = "roborock.vacuum.a19" diff --git a/miio/integrations/vacuum/roborock/vacuum_enums.py b/miio/integrations/vacuum/roborock/vacuum_enums.py new file mode 100644 index 000000000..6f992a939 --- /dev/null +++ b/miio/integrations/vacuum/roborock/vacuum_enums.py @@ -0,0 +1,110 @@ +import enum + + +class TimerState(enum.Enum): + On = "on" + Off = "off" + + +class Consumable(enum.Enum): + MainBrush = "main_brush_work_time" + SideBrush = "side_brush_work_time" + Filter = "filter_work_time" + SensorDirty = "sensor_dirty_time" + + +class FanspeedEnum(enum.Enum): + pass + + +class FanspeedV1(FanspeedEnum): + Silent = 38 + Standard = 60 + Medium = 77 + Turbo = 90 + + +class FanspeedV2(FanspeedEnum): + Silent = 101 + Standard = 102 + Medium = 103 + Turbo = 104 + Gentle = 105 + Auto = 106 + + +class FanspeedV3(FanspeedEnum): + Silent = 38 + Standard = 60 + Medium = 75 + Turbo = 100 + + +class FanspeedE2(FanspeedEnum): + # Original names from the app: Gentle, Silent, Standard, Strong, Max + Gentle = 41 + Silent = 50 + Standard = 68 + Medium = 79 + Turbo = 100 + + +class FanspeedS7(FanspeedEnum): + Silent = 101 + Standard = 102 + Medium = 103 + Turbo = 104 + + +class FanspeedS7_Maxv(FanspeedEnum): + # Original names from the app: Quiet, Balanced, Turbo, Max, Max+ + Silent = 101 + Standard = 102 + Medium = 103 + Turbo = 104 + Mopping = 105 + Max = 108 + + +class WaterFlow(enum.Enum): + """Water flow strength on s5 max.""" + + Minimum = 200 + Low = 201 + High = 202 + Maximum = 203 + + +class MopMode(enum.Enum): + """Mop routing on S7 + S7MAXV.""" + + Standard = 300 + Deep = 301 + DeepPlus = 303 + + +class MopIntensity(enum.Enum): + """Mop scrub intensity on S7 + S7MAXV.""" + + Off = 200 + Mild = 201 + Moderate = 202 + Intense = 203 + + +class CarpetCleaningMode(enum.Enum): + """Type of carpet cleaning/avoidance.""" + + Avoid = 0 + Rise = 1 + Ignore = 2 + + +class DustCollectionMode(enum.Enum): + """Auto emptying mode (S7 + S7MAXV only)""" + + Smart = 0 + Quick = 1 + Daily = 2 + Strong = 3 + Max = 4 diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index 888716cf5..e5c2450d3 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -9,7 +9,7 @@ from miio.devicestatus import sensor, setting from miio.utils import pretty_seconds, pretty_time -from .vacuum import MopIntensity, MopMode +from .vacuum_enums import MopIntensity, MopMode def pretty_area(x: float) -> float: return int(x) / 1000000 From 0c0a6178d8589a25e81ad613f83b6b8afb1d0e21 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 1 Oct 2022 18:11:35 +0200 Subject: [PATCH 24/60] update icons --- miio/integrations/vacuum/roborock/vacuumcontainers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index e5c2450d3..feb46fdec 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -156,13 +156,13 @@ def fanspeed(self) -> int: return int(self.data["fan_power"]) @property - @setting("Mop scrub intensity", choices=MopIntensity, setter_name="set_mop_intensity", icon="mdi:floor-plan") + @setting("Mop scrub intensity", choices=MopIntensity, setter_name="set_mop_intensity", icon="mdi:checkbox-multiple-blank-circle-outline") def mop_intensity(self) -> int: """Current mop intensity.""" return int(self.data["water_box_mode"]) @property - @setting("Mop route", choices=MopMode, setter_name="set_mop_mode", icon="mdi:floor-plan") + @setting("Mop route", choices=MopMode, setter_name="set_mop_mode", icon="mdi:swap-horizontal-variant") def mop_route(self) -> int: """Current mop route.""" return int(self.data["mop_mode"]) From 68a4fb467b44008adb26a5668c8a36f73a18d9db Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 1 Oct 2022 18:49:54 +0200 Subject: [PATCH 25/60] Add auto_dust_collection --- miio/integrations/vacuum/roborock/vacuumcontainers.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index feb46fdec..8faf06db9 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -6,7 +6,7 @@ from pytz import BaseTzInfo from miio.device import DeviceStatus -from miio.devicestatus import sensor, setting +from miio.devicestatus import sensor, setting, switch from miio.utils import pretty_seconds, pretty_time from .vacuum_enums import MopIntensity, MopMode @@ -249,6 +249,14 @@ def is_water_shortage(self) -> Optional[bool]: return self.data["water_shortage_status"] == 1 return None + @property + @switch("Auto dust collection", setter_name="set_dust_collection", icon="mdi:turbine") + def auto_dust_collection(self) -> Optional[bool]: + """Returns True if auto dust collection is enabled, None if sensor not present.""" + if "auto_dust_collection" in self.data: + return self.data["auto_dust_collection"] == 1 + return None + @property @sensor("Error", icon="mdi:alert", enabled_default=False) def got_error(self) -> bool: From 00dfae927f2316a8142bfeda1ae64aeed4a1efa6 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 2 Oct 2022 19:08:07 +0200 Subject: [PATCH 26/60] Last clean per floor --- miio/integrations/vacuum/roborock/vacuum.py | 49 +++++++++++++++++- .../vacuum/roborock/vacuumcontainers.py | 51 +++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/miio/integrations/vacuum/roborock/vacuum.py b/miio/integrations/vacuum/roborock/vacuum.py index b79d62e69..16b163c3b 100644 --- a/miio/integrations/vacuum/roborock/vacuum.py +++ b/miio/integrations/vacuum/roborock/vacuum.py @@ -29,6 +29,7 @@ CleaningDetails, CleaningSummary, ConsumableStatus, + FloorCleanDetails, DNDStatus, SoundInstallStatus, SoundStatus, @@ -126,6 +127,8 @@ def __init__( self.manual_seqnum = -1 self._multi_maps = None self._multi_map_enum = None + self._floor_clean_details = {} + self._searched_clean_id = None @command() def start(self): @@ -319,7 +322,9 @@ def status(self) -> VacuumStatus: status.embed(self.consumable_status()) clean_history = self.clean_history() status.embed(clean_history) - status.embed(self.last_clean_details(history=clean_history)) + clean_details = self.last_clean_all_floor(history=clean_history) + status.embed(clean_details["last"]) + status.embed(FloorCleanDetails(clean_details)) status.embed(self.dnd_status()) return status @@ -485,6 +490,48 @@ def last_clean_details(self, history: Optional[CleaningSummary] = None) -> Optio last_clean_id = history.ids[0] return self.clean_details(last_clean_id) + @command() + def last_clean_all_floor(self, history: Optional[CleaningSummary] = None) -> dict[str, 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() + + N_maps = self.get_multi_maps()["multi_map_count"] + map_ids = list(range(0,N_maps)) + + # if cache empty, fill with None + if not self._floor_clean_details: + self._floor_clean_details["last"] = None + for id in map_ids: + self._floor_clean_details[str(id)] = None + + if not history.ids: + return self._floor_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._floor_clean_details["last"] = clean_detail + + # all floors found + if not map_ids: + break + + self._searched_clean_id = last_clean_id + return self._floor_clean_details + @command( click.argument("id_", type=int, metavar="ID"), ) diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index 8faf06db9..7aa963746 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -399,6 +399,57 @@ 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 + + @property + @sensor("Floor 0 clean start", icon="mdi:clock-time-twelve") + def start_0(self) -> datetime: + """When cleaning was started.""" + if "0" not in self.data: + return None + if self.data["0"] is None: + return None + + return pretty_time(self.data["0"].start) + + @property + @sensor("Floor 1 clean start", icon="mdi:clock-time-twelve") + def start_1(self) -> datetime: + """When cleaning was started.""" + if "1" not in self.data: + return None + if self.data["1"] is None: + return None + + return pretty_time(self.data["1"].start) + + @property + @sensor("Floor 2 clean start", icon="mdi:clock-time-twelve") + def start_2(self) -> datetime: + """When cleaning was started.""" + if "1" not in self.data: + return None + if self.data["1"] is None: + return None + + return pretty_time(self.data["1"].start) + + @property + @sensor("Floor 3 clean start", icon="mdi:clock-time-twelve") + def start_3(self) -> datetime: + """When cleaning was started.""" + if "1" not in self.data: + return None + if self.data["1"] is None: + return None + + return pretty_time(self.data["1"].start) + + 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 From 4d9ca0ef6eb10cee5a2421d2cd6748c9d5ef0785 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 2 Oct 2022 19:36:29 +0200 Subject: [PATCH 27/60] fixes --- .../vacuum/roborock/vacuumcontainers.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index 7aa963746..ce36e2fdd 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -414,7 +414,7 @@ def start_0(self) -> datetime: if self.data["0"] is None: return None - return pretty_time(self.data["0"].start) + return self.data["0"].start @property @sensor("Floor 1 clean start", icon="mdi:clock-time-twelve") @@ -425,29 +425,29 @@ def start_1(self) -> datetime: if self.data["1"] is None: return None - return pretty_time(self.data["1"].start) + return self.data["1"].start @property @sensor("Floor 2 clean start", icon="mdi:clock-time-twelve") def start_2(self) -> datetime: """When cleaning was started.""" - if "1" not in self.data: + if "2" not in self.data: return None - if self.data["1"] is None: + if self.data["2"] is None: return None - return pretty_time(self.data["1"].start) + return self.data["2"].start @property @sensor("Floor 3 clean start", icon="mdi:clock-time-twelve") def start_3(self) -> datetime: """When cleaning was started.""" - if "1" not in self.data: + if "3" not in self.data: return None - if self.data["1"] is None: + if self.data["3"] is None: return None - return pretty_time(self.data["1"].start) + return self.data["3"].start class ConsumableStatus(DeviceStatus): From 6c8bb0eb3cde1868db278a680e0516817e624dcc Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 2 Oct 2022 20:08:21 +0200 Subject: [PATCH 28/60] add entity_categories --- .../vacuum/roborock/vacuumcontainers.py | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index ce36e2fdd..10c3c13a7 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -90,13 +90,13 @@ def __init__(self, data: Dict[str, Any], multi_maps=None) -> None: self._multi_maps = multi_maps @property - @sensor("State code", enabled_default=False) + @sensor("State code", entity_category="diagnostic", enabled_default=False) def state_code(self) -> int: """State code as returned by the device.""" return int(self.data["state"]) @property - @sensor("State") + @sensor("State", entity_category="diagnostic") def state(self) -> str: """Human readable state description, see also :func:`state_code`.""" states = { @@ -130,13 +130,13 @@ def state(self) -> str: return "Definition missing for state %s" % self.state_code @property - @sensor("Error code", icon="mdi:alert", enabled_default=False) + @sensor("Error code", icon="mdi:alert", entity_category="diagnostic", enabled_default=False) def error_code(self) -> int: """Error code as returned by the device.""" return int(self.data["error_code"]) @property - @sensor("Error string", icon="mdi:alert", enabled_default=False) + @sensor("Error string", icon="mdi:alert", entity_category="diagnostic", enabled_default=False) def error(self) -> str: """Human readable error description, see also :func:`error_code`.""" try: @@ -250,7 +250,7 @@ def is_water_shortage(self) -> Optional[bool]: return None @property - @switch("Auto dust collection", setter_name="set_dust_collection", icon="mdi:turbine") + @switch("Auto dust collection", setter_name="set_dust_collection", icon="mdi:turbine", entity_category="config") def auto_dust_collection(self) -> Optional[bool]: """Returns True if auto dust collection is enabled, None if sensor not present.""" if "auto_dust_collection" in self.data: @@ -258,7 +258,7 @@ def auto_dust_collection(self) -> Optional[bool]: return None @property - @sensor("Error", icon="mdi:alert", enabled_default=False) + @sensor("Error", icon="mdi:alert", entity_category="diagnostic", enabled_default=False) def got_error(self) -> bool: """True if an error has occurred.""" return self.error_code != 0 @@ -290,19 +290,19 @@ def __init__(self, data: Union[List[Any], Dict[str, Any]]) -> None: self.data["records"] = [] @property - @sensor("Total clean duration", unit="s", icon="mdi:timer-sand") + @sensor("Total clean duration", unit="s", icon="mdi:timer-sand", entity_category="diagnostic") def total_duration(self) -> timedelta: """Total cleaning duration.""" return pretty_seconds(self.data["clean_time"]) @property - @sensor("Total clean area", unit="m²", icon="mdi:texture-box") + @sensor("Total clean area", unit="m²", icon="mdi:texture-box", entity_category="diagnostic") def total_area(self) -> float: """Total cleaned area.""" return pretty_area(self.data["clean_area"]) @property - @sensor("Total clean count", icon="mdi:counter") + @sensor("Total clean count", icon="mdi:counter", entity_category="diagnostic") def count(self) -> int: """Number of cleaning runs.""" return int(self.data["clean_count"]) @@ -313,7 +313,7 @@ def ids(self) -> List[int]: return list(self.data["records"]) @property - @sensor("Total_dust collection count", icon="mdi:counter") + @sensor("Total dust collection count", icon="mdi:counter", entity_category="diagnostic") def dust_collection_count(self) -> Optional[int]: """Total number of dust collections.""" if "dust_collection_count" in self.data: @@ -343,25 +343,25 @@ def __init__(self, data: Union[List[Any], Dict[str, Any]], multi_maps=None) -> N self.data = data @property - @sensor("Last clean start", icon="mdi:clock-time-twelve") + @sensor("Last clean start", icon="mdi:clock-time-twelve", entity_category="diagnostic") def start(self) -> datetime: """When cleaning was started.""" return pretty_time(self.data["begin"]) @property - @sensor("Last clean end", icon="mdi:clock-time-twelve") + @sensor("Last clean end", icon="mdi:clock-time-twelve", entity_category="diagnostic") def end(self) -> datetime: """When cleaning was finished.""" return pretty_time(self.data["end"]) @property - @sensor("Last clean duration", unit="s", icon="mdi:timer-sand") + @sensor("Last clean duration", unit="s", icon="mdi:timer-sand", entity_category="diagnostic") def duration(self) -> timedelta: """Total duration of the cleaning run.""" return pretty_seconds(self.data["duration"]) @property - @sensor("Last clean area", unit="m²", icon="mdi:texture-box") + @sensor("Last clean area", unit="m²", icon="mdi:texture-box", entity_category="diagnostic") def area(self) -> float: """Total cleaned area.""" return pretty_area(self.data["area"]) @@ -372,7 +372,7 @@ def multi_map_id(self) -> int: return self.data["map_flag"] @property - @sensor("Last clean map name", icon="mdi:floor-plan") + @sensor("Last clean map name", icon="mdi:floor-plan", entity_category="diagnostic") def multi_map_name(self) -> str: """The name of the map used (multi map feature) during the cleaning run.""" if self._multi_maps is None: @@ -406,7 +406,7 @@ def __init__(self, data: Dict[str, Any]) -> None: self.data = data @property - @sensor("Floor 0 clean start", icon="mdi:clock-time-twelve") + @sensor("Floor 0 clean start", icon="mdi:clock-time-twelve", entity_category="diagnostic") def start_0(self) -> datetime: """When cleaning was started.""" if "0" not in self.data: @@ -417,7 +417,7 @@ def start_0(self) -> datetime: return self.data["0"].start @property - @sensor("Floor 1 clean start", icon="mdi:clock-time-twelve") + @sensor("Floor 1 clean start", icon="mdi:clock-time-twelve", entity_category="diagnostic") def start_1(self) -> datetime: """When cleaning was started.""" if "1" not in self.data: @@ -428,7 +428,7 @@ def start_1(self) -> datetime: return self.data["1"].start @property - @sensor("Floor 2 clean start", icon="mdi:clock-time-twelve") + @sensor("Floor 2 clean start", icon="mdi:clock-time-twelve", entity_category="diagnostic") def start_2(self) -> datetime: """When cleaning was started.""" if "2" not in self.data: @@ -439,7 +439,7 @@ def start_2(self) -> datetime: return self.data["2"].start @property - @sensor("Floor 3 clean start", icon="mdi:clock-time-twelve") + @sensor("Floor 3 clean start", icon="mdi:clock-time-twelve", entity_category="diagnostic") def start_3(self) -> datetime: """When cleaning was started.""" if "3" not in self.data: @@ -474,54 +474,54 @@ def __init__(self, data: Dict[str, Any]) -> None: self.sensor_dirty_total = timedelta(hours=30) @property - @sensor("Main brush used", unit="s", icon="mdi:brush", enabled_default=False) + @sensor("Main brush used", unit="s", icon="mdi:brush", entity_category="diagnostic", enabled_default=False) def main_brush(self) -> timedelta: """Main brush usage time.""" return pretty_seconds(self.data["main_brush_work_time"]) @property - @sensor("Main brush left", unit="s", icon="mdi:brush") + @sensor("Main brush left", unit="s", icon="mdi:brush", entity_category="diagnostic") def main_brush_left(self) -> timedelta: """How long until the main brush should be changed.""" return self.main_brush_total - self.main_brush @property - @sensor("Side brush used", unit="s", icon="mdi:brush", enabled_default=False) + @sensor("Side brush used", unit="s", icon="mdi:brush", entity_category="diagnostic", enabled_default=False) def side_brush(self) -> timedelta: """Side brush usage time.""" return pretty_seconds(self.data["side_brush_work_time"]) @property - @sensor("Side brush left", unit="s", icon="mdi:brush") + @sensor("Side brush left", unit="s", icon="mdi:brush", entity_category="diagnostic") def side_brush_left(self) -> timedelta: """How long until the side brush should be changed.""" return self.side_brush_total - self.side_brush @property - @sensor("Filter used", unit="s", icon="mdi:air-filter", enabled_default=False) + @sensor("Filter used", unit="s", icon="mdi:air-filter", entity_category="diagnostic", enabled_default=False) def filter(self) -> timedelta: """Filter usage time.""" return pretty_seconds(self.data["filter_work_time"]) @property - @sensor("Filter left", unit="s", icon="mdi:air-filter") + @sensor("Filter left", unit="s", icon="mdi:air-filter", entity_category="diagnostic") def filter_left(self) -> timedelta: """How long until the filter should be changed.""" return self.filter_total - self.filter @property - @sensor("Sensor dirty used", unit="s", icon="mdi:eye-outline", enabled_default=False) + @sensor("Sensor dirty used", unit="s", icon="mdi:eye-outline", entity_category="diagnostic", enabled_default=False) def sensor_dirty(self) -> timedelta: """Return ``sensor_dirty_time``""" return pretty_seconds(self.data["sensor_dirty_time"]) @property - @sensor("Sensor dirty left", unit="s", icon="mdi:eye-outline") + @sensor("Sensor dirty left", unit="s", icon="mdi:eye-outline", entity_category="diagnostic") def sensor_dirty_left(self) -> timedelta: return self.sensor_dirty_total - self.sensor_dirty @property - @sensor("Dustbin times auto-empty used", icon="mdi:delete", enabled_default=False) + @sensor("Dustbin times auto-empty used", icon="mdi:delete", entity_category="diagnostic", enabled_default=False) def dustbin_auto_empty_used(self) -> int: """Return ``dust_collection_work_times``""" return self.data["dust_collection_work_times"] @@ -536,19 +536,19 @@ def __init__(self, data: Dict[str, Any]): self.data = data @property - @sensor("Do not disturb", icon="mdi:minus-circle-off") + @sensor("Do not disturb", icon="mdi:minus-circle-off", entity_category="diagnostic") def enabled(self) -> bool: """True if DnD is enabled.""" return bool(self.data["enabled"]) @property - @sensor("Do not disturb start", icon="mdi:minus-circle-off", enabled_default=False) + @sensor("Do not disturb start", icon="mdi:minus-circle-off", entity_category="diagnostic", enabled_default=False) def start(self) -> time: """Start time of DnD.""" return time(hour=self.data["start_hour"], minute=self.data["start_minute"]) @property - @sensor("Do not disturb end", icon="mdi:minus-circle-off", enabled_default=False) + @sensor("Do not disturb end", icon="mdi:minus-circle-off", entity_category="diagnostic", enabled_default=False) def end(self) -> time: """End time of DnD.""" return time(hour=self.data["end_hour"], minute=self.data["end_minute"]) From e490cd4faa54536e42234f30a7e3c4afdf911418 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 2 Oct 2022 20:30:54 +0200 Subject: [PATCH 29/60] add device_classes --- .../vacuum/roborock/vacuumcontainers.py | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index 10c3c13a7..1f5d8c4a6 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -168,7 +168,7 @@ def mop_route(self) -> int: return int(self.data["mop_mode"]) @property - @sensor("Current clean duration", unit="s", icon="mdi:timer-sand") + @sensor("Current clean duration", unit="s", icon="mdi:timer-sand", device_class="duration") def clean_time(self) -> timedelta: """Time used for cleaning (if finished, shows how long it took).""" return pretty_seconds(self.data["clean_time"]) @@ -290,7 +290,7 @@ def __init__(self, data: Union[List[Any], Dict[str, Any]]) -> None: self.data["records"] = [] @property - @sensor("Total clean duration", unit="s", icon="mdi:timer-sand", entity_category="diagnostic") + @sensor("Total clean duration", unit="s", icon="mdi:timer-sand", device_class="duration", entity_category="diagnostic") def total_duration(self) -> timedelta: """Total cleaning duration.""" return pretty_seconds(self.data["clean_time"]) @@ -343,19 +343,19 @@ def __init__(self, data: Union[List[Any], Dict[str, Any]], multi_maps=None) -> N self.data = data @property - @sensor("Last clean start", icon="mdi:clock-time-twelve", entity_category="diagnostic") + @sensor("Last clean start", icon="mdi:clock-time-twelve", device_class="timestamp", entity_category="diagnostic") def start(self) -> datetime: """When cleaning was started.""" return pretty_time(self.data["begin"]) @property - @sensor("Last clean end", icon="mdi:clock-time-twelve", entity_category="diagnostic") + @sensor("Last clean end", icon="mdi:clock-time-twelve", device_class="timestamp", entity_category="diagnostic") def end(self) -> datetime: """When cleaning was finished.""" return pretty_time(self.data["end"]) @property - @sensor("Last clean duration", unit="s", icon="mdi:timer-sand", entity_category="diagnostic") + @sensor("Last clean duration", unit="s", icon="mdi:timer-sand", device_class="duration", entity_category="diagnostic") def duration(self) -> timedelta: """Total duration of the cleaning run.""" return pretty_seconds(self.data["duration"]) @@ -406,7 +406,7 @@ def __init__(self, data: Dict[str, Any]) -> None: self.data = data @property - @sensor("Floor 0 clean start", icon="mdi:clock-time-twelve", entity_category="diagnostic") + @sensor("Floor 0 clean start", icon="mdi:clock-time-twelve", device_class="timestamp", entity_category="diagnostic") def start_0(self) -> datetime: """When cleaning was started.""" if "0" not in self.data: @@ -417,7 +417,7 @@ def start_0(self) -> datetime: return self.data["0"].start @property - @sensor("Floor 1 clean start", icon="mdi:clock-time-twelve", entity_category="diagnostic") + @sensor("Floor 1 clean start", icon="mdi:clock-time-twelve", device_class="timestamp", entity_category="diagnostic") def start_1(self) -> datetime: """When cleaning was started.""" if "1" not in self.data: @@ -428,7 +428,7 @@ def start_1(self) -> datetime: return self.data["1"].start @property - @sensor("Floor 2 clean start", icon="mdi:clock-time-twelve", entity_category="diagnostic") + @sensor("Floor 2 clean start", icon="mdi:clock-time-twelve", device_class="timestamp", entity_category="diagnostic") def start_2(self) -> datetime: """When cleaning was started.""" if "2" not in self.data: @@ -439,7 +439,7 @@ def start_2(self) -> datetime: return self.data["2"].start @property - @sensor("Floor 3 clean start", icon="mdi:clock-time-twelve", entity_category="diagnostic") + @sensor("Floor 3 clean start", icon="mdi:clock-time-twelve", device_class="timestamp", entity_category="diagnostic") def start_3(self) -> datetime: """When cleaning was started.""" if "3" not in self.data: @@ -474,49 +474,49 @@ def __init__(self, data: Dict[str, Any]) -> None: self.sensor_dirty_total = timedelta(hours=30) @property - @sensor("Main brush used", unit="s", icon="mdi:brush", entity_category="diagnostic", enabled_default=False) + @sensor("Main brush used", unit="s", icon="mdi:brush", device_class="duration", entity_category="diagnostic", enabled_default=False) def main_brush(self) -> timedelta: """Main brush usage time.""" return pretty_seconds(self.data["main_brush_work_time"]) @property - @sensor("Main brush left", unit="s", icon="mdi:brush", entity_category="diagnostic") + @sensor("Main brush left", unit="s", icon="mdi:brush", device_class="duration", entity_category="diagnostic") def main_brush_left(self) -> timedelta: """How long until the main brush should be changed.""" return self.main_brush_total - self.main_brush @property - @sensor("Side brush used", unit="s", icon="mdi:brush", entity_category="diagnostic", enabled_default=False) + @sensor("Side brush used", unit="s", icon="mdi:brush", device_class="duration", entity_category="diagnostic", enabled_default=False) def side_brush(self) -> timedelta: """Side brush usage time.""" return pretty_seconds(self.data["side_brush_work_time"]) @property - @sensor("Side brush left", unit="s", icon="mdi:brush", entity_category="diagnostic") + @sensor("Side brush left", unit="s", icon="mdi:brush", device_class="duration", entity_category="diagnostic") def side_brush_left(self) -> timedelta: """How long until the side brush should be changed.""" return self.side_brush_total - self.side_brush @property - @sensor("Filter used", unit="s", icon="mdi:air-filter", entity_category="diagnostic", enabled_default=False) + @sensor("Filter used", unit="s", icon="mdi:air-filter", device_class="duration", entity_category="diagnostic", enabled_default=False) def filter(self) -> timedelta: """Filter usage time.""" return pretty_seconds(self.data["filter_work_time"]) @property - @sensor("Filter left", unit="s", icon="mdi:air-filter", entity_category="diagnostic") + @sensor("Filter left", unit="s", icon="mdi:air-filter", device_class="duration", entity_category="diagnostic") def filter_left(self) -> timedelta: """How long until the filter should be changed.""" return self.filter_total - self.filter @property - @sensor("Sensor dirty used", unit="s", icon="mdi:eye-outline", entity_category="diagnostic", enabled_default=False) + @sensor("Sensor dirty used", unit="s", icon="mdi:eye-outline", device_class="duration", entity_category="diagnostic", enabled_default=False) def sensor_dirty(self) -> timedelta: """Return ``sensor_dirty_time``""" return pretty_seconds(self.data["sensor_dirty_time"]) @property - @sensor("Sensor dirty left", unit="s", icon="mdi:eye-outline", entity_category="diagnostic") + @sensor("Sensor dirty left", unit="s", icon="mdi:eye-outline", device_class="duration", entity_category="diagnostic") def sensor_dirty_left(self) -> timedelta: return self.sensor_dirty_total - self.sensor_dirty @@ -542,13 +542,13 @@ def enabled(self) -> bool: return bool(self.data["enabled"]) @property - @sensor("Do not disturb start", icon="mdi:minus-circle-off", entity_category="diagnostic", enabled_default=False) + @sensor("Do not disturb start", icon="mdi:minus-circle-off", device_class="timestamp", entity_category="diagnostic", enabled_default=False) def start(self) -> time: """Start time of DnD.""" return time(hour=self.data["start_hour"], minute=self.data["start_minute"]) @property - @sensor("Do not disturb end", icon="mdi:minus-circle-off", entity_category="diagnostic", enabled_default=False) + @sensor("Do not disturb end", icon="mdi:minus-circle-off", device_class="timestamp", entity_category="diagnostic", enabled_default=False) def end(self) -> time: """End time of DnD.""" return time(hour=self.data["end_hour"], minute=self.data["end_minute"]) From 298a485fbb6100324a4058a4a01c44f94229e394 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sun, 2 Oct 2022 20:34:54 +0200 Subject: [PATCH 30/60] add state_class --- miio/integrations/vacuum/roborock/vacuumcontainers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index 1f5d8c4a6..b92d74d5b 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -302,7 +302,7 @@ def total_area(self) -> float: return pretty_area(self.data["clean_area"]) @property - @sensor("Total clean count", icon="mdi:counter", entity_category="diagnostic") + @sensor("Total clean count", icon="mdi:counter", state_class="total_increasing", entity_category="diagnostic") def count(self) -> int: """Number of cleaning runs.""" return int(self.data["clean_count"]) @@ -313,7 +313,7 @@ def ids(self) -> List[int]: return list(self.data["records"]) @property - @sensor("Total dust collection count", icon="mdi:counter", entity_category="diagnostic") + @sensor("Total dust collection count", icon="mdi:counter", state_class="total_increasing", entity_category="diagnostic") def dust_collection_count(self) -> Optional[int]: """Total number of dust collections.""" if "dust_collection_count" in self.data: From 5fb7f9e16d342629f78ca2e1671eb607c3a4a1c5 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 3 Oct 2022 17:55:58 +0200 Subject: [PATCH 31/60] fix styling --- miio/device.py | 4 +- miio/devicestatus.py | 8 +- miio/integrations/vacuum/roborock/vacuum.py | 48 ++-- .../vacuum/roborock/vacuumcontainers.py | 233 +++++++++++++++--- 4 files changed, 234 insertions(+), 59 deletions(-) diff --git a/miio/device.py b/miio/device.py index 503fe7578..951d68000 100644 --- a/miio/device.py +++ b/miio/device.py @@ -244,7 +244,9 @@ def buttons(self) -> List[ButtonDescriptor]: def settings(self) -> Dict[str, SettingDescriptor]: """Return list of settings.""" - settings = self.status().settings() #NOTE that this already does IO so schould be run in executer job in HA + settings = ( + self.status().settings() + ) # NOTE that this already does IO so schould be run in executer job in HA for setting in settings.values(): # TODO: Bind setter methods, this should probably done only once during init. if setting.setter is None: diff --git a/miio/devicestatus.py b/miio/devicestatus.py index 0076bba18..10d64f430 100644 --- a/miio/devicestatus.py +++ b/miio/devicestatus.py @@ -257,10 +257,10 @@ def decorator_setting(func): 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") + # 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=qualified_name, property=property_name, diff --git a/miio/integrations/vacuum/roborock/vacuum.py b/miio/integrations/vacuum/roborock/vacuum.py index 16b163c3b..a9e6ae696 100644 --- a/miio/integrations/vacuum/roborock/vacuum.py +++ b/miio/integrations/vacuum/roborock/vacuum.py @@ -24,42 +24,42 @@ from miio.exceptions import DeviceException, DeviceInfoUnavailableException from miio.interfaces import FanspeedPresets, VacuumInterface +from .vacuum_enums import ( + CarpetCleaningMode, + Consumable, + DustCollectionMode, + FanspeedE2, + FanspeedEnum, + FanspeedS7, + FanspeedS7_Maxv, + FanspeedV1, + FanspeedV2, + FanspeedV3, + MopIntensity, + MopMode, + TimerState, + WaterFlow, +) from .vacuumcontainers import ( CarpetModeStatus, CleaningDetails, CleaningSummary, ConsumableStatus, - FloorCleanDetails, DNDStatus, + FloorCleanDetails, SoundInstallStatus, SoundStatus, Timer, VacuumStatus, ) -from .vacuum_enums import ( - TimerState, - Consumable, - FanspeedEnum, - FanspeedV1, - FanspeedV2, - FanspeedV3, - FanspeedE2, - FanspeedS7, - FanspeedS7_Maxv, - WaterFlow, - MopMode, - MopIntensity, - CarpetCleaningMode, - DustCollectionMode, -) - _LOGGER = logging.getLogger(__name__) class VacuumException(DeviceException): pass + ROCKROBO_V1 = "rockrobo.vacuum.v1" ROCKROBO_S4 = "roborock.vacuum.s4" ROCKROBO_S4_MAX = "roborock.vacuum.a19" @@ -357,7 +357,7 @@ def map(self): def get_multi_maps(self, skip_cache=False): """Return list of 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': 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}]} # ]} @@ -477,7 +477,9 @@ def clean_history(self) -> CleaningSummary: return CleaningSummary(self.send("get_clean_summary")) @command() - def last_clean_details(self, history: Optional[CleaningSummary] = None) -> 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. @@ -491,7 +493,9 @@ def last_clean_details(self, history: Optional[CleaningSummary] = None) -> Optio return self.clean_details(last_clean_id) @command() - def last_clean_all_floor(self, history: Optional[CleaningSummary] = None) -> dict[str, Optional[CleaningDetails]]: + def last_clean_all_floor( + self, history: Optional[CleaningSummary] = None + ) -> dict[str, Optional[CleaningDetails]]: """Return details from the last cleaning and for each floor. Returns None if there has been no cleanups for that floor. @@ -500,7 +504,7 @@ def last_clean_all_floor(self, history: Optional[CleaningSummary] = None) -> dic history = self.clean_history() N_maps = self.get_multi_maps()["multi_map_count"] - map_ids = list(range(0,N_maps)) + map_ids = list(range(0, N_maps)) # if cache empty, fill with None if not self._floor_clean_details: diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index b92d74d5b..bdd7d5c03 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -11,6 +11,7 @@ from .vacuum_enums import MopIntensity, MopMode + def pretty_area(x: float) -> float: return int(x) / 1000000 @@ -130,13 +131,23 @@ def state(self) -> str: return "Definition missing for state %s" % self.state_code @property - @sensor("Error code", icon="mdi:alert", entity_category="diagnostic", enabled_default=False) + @sensor( + "Error code", + icon="mdi:alert", + entity_category="diagnostic", + enabled_default=False, + ) def error_code(self) -> int: """Error code as returned by the device.""" return int(self.data["error_code"]) @property - @sensor("Error string", icon="mdi:alert", entity_category="diagnostic", enabled_default=False) + @sensor( + "Error string", + icon="mdi:alert", + entity_category="diagnostic", + enabled_default=False, + ) def error(self) -> str: """Human readable error description, see also :func:`error_code`.""" try: @@ -156,19 +167,34 @@ def fanspeed(self) -> int: return int(self.data["fan_power"]) @property - @setting("Mop scrub intensity", choices=MopIntensity, setter_name="set_mop_intensity", icon="mdi:checkbox-multiple-blank-circle-outline") + @setting( + "Mop scrub intensity", + choices=MopIntensity, + setter_name="set_mop_intensity", + icon="mdi:checkbox-multiple-blank-circle-outline", + ) def mop_intensity(self) -> int: """Current mop intensity.""" return int(self.data["water_box_mode"]) @property - @setting("Mop route", choices=MopMode, setter_name="set_mop_mode", icon="mdi:swap-horizontal-variant") + @setting( + "Mop route", + choices=MopMode, + setter_name="set_mop_mode", + icon="mdi:swap-horizontal-variant", + ) def mop_route(self) -> int: """Current mop route.""" return int(self.data["mop_mode"]) @property - @sensor("Current clean duration", unit="s", icon="mdi:timer-sand", device_class="duration") + @sensor( + "Current clean duration", + unit="s", + icon="mdi:timer-sand", + device_class="duration", + ) def clean_time(self) -> timedelta: """Time used for cleaning (if finished, shows how long it took).""" return pretty_seconds(self.data["clean_time"]) @@ -185,10 +211,15 @@ def map(self) -> bool: return bool(self.data["map_present"]) @property - @setting("Multi map", choices_attribute="multi_map_enum", setter_name="load_multi_map_by_enum", icon="mdi:floor-plan") + @setting( + "Multi map", + choices_attribute="multi_map_enum", + setter_name="load_multi_map_by_enum", + icon="mdi:floor-plan", + ) def multi_map_id(self) -> int: """The id of the current map with regards to the multi map feature, [3,7,11,15] -> [0,1,2,3].""" - return int((self.data["map_status"]+1)/4 - 1) + return int((self.data["map_status"] + 1) / 4 - 1) @property def multi_map_name(self) -> str: @@ -250,7 +281,12 @@ def is_water_shortage(self) -> Optional[bool]: return None @property - @switch("Auto dust collection", setter_name="set_dust_collection", icon="mdi:turbine", entity_category="config") + @switch( + "Auto dust collection", + setter_name="set_dust_collection", + icon="mdi:turbine", + entity_category="config", + ) def auto_dust_collection(self) -> Optional[bool]: """Returns True if auto dust collection is enabled, None if sensor not present.""" if "auto_dust_collection" in self.data: @@ -258,7 +294,9 @@ def auto_dust_collection(self) -> Optional[bool]: return None @property - @sensor("Error", icon="mdi:alert", entity_category="diagnostic", enabled_default=False) + @sensor( + "Error", icon="mdi:alert", entity_category="diagnostic", enabled_default=False + ) def got_error(self) -> bool: """True if an error has occurred.""" return self.error_code != 0 @@ -290,19 +328,35 @@ def __init__(self, data: Union[List[Any], Dict[str, Any]]) -> None: self.data["records"] = [] @property - @sensor("Total clean duration", unit="s", icon="mdi:timer-sand", device_class="duration", entity_category="diagnostic") + @sensor( + "Total clean duration", + unit="s", + icon="mdi:timer-sand", + device_class="duration", + entity_category="diagnostic", + ) def total_duration(self) -> timedelta: """Total cleaning duration.""" return pretty_seconds(self.data["clean_time"]) @property - @sensor("Total clean area", unit="m²", icon="mdi:texture-box", entity_category="diagnostic") + @sensor( + "Total clean area", + unit="m²", + icon="mdi:texture-box", + entity_category="diagnostic", + ) def total_area(self) -> float: """Total cleaned area.""" return pretty_area(self.data["clean_area"]) @property - @sensor("Total clean count", icon="mdi:counter", state_class="total_increasing", entity_category="diagnostic") + @sensor( + "Total clean count", + icon="mdi:counter", + state_class="total_increasing", + entity_category="diagnostic", + ) def count(self) -> int: """Number of cleaning runs.""" return int(self.data["clean_count"]) @@ -313,7 +367,12 @@ def ids(self) -> List[int]: return list(self.data["records"]) @property - @sensor("Total dust collection count", icon="mdi:counter", state_class="total_increasing", entity_category="diagnostic") + @sensor( + "Total dust collection count", + icon="mdi:counter", + state_class="total_increasing", + entity_category="diagnostic", + ) def dust_collection_count(self) -> Optional[int]: """Total number of dust collections.""" if "dust_collection_count" in self.data: @@ -343,25 +402,46 @@ def __init__(self, data: Union[List[Any], Dict[str, Any]], multi_maps=None) -> N self.data = data @property - @sensor("Last clean start", icon="mdi:clock-time-twelve", device_class="timestamp", entity_category="diagnostic") + @sensor( + "Last clean start", + icon="mdi:clock-time-twelve", + device_class="timestamp", + entity_category="diagnostic", + ) def start(self) -> datetime: """When cleaning was started.""" return pretty_time(self.data["begin"]) @property - @sensor("Last clean end", icon="mdi:clock-time-twelve", device_class="timestamp", entity_category="diagnostic") + @sensor( + "Last clean end", + icon="mdi:clock-time-twelve", + device_class="timestamp", + entity_category="diagnostic", + ) def end(self) -> datetime: """When cleaning was finished.""" return pretty_time(self.data["end"]) @property - @sensor("Last clean duration", unit="s", icon="mdi:timer-sand", device_class="duration", entity_category="diagnostic") + @sensor( + "Last clean duration", + unit="s", + icon="mdi:timer-sand", + device_class="duration", + entity_category="diagnostic", + ) def duration(self) -> timedelta: """Total duration of the cleaning run.""" return pretty_seconds(self.data["duration"]) @property - @sensor("Last clean area", unit="m²", icon="mdi:texture-box", entity_category="diagnostic") + @sensor( + "Last clean area", + unit="m²", + icon="mdi:texture-box", + entity_category="diagnostic", + ) def area(self) -> float: """Total cleaned area.""" return pretty_area(self.data["area"]) @@ -406,7 +486,12 @@ def __init__(self, data: Dict[str, Any]) -> None: self.data = data @property - @sensor("Floor 0 clean start", icon="mdi:clock-time-twelve", device_class="timestamp", entity_category="diagnostic") + @sensor( + "Floor 0 clean start", + icon="mdi:clock-time-twelve", + device_class="timestamp", + entity_category="diagnostic", + ) def start_0(self) -> datetime: """When cleaning was started.""" if "0" not in self.data: @@ -417,7 +502,12 @@ def start_0(self) -> datetime: return self.data["0"].start @property - @sensor("Floor 1 clean start", icon="mdi:clock-time-twelve", device_class="timestamp", entity_category="diagnostic") + @sensor( + "Floor 1 clean start", + icon="mdi:clock-time-twelve", + device_class="timestamp", + entity_category="diagnostic", + ) def start_1(self) -> datetime: """When cleaning was started.""" if "1" not in self.data: @@ -428,7 +518,12 @@ def start_1(self) -> datetime: return self.data["1"].start @property - @sensor("Floor 2 clean start", icon="mdi:clock-time-twelve", device_class="timestamp", entity_category="diagnostic") + @sensor( + "Floor 2 clean start", + icon="mdi:clock-time-twelve", + device_class="timestamp", + entity_category="diagnostic", + ) def start_2(self) -> datetime: """When cleaning was started.""" if "2" not in self.data: @@ -439,7 +534,12 @@ def start_2(self) -> datetime: return self.data["2"].start @property - @sensor("Floor 3 clean start", icon="mdi:clock-time-twelve", device_class="timestamp", entity_category="diagnostic") + @sensor( + "Floor 3 clean start", + icon="mdi:clock-time-twelve", + device_class="timestamp", + entity_category="diagnostic", + ) def start_3(self) -> datetime: """When cleaning was started.""" if "3" not in self.data: @@ -474,54 +574,111 @@ def __init__(self, data: Dict[str, Any]) -> None: self.sensor_dirty_total = timedelta(hours=30) @property - @sensor("Main brush used", unit="s", icon="mdi:brush", device_class="duration", entity_category="diagnostic", enabled_default=False) + @sensor( + "Main brush used", + unit="s", + icon="mdi:brush", + device_class="duration", + entity_category="diagnostic", + enabled_default=False, + ) def main_brush(self) -> timedelta: """Main brush usage time.""" return pretty_seconds(self.data["main_brush_work_time"]) @property - @sensor("Main brush left", unit="s", icon="mdi:brush", device_class="duration", entity_category="diagnostic") + @sensor( + "Main brush left", + unit="s", + icon="mdi:brush", + device_class="duration", + entity_category="diagnostic", + ) def main_brush_left(self) -> timedelta: """How long until the main brush should be changed.""" return self.main_brush_total - self.main_brush @property - @sensor("Side brush used", unit="s", icon="mdi:brush", device_class="duration", entity_category="diagnostic", enabled_default=False) + @sensor( + "Side brush used", + unit="s", + icon="mdi:brush", + device_class="duration", + entity_category="diagnostic", + enabled_default=False, + ) def side_brush(self) -> timedelta: """Side brush usage time.""" return pretty_seconds(self.data["side_brush_work_time"]) @property - @sensor("Side brush left", unit="s", icon="mdi:brush", device_class="duration", entity_category="diagnostic") + @sensor( + "Side brush left", + unit="s", + icon="mdi:brush", + device_class="duration", + entity_category="diagnostic", + ) def side_brush_left(self) -> timedelta: """How long until the side brush should be changed.""" return self.side_brush_total - self.side_brush @property - @sensor("Filter used", unit="s", icon="mdi:air-filter", device_class="duration", entity_category="diagnostic", enabled_default=False) + @sensor( + "Filter used", + unit="s", + icon="mdi:air-filter", + device_class="duration", + entity_category="diagnostic", + enabled_default=False, + ) def filter(self) -> timedelta: """Filter usage time.""" return pretty_seconds(self.data["filter_work_time"]) @property - @sensor("Filter left", unit="s", icon="mdi:air-filter", device_class="duration", entity_category="diagnostic") + @sensor( + "Filter left", + unit="s", + icon="mdi:air-filter", + device_class="duration", + entity_category="diagnostic", + ) def filter_left(self) -> timedelta: """How long until the filter should be changed.""" return self.filter_total - self.filter @property - @sensor("Sensor dirty used", unit="s", icon="mdi:eye-outline", device_class="duration", entity_category="diagnostic", enabled_default=False) + @sensor( + "Sensor dirty used", + unit="s", + icon="mdi:eye-outline", + device_class="duration", + entity_category="diagnostic", + enabled_default=False, + ) def sensor_dirty(self) -> timedelta: """Return ``sensor_dirty_time``""" return pretty_seconds(self.data["sensor_dirty_time"]) @property - @sensor("Sensor dirty left", unit="s", icon="mdi:eye-outline", device_class="duration", entity_category="diagnostic") + @sensor( + "Sensor dirty left", + unit="s", + icon="mdi:eye-outline", + device_class="duration", + entity_category="diagnostic", + ) def sensor_dirty_left(self) -> timedelta: return self.sensor_dirty_total - self.sensor_dirty @property - @sensor("Dustbin times auto-empty used", icon="mdi:delete", entity_category="diagnostic", enabled_default=False) + @sensor( + "Dustbin times auto-empty used", + icon="mdi:delete", + entity_category="diagnostic", + enabled_default=False, + ) def dustbin_auto_empty_used(self) -> int: """Return ``dust_collection_work_times``""" return self.data["dust_collection_work_times"] @@ -542,13 +699,25 @@ def enabled(self) -> bool: return bool(self.data["enabled"]) @property - @sensor("Do not disturb start", icon="mdi:minus-circle-off", device_class="timestamp", entity_category="diagnostic", enabled_default=False) + @sensor( + "Do not disturb start", + icon="mdi:minus-circle-off", + device_class="timestamp", + entity_category="diagnostic", + enabled_default=False, + ) def start(self) -> time: """Start time of DnD.""" return time(hour=self.data["start_hour"], minute=self.data["start_minute"]) @property - @sensor("Do not disturb end", icon="mdi:minus-circle-off", device_class="timestamp", entity_category="diagnostic", enabled_default=False) + @sensor( + "Do not disturb end", + icon="mdi:minus-circle-off", + device_class="timestamp", + entity_category="diagnostic", + enabled_default=False, + ) def end(self) -> time: """End time of DnD.""" return time(hour=self.data["end_hour"], minute=self.data["end_minute"]) From f80a13e26227cc29765a8bf12f1f39776531bc1d Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 3 Oct 2022 18:06:28 +0200 Subject: [PATCH 32/60] docformatter --- miio/integrations/vacuum/roborock/vacuum.py | 17 ++++---- .../vacuum/roborock/vacuumcontainers.py | 41 +++++++++++-------- 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/miio/integrations/vacuum/roborock/vacuum.py b/miio/integrations/vacuum/roborock/vacuum.py index a9e6ae696..3638f4138 100644 --- a/miio/integrations/vacuum/roborock/vacuum.py +++ b/miio/integrations/vacuum/roborock/vacuum.py @@ -139,8 +139,8 @@ def start(self): def stop(self): """Stop cleaning. - Note, prefer 'pause' instead of this for wider support. Some newer vacuum models - do not support this command. + Note, prefer 'pause' instead of this for wider support. Some + newer vacuum models do not support this command. """ return self.send("app_stop") @@ -168,8 +168,9 @@ def resume_or_start(self): def _fetch_info(self) -> DeviceInfo: """Return info about the device. - This is overrides the base class info to account for gen1 devices that do not - respond to info query properly when not connected to the cloud. + This is overrides the base class info to account for gen1 + devices that do not respond to info query properly when not + connected to the cloud. """ try: info = super()._fetch_info() @@ -179,8 +180,9 @@ def _fetch_info(self) -> DeviceInfo: def create_dummy_mac(addr): """Returns a dummy mac for a given IP address. - This squats the FF:FF: OUI for a dummy mac presentation to - allow presenting a unique identifier for homeassistant. + This squats the FF:FF: OUI for a dummy mac + presentation to allow presenting a unique identifier for + homeassistant. """ from ipaddress import ip_address @@ -467,7 +469,8 @@ def create_nogo_zone(self, x1, y1, x2, y2, x3, y3, x4, y4): def enable_lab_mode(self, enable): """Enable persistent maps and software barriers. - This is required to use create_nogo_zone and create_software_barrier commands. + This is required to use create_nogo_zone and + create_software_barrier commands. """ return self.send("set_lab_status", int(enable))["ok"] diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index bdd7d5c03..4b0f738bb 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -218,12 +218,14 @@ def map(self) -> bool: icon="mdi:floor-plan", ) def multi_map_id(self) -> int: - """The id of the current map with regards to the multi map feature, [3,7,11,15] -> [0,1,2,3].""" + """The id of the current map with regards to the multi map feature, + [3,7,11,15] -> [0,1,2,3].""" return int((self.data["map_status"] + 1) / 4 - 1) @property def multi_map_name(self) -> str: - """The name of the current map with regards to the multi map feature.""" + """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) @@ -266,8 +268,8 @@ def is_water_box_attached(self) -> Optional[bool]: @property @sensor("Mop attached") def is_water_box_carriage_attached(self) -> Optional[bool]: - """Return True if water box carriage (mop) is installed, None if sensor not - present.""" + """Return True if water box carriage (mop) is installed, None if sensor + not present.""" if "water_box_carriage_status" in self.data: return self.data["water_box_carriage_status"] == 1 return None @@ -275,7 +277,8 @@ def is_water_box_carriage_attached(self) -> Optional[bool]: @property @sensor("Water level low", icon="mdi:water-alert-outline") def is_water_shortage(self) -> Optional[bool]: - """Returns True if water is low in the tank, None if sensor not present.""" + """Returns True if water is low in the tank, None if sensor not + present.""" if "water_shortage_status" in self.data: return self.data["water_shortage_status"] == 1 return None @@ -288,7 +291,8 @@ def is_water_shortage(self) -> Optional[bool]: entity_category="config", ) def auto_dust_collection(self) -> Optional[bool]: - """Returns True if auto dust collection is enabled, None if sensor not present.""" + """Returns True if auto dust collection is enabled, None if sensor not + present.""" if "auto_dust_collection" in self.data: return self.data["auto_dust_collection"] == 1 return None @@ -363,7 +367,8 @@ def count(self) -> int: @property def ids(self) -> List[int]: - """A list of available cleaning IDs, see also :class:`CleaningDetails`.""" + """A list of available cleaning IDs, see also + :class:`CleaningDetails`.""" return list(self.data["records"]) @property @@ -454,7 +459,8 @@ def multi_map_id(self) -> int: @property @sensor("Last clean map name", icon="mdi:floor-plan", entity_category="diagnostic") def multi_map_name(self) -> str: - """The name of the map used (multi map feature) during the cleaning run.""" + """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) @@ -551,9 +557,9 @@ def start_3(self) -> datetime: 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 - on the following lifetimes: + """Container for consumable status information, including information about + brushes and duration until they should be changed. The methods returning + time left are based on the following lifetimes: - Sensor cleanup time: XXX FIXME - Main brush: 300 hours @@ -726,8 +732,8 @@ def end(self) -> time: class Timer(DeviceStatus): """A container for scheduling. - The timers are accessed using an integer ID, which is based on the unix timestamp of - the creation time. + The timers are accessed using an integer ID, which is based on the + unix timestamp of the creation time. """ def __init__(self, data: List[Any], timezone: BaseTzInfo) -> None: @@ -748,9 +754,9 @@ def __init__(self, data: List[Any], timezone: BaseTzInfo) -> None: def id(self) -> str: """Unique identifier for timer. - Usually a unix timestamp of when the timer was created, but it is not - guaranteed. For example, valetudo apparently allows using arbitrary strings for - this. + Usually a unix timestamp of when the timer was created, but it + is not guaranteed. For example, valetudo apparently allows using + arbitrary strings for this. """ return self.data[0] @@ -784,7 +790,8 @@ def action(self) -> str: def next_schedule(self) -> datetime: """Next schedule for the timer. - Note, this value will not be updated after the Timer object has been created. + Note, this value will not be updated after the Timer object has + been created. """ if self._next_schedule is None: self._next_schedule = self.croniter.get_next(ret_type=datetime) From 39d97814f33313cc45de3c9fd67bef2dcf7a5f85 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 3 Oct 2022 18:08:28 +0200 Subject: [PATCH 33/60] docformatter correct lenght --- miio/integrations/vacuum/roborock/vacuum.py | 17 ++++----- .../vacuum/roborock/vacuumcontainers.py | 36 +++++++++---------- 2 files changed, 24 insertions(+), 29 deletions(-) diff --git a/miio/integrations/vacuum/roborock/vacuum.py b/miio/integrations/vacuum/roborock/vacuum.py index 3638f4138..a9e6ae696 100644 --- a/miio/integrations/vacuum/roborock/vacuum.py +++ b/miio/integrations/vacuum/roborock/vacuum.py @@ -139,8 +139,8 @@ def start(self): def stop(self): """Stop cleaning. - Note, prefer 'pause' instead of this for wider support. Some - newer vacuum models do not support this command. + Note, prefer 'pause' instead of this for wider support. Some newer vacuum models + do not support this command. """ return self.send("app_stop") @@ -168,9 +168,8 @@ def resume_or_start(self): def _fetch_info(self) -> DeviceInfo: """Return info about the device. - This is overrides the base class info to account for gen1 - devices that do not respond to info query properly when not - connected to the cloud. + This is overrides the base class info to account for gen1 devices that do not + respond to info query properly when not connected to the cloud. """ try: info = super()._fetch_info() @@ -180,9 +179,8 @@ def _fetch_info(self) -> DeviceInfo: def create_dummy_mac(addr): """Returns a dummy mac for a given IP address. - This squats the FF:FF: OUI for a dummy mac - presentation to allow presenting a unique identifier for - homeassistant. + This squats the FF:FF: OUI for a dummy mac presentation to + allow presenting a unique identifier for homeassistant. """ from ipaddress import ip_address @@ -469,8 +467,7 @@ def create_nogo_zone(self, x1, y1, x2, y2, x3, y3, x4, y4): def enable_lab_mode(self, enable): """Enable persistent maps and software barriers. - This is required to use create_nogo_zone and - create_software_barrier commands. + This is required to use create_nogo_zone and create_software_barrier commands. """ return self.send("set_lab_status", int(enable))["ok"] diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index 4b0f738bb..05f0c9f6d 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -219,13 +219,14 @@ def map(self) -> bool: ) def multi_map_id(self) -> int: """The id of the current map with regards to the multi map feature, - [3,7,11,15] -> [0,1,2,3].""" + + [3,7,11,15] -> [0,1,2,3]. + """ return int((self.data["map_status"] + 1) / 4 - 1) @property def multi_map_name(self) -> str: - """The name of the current map with regards to the multi map - feature.""" + """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) @@ -268,8 +269,8 @@ def is_water_box_attached(self) -> Optional[bool]: @property @sensor("Mop attached") def is_water_box_carriage_attached(self) -> Optional[bool]: - """Return True if water box carriage (mop) is installed, None if sensor - not present.""" + """Return True if water box carriage (mop) is installed, None if sensor not + present.""" if "water_box_carriage_status" in self.data: return self.data["water_box_carriage_status"] == 1 return None @@ -277,8 +278,7 @@ def is_water_box_carriage_attached(self) -> Optional[bool]: @property @sensor("Water level low", icon="mdi:water-alert-outline") def is_water_shortage(self) -> Optional[bool]: - """Returns True if water is low in the tank, None if sensor not - present.""" + """Returns True if water is low in the tank, None if sensor not present.""" if "water_shortage_status" in self.data: return self.data["water_shortage_status"] == 1 return None @@ -459,8 +459,7 @@ def multi_map_id(self) -> int: @property @sensor("Last clean map name", icon="mdi:floor-plan", entity_category="diagnostic") def multi_map_name(self) -> str: - """The name of the map used (multi map feature) during the cleaning - run.""" + """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) @@ -557,9 +556,9 @@ def start_3(self) -> datetime: 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 on the following lifetimes: + """Container for consumable status information, including information about brushes + and duration until they should be changed. The methods returning time left are based + on the following lifetimes: - Sensor cleanup time: XXX FIXME - Main brush: 300 hours @@ -732,8 +731,8 @@ def end(self) -> time: class Timer(DeviceStatus): """A container for scheduling. - The timers are accessed using an integer ID, which is based on the - unix timestamp of the creation time. + The timers are accessed using an integer ID, which is based on the unix timestamp of + the creation time. """ def __init__(self, data: List[Any], timezone: BaseTzInfo) -> None: @@ -754,9 +753,9 @@ def __init__(self, data: List[Any], timezone: BaseTzInfo) -> None: def id(self) -> str: """Unique identifier for timer. - Usually a unix timestamp of when the timer was created, but it - is not guaranteed. For example, valetudo apparently allows using - arbitrary strings for this. + Usually a unix timestamp of when the timer was created, but it is not + guaranteed. For example, valetudo apparently allows using arbitrary strings for + this. """ return self.data[0] @@ -790,8 +789,7 @@ def action(self) -> str: def next_schedule(self) -> datetime: """Next schedule for the timer. - Note, this value will not be updated after the Timer object has - been created. + Note, this value will not be updated after the Timer object has been created. """ if self._next_schedule is None: self._next_schedule = self.croniter.get_next(ret_type=datetime) From fd49e945857b7ba233c3832a7b1bd0cb90875923 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 3 Oct 2022 19:20:11 +0200 Subject: [PATCH 34/60] fix mypy --- miio/descriptors.py | 1 + miio/device.py | 14 ++++++++++---- miio/devicestatus.py | 4 +++- miio/integrations/vacuum/roborock/vacuum.py | 6 +++--- .../vacuum/roborock/vacuumcontainers.py | 8 ++++---- 5 files changed, 21 insertions(+), 12 deletions(-) diff --git a/miio/descriptors.py b/miio/descriptors.py index 7447bddb0..333719611 100644 --- a/miio/descriptors.py +++ b/miio/descriptors.py @@ -65,6 +65,7 @@ class SettingDescriptor: name: str property: str unit: str + type: SettingType setter: Optional[Callable] = None setter_name: Optional[str] = None diff --git a/miio/device.py b/miio/device.py index 951d68000..993b5ac1e 100644 --- a/miio/device.py +++ b/miio/device.py @@ -7,8 +7,11 @@ from .click_common import DeviceGroupMeta, LiteralParamType, command, format_output from .descriptors import ( ButtonDescriptor, + EnumSettingDescriptor, + NumberSettingDescriptor, SensorDescriptor, SettingDescriptor, + SettingType, SwitchDescriptor, ) from .deviceinfo import DeviceInfo @@ -242,7 +245,9 @@ def buttons(self) -> List[ButtonDescriptor]: """Return a list of button-like, clickable actions of the device.""" return [] - def settings(self) -> Dict[str, SettingDescriptor]: + def settings( + self, + ) -> Dict[str, Union[EnumSettingDescriptor, NumberSettingDescriptor]]: """Return list of settings.""" settings = ( self.status().settings() @@ -257,9 +262,10 @@ def settings(self) -> Dict[str, SettingDescriptor]: ) setting.setter = getattr(self, setting.setter_name) - if setting.choices_attribute is not None: - retrieve_choices_function = getattr(self, setting.choices_attribute) - setting.choices = retrieve_choices_function() + if isinstance(setting, EnumSettingDescriptor): + if setting.choices_attribute is not None: + retrieve_choices_function = getattr(self, setting.choices_attribute) + setting.choices = retrieve_choices_function() return settings diff --git a/miio/devicestatus.py b/miio/devicestatus.py index 10d64f430..8146f97c5 100644 --- a/miio/devicestatus.py +++ b/miio/devicestatus.py @@ -100,7 +100,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, Union[EnumSettingDescriptor, NumberSettingDescriptor]]: """Return the dict of settings exposed by the status container. You can use @setting decorator to define sensors inside your status class. diff --git a/miio/integrations/vacuum/roborock/vacuum.py b/miio/integrations/vacuum/roborock/vacuum.py index a9e6ae696..8d11712d0 100644 --- a/miio/integrations/vacuum/roborock/vacuum.py +++ b/miio/integrations/vacuum/roborock/vacuum.py @@ -127,8 +127,8 @@ def __init__( self.manual_seqnum = -1 self._multi_maps = None self._multi_map_enum = None - self._floor_clean_details = {} - self._searched_clean_id = None + self._floor_clean_details: dict[str, Optional[CleaningDetails]] = {} + self._searched_clean_id: Optional[int] = None @command() def start(self): @@ -368,7 +368,7 @@ def get_multi_maps(self, skip_cache=False): return self._multi_maps @command() - def multi_map_enum(self, skip_cache=False) -> enum.Enum: + def multi_map_enum(self, skip_cache=False) -> Optional[enum.Enum]: """Enum of the available map names.""" if self._multi_map_enum is not None and not skip_cache: return self._multi_map_enum diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index 05f0c9f6d..48021fc55 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -497,7 +497,7 @@ def __init__(self, data: Dict[str, Any]) -> None: device_class="timestamp", entity_category="diagnostic", ) - def start_0(self) -> datetime: + def start_0(self) -> Optional[datetime]: """When cleaning was started.""" if "0" not in self.data: return None @@ -513,7 +513,7 @@ def start_0(self) -> datetime: device_class="timestamp", entity_category="diagnostic", ) - def start_1(self) -> datetime: + def start_1(self) -> Optional[datetime]: """When cleaning was started.""" if "1" not in self.data: return None @@ -529,7 +529,7 @@ def start_1(self) -> datetime: device_class="timestamp", entity_category="diagnostic", ) - def start_2(self) -> datetime: + def start_2(self) -> Optional[datetime]: """When cleaning was started.""" if "2" not in self.data: return None @@ -545,7 +545,7 @@ def start_2(self) -> datetime: device_class="timestamp", entity_category="diagnostic", ) - def start_3(self) -> datetime: + def start_3(self) -> Optional[datetime]: """When cleaning was started.""" if "3" not in self.data: return None From 142dfb056a06c8c172b9d69c10ae9f3efcf4b829 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 3 Oct 2022 19:24:57 +0200 Subject: [PATCH 35/60] fix flake8 --- miio/descriptors.py | 1 - miio/device.py | 12 ++++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/miio/descriptors.py b/miio/descriptors.py index 333719611..7447bddb0 100644 --- a/miio/descriptors.py +++ b/miio/descriptors.py @@ -65,7 +65,6 @@ class SettingDescriptor: name: str property: str unit: str - type: SettingType setter: Optional[Callable] = None setter_name: Optional[str] = None diff --git a/miio/device.py b/miio/device.py index 993b5ac1e..2119e02d8 100644 --- a/miio/device.py +++ b/miio/device.py @@ -10,8 +10,6 @@ EnumSettingDescriptor, NumberSettingDescriptor, SensorDescriptor, - SettingDescriptor, - SettingType, SwitchDescriptor, ) from .deviceinfo import DeviceInfo @@ -262,10 +260,12 @@ def settings( ) setting.setter = getattr(self, setting.setter_name) - if isinstance(setting, EnumSettingDescriptor): - if setting.choices_attribute is not None: - retrieve_choices_function = getattr(self, setting.choices_attribute) - setting.choices = retrieve_choices_function() + if ( + isinstance(setting, EnumSettingDescriptor) + and setting.choices_attribute is not None + ): + retrieve_choices_function = getattr(self, setting.choices_attribute) + setting.choices = retrieve_choices_function() return settings From 4485c79edc7391c06f152ab0d7a310af04627d80 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 3 Oct 2022 19:33:14 +0200 Subject: [PATCH 36/60] fix python compatibility --- miio/integrations/vacuum/roborock/vacuum.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/miio/integrations/vacuum/roborock/vacuum.py b/miio/integrations/vacuum/roborock/vacuum.py index 8d11712d0..ddd30cb31 100644 --- a/miio/integrations/vacuum/roborock/vacuum.py +++ b/miio/integrations/vacuum/roborock/vacuum.py @@ -7,7 +7,7 @@ import os import pathlib import time -from typing import List, Optional, Type, Union +from typing import Dict, List, Optional, Type, Union import click import pytz @@ -127,7 +127,7 @@ def __init__( self.manual_seqnum = -1 self._multi_maps = None self._multi_map_enum = None - self._floor_clean_details: dict[str, Optional[CleaningDetails]] = {} + self._floor_clean_details: Dict[str, Optional[CleaningDetails]] = {} self._searched_clean_id: Optional[int] = None @command() @@ -495,7 +495,7 @@ def last_clean_details( @command() def last_clean_all_floor( self, history: Optional[CleaningSummary] = None - ) -> dict[str, Optional[CleaningDetails]]: + ) -> Dict[str, Optional[CleaningDetails]]: """Return details from the last cleaning and for each floor. Returns None if there has been no cleanups for that floor. From 6f21e311989575f63021e90263205131f6422fb8 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 3 Oct 2022 19:44:51 +0200 Subject: [PATCH 37/60] fix vacuum test --- .../vacuum/roborock/tests/test_vacuum.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/miio/integrations/vacuum/roborock/tests/test_vacuum.py b/miio/integrations/vacuum/roborock/tests/test_vacuum.py index 35665167f..e59a1baf4 100644 --- a/miio/integrations/vacuum/roborock/tests/test_vacuum.py +++ b/miio/integrations/vacuum/roborock/tests/test_vacuum.py @@ -83,6 +83,35 @@ def __init__(self, *args, **kwargs): "miIO.info": "dummy info", } + self._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}], + }, + ], + } + super().__init__(args, kwargs) def change_mode(self, new_mode): From 8c3eae4cbd318ce8eec592376087991655fe7b7b Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 3 Oct 2022 20:35:58 +0200 Subject: [PATCH 38/60] fix tests --- .../vacuum/roborock/tests/test_vacuum.py | 74 ++++++++++++------- .../vacuum/roborock/vacuumcontainers.py | 2 +- 2 files changed, 47 insertions(+), 29 deletions(-) diff --git a/miio/integrations/vacuum/roborock/tests/test_vacuum.py b/miio/integrations/vacuum/roborock/tests/test_vacuum.py index e59a1baf4..b1267fe9a 100644 --- a/miio/integrations/vacuum/roborock/tests/test_vacuum.py +++ b/miio/integrations/vacuum/roborock/tests/test_vacuum.py @@ -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], @@ -81,36 +120,15 @@ 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 = { - "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._multi_maps = None + + self._floor_clean_details = {} + self._searched_clean_id = None super().__init__(args, kwargs) diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index 48021fc55..59d05d4d3 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -454,7 +454,7 @@ def area(self) -> float: @property def multi_map_id(self) -> int: """Map id used (multi map feature) during the cleaning run.""" - return self.data["map_flag"] + return self.data.get("map_flag", 0) @property @sensor("Last clean map name", icon="mdi:floor-plan", entity_category="diagnostic") From 1b009d58af8b4b50ac84f603c24c1a5f2398ad32 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 7 Oct 2022 12:30:16 +0200 Subject: [PATCH 39/60] Change new fan mode Mopping to Off --- miio/integrations/vacuum/roborock/vacuum_enums.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miio/integrations/vacuum/roborock/vacuum_enums.py b/miio/integrations/vacuum/roborock/vacuum_enums.py index 6f992a939..3cf0cab94 100644 --- a/miio/integrations/vacuum/roborock/vacuum_enums.py +++ b/miio/integrations/vacuum/roborock/vacuum_enums.py @@ -58,11 +58,11 @@ class FanspeedS7(FanspeedEnum): class FanspeedS7_Maxv(FanspeedEnum): # Original names from the app: Quiet, Balanced, Turbo, Max, Max+ + Off = 105 Silent = 101 Standard = 102 Medium = 103 Turbo = 104 - Mopping = 105 Max = 108 From a86d7cc0c9d7bef4d24832aa3e1eed00aa785da4 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 7 Oct 2022 15:56:45 +0200 Subject: [PATCH 40/60] Reduce device calls by caching the status --- miio/device.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/miio/device.py b/miio/device.py index 2119e02d8..a831712b8 100644 --- a/miio/device.py +++ b/miio/device.py @@ -55,6 +55,7 @@ def __init__( self.token: Optional[str] = token self._model: Optional[str] = model self._info: Optional[DeviceInfo] = None + self._status: Optional[DeviceStatus] = None timeout = timeout if timeout is not None else self.timeout self._protocol = MiIOProtocol( ip, token, start_id, debug, lazy_discover, timeout @@ -239,6 +240,13 @@ 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 [] @@ -248,7 +256,7 @@ def settings( ) -> Dict[str, Union[EnumSettingDescriptor, NumberSettingDescriptor]]: """Return list of settings.""" settings = ( - self.status().settings() + self.cached_status().settings() ) # NOTE that this already does IO so schould be run in executer job in HA for setting in settings.values(): # TODO: Bind setter methods, this should probably done only once during init. @@ -272,12 +280,12 @@ def 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: From cd86f24bb42efc5040dc9904aca72ca2bdf0d70f Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 7 Oct 2022 17:23:27 +0200 Subject: [PATCH 41/60] split out vacuum_status --- miio/integrations/vacuum/roborock/vacuum.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/miio/integrations/vacuum/roborock/vacuum.py b/miio/integrations/vacuum/roborock/vacuum.py index ddd30cb31..a4fa506d4 100644 --- a/miio/integrations/vacuum/roborock/vacuum.py +++ b/miio/integrations/vacuum/roborock/vacuum.py @@ -318,7 +318,7 @@ def manual_control( @command() def status(self) -> VacuumStatus: """Return status of the vacuum.""" - status = VacuumStatus(self.send("get_status")[0], self.get_multi_maps()) + status = self.vacuum_status() status.embed(self.consumable_status()) clean_history = self.clean_history() status.embed(clean_history) @@ -328,6 +328,11 @@ def status(self) -> VacuumStatus: 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], self.get_multi_maps()) + def enable_log_upload(self): raise NotImplementedError("unknown parameters") # return self.send("enable_log_upload") From 46c2b806ab6532fdb871d8d120e2a6063f04d591 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 7 Oct 2022 17:37:23 +0200 Subject: [PATCH 42/60] simplify return --- miio/devicestatus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miio/devicestatus.py b/miio/devicestatus.py index 8146f97c5..132c4c1c7 100644 --- a/miio/devicestatus.py +++ b/miio/devicestatus.py @@ -102,7 +102,7 @@ def switches(self) -> Dict[str, SwitchDescriptor]: def settings( self, - ) -> Dict[str, Union[EnumSettingDescriptor, NumberSettingDescriptor]]: + ) -> 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. From f53edc395f24c0b1c06908abedaa72941d780668 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Fri, 7 Oct 2022 17:42:46 +0200 Subject: [PATCH 43/60] notes --- miio/device.py | 3 +-- miio/devicestatus.py | 4 ---- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/miio/device.py b/miio/device.py index a831712b8..a01cb09a5 100644 --- a/miio/device.py +++ b/miio/device.py @@ -273,13 +273,12 @@ def settings( and setting.choices_attribute is not None ): retrieve_choices_function = getattr(self, setting.choices_attribute) - setting.choices = retrieve_choices_function() + 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.cached_status().sensors() return sensors diff --git a/miio/devicestatus.py b/miio/devicestatus.py index 132c4c1c7..450c94a32 100644 --- a/miio/devicestatus.py +++ b/miio/devicestatus.py @@ -259,10 +259,6 @@ def decorator_setting(func): 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=qualified_name, property=property_name, From 28f3340225132cd8208249e4fcb72d6bc3268999 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 8 Oct 2022 10:46:38 +0200 Subject: [PATCH 44/60] return FloorCleanDetails from last_clean_all_floor --- miio/integrations/vacuum/roborock/vacuum.py | 22 ++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/miio/integrations/vacuum/roborock/vacuum.py b/miio/integrations/vacuum/roborock/vacuum.py index a4fa506d4..8111dfca4 100644 --- a/miio/integrations/vacuum/roborock/vacuum.py +++ b/miio/integrations/vacuum/roborock/vacuum.py @@ -7,7 +7,7 @@ import os import pathlib import time -from typing import Dict, List, Optional, Type, Union +from typing import Dict, List, Optional, Tuple, Type, Union import click import pytz @@ -128,6 +128,7 @@ def __init__( self._multi_maps = None self._multi_map_enum = None self._floor_clean_details: Dict[str, Optional[CleaningDetails]] = {} + self._last_clean_details: Optional[CleaningDetails] = None self._searched_clean_id: Optional[int] = None @command() @@ -322,9 +323,9 @@ def status(self) -> VacuumStatus: status.embed(self.consumable_status()) clean_history = self.clean_history() status.embed(clean_history) - clean_details = self.last_clean_all_floor(history=clean_history) - status.embed(clean_details["last"]) - status.embed(FloorCleanDetails(clean_details)) + (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 @@ -495,12 +496,16 @@ def last_clean_details( return None last_clean_id = history.ids[0] - return self.clean_details(last_clean_id) + 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 - ) -> Dict[str, Optional[CleaningDetails]]: + ) -> Tuple[FloorCleanDetails, CleaningDetails]: """Return details from the last cleaning and for each floor. Returns None if there has been no cleanups for that floor. @@ -513,7 +518,6 @@ def last_clean_all_floor( # if cache empty, fill with None if not self._floor_clean_details: - self._floor_clean_details["last"] = None for id in map_ids: self._floor_clean_details[str(id)] = None @@ -532,14 +536,14 @@ def last_clean_all_floor( map_ids.remove(clean_detail.multi_map_id) if id == last_clean_id: - self._floor_clean_details["last"] = clean_detail + self._last_clean_details = clean_detail # all floors found if not map_ids: break self._searched_clean_id = last_clean_id - return self._floor_clean_details + return (FloorCleanDetails(self._floor_clean_details), self._last_clean_details) @command( click.argument("id_", type=int, metavar="ID"), From 65037f931d2ee052388143af607d6f6c01babefa Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 8 Oct 2022 12:02:02 +0200 Subject: [PATCH 45/60] Use MultiMapList container --- miio/integrations/vacuum/roborock/vacuum.py | 15 +++--- .../vacuum/roborock/vacuumcontainers.py | 51 +++++++++++++++++-- 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/miio/integrations/vacuum/roborock/vacuum.py b/miio/integrations/vacuum/roborock/vacuum.py index 8111dfca4..0c87b835e 100644 --- a/miio/integrations/vacuum/roborock/vacuum.py +++ b/miio/integrations/vacuum/roborock/vacuum.py @@ -51,6 +51,7 @@ SoundStatus, Timer, VacuumStatus, + MultiMapList, ) _LOGGER = logging.getLogger(__name__) @@ -125,7 +126,7 @@ def __init__( ): super().__init__(ip, token, start_id, debug, model=model) self.manual_seqnum = -1 - self._multi_maps = None + self._multi_maps: Optional[MultiMapList] = None self._multi_map_enum = None self._floor_clean_details: Dict[str, Optional[CleaningDetails]] = {} self._last_clean_details: Optional[CleaningDetails] = None @@ -360,7 +361,7 @@ def map(self): return self.send("get_map_v1") @command() - def get_multi_maps(self, skip_cache=False): + def get_multi_maps(self, skip_cache=False) -> MultiMapList: """Return list of 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}]}, @@ -370,7 +371,7 @@ def get_multi_maps(self, skip_cache=False): if self._multi_maps is not None and not skip_cache: return self._multi_maps - self._multi_maps = self.send("get_multi_maps_list")[0] + self._multi_maps = MultiMapList(self.send("get_multi_maps_list")[0]) return self._multi_maps @command() @@ -380,11 +381,8 @@ def multi_map_enum(self, skip_cache=False) -> Optional[enum.Enum]: return self._multi_map_enum multi_maps = self.get_multi_maps() - maps_dict = {} - for map in multi_maps["map_info"]: - maps_dict[map["name"]] = map["mapFlag"] - self._multi_map_enum = enum.Enum("multi_map_enum", maps_dict) + self._multi_map_enum = enum.Enum("multi_map_enum", multi_maps.map_name_dict) return self._multi_map_enum @command(click.argument("multi_map_id", type=int)) @@ -513,8 +511,7 @@ def last_clean_all_floor( if history is None: history = self.clean_history() - N_maps = self.get_multi_maps()["multi_map_count"] - map_ids = list(range(0, N_maps)) + map_ids = self.get_multi_maps().map_id_list # if cache empty, fill with None if not self._floor_clean_details: diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index 59d05d4d3..5cf409049 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -1,3 +1,4 @@ +import logging from datetime import datetime, time, timedelta from enum import IntEnum from typing import Any, Dict, List, Optional, Union @@ -12,6 +13,9 @@ from .vacuum_enums import MopIntensity, MopMode +_LOGGER = logging.getLogger(__name__) + + def pretty_area(x: float) -> float: return int(x) / 1000000 @@ -47,7 +51,7 @@ def pretty_area(x: float) -> float: class VacuumStatus(DeviceStatus): """Container for status reports from the vacuum.""" - def __init__(self, data: Dict[str, Any], multi_maps=None) -> 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}], @@ -230,7 +234,7 @@ def multi_map_name(self) -> str: if self._multi_maps is None: return str(self.multi_map_id) - return self._multi_maps["map_info"][self.multi_map_id]["name"] + return self._multi_maps.map_list[self.multi_map_id]["name"] @property def in_zone_cleaning(self) -> bool: @@ -305,6 +309,45 @@ def got_error(self) -> bool: """True if an error has occurred.""" return self.error_code != 0 +class MultiMapList(DeviceStatus): + """Contains a information about the maps/floors of the vacuum.""" + + def __init__(self, data: Dict[str, Any]) -> None: + # {'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.data = data + if self.map_count != len(self.data['map_info']): + _LOGGER.warning("Roborock multi_map_count does not equal amount of maps") + + self._map_name_dict = {} + for idx, map in enumerate(self.data['map_info']): + self._map_name_dict[map['name']] = map['mapFlag'] + if map['mapFlag'] != idx: + _LOGGER.warning("Roborock mapFlag does not equal map_info list index") + + @property + def map_count(self) -> int: + """Amount of multi maps stored.""" + return self.data["multi_map_count"] + + @property + def map_id_list(self) -> List[int]: + """List of multi map ids.""" + return self._map_name_dict.values() + + @property + def map_list(self) -> List[Dict[str, Any]]: + """List of map info.""" + return self.data["map_info"] + + @property + def map_name_dict(self) -> Dict[str, int]: + """Dictionary of map names (keys) with there ids (values).""" + return self._map_name_dict + class CleaningSummary(DeviceStatus): """Contains summarized information about available cleaning runs.""" @@ -389,7 +432,7 @@ 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]], multi_maps=None) -> 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 @@ -463,7 +506,7 @@ def multi_map_name(self) -> str: if self._multi_maps is None: return str(self.multi_map_id) - return self._multi_maps["map_info"][self.multi_map_id]["name"] + return self._multi_maps.map_list[self.multi_map_id]["name"] @property def error_code(self) -> int: From edc42d4f7e7622416b36e82514906776a8e7f889 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 8 Oct 2022 13:25:28 +0200 Subject: [PATCH 46/60] dynamically create FloorCleanDetail settings --- miio/devicestatus.py | 2 +- .../vacuum/roborock/vacuumcontainers.py | 167 +++++++----------- 2 files changed, 66 insertions(+), 103 deletions(-) diff --git a/miio/devicestatus.py b/miio/devicestatus.py index 450c94a32..412e55815 100644 --- a/miio/devicestatus.py +++ b/miio/devicestatus.py @@ -145,7 +145,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 diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index 5cf409049..4b3eafafd 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -9,6 +9,7 @@ from miio.device import DeviceStatus from miio.devicestatus import sensor, setting, switch from miio.utils import pretty_seconds, pretty_time +from miio.descriptors import SensorDescriptor from .vacuum_enums import MopIntensity, MopMode @@ -48,6 +49,46 @@ def pretty_area(x: float) -> float: } +class MultiMapList(DeviceStatus): + """Contains a information about the maps/floors of the vacuum.""" + + def __init__(self, data: Dict[str, Any]) -> None: + # {'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.data = data + if self.map_count != len(self.data['map_info']): + _LOGGER.warning("Roborock multi_map_count does not equal amount of maps") + + self._map_name_dict = {} + for idx, map in enumerate(self.data['map_info']): + self._map_name_dict[map['name']] = map['mapFlag'] + if map['mapFlag'] != idx: + _LOGGER.warning("Roborock mapFlag does not equal map_info list index") + + @property + def map_count(self) -> int: + """Amount of multi maps stored.""" + return self.data["multi_map_count"] + + @property + def map_id_list(self) -> List[int]: + """List of multi map ids.""" + return list(self._map_name_dict.values()) + + @property + def map_list(self) -> List[Dict[str, Any]]: + """List of map info.""" + return self.data["map_info"] + + @property + def map_name_dict(self) -> Dict[str, int]: + """Dictionary of map names (keys) with there ids (values).""" + return self._map_name_dict + + class VacuumStatus(DeviceStatus): """Container for status reports from the vacuum.""" @@ -309,45 +350,6 @@ def got_error(self) -> bool: """True if an error has occurred.""" return self.error_code != 0 -class MultiMapList(DeviceStatus): - """Contains a information about the maps/floors of the vacuum.""" - - def __init__(self, data: Dict[str, Any]) -> None: - # {'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.data = data - if self.map_count != len(self.data['map_info']): - _LOGGER.warning("Roborock multi_map_count does not equal amount of maps") - - self._map_name_dict = {} - for idx, map in enumerate(self.data['map_info']): - self._map_name_dict[map['name']] = map['mapFlag'] - if map['mapFlag'] != idx: - _LOGGER.warning("Roborock mapFlag does not equal map_info list index") - - @property - def map_count(self) -> int: - """Amount of multi maps stored.""" - return self.data["multi_map_count"] - - @property - def map_id_list(self) -> List[int]: - """List of multi map ids.""" - return self._map_name_dict.values() - - @property - def map_list(self) -> List[Dict[str, Any]]: - """List of map info.""" - return self.data["map_info"] - - @property - def map_name_dict(self) -> Dict[str, int]: - """Dictionary of map names (keys) with there ids (values).""" - return self._map_name_dict - class CleaningSummary(DeviceStatus): """Contains summarized information about available cleaning runs.""" @@ -533,69 +535,30 @@ class FloorCleanDetails(DeviceStatus): def __init__(self, data: Dict[str, Any]) -> None: self.data = data - @property - @sensor( - "Floor 0 clean start", - icon="mdi:clock-time-twelve", - device_class="timestamp", - entity_category="diagnostic", - ) - def start_0(self) -> Optional[datetime]: - """When cleaning was started.""" - if "0" not in self.data: - return None - if self.data["0"] is None: - return None - - return self.data["0"].start - - @property - @sensor( - "Floor 1 clean start", - icon="mdi:clock-time-twelve", - device_class="timestamp", - entity_category="diagnostic", - ) - def start_1(self) -> Optional[datetime]: - """When cleaning was started.""" - if "1" not in self.data: - return None - if self.data["1"] is None: - return None - - return self.data["1"].start - - @property - @sensor( - "Floor 2 clean start", - icon="mdi:clock-time-twelve", - device_class="timestamp", - entity_category="diagnostic", - ) - def start_2(self) -> Optional[datetime]: - """When cleaning was started.""" - if "2" not in self.data: - return None - if self.data["2"] is None: - return None - - return self.data["2"].start - - @property - @sensor( - "Floor 3 clean start", - icon="mdi:clock-time-twelve", - device_class="timestamp", - entity_category="diagnostic", - ) - def start_3(self) -> Optional[datetime]: - """When cleaning was started.""" - if "3" not in self.data: - return None - if self.data["3"] is None: - return None - - return self.data["3"].start + for map_id in self.data: + if self.data[map_id] is None: + setattr(self, f"start_{map_id}", None) + continue + setattr(self, f"start_{map_id}", self.data[map_id].start) + + 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): From 6068336a0fe3ddf5cdd2d56e69525ecdd47777e6 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 8 Oct 2022 13:40:23 +0200 Subject: [PATCH 47/60] fix __repr__ --- .../vacuum/roborock/vacuumcontainers.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index 4b3eafafd..7b81bd14a 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -541,6 +541,20 @@ def __init__(self, data: Dict[str, Any]) -> None: continue setattr(self, f"start_{map_id}", self.data[map_id].start) + def __repr__(self): + + s = f"<{self.__class__.__name__}" + for map_id in self.data: + 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] From e47a9ded9f00b89d7804d704b8a93dc176ae5a76 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 8 Oct 2022 13:50:33 +0200 Subject: [PATCH 48/60] Embed CleanDetails for each floor in FloorCleanDetails container --- miio/integrations/vacuum/roborock/vacuumcontainers.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index 7b81bd14a..22d13b8f4 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -537,14 +537,20 @@ def __init__(self, data: Dict[str, Any]) -> None: 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) def __repr__(self): s = f"<{self.__class__.__name__}" 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}" From cdc140e4908a4c9de9f3e0195255fbf0c851aacd Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 8 Oct 2022 14:59:40 +0200 Subject: [PATCH 49/60] Add button support --- miio/device.py | 10 +++++++++- miio/devicestatus.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/miio/device.py b/miio/device.py index a01cb09a5..b22b76f80 100644 --- a/miio/device.py +++ b/miio/device.py @@ -1,4 +1,5 @@ import logging +from inspect import getmembers from enum import Enum from typing import Any, Dict, List, Optional, Union # noqa: F401 @@ -56,6 +57,7 @@ def __init__( 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 @@ -249,7 +251,13 @@ def cached_status(self) -> DeviceStatus: 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 + self._buttons.append(method._button) + + return self._buttons def settings( self, diff --git a/miio/devicestatus.py b/miio/devicestatus.py index 412e55815..defb3d559 100644 --- a/miio/devicestatus.py +++ b/miio/devicestatus.py @@ -19,6 +19,7 @@ SensorDescriptor, SettingDescriptor, SwitchDescriptor, + ButtonDescriptor, ) _LOGGER = logging.getLogger(__name__) @@ -280,3 +281,32 @@ def decorator_setting(func): return func return decorator_setting + + +def button(name: str, **kwargs): + """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=func, + extras=kwargs, + ) + func._button = descriptor + + return func + + return decorator_button From c051028db337101eac65e6c47d084678bc819ba7 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 8 Oct 2022 15:00:14 +0200 Subject: [PATCH 50/60] add start_dust_collection button --- miio/integrations/vacuum/roborock/vacuum.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/miio/integrations/vacuum/roborock/vacuum.py b/miio/integrations/vacuum/roborock/vacuum.py index 0c87b835e..dcc7af812 100644 --- a/miio/integrations/vacuum/roborock/vacuum.py +++ b/miio/integrations/vacuum/roborock/vacuum.py @@ -23,6 +23,7 @@ from miio.device import Device, DeviceInfo from miio.exceptions import DeviceException, DeviceInfoUnavailableException from miio.interfaces import FanspeedPresets, VacuumInterface +from miio.devicestatus import button from .vacuum_enums import ( CarpetCleaningMode, @@ -870,6 +871,7 @@ def set_dust_collection_mode(self, mode: DustCollectionMode) -> bool: return self.send("set_dust_collection_mode", {"mode": mode.value})[0] == "ok" @command() + @button(name="Start dust collection") def start_dust_collection(self): """Activate automatic dust collection.""" self._verify_auto_empty_support() From 6ad0e45884c196e8d2b1b7ea786005d1a51964cd Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 8 Oct 2022 15:04:49 +0200 Subject: [PATCH 51/60] Add stop dust collection button --- miio/integrations/vacuum/roborock/vacuum.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/miio/integrations/vacuum/roborock/vacuum.py b/miio/integrations/vacuum/roborock/vacuum.py index dcc7af812..ba635d6df 100644 --- a/miio/integrations/vacuum/roborock/vacuum.py +++ b/miio/integrations/vacuum/roborock/vacuum.py @@ -871,13 +871,14 @@ def set_dust_collection_mode(self, mode: DustCollectionMode) -> bool: return self.send("set_dust_collection_mode", {"mode": mode.value})[0] == "ok" @command() - @button(name="Start dust collection") + @button(name="Start dust collection", icon="mdi:turbine") def start_dust_collection(self): """Activate automatic dust collection.""" self._verify_auto_empty_support() return self.send("app_start_collect_dust") @command() + @button(name="Stop dust collection", icon="mdi:turbine") def stop_dust_collection(self): """Abort in progress dust collection.""" self._verify_auto_empty_support() From 14a7d6e1e44a09500e29a5bd9d2351d89ee7bd10 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 8 Oct 2022 15:48:02 +0200 Subject: [PATCH 52/60] bind button method --- miio/device.py | 4 +++- miio/devicestatus.py | 9 ++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/miio/device.py b/miio/device.py index b22b76f80..58c21f176 100644 --- a/miio/device.py +++ b/miio/device.py @@ -255,7 +255,9 @@ def buttons(self) -> List[ButtonDescriptor]: self._buttons = [] for button_tuple in getmembers(self, lambda o: hasattr(o, '_button')): method_name, method = button_tuple - self._buttons.append(method._button) + button = method._button + button.method = method # bind the method + self._buttons.append(button) return self._buttons diff --git a/miio/devicestatus.py b/miio/devicestatus.py index defb3d559..066ed6164 100644 --- a/miio/devicestatus.py +++ b/miio/devicestatus.py @@ -219,7 +219,6 @@ def decorator_switch(func): def setting( name: str, *, - setter: Optional[Callable] = None, setter_name: Optional[str] = None, unit: Optional[str] = None, min_value: Optional[int] = None, @@ -243,8 +242,8 @@ def decorator_setting(func): 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( @@ -252,7 +251,7 @@ def decorator_setting(func): property=property_name, name=name, unit=unit, - setter=setter, + setter=None, setter_name=setter_name, min_value=min_value or 0, max_value=max_value, @@ -302,7 +301,7 @@ def decorator_button(func): id=qualified_name, name=name, method_name=property_name, - method=func, + method=None, extras=kwargs, ) func._button = descriptor From 2f5f3b3a1f995a82e3ee6ba5d589a7bd6b6d0aca Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 8 Oct 2022 15:50:31 +0200 Subject: [PATCH 53/60] fix --- miio/devicestatus.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/miio/devicestatus.py b/miio/devicestatus.py index 066ed6164..2b935e442 100644 --- a/miio/devicestatus.py +++ b/miio/devicestatus.py @@ -264,7 +264,7 @@ def decorator_setting(func): property=property_name, name=name, unit=unit, - setter=setter, + setter=None, setter_name=setter_name, choices=choices, choices_attribute=choices_attribute, From ee90954ecd46ad0986dfc70925b61848a8f12170 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 8 Oct 2022 16:38:44 +0200 Subject: [PATCH 54/60] fix styling --- miio/device.py | 6 ++-- miio/devicestatus.py | 2 +- miio/integrations/vacuum/roborock/vacuum.py | 10 +++--- .../vacuum/roborock/vacuumcontainers.py | 31 +++++++++++-------- 4 files changed, 28 insertions(+), 21 deletions(-) diff --git a/miio/device.py b/miio/device.py index 58c21f176..c1d6abac7 100644 --- a/miio/device.py +++ b/miio/device.py @@ -1,6 +1,6 @@ import logging -from inspect import getmembers from enum import Enum +from inspect import getmembers from typing import Any, Dict, List, Optional, Union # noqa: F401 import click @@ -253,7 +253,7 @@ def buttons(self) -> List[ButtonDescriptor]: """Return a list of button-like, clickable actions of the device.""" if self._buttons is None: self._buttons = [] - for button_tuple in getmembers(self, lambda o: hasattr(o, '_button')): + 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 @@ -283,7 +283,7 @@ def settings( and setting.choices_attribute is not None ): retrieve_choices_function = getattr(self, setting.choices_attribute) - setting.choices = retrieve_choices_function() # This can do IO + setting.choices = retrieve_choices_function() # This can do IO return settings diff --git a/miio/devicestatus.py b/miio/devicestatus.py index 2b935e442..e34452836 100644 --- a/miio/devicestatus.py +++ b/miio/devicestatus.py @@ -14,12 +14,12 @@ ) from .descriptors import ( + ButtonDescriptor, EnumSettingDescriptor, NumberSettingDescriptor, SensorDescriptor, SettingDescriptor, SwitchDescriptor, - ButtonDescriptor, ) _LOGGER = logging.getLogger(__name__) diff --git a/miio/integrations/vacuum/roborock/vacuum.py b/miio/integrations/vacuum/roborock/vacuum.py index ba635d6df..6699eec75 100644 --- a/miio/integrations/vacuum/roborock/vacuum.py +++ b/miio/integrations/vacuum/roborock/vacuum.py @@ -21,9 +21,9 @@ command, ) from miio.device import Device, DeviceInfo +from miio.devicestatus import button from miio.exceptions import DeviceException, DeviceInfoUnavailableException from miio.interfaces import FanspeedPresets, VacuumInterface -from miio.devicestatus import button from .vacuum_enums import ( CarpetCleaningMode, @@ -48,11 +48,11 @@ ConsumableStatus, DNDStatus, FloorCleanDetails, + MultiMapList, SoundInstallStatus, SoundStatus, Timer, VacuumStatus, - MultiMapList, ) _LOGGER = logging.getLogger(__name__) @@ -325,7 +325,9 @@ def status(self) -> VacuumStatus: status.embed(self.consumable_status()) clean_history = self.clean_history() status.embed(clean_history) - (details_floors, details_last) = self.last_clean_all_floor(history=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()) @@ -497,7 +499,7 @@ def last_clean_details( 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 diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index 22d13b8f4..adc175dcf 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -6,14 +6,13 @@ from croniter import croniter from pytz import BaseTzInfo +from miio.descriptors import SensorDescriptor from miio.device import DeviceStatus from miio.devicestatus import sensor, setting, switch from miio.utils import pretty_seconds, pretty_time -from miio.descriptors import SensorDescriptor from .vacuum_enums import MopIntensity, MopMode - _LOGGER = logging.getLogger(__name__) @@ -59,13 +58,13 @@ def __init__(self, data: Dict[str, Any]) -> None: # {'mapFlag': 2, 'add_time': 1663580384, 'length': 5, 'name': 'Attic', 'bak_maps': [{'mapFlag': 6, 'add_time': 1663577765}]} # ]} self.data = data - if self.map_count != len(self.data['map_info']): + if self.map_count != len(self.data["map_info"]): _LOGGER.warning("Roborock multi_map_count does not equal amount of maps") self._map_name_dict = {} - for idx, map in enumerate(self.data['map_info']): - self._map_name_dict[map['name']] = map['mapFlag'] - if map['mapFlag'] != idx: + for idx, map in enumerate(self.data["map_info"]): + self._map_name_dict[map["name"]] = map["mapFlag"] + if map["mapFlag"] != idx: _LOGGER.warning("Roborock mapFlag does not equal map_info list index") @property @@ -92,7 +91,9 @@ def map_name_dict(self) -> Dict[str, int]: class VacuumStatus(DeviceStatus): """Container for status reports from the vacuum.""" - def __init__(self, data: Dict[str, Any], multi_maps: Optional[MultiMapList]=None) -> 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}], @@ -434,7 +435,11 @@ 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]], multi_maps: Optional[MultiMapList]=None) -> 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 @@ -550,7 +555,7 @@ def __repr__(self): 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}" @@ -564,7 +569,7 @@ def __repr__(self): 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}", @@ -572,9 +577,9 @@ def sensors(self) -> Dict[str, SensorDescriptor]: name=f"Floor {map_id} clean start", type="sensor", extras={ - "icon":"mdi:clock-time-twelve", - "device_class":"timestamp", - "entity_category":"diagnostic", + "icon": "mdi:clock-time-twelve", + "device_class": "timestamp", + "entity_category": "diagnostic", }, ) From 758e1858be0a5f86a1b4673590857a9bf7bbc108 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 8 Oct 2022 16:40:31 +0200 Subject: [PATCH 55/60] fix imports --- miio/devicestatus.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/miio/devicestatus.py b/miio/devicestatus.py index e34452836..26b3bf83d 100644 --- a/miio/devicestatus.py +++ b/miio/devicestatus.py @@ -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 ( ButtonDescriptor, From 252c1b692dd5b1105d49dbcfa14a099d3b06628a Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 8 Oct 2022 16:52:03 +0200 Subject: [PATCH 56/60] fix mypy issues --- miio/device.py | 2 +- miio/integrations/vacuum/roborock/vacuum.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/miio/device.py b/miio/device.py index c1d6abac7..81cec940b 100644 --- a/miio/device.py +++ b/miio/device.py @@ -263,7 +263,7 @@ def buttons(self) -> List[ButtonDescriptor]: def settings( self, - ) -> Dict[str, Union[EnumSettingDescriptor, NumberSettingDescriptor]]: + ) -> Dict[str, SettingDescriptor]: """Return list of settings.""" settings = ( self.cached_status().settings() diff --git a/miio/integrations/vacuum/roborock/vacuum.py b/miio/integrations/vacuum/roborock/vacuum.py index 6699eec75..f0ae04c05 100644 --- a/miio/integrations/vacuum/roborock/vacuum.py +++ b/miio/integrations/vacuum/roborock/vacuum.py @@ -506,7 +506,7 @@ def last_clean_details( @command() def last_clean_all_floor( self, history: Optional[CleaningSummary] = None - ) -> Tuple[FloorCleanDetails, CleaningDetails]: + ) -> 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. @@ -522,7 +522,7 @@ def last_clean_all_floor( self._floor_clean_details[str(id)] = None if not history.ids: - return self._floor_clean_details + return (FloorCleanDetails(self._floor_clean_details), self._last_clean_details) last_clean_id = history.ids[0] for id in history.ids: From de9dac966518542aa5758753e2f66d7789096de3 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 8 Oct 2022 16:54:37 +0200 Subject: [PATCH 57/60] fix styling --- miio/device.py | 2 +- miio/integrations/vacuum/roborock/vacuum.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/miio/device.py b/miio/device.py index 81cec940b..1704509fd 100644 --- a/miio/device.py +++ b/miio/device.py @@ -9,8 +9,8 @@ from .descriptors import ( ButtonDescriptor, EnumSettingDescriptor, - NumberSettingDescriptor, SensorDescriptor, + SettingDescriptor, SwitchDescriptor, ) from .deviceinfo import DeviceInfo diff --git a/miio/integrations/vacuum/roborock/vacuum.py b/miio/integrations/vacuum/roborock/vacuum.py index f0ae04c05..d8c48c702 100644 --- a/miio/integrations/vacuum/roborock/vacuum.py +++ b/miio/integrations/vacuum/roborock/vacuum.py @@ -522,7 +522,10 @@ def last_clean_all_floor( self._floor_clean_details[str(id)] = None if not history.ids: - return (FloorCleanDetails(self._floor_clean_details), self._last_clean_details) + return ( + FloorCleanDetails(self._floor_clean_details), + self._last_clean_details, + ) last_clean_id = history.ids[0] for id in history.ids: From 180dd5c205d7bb4cae3346bb1380aff4cbd90eca Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 9 Nov 2022 18:41:18 +0100 Subject: [PATCH 58/60] Add dock_error and dock_error_code --- .../vacuum/roborock/vacuumcontainers.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index adc175dcf..174140351 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -45,6 +45,15 @@ def pretty_area(x: float) -> float: 22: "Clean the dock charging contacts", 23: "Docking station not reachable", 24: "No-go zone or invisible wall detected", + 26: "Wall sensor is dirty", + 27: "VibraRise system is jammed", + 28: "Roborock is on carpet", +} + +dock_error_codes = { # from vacuum_cleaner-EN.pdf + 0: "No error", + 38: "Clean water tank empty", + 39: "Dirty water tank full", } @@ -201,6 +210,31 @@ def error(self) -> str: except KeyError: return "Definition missing for error %s" % self.error_code + @property + @sensor( + "Dock error code", + icon="mdi:alert", + entity_category="diagnostic", + enabled_default=False, + ) + def dock_error_code(self) -> int: + """Dock error status as returned by the device.""" + return int(self.data["dock_error_status"]) + + @property + @sensor( + "Dock error string", + icon="mdi:alert", + entity_category="diagnostic", + enabled_default=False, + ) + def dock_error(self) -> str: + """Human readable dock error description, see also :func:`dock_error_code`.""" + try: + return dock_error_codes[self.dock_error_code] + except KeyError: + return "Definition missing for dock error %s" % self.dock_error_code + @property @sensor("Battery", unit="%", device_class="battery", enabled_default=False) def battery(self) -> int: From 83e7e900b71ded390b66c2014c73afa8e2feca10 Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 9 Nov 2022 20:09:41 +0100 Subject: [PATCH 59/60] Update to latest miio --- miio/devicestatus.py | 46 ++++---------------------------------------- 1 file changed, 4 insertions(+), 42 deletions(-) diff --git a/miio/devicestatus.py b/miio/devicestatus.py index c1f23ac77..bfd70c775 100644 --- a/miio/devicestatus.py +++ b/miio/devicestatus.py @@ -171,34 +171,6 @@ def _sensor_type_for_return_type(func): return decorator_sensor -def switch(name: str, *, setter_name: str, **kwargs): - """Syntactic sugar to create SwitchDescriptor 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.SwitchDescriptor.extras`, - and can be interpreted downstream users as they wish. - """ - - def decorator_switch(func): - property_name = str(func.__name__) - qualified_name = str(func.__qualname__) - - descriptor = SwitchDescriptor( - id=qualified_name, - property=property_name, - name=name, - setter_name=setter_name, - extras=kwargs, - ) - func._switch = descriptor - - return func - - return decorator_switch - def setting( name: str, *, @@ -230,8 +202,8 @@ def decorator_setting(func): raise Exception("Setter_name needs to be defined") common_values = { - "id": str(property_name), - "property": str(property_name), + "id": qualified_name, + "property": property_name, "name": name, "unit": unit, "setter": setter, @@ -241,24 +213,14 @@ def decorator_setting(func): if min_value or max_value: descriptor = NumberSettingDescriptor( - id=qualified_name, - property=property_name, - name=name, - unit=unit, - setter=None, - setter_name=setter_name, + **common_values, min_value=min_value or 0, max_value=max_value, step=step or 1, ) elif choices or choices_attribute: descriptor = EnumSettingDescriptor( - id=qualified_name, - property=property_name, - name=name, - unit=unit, - setter=None, - setter_name=setter_name, + **common_values, choices=choices, choices_attribute=choices_attribute, ) From ac58bf31c3d52292b2d07a92ca3d9931f593f31a Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Wed, 9 Nov 2022 20:18:17 +0100 Subject: [PATCH 60/60] Update to latest miio --- miio/device.py | 16 ---------------- miio/devicestatus.py | 7 ------- .../vacuum/roborock/vacuumcontainers.py | 4 ++-- 3 files changed, 2 insertions(+), 25 deletions(-) diff --git a/miio/device.py b/miio/device.py index 99d45547e..47cf76f3f 100644 --- a/miio/device.py +++ b/miio/device.py @@ -11,7 +11,6 @@ EnumSettingDescriptor, SensorDescriptor, SettingDescriptor, - SwitchDescriptor, ) from .deviceinfo import DeviceInfo from .devicestatus import DeviceStatus @@ -299,20 +298,5 @@ def sensors(self) -> Dict[str, SensorDescriptor]: sensors = self.cached_status().sensors() return sensors - def switches(self) -> Dict[str, SwitchDescriptor]: - """Return toggleable 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: - if switch.setter_name is None: - # TODO: this is ugly, how to fix the issue where setter_name is optional and thus not acceptable for getattr? - raise Exception( - f"Neither setter or setter_name was defined for {switch}" - ) - switch.setter = getattr(self, switch.setter_name) - - return switches - def __repr__(self): return f"<{self.__class__.__name__ }: {self.ip} (token: {self.token})>" diff --git a/miio/devicestatus.py b/miio/devicestatus.py index bfd70c775..73975affe 100644 --- a/miio/devicestatus.py +++ b/miio/devicestatus.py @@ -84,13 +84,6 @@ def sensors(self) -> Dict[str, SensorDescriptor]: """ return self._sensors # type: ignore[attr-defined] - def switches(self) -> Dict[str, SwitchDescriptor]: - """Return the dict of sensors exposed by the status container. - - You can use @sensor decorator to define sensors inside your status class. - """ - return self._switches # type: ignore[attr-defined] - def settings(self) -> Dict[str, SettingDescriptor]: """Return the dict of settings exposed by the status container. diff --git a/miio/integrations/vacuum/roborock/vacuumcontainers.py b/miio/integrations/vacuum/roborock/vacuumcontainers.py index dbf7745e3..cb496afc1 100644 --- a/miio/integrations/vacuum/roborock/vacuumcontainers.py +++ b/miio/integrations/vacuum/roborock/vacuumcontainers.py @@ -8,7 +8,7 @@ from miio.descriptors import SensorDescriptor from miio.device import DeviceStatus -from miio.devicestatus import sensor, setting, switch +from miio.devicestatus import sensor, setting from miio.interfaces.vacuuminterface import VacuumDeviceStatus, VacuumState from miio.utils import pretty_seconds, pretty_time @@ -384,7 +384,7 @@ def is_water_shortage(self) -> Optional[bool]: return None @property - @switch( + @setting( "Auto dust collection", setter_name="set_dust_collection", icon="mdi:turbine",