Skip to content

Commit

Permalink
Split genericmiot into parts (#1725)
Browse files Browse the repository at this point in the history
Just some janitoring to avoid piling everything into a single module.
This will allow nicer test structuring when genericmiot gets some.
  • Loading branch information
rytilahti authored Feb 9, 2023
1 parent e4e7c38 commit d1b8bb5
Show file tree
Hide file tree
Showing 3 changed files with 182 additions and 162 deletions.
54 changes: 54 additions & 0 deletions miio/integrations/genericmiot/cli_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from typing import Dict, cast

from miio.descriptors import ActionDescriptor, SettingDescriptor
from miio.miot_models import MiotProperty, MiotService

# TODO: these should be moved to a generic implementation covering all actions and settings


def pretty_actions(result: Dict[str, ActionDescriptor]):
"""Pretty print actions."""
out = ""
service = None
for _, desc in result.items():
miot_prop: MiotProperty = desc.extras["miot_action"]
# service is marked as optional due pydantic backrefs..
serv = cast(MiotService, miot_prop.service)
if service is None or service.siid != serv.siid:
service = serv
out += f"[bold]{service.description} ({service.name})[/bold]\n"

out += f"\t{desc.id}\t\t{desc.name}"
if desc.inputs:
for idx, input_ in enumerate(desc.inputs, start=1):
param = input_.extras[
"miot_property"
] # TODO: hack until descriptors get support for descriptions
param_desc = f"\n\t\tParameter #{idx}: {param.name} ({param.description}) ({param.format}) {param.pretty_input_constraints}"
out += param_desc

out += "\n"

return out


def pretty_settings(result: Dict[str, SettingDescriptor]):
"""Pretty print settings."""
out = ""
verbose = False
service = None
for _, desc in result.items():
miot_prop: MiotProperty = desc.extras["miot_property"]
# service is marked as optional due pydantic backrefs..
serv = cast(MiotService, miot_prop.service)
if service is None or service.siid != serv.siid:
service = serv
out += f"[bold]{service.name}[/bold] ({service.description})\n"

out += f"\t{desc.name} ({desc.id}, access: {miot_prop.pretty_access})\n"
if verbose:
out += f' urn: {repr(desc.extras["urn"])}\n'
out += f' siid: {desc.extras["siid"]}\n'
out += f' piid: {desc.extras["piid"]}\n'

return out
167 changes: 5 additions & 162 deletions miio/integrations/genericmiot/genericmiot.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import logging
from functools import partial
from typing import Dict, Iterable, List, Optional, cast
from typing import Dict, List, Optional

import click

from miio import DeviceInfo, DeviceStatus, MiotDevice
from miio import DeviceInfo, MiotDevice
from miio.click_common import LiteralParamType, command, format_output
from miio.descriptors import ActionDescriptor, SensorDescriptor, SettingDescriptor
from miio.miot_cloud import MiotCloud
Expand All @@ -17,167 +17,10 @@
MiotService,
)

_LOGGER = logging.getLogger(__name__)

from .cli_helpers import pretty_actions, pretty_settings
from .status import GenericMiotStatus

def pretty_actions(result: Dict[str, ActionDescriptor]):
"""Pretty print actions."""
out = ""
service = None
for _, desc in result.items():
miot_prop: MiotProperty = desc.extras["miot_action"]
# service is marked as optional due pydantic backrefs..
serv = cast(MiotService, miot_prop.service)
if service is None or service.siid != serv.siid:
service = serv
out += f"[bold]{service.description} ({service.name})[/bold]\n"

out += f"\t{desc.id}\t\t{desc.name}"
if desc.inputs:
for idx, input_ in enumerate(desc.inputs, start=1):
param = input_.extras[
"miot_property"
] # TODO: hack until descriptors get support for descriptions
param_desc = f"\n\t\tParameter #{idx}: {param.name} ({param.description}) ({param.format}) {param.pretty_input_constraints}"
out += param_desc

out += "\n"

return out


def pretty_settings(result: Dict[str, SettingDescriptor]):
"""Pretty print settings."""
out = ""
verbose = False
service = None
for _, desc in result.items():
miot_prop: MiotProperty = desc.extras["miot_property"]
# service is marked as optional due pydantic backrefs..
serv = cast(MiotService, miot_prop.service)
if service is None or service.siid != serv.siid:
service = serv
out += f"[bold]{service.name}[/bold] ({service.description})\n"

out += f"\t{desc.name} ({desc.id}, access: {miot_prop.pretty_access})\n"
if verbose:
out += f' urn: {repr(desc.extras["urn"])}\n'
out += f' siid: {desc.extras["siid"]}\n'
out += f' piid: {desc.extras["piid"]}\n'

return out


class GenericMiotStatus(DeviceStatus):
"""Generic status for miot devices."""

def __init__(self, response, dev):
self._model: DeviceModel = dev._miot_model
self._dev = dev
self._data = {elem["did"]: elem["value"] for elem in response}
# for hardcoded json output.. see click_common.json_output
self.data = self._data

self._data_by_siid_piid = {
(elem["siid"], elem["piid"]): elem["value"] for elem in response
}
self._data_by_normalized_name = {
self._normalize_name(elem["did"]): elem["value"] for elem in response
}

