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

Support of the unified command line interface for all devices #289

Merged
merged 18 commits into from
Apr 8, 2018
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
1 change: 1 addition & 0 deletions miio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@
from miio.wifirepeater import WifiRepeater
from miio.wifispeaker import WifiSpeaker
from miio.yeelight import Yeelight

Copy link
Owner

Choose a reason for hiding this comment

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

Remove the newline.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I made this newline because miio.discovery cannot be moved to line 10. The position of this import is important. The newline shall help here. ;-)

Copy link
Owner

Choose a reason for hiding this comment

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

Hmm, I'm not following? Why it cannot be moved/why it is important?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

If I move the discovery import to the top the tests are failing:

_______________________________________________________________________________________ ERROR collecting miio/tests/test_airconditioningcompanion.py _________________________________________________________________________________________
ImportError while importing test module '/home/sebastian/src/python-miio/miio/tests/test_airconditioningcompanion.py'.                                                                                                                         
Hint: make sure your test modules/packages have valid Python names.                                                                                                                                                                            
Traceback:                                                                                                                                                                                                                                     
miio/__init__.py:2: in <module>                                                                                                                                                                                                                
    from miio.discovery import Discovery                                                                                                                                                                                                       
miio/discovery.py:10: in <module>                                                                                                                                                                                                              
    from . import (Device, Vacuum, ChuangmiPlug, PowerStrip, AirPurifier, Ceil,                                                                                                                                                                
E   ImportError: cannot import name 'Device'                                                                                                                                                                                                   
______________________________________________________________________________________________ ERROR collecting miio/tests/test_airhumidifier.py ______________________________________________________________________________________________
ImportError while importing test module '/home/sebastian/src/python-miio/miio/tests/test_airhumidifier.py'.                                                                                                                                    
Hint: make sure your test modules/packages have valid Python names.                                                                                                                                                                            
Traceback:                                                                                                                                                                                                                                     
miio/__init__.py:2: in <module>                                                                                                                                                                                                                
    from miio.discovery import Discovery                                                                                                                                                                                                       
miio/discovery.py:10: in <module>                                                                                                                                                                                                              
    from . import (Device, Vacuum, ChuangmiPlug, PowerStrip, AirPurifier, Ceil,                                                                                                                                                                
E   ImportError: cannot import name 'Device'

Copy link
Owner

Choose a reason for hiding this comment

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

Ohh, I see. I just didn't understand why the new line was necessary (as it should be just about the order, right?), but this is fine for me.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Correct! :-)

from miio.discovery import Discovery
73 changes: 63 additions & 10 deletions miio/airconditioningcompanion.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import enum
from typing import Optional

import click

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


Expand Down Expand Up @@ -166,50 +169,104 @@ def __repr__(self) -> str:
self.mode)
return s

def __json__(self):
return self.data


class AirConditioningCompanion(Device):
"""Main class representing Xiaomi Air Conditioning Companion."""

@command(
default_output=format_output(
"",
Copy link
Owner

Choose a reason for hiding this comment

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

Maybe it would make sense to use a keyword argument here, otherwise it remains unclear what does "" do.

"Power: {result.power}\n"
"Load power: {result.load_power}\n"
"Air Condition model: {result.air_condition_model}\n"
"LED: {result.led}\n"
"Target temperature: {result.target_temperature} °C\n"
"Swing mode: {result.swing_mode}\n"
"Fan speed: {result.fan_speed}\n"
"Mode: {result.mode}\n"
)
)
def status(self) -> AirConditioningCompanionStatus:
"""Return device status."""
status = self.send("get_model_and_state", [])
return AirConditioningCompanionStatus(status)

@command(
default_output=format_output("Powering the air condition on"),
)
Copy link
Owner

Choose a reason for hiding this comment

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

When it all fits into one line (and there are no arguments), I think it'd be nice to have it all on a single line.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Are you sure? It feels hard to read because of the braces and the little spaces.

Copy link
Owner

Choose a reason for hiding this comment

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

Well, I think it would be nicer for grepping the code, but it's not so important and can be changed later if needed.

def on(self):
"""Turn the air condition on by infrared."""
return self.send("set_power", ["on"])

@command(
default_output=format_output("Powering the air condition off"),
)
def off(self):
"""Turn the air condition off by infared."""
"""Turn the air condition off by infrared."""
return self.send("set_power", ["off"])

@command(
click.argument("slot", type=int),
default_output=format_output(
"Learning infrared command into storage slot {slot}")
)
def learn(self, slot: int=STORAGE_SLOT_ID):
"""Learn an infrared command."""
return self.send("start_ir_learn", [slot])

@command(
default_output=format_output("Reading learned infrared commands")
)
def learn_result(self):
"""Read the learned command."""
return self.send("get_ir_learn_result", [])

@command(
click.argument("slot", type=int),
default_output=format_output(
"Learning infrared command into storage slot {slot} stopped")
)
def learn_stop(self, slot: int=STORAGE_SLOT_ID):
"""Stop learning of a infrared command."""
return self.send("end_ir_learn", [slot])

@command(
click.argument("command", type=str),
default_output=format_output("Sending the supplied infrared command")
)
def send_ir_code(self, command: str):
"""Play a captured command.

:param str command: Command to execute"""
return self.send("send_ir_code", [str(command)])

@command(
click.argument("command", type=str),
default_output=format_output("Sending a command to the air conditioner")
)
def send_command(self, command: str):
"""Send a command to the air conditioner.

:param str command: Command to execute"""
return self.send("send_cmd", [str(command)])

@command(
click.argument("model", type=str),
click.argument("power", type=EnumType(Power, False)),
click.argument("operation_mode", type=EnumType(OperationMode, False)),
click.argument("target_temperature", type=int),
click.argument("fan_speed", type=EnumType(FanSpeed, False)),
click.argument("swing_mode", type=EnumType(SwingMode, False)),
click.argument("led", type=EnumType(Led, False)),
default_output=format_output(
"Sending a configuration to the air conditioner")
)
def send_configuration(self, model: str, power: Power,
operation_mode: OperationMode,
target_temperature: float, fan_speed: FanSpeed,
target_temperature: int, fan_speed: FanSpeed,
swing_mode: SwingMode, led: Led):

prefix = str(model[0:2] + model[8:16])
Expand All @@ -231,20 +288,16 @@ def send_configuration(self, model: str, power: Power,
configuration = configuration.replace('[mo]', str(operation_mode.value))
configuration = configuration.replace('[wi]', str(fan_speed.value))
configuration = configuration.replace('[sw]', str(swing_mode.value))
configuration = configuration.replace(
'[tt]', hex(int(target_temperature))[2:])
configuration = configuration.replace('[tt]', format(target_temperature, 'X'))
configuration = configuration.replace('[li]', str(led.value))

temperature = (1 + int(target_temperature) - 17) % 16
temperature = hex(temperature)[2:].upper()
temperature = format((1 + target_temperature - 17) % 16, 'X')
configuration = configuration.replace('[tt1]', temperature)

temperature = (4 + int(target_temperature) - 17) % 16
temperature = hex(temperature)[2:].upper()
temperature = format((4 + target_temperature - 17) % 16, 'X')
configuration = configuration.replace('[tt4]', temperature)

temperature = (7 + int(target_temperature) - 17) % 16
temperature = hex(temperature)[2:].upper()
temperature = format((7 + target_temperature - 17) % 16, 'X')
configuration = configuration.replace('[tt7]', temperature)

configuration = configuration + suffix
Expand Down
66 changes: 66 additions & 0 deletions miio/airhumidifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
from collections import defaultdict
from typing import Any, Dict, Optional

import click

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

_LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -160,10 +163,33 @@ def __repr__(self) -> str:
self.button_pressed)
return s

def __json__(self):
return self.data


class AirHumidifier(Device):
"""Implementation of Xiaomi Mi Air Humidifier."""

@command(
default_output=format_output(
"",
"Power: {result.power}\n"
"Mode: {result.mode}\n"
"Temperature: {result.temperature} °C\n"
"Humidity: {result.humidity} %\n"
"LED brightness: {result.led_brightness}\n"
"Buzzer: {result.buzzer}\n"
"Child lock: {result.child_lock}\n"
"Target humidity: {result.target_humidity} %\n"
"Trans level: {result.trans_level}\n"
"Speed: {result.speed}\n"
"Depth: {result.depth}\n"
"Dry: {result.dry}\n"
"Use time: {result.use_time}\n"
"Hardware version: {result.hardware_version}\n"
"Button pressed: {result.button_pressed}\n"
)
)
def status(self) -> AirHumidifierStatus:
"""Retrieve properties."""

Expand All @@ -188,36 +214,69 @@ def status(self) -> AirHumidifierStatus:
return AirHumidifierStatus(
defaultdict(lambda: None, zip(properties, values)))

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

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

@command(
click.argument("mode", type=EnumType(OperationMode, False)),
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("brightness", type=EnumType(LedBrightness, False)),
default_output=format_output(
"Setting LED brightness to {brightness}")
)
def set_led_brightness(self, brightness: LedBrightness):
"""Set led brightness."""
return self.send("set_led_b", [brightness.value])

@command(
click.argument("buzzer", type=bool),
default_output=format_output(
lambda buzzer: "Turning on buzzer"
if buzzer else "Turning off buzzer"
)
)
def set_buzzer(self, buzzer: bool):
"""Set buzzer on/off."""
if buzzer:
return self.send("set_buzzer", ["on"])
else:
return self.send("set_buzzer", ["off"])

@command(
click.argument("lock", type=bool),
default_output=format_output(
lambda lock: "Turning on child lock"
if lock else "Turning off child lock"
)
)
def set_child_lock(self, lock: bool):
"""Set child lock on/off."""
if lock:
return self.send("set_child_lock", ["on"])
else:
return self.send("set_child_lock", ["off"])

@command(
click.argument("humidity", type=int),
default_output=format_output("Setting target humidity to {humidity}")
)
def set_target_humidity(self, humidity: int):
"""Set the target humidity."""
if humidity not in [30, 40, 50, 60, 70, 80]:
Expand All @@ -226,6 +285,13 @@ def set_target_humidity(self, humidity: int):

return self.send("set_limit_hum", [humidity])

@command(
click.argument("dry", type=bool),
default_output=format_output(
lambda dry: "Turning on dry mode"
if dry else "Turning off dry mode"
)
)
def set_dry(self, dry: bool):
"""Set dry mode on/off."""
if dry:
Expand Down
4 changes: 2 additions & 2 deletions miio/airpurifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,9 +387,9 @@ class AirPurifier(Device):
"Sound volume: {result.volume} %\n"
"Filter RFID product id: {result.filter_rfid_product_id}\n"
"Filter RFID tag: {result.filter_rfid_tag}\n"
"Filter type: {result.filter_type.value}\n"
"Filter type: {result.filter_type}\n"
"Learn mode: {result.learn_mode}\n"
"Sleep mode: {result.sleep_mode.value}\n"
"Sleep mode: {result.sleep_mode}\n"
"Sleep time: {result.sleep_time}\n"
"Sleep mode learn count: {result.sleep_mode_learn_count}\n"
"AQI sensor enabled on power off: {result.auto_detect}\n"
Expand Down
54 changes: 51 additions & 3 deletions miio/airqualitymonitor.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import logging
from collections import defaultdict

from .device import Device, DeviceException
import click

_LOGGER = logging.getLogger(__name__)
from .click_common import command, format_output
from .device import Device, DeviceException


class AirQualityMonitorException(DeviceException):
Expand Down Expand Up @@ -80,10 +80,23 @@ def __repr__(self) -> str:
self.display_clock)
return s

