Skip to content

Commit

Permalink
Support for configurable output options + JSON output implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
yawor committed Feb 12, 2018
1 parent 71a2f35 commit 3fb5de5
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 47 deletions.
33 changes: 18 additions & 15 deletions miio/airpurifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from collections import defaultdict
import click
from .device import Device, DeviceException
from .click_common import command, echo_return_status
from .click_common import command, format_output

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -324,12 +324,15 @@ def __repr__(self) -> str:
self.auto_detect)
return s

def __json__(self):
return self.data


class AirPurifier(Device):
"""Main class representing the air purifier."""

@command(
echo_return_status(
default_output=format_output(
"",
"Power: {result.power}\n"
"AQI: {result.aqi} μg/m³\n"
Expand Down Expand Up @@ -392,30 +395,30 @@ def status(self) -> AirPurifierStatus:
defaultdict(lambda: None, zip(properties, values)))

@command(
echo_return_status("Powering on"),
default_output=format_output("Powering on"),
)
def on(self):
"""Power on."""
return self.send("set_power", ["on"])

@command(
echo_return_status("Powering off")
format_output("Powering off")
)
def off(self):
"""Power off."""
return self.send("set_power", ["off"])

@command(
click.argument("mode", type=OperationMode),
echo_return_status("Setting mode to '{mode.value}'")
default_output=format_output("Setting mode to '{mode.value}'")
)
def set_mode(self, mode: OperationMode):
"""Set mode."""
return self.send("set_mode", [mode.value])

@command(
click.argument("level", type=int),
echo_return_status("Setting favorite level to {level}")
default_output=format_output("Setting favorite level to {level}")
)
def set_favorite_level(self, level: int):
"""Set favorite level."""
Expand All @@ -430,7 +433,7 @@ def set_favorite_level(self, level: int):

@command(
click.argument("brightness", type=LedBrightness),
echo_return_status(
default_output=format_output(
"Setting LED brightness to {brightness}")
)
def set_led_brightness(self, brightness: LedBrightness):
Expand All @@ -439,7 +442,7 @@ def set_led_brightness(self, brightness: LedBrightness):

@command(
click.argument("led", type=bool),
echo_return_status(
default_output=format_output(
lambda led: "Turning on LED"
if led else "Turning off LED"
)
Expand All @@ -453,7 +456,7 @@ def set_led(self, led: bool):

@command(
click.argument("buzzer", type=bool),
echo_return_status(
default_output=format_output(
lambda buzzer: "Turning on buzzer"
if buzzer else "Turning off buzzer"
)
Expand All @@ -467,7 +470,7 @@ def set_buzzer(self, buzzer: bool):

@command(
click.argument("lock", type=bool),
echo_return_status(
default_output=format_output(
lambda lock: "Turning on child lock"
if lock else "Turning off child lock"
)
Expand All @@ -481,7 +484,7 @@ def set_child_lock(self, lock: bool):

@command(
click.argument("volume", type=int),
echo_return_status("Setting favorite level to {volume}")
default_output=format_output("Setting favorite level to {volume}")
)
def set_volume(self, volume: int):
"""Set volume of sound notifications [0-100]."""
Expand All @@ -492,7 +495,7 @@ def set_volume(self, volume: int):

@command(
click.argument("learn_mode", type=bool),
echo_return_status(
default_output=format_output(
lambda learn_mode: "Turning on learn mode"
if learn_mode else "Turning off learn mode"
)
Expand All @@ -506,7 +509,7 @@ def set_learn_mode(self, learn_mode: bool):

@command(
click.argument("auto_detect", type=bool),
echo_return_status(
default_output=format_output(
lambda auto_detect: "Turning on auto detect"
if auto_detect else "Turning off auto detect"
)
Expand All @@ -520,7 +523,7 @@ def set_auto_detect(self, auto_detect: bool):

@command(
click.argument("value", type=int),
echo_return_status("Setting extra to {value}")
default_output=format_output("Setting extra to {value}")
)
def set_extra_features(self, value: int):
"""Storage register to enable extra features at the app.
Expand All @@ -533,7 +536,7 @@ def set_extra_features(self, value: int):
return self.send("set_app_extra", [value])

@command(
echo_return_status("Resetting filter")
default_output=format_output("Resetting filter")
)
def reset_filter(self):
"""Resets filter hours used and remaining life."""
Expand Down
16 changes: 13 additions & 3 deletions miio/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,34 @@
import logging
import click
from miio.click_common import (
ExceptionHandlerGroup, DeviceGroupMeta, GlobalContextObject
ExceptionHandlerGroup, DeviceGroupMeta, GlobalContextObject,
json_output,
)

_LOGGER = logging.getLogger(__name__)


@click.group(cls=ExceptionHandlerGroup)
@click.option('-d', '--debug', default=False, count=True)
@click.option('-o', '--output', type=click.Choice([
'default', 'json', 'json_pretty',
]), default='default')
@click.pass_context
def cli(ctx, debug: int):
def cli(ctx, debug: int, output: str):
if debug:
logging.basicConfig(level=logging.DEBUG)
_LOGGER.info("Debug mode active")
else:
logging.basicConfig(level=logging.INFO)

if output in ('json', 'json_pretty'):
output_func = json_output(pretty=output == 'json_pretty')
else:
output_func = None

ctx.obj = GlobalContextObject(
debug=debug
debug=debug,
output=output_func,
)


Expand Down
55 changes: 47 additions & 8 deletions miio/click_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
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)
Expand All @@ -11,9 +12,11 @@
import ipaddress
import miio
import logging
import json
from typing import Union
from functools import wraps
from functools import partial
from .exceptions import DeviceError


_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -54,8 +57,9 @@ def __call__(self, *args, **kwargs):


class GlobalContextObject:
def __init__(self, debug: int=0):
def __init__(self, debug: int=0, output: callable=None):
self.debug = debug
self.output = output


class DeviceGroupMeta(type):
Expand Down Expand Up @@ -100,10 +104,12 @@ def get_device_group(dcls):
class DeviceGroup(click.MultiCommand):

class Command:
def __init__(self, name, decorators, **kwargs):
def __init__(self, name, decorators, *, default_output=None, **kwargs):
self.name = name
self.decorators = list(decorators)
self.decorators.reverse()
self.default_output = default_output

self.kwargs = kwargs

def __call__(self, func):
Expand All @@ -116,7 +122,18 @@ def __call__(self, func):
def command_name(self):
return self.name or self.func.__name__.lower()

def wrap(self, func):
def wrap(self, ctx, func):
gco = ctx.find_object(GlobalContextObject)
if gco is not None and gco.output is not None:
output = gco.output
elif self.default_output:
output = self.default_output
else:
output = format_output(
"Running command {0}".format(self.command_name)
)

func = output(func)
for decorator in self.decorators:
func = decorator(func)
return click.command(self.command_name, **self.kwargs)(func)
Expand Down Expand Up @@ -165,20 +182,22 @@ def command_callback(self, command, device, *args, **kwargs):

def get_command(self, ctx, cmd_name):
cmd = self.commands[cmd_name]
return self.commands[cmd_name].wrap(self.device_pass(partial(
return self.commands[cmd_name].wrap(ctx, self.device_pass(partial(
self.command_callback, cmd
)))

def list_commands(self, ctx):
return sorted(self.commands.keys())


def command(*decorators, name=None, **kwargs):
return DeviceGroup.Command(name, decorators, **kwargs)
def command(*decorators, name=None, default_output=None, **kwargs):
return DeviceGroup.Command(
name, decorators, default_output=default_output, **kwargs
)


def echo_return_status(msg_fmt: Union[str, callable]="",
result_msg_fmt: Union[str, callable]="{result}"):
def format_output(msg_fmt: Union[str, callable]= "",
result_msg_fmt: Union[str, callable]="{result}"):
def decorator(func):
@wraps(func)
def wrap(*args, **kwargs):
Expand All @@ -199,3 +218,23 @@ def wrap(*args, **kwargs):
click.echo(result_msg.strip())
return wrap
return decorator


def json_output(pretty=False):
indent = 2 if pretty else None
def decorator(func):
@wraps(func)
def wrap(*args, **kwargs):
try:
result = func(*args, **kwargs)
except DeviceError as ex:
click.echo(json.dumps(ex.args[0], indent=indent))
return

get_json_data_func = getattr(result, '__json__', None)
if get_json_data_func is not None:
result = get_json_data_func()
click.echo(json.dumps(result, indent=indent))

return wrap
return decorator
29 changes: 12 additions & 17 deletions miio/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,14 @@
from typing import Any, List, Optional # noqa: F401

from .click_common import (
DeviceGroupMeta, command, echo_return_status
DeviceGroupMeta, command, format_output
)
from .protocol import Message
from .exceptions import DeviceException, DeviceError

_LOGGER = logging.getLogger(__name__)


class DeviceException(Exception):
"""Exception wrapping any communication errors with the device."""
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,
Expand Down Expand Up @@ -59,6 +50,9 @@ def __repr__(self):
self.network_interface["localIp"],
self.data["token"])

def __json__(self):
return self.data

@property
def network_interface(self):
"""Information about network configuration."""
Expand Down Expand Up @@ -285,12 +279,13 @@ def raw_command(self, cmd, params):
return self.send(cmd, params)

@command(
echo_return_status("",
"Model: {result.model}\n"
"Hardware version: {result.hardware_version}\n"
"Firmware version: {result.firmware_version}\n"
"Network: {result.network_interface}\n"
"AP: {result.accesspoint}\n")
default_output=format_output(
"",
"Model: {result.model}\n"
"Hardware version: {result.hardware_version}\n"
"Firmware version: {result.firmware_version}\n"
"Network: {result.network_interface}\n"
"AP: {result.accesspoint}\n")
)
def info(self) -> DeviceInfo:
"""Get miIO protocol information from the device.
Expand Down
8 changes: 8 additions & 0 deletions miio/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class DeviceException(Exception):
"""Exception wrapping any communication errors with the device."""
pass


class DeviceError(DeviceException):
"""Exception communicating an error delivered by the target device."""
pass
Loading

0 comments on commit 3fb5de5

Please sign in to comment.