From ff11aed4875e8366fd1cb99085fe12f93b2d742b Mon Sep 17 00:00:00 2001 From: Teemu R Date: Sat, 27 Jan 2018 18:11:11 +0100 Subject: [PATCH] Revise error handling to be more consistent for library users (#180) * Wrap errors coming from the device inside DeviceError exception, raise DeviceException for invalid tokens / checksum errors * Use common error handling for all cli tools * DeviceExceptions are catched in the base group (ExceptionHandlerGroup) * Token & ip validation is moved to click_common module * move version checke to click_common * add a short note about exceptionhandlergroup * make hound happy --- miio/ceil_cli.py | 27 ++++------------------ miio/click_common.py | 46 +++++++++++++++++++++++++++++++++++++ miio/device.py | 15 +++++++++++- miio/philips_eyecare_cli.py | 27 ++++------------------ miio/plug_cli.py | 27 ++++------------------ miio/vacuum_cli.py | 31 +++---------------------- 6 files changed, 75 insertions(+), 98 deletions(-) create mode 100644 miio/click_common.py diff --git a/miio/ceil_cli.py b/miio/ceil_cli.py index 27602dc8b..8c0177bec 100644 --- a/miio/ceil_cli.py +++ b/miio/ceil_cli.py @@ -2,15 +2,11 @@ import logging import click import sys -import ipaddress - -if sys.version_info < (3, 4): - print("To use this script you need python 3.4 or newer, got %s" % - sys.version_info) - sys.exit(1) - +from miio.click_common import (ExceptionHandlerGroup, validate_ip, + validate_token) import miio # noqa: E402 + _LOGGER = logging.getLogger(__name__) pass_dev = click.make_pass_decorator(miio.Ceil) @@ -36,22 +32,7 @@ def validate_scene(ctx, param, value): return value -def validate_ip(ctx, param, value): - try: - ipaddress.ip_address(value) - return value - except ValueError as ex: - raise click.BadParameter("Invalid IP: %s" % ex) - - -def validate_token(ctx, param, value): - token_len = len(value) - if token_len != 32: - raise click.BadParameter("Token length != 32 chars: %s" % token_len) - return value - - -@click.group(invoke_without_command=True) +@click.group(invoke_without_command=True, cls=ExceptionHandlerGroup) @click.option('--ip', envvar="DEVICE_IP", callback=validate_ip) @click.option('--token', envvar="DEVICE_TOKEN", callback=validate_token) @click.option('-d', '--debug', default=False, count=True) diff --git a/miio/click_common.py b/miio/click_common.py new file mode 100644 index 000000000..4fc773e9c --- /dev/null +++ b/miio/click_common.py @@ -0,0 +1,46 @@ +"""Click commons. + +This file contains common functions for cli tools. +""" +import sys +if sys.version_info < (3, 4): + print("To use this script you need python 3.4 or newer, got %s" % + sys.version_info) + sys.exit(1) +import click +import ipaddress +import miio +import logging + + +_LOGGER = logging.getLogger(__name__) + + +def validate_ip(ctx, param, value): + try: + ipaddress.ip_address(value) + return value + except ValueError as ex: + raise click.BadParameter("Invalid IP: %s" % ex) + + +def validate_token(ctx, param, value): + token_len = len(value) + if token_len != 32: + raise click.BadParameter("Token length != 32 chars: %s" % token_len) + return value + + +class ExceptionHandlerGroup(click.Group): + """Add a simple group for catching the miio-related exceptions. + + This simplifies catching the exceptions from different click commands. + + Idea from https://stackoverflow.com/a/44347763 + """ + def __call__(self, *args, **kwargs): + try: + return self.main(*args, **kwargs) + except miio.DeviceException as ex: + _LOGGER.debug("Exception: %s", ex, exc_info=True) + click.echo(click.style("Error: %s" % ex, fg='red', bold=True)) diff --git a/miio/device.py b/miio/device.py index cb279efc8..1093c6529 100644 --- a/miio/device.py +++ b/miio/device.py @@ -2,6 +2,7 @@ import datetime import socket import logging +import construct from typing import Any, List, Optional # noqa: F401 from .protocol import Message @@ -14,6 +15,11 @@ class DeviceException(Exception): pass +class DeviceError(DeviceException): + """Exception communicating an error delivered by the target device.""" + pass + + class DeviceInfo: """Container of miIO device information. Hardware properties such as device model, MAC address, memory information, @@ -242,10 +248,17 @@ def send(self, command: str, parameters: Any=None, retry_count=3) -> Any: m.header.value.ts, m.data.value["id"], m.data.value) + if "error" in m.data.value: + raise DeviceError(m.data.value["error"]) + try: return m.data.value["result"] except KeyError: return m.data.value + except construct.core.ChecksumError as ex: + raise DeviceException("Got checksum error which indicates use " + "of an invalid token. " + "Please check your token!") from ex except OSError as ex: _LOGGER.error("Got error when receiving: %s", ex) if retry_count > 0: @@ -253,7 +266,7 @@ def send(self, command: str, parameters: Any=None, retry_count=3) -> Any: "retries left: %s", retry_count) self.__id += 100 return self.send(command, parameters, retry_count - 1) - raise DeviceException from ex + raise DeviceException("No response from the device") from ex def raw_command(self, cmd, params): """Send a raw command to the device. diff --git a/miio/philips_eyecare_cli.py b/miio/philips_eyecare_cli.py index c60239c34..0dbd988e1 100644 --- a/miio/philips_eyecare_cli.py +++ b/miio/philips_eyecare_cli.py @@ -2,15 +2,11 @@ import logging import click import sys -import ipaddress - -if sys.version_info < (3, 4): - print("To use this script you need python 3.4 or newer, got %s" % - sys.version_info) - sys.exit(1) - +from miio.click_common import (ExceptionHandlerGroup, validate_ip, + validate_token) import miio # noqa: E402 + _LOGGER = logging.getLogger(__name__) pass_dev = click.make_pass_decorator(miio.PhilipsEyecare) @@ -36,22 +32,7 @@ def validate_scene(ctx, param, value): return value -def validate_ip(ctx, param, value): - try: - ipaddress.ip_address(value) - return value - except ValueError as ex: - raise click.BadParameter("Invalid IP: %s" % ex) - - -def validate_token(ctx, param, value): - token_len = len(value) - if token_len != 32: - raise click.BadParameter("Token length != 32 chars: %s" % token_len) - return value - - -@click.group(invoke_without_command=True) +@click.group(invoke_without_command=True, cls=ExceptionHandlerGroup) @click.option('--ip', envvar="DEVICE_IP", callback=validate_ip) @click.option('--token', envvar="DEVICE_TOKEN", callback=validate_token) @click.option('-d', '--debug', default=False, count=True) diff --git a/miio/plug_cli.py b/miio/plug_cli.py index 67995a16a..2564a71d8 100644 --- a/miio/plug_cli.py +++ b/miio/plug_cli.py @@ -3,36 +3,17 @@ import click import ast import sys -import ipaddress from typing import Any # noqa: F401 - -if sys.version_info < (3, 4): - print("To use this script you need python 3.4 or newer, got %s" % - sys.version_info) - sys.exit(1) - +from miio.click_common import (ExceptionHandlerGroup, validate_ip, + validate_token) import miio # noqa: E402 + _LOGGER = logging.getLogger(__name__) pass_dev = click.make_pass_decorator(miio.Plug) -def validate_ip(ctx, param, value): - try: - ipaddress.ip_address(value) - return value - except ValueError as ex: - raise click.BadParameter("Invalid IP: %s" % ex) - - -def validate_token(ctx, param, value): - token_len = len(value) - if token_len != 32: - raise click.BadParameter("Token length != 32 chars: %s" % token_len) - return value - - -@click.group(invoke_without_command=True) +@click.group(invoke_without_command=True, cls=ExceptionHandlerGroup) @click.option('--ip', envvar="DEVICE_IP", callback=validate_ip) @click.option('--token', envvar="DEVICE_TOKEN", callback=validate_token) @click.option('-d', '--debug', default=False, count=True) diff --git a/miio/vacuum_cli.py b/miio/vacuum_cli.py index 3a231e2f4..15cbd5566 100644 --- a/miio/vacuum_cli.py +++ b/miio/vacuum_cli.py @@ -5,45 +5,20 @@ import ast import sys import json -import ipaddress import time import pathlib from appdirs import user_cache_dir from pprint import pformat as pf from typing import Any # noqa: F401 - - -if sys.version_info < (3, 4): - print("To use this script you need python 3.4 or newer, got %s" % - sys.version_info) - sys.exit(1) - +from miio.click_common import (ExceptionHandlerGroup, validate_ip, + validate_token) import miio # noqa: E402 _LOGGER = logging.getLogger(__name__) pass_dev = click.make_pass_decorator(miio.Device, ensure=True) -def validate_ip(ctx, param, value): - if value is None: - return value - try: - ipaddress.ip_address(value) - return value - except ValueError as ex: - raise click.BadParameter("Invalid IP: %s" % ex) - - -def validate_token(ctx, param, value): - if value is None: - return value - token_len = len(value) - if token_len != 32: - raise click.BadParameter("Token length != 32 chars: %s" % token_len) - return value - - -@click.group(invoke_without_command=True) +@click.group(invoke_without_command=True, cls=ExceptionHandlerGroup) @click.option('--ip', envvar="MIROBO_IP", callback=validate_ip) @click.option('--token', envvar="MIROBO_TOKEN", callback=validate_token) @click.option('-d', '--debug', default=False, count=True)