From 74577e42d3b83e220700ea7fa1b609735f261766 Mon Sep 17 00:00:00 2001 From: Teemu R Date: Mon, 9 Jan 2023 05:22:43 +0100 Subject: [PATCH] Handle non-readable miot properties (#1662) This adapts the genericmiot to expose also the defined write-only properties and converts the earlier warning to a debug message. Suppressing the warning is likely fine, as the properties without access defined is now considered to be defined for other reasons (e.g., to define input parameters for actions). --- miio/integrations/genericmiot/genericmiot.py | 60 +++++++++++++------- miio/miot_models.py | 38 ++++++++++++- miio/tests/test_miot_models.py | 3 +- 3 files changed, 79 insertions(+), 22 deletions(-) diff --git a/miio/integrations/genericmiot/genericmiot.py b/miio/integrations/genericmiot/genericmiot.py index fa74ebfef..2f4a51aab 100644 --- a/miio/integrations/genericmiot/genericmiot.py +++ b/miio/integrations/genericmiot/genericmiot.py @@ -17,7 +17,13 @@ ) from miio.miot_cloud import MiotCloud from miio.miot_device import MiotMapping -from miio.miot_models import DeviceModel, MiotAction, MiotProperty, MiotService +from miio.miot_models import ( + DeviceModel, + MiotAccess, + MiotAction, + MiotProperty, + MiotService, +) _LOGGER = logging.getLogger(__name__) @@ -26,13 +32,20 @@ def pretty_status(result: "GenericMiotStatus"): """Pretty print status information.""" out = "" props = result.property_dict() + service = None for _name, prop in props.items(): - pretty_value = prop.pretty_value + miot_prop: MiotProperty = prop.extras["miot_property"] + if service is None or miot_prop.siid != service.siid: + service = miot_prop.service + out += f"Service [bold]{service.description} ({service.name})[/bold]\n" # type: ignore # FIXME - if "write" in prop.access: - out += "[S] " + out += f"\t{prop.description} ({prop.name}, access: {prop.pretty_access}): {prop.pretty_value}" - out += f"{prop.description} ({prop.name}): {pretty_value}" + if MiotAccess.Write in miot_prop.access: + out += f" ({prop.format}" + if prop.pretty_input_constraints is not None: + out += f", {prop.pretty_input_constraints}" + out += ")" if prop.choices is not None: # TODO: hide behind verbose flag? out += ( @@ -105,6 +118,11 @@ def __getattr__(self, item): return self._data[item] + @property + def device(self) -> "GenericMiot": + """Return the device which returned this status.""" + return self._dev + def property_dict(self) -> Dict[str, MiotProperty]: """Return name-keyed dictionary of properties.""" res = {} @@ -127,9 +145,8 @@ def __repr__(self): class GenericMiot(MiotDevice): - _supported_models = [ - "*" - ] # we support all devices, if not, it is a responsibility of caller to verify that + # we support all devices, if not, it is a responsibility of caller to verify that + _supported_models = ["*"] def __init__( self, @@ -176,14 +193,11 @@ def status(self) -> GenericMiotStatus: """Return status based on the miot model.""" properties = [] for prop in self._properties: - if "read" not in prop.access: - _LOGGER.debug("Property has no read access, skipping: %s", prop) + if MiotAccess.Read not in prop.access: continue - siid = prop.siid - piid = prop.piid - name = prop.name # f"{prop.service.urn.name}:{prop.name}" - q = {"siid": siid, "piid": piid, "did": name} + name = prop.name + q = {"siid": prop.siid, "piid": prop.piid, "did": name} properties.append(q) # TODO: max properties needs to be made configurable (or at least splitted to avoid too large udp datagrams @@ -250,11 +264,16 @@ def _create_sensor(self, prop: MiotProperty) -> SensorDescriptor: def _create_sensors_and_settings(self, serv: MiotService): """Create sensor and setting descriptors for a service.""" for prop in serv.properties: - if prop.access == ["notify"]: + if prop.access == [MiotAccess.Notify]: _LOGGER.debug("Skipping notify-only property: %s", prop) continue - if "read" not in prop.access: # TODO: handle write-only properties - _LOGGER.warning("Skipping write-only: %s", prop) + if not prop.access: + # some properties are defined only to be used as inputs for actions + _LOGGER.debug( + "%s (%s) reported no access information", + prop.name, + prop.description, + ) continue desc = self._descriptor_for_property(prop) @@ -279,6 +298,7 @@ def _descriptor_for_property(self, prop: MiotProperty): extras["urn"] = prop.urn extras["siid"] = prop.siid extras["piid"] = prop.piid + extras["miot_property"] = prop # Handle settable ranged properties if prop.range is not None: @@ -292,7 +312,7 @@ def _descriptor_for_property(self, prop: MiotProperty): ) # Handle settable booleans - elif "write" in prop.access and prop.format == bool: + elif MiotAccess.Write in prop.access and prop.format == bool: return BooleanSettingDescriptor( id=property_name, name=name, @@ -326,7 +346,7 @@ def _create_choices_setting( choices=choices, extras=extras, ) - if "write" in prop.access: + if MiotAccess.Write in prop.access: desc.setter = setter return desc else: @@ -344,7 +364,7 @@ def _create_range_setting(self, name, prop, property_name, setter, extras): unit=prop.unit, extras=extras, ) - if "write" in prop.access: + if MiotAccess.Write in prop.access: desc.setter = setter return desc else: diff --git a/miio/miot_models.py b/miio/miot_models.py index 6490334e1..9813e3169 100644 --- a/miio/miot_models.py +++ b/miio/miot_models.py @@ -1,5 +1,6 @@ import logging from datetime import timedelta +from enum import Enum from typing import Any, Dict, List, Optional from pydantic import BaseModel, Field, PrivateAttr, root_validator @@ -138,13 +139,19 @@ class Config: extra = "forbid" +class MiotAccess(Enum): + Read = "read" + Write = "write" + Notify = "notify" + + class MiotProperty(MiotBaseModel): """Property presentation for miot.""" piid: int = Field(alias="iid") format: MiotFormat - access: Any = Field(default=["read"]) + access: List[MiotAccess] = Field(default=["read"]) unit: Optional[str] = None range: Optional[List[int]] = Field(alias="value-range") @@ -183,6 +190,35 @@ def pretty_value(self): return value + @property + def pretty_access(self): + """Return pretty-printable access.""" + acc = "" + if MiotAccess.Read in self.access: + acc += "R" + if MiotAccess.Write in self.access: + acc += "W" + # Just for completeness, as notifications are not supported + # if MiotAccess.Notify in self.access: + # acc += "N" + + return acc + + @property + def pretty_input_constraints(self) -> str: + """Return input constraints for writable settings.""" + out = "" + if self.choices is not None: + out += ( + "choices: " + + ", ".join([f"{c.description} ({c.value})" for c in self.choices]) + + "" + ) + if self.range is not None: + out += f"min: {self.range[0]}, max: {self.range[1]}, step: {self.range[2]}" + + return out + class Config: extra = "forbid" diff --git a/miio/tests/test_miot_models.py b/miio/tests/test_miot_models.py index f87f86f4c..a5985b837 100644 --- a/miio/tests/test_miot_models.py +++ b/miio/tests/test_miot_models.py @@ -5,6 +5,7 @@ from miio.miot_models import ( URN, + MiotAccess, MiotAction, MiotEnumValue, MiotEvent, @@ -206,7 +207,7 @@ def test_property(): assert prop.piid == 1 assert prop.urn.type == "property" assert prop.format == str - assert prop.access == ["read"] + assert prop.access == [MiotAccess.Read] assert prop.description == "Device Manufacturer" assert prop.plain_name == "manufacturer"