def _normalize_name(self, id_: str) -> str:
"""Return a cleaned id for dict searches."""
return id_.replace(":", "_").replace("-", "_")

def __getattr__(self, item):
"""Return attribute for name.
This is overridden to provide access to properties using (siid, piid) tuple.
"""
# let devicestatus handle dunder methods
if item.startswith("__") and item.endswith("__"):
return super().__getattr__(item)

normalized_name = self._normalize_name(item)
if normalized_name in self._data_by_normalized_name:
return self._data_by_normalized_name[normalized_name]

# TODO: create a helper method and prohibit using non-normalized names
if ":" in item:
_LOGGER.warning("Use normalized names for accessing properties")
serv, prop = item.split(":")
prop = self._model.get_property(serv, prop)
value = self._data[item]

# TODO: this feels like a wrong place to convert value to enum..
if prop.choices is not None:
for choice in prop.choices:
if choice.value == value:
return choice.description

_LOGGER.warning(
"Unable to find choice for value: %s: %s", value, prop.choices
)

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 = {}

# We use (siid, piid) to locate the property as not all devices mirror the did in response
for (siid, piid), value in self._data_by_siid_piid.items():
prop = self._model.get_property_by_siid_piid(siid, piid)
prop.value = value
res[prop.name] = prop

return res

@property
def __cli_output__(self):
"""Return a CLI printable status."""
out = ""
props = self.property_dict()
service = None
for _name, prop in props.items():
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

out += f"\t{prop.description} ({prop.name}, access: {prop.pretty_access}): {prop.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 self.device._debug > 1:
out += "\n\t[bold]Extras[/bold]\n"
for extra_key, extra_value in prop.extras.items():
out += f"\t\t{extra_key} = {extra_value}\n"

out += "\n"

return out

def __dir__(self) -> Iterable[str]:
"""Return a list of properties."""
return list(super().__dir__()) + list(self._data_by_normalized_name.keys())

def __repr__(self):
s = f"<{self.__class__.__name__}"
for name, value in self.property_dict().items():
s += f" {name}={value}"
s += ">"

return s
_LOGGER = logging.getLogger(__name__)


class GenericMiot(MiotDevice):
Expand Down
123 changes: 123 additions & 0 deletions miio/integrations/genericmiot/status.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import logging
from typing import TYPE_CHECKING, Dict, Iterable

from miio import DeviceStatus
from miio.miot_models import DeviceModel, MiotAccess, MiotProperty

_LOGGER = logging.getLogger(__name__)


if TYPE_CHECKING:
from .genericmiot import GenericMiot


class GenericMiotStatus(DeviceStatus):
"""Generic status for miot devices."""

def __init__(self, response, dev):
self._model: DeviceModel = dev._miot_model
self._dev = dev
self._data = {elem["did"]: elem["value"] for elem in response}
# for hardcoded json output.. see click_common.json_output
self.data = self._data

self._data_by_siid_piid = {
(elem["siid"], elem["piid"]): elem["value"] for elem in response
}
self._data_by_normalized_name = {
self._normalize_name(elem["did"]): elem["value"] for elem in response
}

def _normalize_name(self, id_: str) -> str:
"""Return a cleaned id for dict searches."""
return id_.replace(":", "_").replace("-", "_")

def __getattr__(self, item):
"""Return attribute for name.
This is overridden to provide access to properties using (siid, piid) tuple.
"""
# let devicestatus handle dunder methods
if item.startswith("__") and item.endswith("__"):
return super().__getattr__(item)

normalized_name = self._normalize_name(item)
if normalized_name in self._data_by_normalized_name:
return self._data_by_normalized_name[normalized_name]

# TODO: create a helper method and prohibit using non-normalized names
if ":" in item:
_LOGGER.warning("Use normalized names for accessing properties")
serv, prop = item.split(":")
prop = self._model.get_property(serv, prop)
value = self._data[item]

# TODO: this feels like a wrong place to convert value to enum..
if prop.choices is not None:
for choice in prop.choices:
if choice.value == value:
return choice.description

_LOGGER.warning(
"Unable to find choice for value: %s: %s", value, prop.choices
)

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 = {}

# We use (siid, piid) to locate the property as not all devices mirror the did in response
for (siid, piid), value in self._data_by_siid_piid.items():
prop = self._model.get_property_by_siid_piid(siid, piid)
prop.value = value
res[prop.name] = prop

return res

@property
def __cli_output__(self):
"""Return a CLI printable status."""
out = ""
props = self.property_dict()
service = None
for _name, prop in props.items():
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

out += f"\t{prop.description} ({prop.name}, access: {prop.pretty_access}): {prop.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 self.device._debug > 1:
out += "\n\t[bold]Extras[/bold]\n"
for extra_key, extra_value in prop.extras.items():
out += f"\t\t{extra_key} = {extra_value}\n"

out += "\n"

return out

def __dir__(self) -> Iterable[str]:
"""Return a list of properties."""
return list(super().__dir__()) + list(self._data_by_normalized_name.keys())

def __repr__(self):
s = f"<{self.__class__.__name__}"
for name, value in self.property_dict().items():
s += f" {name}={value}"
s += ">"

return s

0 comments on commit d1b8bb5

Please sign in to comment.