Skip to content

Commit

Permalink
Improve MiotDevice API (get_property_by, set_property_by, call_action…
Browse files Browse the repository at this point in the history
…, call_action_by) (#905)

* Improve MiotDevice API (get_property_by, set_property_by commands)

* Also, code cleanup for passing the mapping.
* Now the mapping is a class attribute, avoiding unnecessary __init__ overloading

* Add call_action(name,params) and call_action_by(siid,aiid,params)

Also, fix passing booleans to set_property_by

* Add some unittests

* Convert a couple of missed/new classes to use new mapping

* Fix linting

* make vacuum tests pass
  • Loading branch information
rytilahti authored Mar 8, 2021
1 parent 6667b74 commit 7c0a5a5
Show file tree
Hide file tree
Showing 13 changed files with 192 additions and 124 deletions.
5 changes: 3 additions & 2 deletions docs/troubleshooting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,13 @@ The connectivity will get restored by device's internal watchdog restarting the
Roborock Vacuum not detected
----------------------------

It seems that a Roborock vacuum connected through the Roborock app (and not the Xiaomi Home app) won't allow control over local network, even with a valid token, leading to the following exception:
It seems that a Roborock vacuum connected through the Roborock app (and not the Xiaomi Home app) won't allow control over local network,
even with a valid token, leading to the following exception:

.. code-block:: text
mirobo.device.DeviceException: Unable to discover the device x.x.x.x
Resetting the device's wifi and pairing it again with the Xiaomi Home app should solve the issue.

.. hint::
Expand Down
10 changes: 1 addition & 9 deletions miio/airconditioner_miot.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,15 +337,7 @@ def __json__(self):
class AirConditionerMiot(MiotDevice):
"""Main class representing the air conditioner which uses MIoT protocol."""

def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
) -> None:
super().__init__(_MAPPING, ip, token, start_id, debug, lazy_discover)
mapping = _MAPPING

