Skip to content

Commit

Permalink
Merge branch 'master' into patch-1
Browse files Browse the repository at this point in the history
  • Loading branch information
ckesc authored Apr 19, 2020
2 parents e738f02 + cc242cc commit b6f4a16
Show file tree
Hide file tree
Showing 11 changed files with 265 additions and 113 deletions.
3 changes: 2 additions & 1 deletion docs/vacuum.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ Status reporting
Fanspeed: 60
Cleaning since: 0:00:00
Cleaned area: 0.0 m²

Water box attached: False
Start cleaning
~~~~~~~~~~~~~~

Expand Down
29 changes: 27 additions & 2 deletions miio/chuangmi_camera.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
"""Xiaomi Chuangmi camera (chuangmi.camera.ipc009) support."""
"""Xiaomi Chuangmi camera (chuangmi.camera.ipc009, ipc019) support."""

import enum
import logging
from typing import Any, Dict

from .click_common import command, format_output
import click

from .click_common import EnumType, command, format_output
from .device import Device

_LOGGER = logging.getLogger(__name__)


class Direction(enum.Enum):
"""Rotation direction."""

Left = 1
Right = 2
Up = 3
Down = 4


class CameraStatus:
"""Container for status reports from the Xiaomi Chuangmi Camera."""

Expand Down Expand Up @@ -269,3 +281,16 @@ def night_mode_off(self):
def night_mode_on(self):
"""Night mode always on."""
return self.send("set_night_mode", [2])

@command(
click.argument("mode", type=EnumType(Direction, False)),
default_output=format_output("Rotating to direction '{direction.name}'"),
)
def rotate(self, direction: Direction):
"""Rotate camera to given direction (left, right, up, down)."""
return self.send("set_motor", {"operation": direction.value})

@command()
def alarm(self):
"""Sound a loud alarm for 10 seconds."""
return self.send("alarm_sound")
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
3 changes: 2 additions & 1 deletion miio/discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,8 @@
"zhimi-airpurifier-mc1": AirPurifier, # mc1
"zhimi-airpurifier-mb3": AirPurifierMiot, # mb3 (3/3H)
"zhimi-airpurifier-ma4": AirPurifierMiot, # ma4 (3)
"chuangmi.camera.ipc009": ChuangmiCamera,
"chuangmi-camera-ipc009": ChuangmiCamera,
"chuangmi-camera-ipc019": ChuangmiCamera,
"chuangmi-ir-v2": ChuangmiIr,
"chuangmi-remote-h102a03_": ChuangmiIr,
"zhimi-humidifier-v1": partial(AirHumidifier, model=MODEL_HUMIDIFIER_V1),
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
92 changes: 65 additions & 27 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 @@ -59,9 +59,10 @@ def send_handshake(self) -> Message:
:raises DeviceException: if the device could not be discovered."""
m = MiIOProtocol.discover(self.ip)
header = m.header.value
if m is not None:
self._device_id = m.header.value.device_id
self._device_ts = m.header.value.ts
self._device_id = header.device_id
self._device_ts = header.ts
self._discovered = True
if self.debug > 1:
_LOGGER.debug(m)
Expand Down Expand Up @@ -126,25 +127,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 +158,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 @@ -176,29 +180,31 @@ def send(self, command: str, parameters: Any = None, retry_count=3) -> Any:
try:
data, addr = s.recvfrom(1024)
m = Message.parse(data, token=self.token)
self._device_ts = m.header.value.ts

header = m.header.value
payload = m.data.value

self.__id = payload["id"]
self._device_ts = header.ts

if self.debug > 1:
_LOGGER.debug("recv from %s: %s", addr[0], m)

self.__id = m.data.value["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,
header.ts,
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 +218,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 +233,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 +254,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

0 comments on commit b6f4a16

Please sign in to comment.