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

Gateway raw_child_command #651

Closed
wants to merge 7 commits into from
Closed
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
28 changes: 26 additions & 2 deletions miio/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,34 @@ def __init__(
self.token = token
self._protocol = MiIOProtocol(ip, token, start_id, debug, lazy_discover)

def send(self, command: str, parameters: Any = None, retry_count=3) -> Any:
return self._protocol.send(command, parameters, retry_count)
def send(
self,
command: str,
parameters: Any = None,
retry_count=3,
*,
extra_parameters=None
) -> Any:
"""Send a command to the device.

Basic format of the request:
{"id": 1234, "method": command, "parameters": parameters}

`extra_parameters` allows passing elements to the top-level of the request.
This is necessary for some devices, such as gateway devices, which expect
the sub-device identifier to be on the top-level.

:param str command: Command to send
:param dict parameters: Parameters to send
:param int retry_count: How many times to retry on error
:param dict extra_parameters: Extra top-level parameters
"""
return self._protocol.send(
command, parameters, retry_count, extra_parameters=extra_parameters
)

def send_handshake(self):
"""Send initial handshake to the device."""
return self._protocol.send_handshake()

@command(
Expand Down
4 changes: 0 additions & 4 deletions miio/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
class DeviceException(Exception):
"""Exception wrapping any communication errors with the device."""

pass


class DeviceError(DeviceException):
"""Exception communicating an error delivered by the target device."""
Expand All @@ -14,5 +12,3 @@ def __init__(self, error):

class RecoverableError(DeviceError):
"""Exception communicating an recoverable error delivered by the target device."""

pass
30 changes: 26 additions & 4 deletions miio/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import click

from .click_common import command, format_output
from .click_common import LiteralParamType, command, format_output
from .device import Device
from .utils import brightness_and_color_to_int, int_to_brightness, int_to_rgb

Expand Down Expand Up @@ -40,7 +40,8 @@ class DeviceType(IntEnum):
AqaraSwitch = 51
AqaraMotion = 52
AqaraMagnet = 53

AqaraRelay = 54
AqaraSwitch2 = 135

class Gateway(Device):
"""Main class representing the Xiaomi Gateway.
Expand Down Expand Up @@ -77,8 +78,14 @@ class Gateway(Device):
## scene
* get_lumi_bind ["scene", <page number>] for rooms/devices"""

def __init__(self, ip: str = None, token: str = None) -> None:
super().__init__(ip, token)
def __init__(
self,
ip: str = None,
token: str = None,
start_id: int = 0,
debug: int = 0,
) -> None:
super().__init__(ip, token, start_id)
self._alarm = GatewayAlarm(self)
self._radio = GatewayRadio(self)
self._zigbee = GatewayZigbee(self)
Expand Down Expand Up @@ -132,6 +139,21 @@ def set_device_prop(self, sid, property, value):
"""Set the device property."""
return self.send("set_device_prop", {"sid": sid, property: value})

@command(
click.argument("sid"),
click.argument("command", type=str, required=True),
click.argument("parameters", type=LiteralParamType(), required=False),
)
def raw_child_command(self, sid, command, parameters):
"""Send a raw command to a child device.
This is mostly useful when trying out commands which are not
implemented by a given device instance.

:param str sid: SID of child device
:param str command: Command to send
:param dict parameters: Parameters to send"""
return self.send(command, parameters, extra_parameters={"sid": sid})

@command()
def clock(self):
"""Alarm clock"""
Expand Down
79 changes: 56 additions & 23 deletions miio/miioprotocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import datetime
import logging
import socket
from typing import Any, List
from typing import Any, Dict, List

import construct

Expand Down Expand Up @@ -126,25 +126,28 @@ def discover(addr: str = None) -> Any:
_LOGGER.warning("error while reading discover results: %s", ex)
break

def send(self, command: str, parameters: Any = None, retry_count=3) -> Any:
def send(
self,
command: str,
parameters: Any = None,
retry_count: int = 3,
*,
extra_parameters: Dict = None
) -> Any:
"""Build and send the given command.
Note that this will implicitly call :func:`send_handshake` to do a handshake,
and will re-try in case of errors while incrementing the `_id` by 100.

:param str command: Command to send
:param dict parameters: Parameters to send, or an empty list FIXME
:param dict parameters: Parameters to send, or an empty list
:param retry_count: How many times to retry in case of failure
:param dict extra_parameters: Extra top-level parameters
:raises DeviceException: if an error has occurred during communication."""

if not self.lazy_discover or not self._discovered:
self.send_handshake()

cmd = {"id": self._id, "method": command}

if parameters is not None:
cmd["params"] = parameters
else:
cmd["params"] = []
request = self._create_request(command, parameters, extra_parameters)

send_ts = self._device_ts + datetime.timedelta(seconds=1)
header = {
Expand All @@ -154,9 +157,9 @@ def send(self, command: str, parameters: Any = None, retry_count=3) -> Any:
"ts": send_ts,
}

msg = {"data": {"value": cmd}, "header": {"value": header}, "checksum": 0}
msg = {"data": {"value": request}, "header": {"value": header}, "checksum": 0}
m = Message.build(msg, token=self.token)
_LOGGER.debug("%s:%s >>: %s", self.ip, self.port, cmd)
_LOGGER.debug("%s:%s >>: %s", self.ip, self.port, request)
if self.debug > 1:
_LOGGER.debug(
"send (timeout %s): %s",
Expand All @@ -180,25 +183,23 @@ def send(self, command: str, parameters: Any = None, retry_count=3) -> Any:
if self.debug > 1:
_LOGGER.debug("recv from %s: %s", addr[0], m)

self.__id = m.data.value["id"]
payload = m.data.value
self.__id = payload["id"]
_LOGGER.debug(
"%s:%s (ts: %s, id: %s) << %s",
self.ip,
self.port,
m.header.value.ts,
m.data.value["id"],
m.data.value,
payload["id"],
payload,
)
if "error" in m.data.value:
error = m.data.value["error"]
if "code" in error and error["code"] == -30001:
raise RecoverableError(error)
raise DeviceError(error)
if "error" in payload:
self._handle_error(payload["error"])

try:
return m.data.value["result"]
return payload["result"]
except KeyError:
return m.data.value
return payload
except construct.core.ChecksumError as ex:
raise DeviceException(
"Got checksum error which indicates use "
Expand All @@ -212,7 +213,12 @@ def send(self, command: str, parameters: Any = None, retry_count=3) -> Any:
)
self.__id += 100
self._discovered = False
return self.send(command, parameters, retry_count - 1)
return self.send(
command,
parameters,
retry_count - 1,
extra_parameters=extra_parameters,
)

_LOGGER.error("Got error when receiving: %s", ex)
raise DeviceException("No response from the device") from ex
Expand All @@ -222,7 +228,12 @@ def send(self, command: str, parameters: Any = None, retry_count=3) -> Any:
_LOGGER.debug(
"Retrying to send failed command, retries left: %s", retry_count
)
return self.send(command, parameters, retry_count - 1)
return self.send(
command,
parameters,
retry_count - 1,
extra_parameters=extra_parameters,
)

_LOGGER.error("Got error when receiving: %s", ex)
raise DeviceException("Unable to recover failed command") from ex
Expand All @@ -238,3 +249,25 @@ def _id(self) -> int:
@property
def raw_id(self):
return self.__id

def _handle_error(self, error):
"""Raise exception based on the given error code."""
if "code" in error and error["code"] == -30001:
raise RecoverableError(error)
raise DeviceError(error)

def _create_request(
self, command: str, parameters: Any, extra_parameters: Dict = None
):
"""Create request payload."""
request = {"id": self._id, "method": command}

if parameters is not None:
request["params"] = parameters
else:
request["params"] = []

if extra_parameters is not None:
request = {**request, **extra_parameters}

return request
2 changes: 1 addition & 1 deletion miio/tests/dummies.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ def __init__(self, dummy_device):
# return_values) is a temporary workaround to minimize diff size.
self.dummy_device = dummy_device

def send(self, command: str, parameters=None, retry_count=3):
def send(self, command: str, parameters=None, retry_count=3, extra_parameters=None):
"""Overridden send() to return values from `self.return_values`."""
return self.dummy_device.return_values[command](parameters)

Expand Down
Loading