Skip to content

Commit

Permalink
Handle non-readable miot properties (#1662)
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
rytilahti committed Jan 9, 2023
1 parent e594007 commit 74577e4
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 22 deletions.
60 changes: 40 additions & 20 deletions miio/integrations/genericmiot/genericmiot.py
Expand Up @@ -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__)

Expand All @@ -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 += (
Expand Down Expand Up @@ -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 = {}
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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:
Expand All @@ -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,
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down
38 changes: 37 additions & 1 deletion 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
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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"

Expand Down
3 changes: 2 additions & 1 deletion miio/tests/test_miot_models.py
Expand Up @@ -5,6 +5,7 @@

from miio.miot_models import (
URN,
MiotAccess,
MiotAction,
MiotEnumValue,
MiotEvent,
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit 74577e4

Please sign in to comment.