def __json__(self):
return self.data


class AirQualityMonitor(Device):
"""Xiaomi PM2.5 Air Quality Monitor."""

@command(
default_output=format_output(
"",
"Power: {result.power}\n"
"USB power: {result.usb_power}\n"
"AQI: {result.aqi}\n"
"Battery: {result.battery}\n"
"Display clock: {result.display_clock}\n"
)
)
def status(self) -> AirQualityMonitorStatus:
"""Return device status."""

Expand All @@ -107,35 +120,70 @@ def status(self) -> AirQualityMonitorStatus:
return AirQualityMonitorStatus(
defaultdict(lambda: None, zip(properties, values)))

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

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

@command(
click.argument("display_clock", type=bool),
default_output=format_output(
lambda led: "Turning on display clock"
if led else "Turning off display clock"
)
)
def set_display_clock(self, display_clock: bool):
"""Enable/disable displaying a clock instead the AQI."""
if display_clock:
self.send("set_time_state", ["on"])
else:
self.send("set_time_state", ["off"])

@command(
click.argument("auto_close", type=bool),
default_output=format_output(
lambda led: "Turning on auto close"
if led else "Turning off auto close"
)
)
def set_auto_close(self, auto_close: bool):
"""Purpose unknown."""
if auto_close:
self.send("set_auto_close", ["on"])
else:
self.send("set_auto_close", ["off"])

@command(
click.argument("night_mode", type=bool),
default_output=format_output(
lambda led: "Turning on night mode"
if led else "Turning off night mode"
)
)
def set_night_mode(self, night_mode: bool):
"""Decrease the brightness of the display."""
if night_mode:
self.send("set_night_state", ["on"])
else:
self.send("set_night_state", ["off"])

@command(
click.argument("begin_hour", type=int),
click.argument("begin_minute", type=int),
click.argument("end_hour", type=int),
click.argument("end_minute", type=int),
default_output=format_output(
"Setting night time to {begin_hour}:{begin_minute} - {end_hour}:{end_minute}")
)
def set_night_time(self, begin_hour: int, begin_minute: int,
end_hour: int, end_minute: int):
"""Enable night mode daily at bedtime."""
Expand Down
Loading