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

Improve MiotDevice API (get_property_by, set_property_by, call_action, call_action_by) #905

Merged
merged 6 commits into from
Mar 8, 2021
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
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello,
This seems to be a breaking change for MiotDevice. We can no longer use

MiotDevice(ip=host, token=token, mapping=mapping)

to instantiate a MiotDevice with a given mapping (ha0y/xiaomi_miot_raw#79).
Is there a method to keep backward compatibility?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, sorry about that! You could assign the mapping directly after constructing the MiotDevice object to fix this for now:

dev = MiotDevice(...)
dev.mapping = my_mapping

Otherwise, feel free to open an issue to re-add the kwarg variant of this parameter back to the constructor!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your prompt reply!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunatelly, this doesn't solve the problem, as this code doesn't work in version 0.5.4. mapping is required in 0.5.4 but unexpected in 0.5.5. I have opened an issue #982. Hope this will be fixed soon because it is a breaking change and may affect those who uses this library.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, I hadn't thought about that, sorry... See my comment on the issue how I plan to rectify it.


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