@command(
default_output=format_output(
Expand Down
10 changes: 1 addition & 9 deletions miio/airhumidifier_miot.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,15 +277,7 @@ def __repr__(self) -> str:
class AirHumidifierMiot(MiotDevice):
"""Main class representing the air humidifier which uses MIoT protocol."""

def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
) -> None:
super().__init__(_MAPPING, ip, token, start_id, debug, lazy_discover)
mapping = _MAPPING

@command(
default_output=format_output(
Expand Down
22 changes: 2 additions & 20 deletions miio/airpurifier_miot.py
Original file line number Diff line number Diff line change
Expand Up @@ -437,15 +437,7 @@ def set_child_lock(self, lock: bool):
class AirPurifierMiot(BasicAirPurifierMiot):
"""Main class representing the air purifier which uses MIoT protocol."""

def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
) -> None:
super().__init__(_MAPPING, ip, token, start_id, debug, lazy_discover)
mapping = _MAPPING

@command(
default_output=format_output(
Expand Down Expand Up @@ -541,17 +533,7 @@ def set_led(self, led: bool):
class AirPurifierMB4(BasicAirPurifierMiot):
"""Main class representing the air purifier which uses MIoT protocol."""

def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
) -> None:
super().__init__(
_MODEL_AIRPURIFIER_MB4, ip, token, start_id, debug, lazy_discover
)
mapping = _MODEL_AIRPURIFIER_MB4

@command(
default_output=format_output(
Expand Down
10 changes: 1 addition & 9 deletions miio/airqualitymonitor_miot.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,15 +198,7 @@ def __repr__(self) -> str:
class AirQualityMonitorCGDN1(MiotDevice):
"""Qingping Air Monitor Lite."""

def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
) -> None:
super().__init__(_MAPPING_CGDN1, ip, token, start_id, debug, lazy_discover)
mapping = _MAPPING_CGDN1

@command(
default_output=format_output(
Expand Down
10 changes: 1 addition & 9 deletions miio/curtain_youpin.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,15 +138,7 @@ def __repr__(self) -> str:
class CurtainMiot(MiotDevice):
"""Main class representing the lumi.curtain.hagl05 curtain."""

def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
) -> None:
super().__init__(_MAPPING, ip, token, start_id, debug, lazy_discover)
mapping = _MAPPING

@command(
default_output=format_output(
Expand Down
35 changes: 6 additions & 29 deletions miio/fan_miot.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ def __repr__(self) -> str:


class FanMiot(MiotDevice):
mapping = MIOT_MAPPING[MODEL_FAN_P10]

def __init__(
self,
ip: str = None,
Expand All @@ -179,10 +181,9 @@ def __init__(
if model not in MIOT_MAPPING:
raise FanException("Invalid FanMiot model: %s" % model)

super().__init__(ip, token, start_id, debug, lazy_discover)
self.model = model

super().__init__(MIOT_MAPPING[model], ip, token, start_id, debug, lazy_discover)

@command(
default_output=format_output(
"",
Expand Down Expand Up @@ -320,36 +321,12 @@ def set_rotate(self, direction: MoveDirection):


class FanP9(FanMiot):
def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
) -> None:
super().__init__(ip, token, start_id, debug, lazy_discover, model=MODEL_FAN_P9)
mapping = MIOT_MAPPING[MODEL_FAN_P9]


class FanP10(FanMiot):
def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
) -> None:
super().__init__(ip, token, start_id, debug, lazy_discover, model=MODEL_FAN_P10)
mapping = MIOT_MAPPING[MODEL_FAN_P10]


class FanP11(FanMiot):
def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
) -> None:
super().__init__(ip, token, start_id, debug, lazy_discover, model=MODEL_FAN_P11)
mapping = MIOT_MAPPING[MODEL_FAN_P11]
10 changes: 1 addition & 9 deletions miio/heater_miot.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,15 +125,7 @@ def __repr__(self) -> str:
class HeaterMiot(MiotDevice):
"""Main class representing the Xiaomi Smart Space Heater S (zhimi.heater.mc2)."""

def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
) -> None:
super().__init__(_MAPPING, ip, token, start_id, debug, lazy_discover)
mapping = _MAPPING

@command(
default_output=format_output(
Expand Down
12 changes: 7 additions & 5 deletions miio/huizuo.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,8 @@ class Huizuo(MiotDevice):
If your device does't support some properties, the 'None' will be returned
"""

mapping = _MAPPING

def __init__(
self,
ip: str = None,
Expand All @@ -244,15 +246,15 @@ def __init__(
) -> None:

if model in MODELS_WITH_FAN_WY:
_MAPPING.update(_ADDITIONAL_MAPPING_FAN_WY)
self.mapping.update(_ADDITIONAL_MAPPING_FAN_WY)
if model in MODELS_WITH_FAN_WY2:
_MAPPING.update(_ADDITIONAL_MAPPING_FAN_WY2)
self.mapping.update(_ADDITIONAL_MAPPING_FAN_WY2)
if model in MODELS_WITH_SCENES:
_MAPPING.update(_ADDITIONAL_MAPPING_SCENE)
self.mapping.update(_ADDITIONAL_MAPPING_SCENE)
if model in MODELS_WITH_HEATER:
_MAPPING.update(_ADDITIONAL_MAPPING_HEATER)
self.mapping.update(_ADDITIONAL_MAPPING_HEATER)

super().__init__(_MAPPING, ip, token, start_id, debug, lazy_discover)
super().__init__(ip, token, start_id, debug, lazy_discover)

if model in MODELS_SUPPORTED:
self.model = model
Expand Down
107 changes: 93 additions & 14 deletions miio/miot_device.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,117 @@
import logging
from enum import Enum
from functools import partial
from typing import Any, Union

import click

from .click_common import EnumType, LiteralParamType, command
from .device import Device
from .exceptions import DeviceException

_LOGGER = logging.getLogger(__name__)


def _str2bool(x):
"""Helper to convert string to boolean."""
return x.lower() in ("true", "1")


# partial is required here for str2bool, see https://stackoverflow.com/a/40339397
class MiotValueType(Enum):
Int = int
Float = float
Bool = partial(_str2bool)
Str = str


class MiotDevice(Device):
"""Main class representing a MIoT device."""

def __init__(
self,
mapping: dict,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
lazy_discover: bool = True,
) -> None:
self.mapping = mapping
super().__init__(ip, token, start_id, debug, lazy_discover)
mapping = None

def get_properties_for_mapping(self) -> list:
"""Retrieve raw properties based on mapping."""

# We send property key in "did" because it's sent back via response and we can identify the property.
properties = [{"did": k, **v} for k, v in self.mapping.items()]
properties = [
{"did": k, **v} for k, v in self.mapping.items() if "aiid" not in v
]

return self.get_properties(
properties, property_getter="get_properties", max_properties=15
)

def set_property(self, property_key: str, value):
"""Sets property value."""
@command(
click.argument("name", type=str),
click.argument("params", type=LiteralParamType(), required=False),
)
def call_action(self, name: str, params=None):
"""Call an action by a name in the mapping."""
action = self.mapping.get(name)
if "siid" not in action or "aiid" not in action:
raise DeviceException(f"{name} is not an action (missing siid or aiid)")

return self.call_action_by(action["siid"], action["aiid"], params)

@command(
click.argument("siid", type=int),
click.argument("aiid", type=int),
click.argument("params", type=LiteralParamType(), required=False),
)
def call_action_by(self, siid, aiid, params=None):
"""Call an action."""
if params is None:
params = []
payload = {
"did": f"call-{siid}-{aiid}",
"siid": siid,
"aiid": aiid,
"in": params,
}

return self.send("action", payload)

@command(
click.argument("siid", type=int),
click.argument("piid", type=int),
)
def get_property_by(self, siid: int, piid: int):
"""Get a single property (siid/piid)."""
return self.send(
"get_properties", [{"did": f"{siid}-{piid}", "siid": siid, "piid": piid}]
)

@command(
click.argument("siid", type=int),
click.argument("piid", type=int),
click.argument("value"),
click.argument(
"value_type", type=EnumType(MiotValueType), required=False, default=None
),
)
def set_property_by(
self,
siid: int,
piid: int,
value: Union[int, float, str, bool],
value_type: Any = None,
):
"""Set a single property (siid/piid) to given value.
value_type can be given to convert the value to wanted type, allowed types are:
int, float, bool, str
"""
if value_type is not None:
value = value_type.value(value)

return self.send(
"set_properties",
[{"did": f"set-{siid}-{piid}", "siid": siid, "piid": piid, "value": value}],
)

def set_property(self, property_key: str, value):
"""Sets property value using the existing mapping."""
return self.send(
"set_properties",
[{"did": property_key, **self.mapping[property_key], "value": value}],
Expand Down
Loading

0 comments on commit 7c0a5a5

Please sign in to comment.