Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle non-readable miot properties #1662

Merged
merged 4 commits into from
Jan 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 40 additions & 20 deletions miio/integrations/genericmiot/genericmiot.py
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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