generalize smartdevice class and add bulb support for the cli tool (#50)

Fixes #48 and #51. The basic functionality should work on all types of supported devices, for bulb specific commands it is currently necessary to specify ```--bulb```.
This commit is contained in:
Teemu R 2017-04-24 19:28:22 +02:00 committed by GitHub
parent 86f14642c8
commit 09e8948790
5 changed files with 204 additions and 92 deletions

View File

@ -9,16 +9,18 @@ if sys.version_info < (3, 4):
sys.version_info) sys.version_info)
sys.exit(1) sys.exit(1)
from pyHS100 import SmartPlug, TPLinkSmartHomeProtocol # noqa: E402 from pyHS100 import (SmartDevice, SmartPlug, SmartBulb,
TPLinkSmartHomeProtocol) # noqa: E402
pass_dev = click.make_pass_decorator(SmartPlug) pass_dev = click.make_pass_decorator(SmartDevice)
@click.group(invoke_without_command=True) @click.group(invoke_without_command=True)
@click.option('--ip', envvar="PYHS100_IP", required=False) @click.option('--ip', envvar="PYHS100_IP", required=False)
@click.option('--debug/--normal', default=False) @click.option('--debug/--normal', default=False)
@click.option('--bulb', default=False, is_flag=True)
@click.pass_context @click.pass_context
def cli(ctx, ip, debug): def cli(ctx, ip, debug, bulb):
"""A cli tool for controlling TP-Link smart home plugs.""" """A cli tool for controlling TP-Link smart home plugs."""
if debug: if debug:
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
@ -32,8 +34,11 @@ def cli(ctx, ip, debug):
click.echo("You must specify the IP!") click.echo("You must specify the IP!")
sys.exit(-1) sys.exit(-1)
plug = SmartPlug(ip) if bulb:
ctx.obj = plug dev = SmartBulb(ip)
else:
dev = SmartPlug(ip)
ctx.obj = dev
if ctx.invoked_subcommand is None: if ctx.invoked_subcommand is None:
ctx.invoke(state) ctx.invoke(state)
@ -50,29 +55,31 @@ def discover(timeout):
@cli.command() @cli.command()
@pass_dev @pass_dev
def sysinfo(plug): def sysinfo(dev):
"""Print out full system information.""" """Print out full system information."""
click.echo(click.style("== System info ==", bold=True)) click.echo(click.style("== System info ==", bold=True))
click.echo(pformat(plug.sys_info)) click.echo(pformat(dev.sys_info))
@cli.command() @cli.command()
@pass_dev @pass_dev
@click.pass_context @click.pass_context
def state(ctx, plug): def state(ctx, dev):
"""Print out device state and versions.""" """Print out device state and versions."""
click.echo(click.style("== %s - %s ==" % (plug.alias, plug.model), click.echo(click.style("== %s - %s ==" % (dev.alias, dev.model),
bold=True)) bold=True))
click.echo(click.style("Device state: %s" % plug.state, click.echo(click.style("Device state: %s" % "ON" if dev.is_on else "OFF",
fg="green" if plug.is_on else "red")) fg="green" if dev.is_on else "red"))
click.echo("LED state: %s" % plug.led) for k, v in dev.state_information.items():
click.echo("Time: %s" % plug.time) click.echo("%s: %s" % (k, v))
click.echo("On since: %s" % plug.on_since) click.echo(click.style("== Generic information ==", bold=True))
click.echo("Hardware: %s" % plug.hw_info["hw_ver"]) click.echo("Time: %s" % dev.time)
click.echo("Software: %s" % plug.hw_info["sw_ver"]) click.echo("Hardware: %s" % dev.hw_info["hw_ver"])
click.echo("MAC (rssi): %s (%s)" % (plug.mac, plug.rssi)) click.echo("Software: %s" % dev.hw_info["sw_ver"])
click.echo("Location: %s" % plug.location) click.echo("MAC (rssi): %s (%s)" % (dev.mac, dev.rssi))
click.echo("Location: %s" % dev.location)
ctx.invoke(emeter) ctx.invoke(emeter)
@ -83,37 +90,74 @@ def state(ctx, plug):
@click.option('--month', type=Datetime(format='%Y-%m'), @click.option('--month', type=Datetime(format='%Y-%m'),
default=None, required=False) default=None, required=False)
@click.option('--erase', is_flag=True) @click.option('--erase', is_flag=True)
def emeter(plug, year, month, erase): def emeter(dev, year, month, erase):
"""Query emeter for historical consumption.""" """Query emeter for historical consumption."""
click.echo(click.style("== Emeter ==", bold=True)) click.echo(click.style("== Emeter ==", bold=True))
if not plug.has_emeter: if not dev.has_emeter:
click.echo("Device has no emeter") click.echo("Device has no emeter")
return return
if erase: if erase:
click.echo("Erasing emeter statistics..") click.echo("Erasing emeter statistics..")
plug.erase_emeter_stats() dev.erase_emeter_stats()
return return
click.echo("Current state: %s" % plug.get_emeter_realtime()) click.echo("Current state: %s" % dev.get_emeter_realtime())
if year: if year:
click.echo("== For year %s ==" % year.year) click.echo("== For year %s ==" % year.year)
click.echo(plug.get_emeter_monthly(year.year)) click.echo(dev.get_emeter_monthly(year.year))
elif month: elif month:
click.echo("== For month %s of %s ==" % (month.month, month.year)) click.echo("== For month %s of %s ==" % (month.month, month.year))
plug.get_emeter_daily(year=month.year, month=month.month) dev.get_emeter_daily(year=month.year, month=month.month)
@cli.command()
@click.argument("brightness", type=click.IntRange(0, 100), default=None)
@pass_dev
def brightness(dev, value):
"""Get or set brightness. (Bulb Only)"""
if value is None:
click.echo("Brightness: %s" % dev.brightness)
else:
click.echo("Setting brightness to %s" % value)
dev.brightness = value
@cli.command()
@click.argument("temperature", type=click.IntRange(2700, 6500), default=None)
@pass_dev
def temperature(dev, value):
"""Get or set color temperature. (Bulb only)"""
if value is None:
click.echo("Color temperature: %s" % dev.color_temp)
else:
click.echo("Setting color temperature to %s" % value)
dev.color_temp = value
@cli.command()
@click.argument("h", type=click.IntRange(0, 255), default=None)
@click.argument("s", type=click.IntRange(0, 255), default=None)
@click.argument("v", type=click.IntRange(0, 255), default=None)
def hsv(dev, h, s, v):
"""Get or set color in HSV. (Bulb only)"""
if h is None or s is None or v is None:
click.echo("Current HSV: %s" % dev.hsv)
else:
click.echo("Setting HSV: %s %s %s" % (h, s, v))
dev.hsv = (h, s, v)
@cli.command() @cli.command()
@click.argument('state', type=bool, required=False) @click.argument('state', type=bool, required=False)
@pass_dev @pass_dev
def led(plug, state): def led(dev, state):
"""Get or set led state.""" """Get or set led state. (Plug only)"""
if state is not None: if state is not None:
click.echo("Turning led to %s" % state) click.echo("Turning led to %s" % state)
plug.led = state dev.led = state
else: else:
click.echo("LED state: %s" % plug.led) click.echo("LED state: %s" % dev.led)
@cli.command() @cli.command()

View File

@ -16,6 +16,7 @@ http://www.apache.org/licenses/LICENSE-2.0
import datetime import datetime
import logging import logging
import socket import socket
import enum
from .protocol import TPLinkSmartHomeProtocol from .protocol import TPLinkSmartHomeProtocol
@ -29,7 +30,20 @@ class SmartPlugException(Exception):
pass pass
class DeviceType(enum.Enum):
Unknown = -1,
Plug = 0,
Switch = 1
Bulb = 2
class SmartDevice(object): class SmartDevice(object):
# possible device features
FEATURE_ENERGY_METER = 'ENE'
FEATURE_TIMER = 'TIM'
ALL_FEATURES = (FEATURE_ENERGY_METER, FEATURE_TIMER)
def __init__(self, ip_address, protocol=None): def __init__(self, ip_address, protocol=None):
""" """
Create a new SmartDevice instance, identified through its IP address. Create a new SmartDevice instance, identified through its IP address.
@ -78,32 +92,43 @@ class SmartDevice(object):
return result return result
@property
def features(self):
"""
Returns features of the devices
:return: list of features
:rtype: list
"""
features = self.sys_info['feature'].split(':')
for feature in features:
if feature not in SmartDevice.ALL_FEATURES:
_LOGGER.warning("Unknown feature %s on device %s.",
feature, self.model)
return features
@property
def has_emeter(self):
"""
Checks feature list for energey meter support.
:return: True if energey meter is available
False if energymeter is missing
"""
return SmartDevice.FEATURE_ENERGY_METER in self.features
@property @property
def sys_info(self): def sys_info(self):
# TODO use volyptuous """
Returns the complete system information from the device.
:return: System information dict.
:rtype: dict
"""
return self.get_sysinfo() return self.get_sysinfo()
@property
def is_off(self):
"""
Returns whether device is off.
:return: True if device is off, False otherwise.
:rtype: bool
"""
return not self.is_on
@property
def is_on(self):
"""
Returns whether the device is on.
:return: True if the device is on, False otherwise.
:rtype: bool
:return:
"""
raise NotImplementedError("Your subclass needs to implement this.")
def get_sysinfo(self): def get_sysinfo(self):
""" """
Retrieve system information. Retrieve system information.
@ -402,3 +427,45 @@ class SmartDevice(object):
return response['power_mw'] return response['power_mw']
else: else:
return response['power'] return response['power']
def turn_off(self):
"""
Turns the device off.
"""
raise NotImplementedError("Device subclass needs to implement this.")
@property
def is_off(self):
"""
Returns whether device is off.
:return: True if device is off, False otherwise.
:rtype: bool
"""
return not self.is_on
def turn_on(self):
"""
Turns the device on.
"""
raise NotImplementedError("Device subclass needs to implement this.")
@property
def is_on(self):
"""
Returns whether the device is on.
:return: True if the device is on, False otherwise.
:rtype: bool
:return:
"""
raise NotImplementedError("Device subclass needs to implement this.")
@property
def state_information(self):
"""
Returns device-type specific, end-user friendly state information.
:return: dict with state information.
:rtype: dict
"""
raise NotImplementedError("Device subclass needs to implement this.")

View File

@ -106,7 +106,7 @@ class SmartBulb(SmartDevice):
saturation = light_state['saturation'] saturation = light_state['saturation']
value = int(light_state['brightness'] * 255 / 100) value = int(light_state['brightness'] * 255 / 100)
return(hue, saturation, value) return hue, saturation, value
@hsv.setter @hsv.setter
def hsv(self, state): def hsv(self, state):
@ -139,9 +139,9 @@ class SmartBulb(SmartDevice):
light_state = self.get_light_state() light_state = self.get_light_state()
if not self.is_on: if not self.is_on:
return(light_state['dft_on_state']['color_temp']) return light_state['dft_on_state']['color_temp']
else: else:
return(light_state['color_temp']) return light_state['color_temp']
@color_temp.setter @color_temp.setter
def color_temp(self, temp): def color_temp(self, temp):
@ -171,9 +171,9 @@ class SmartBulb(SmartDevice):
light_state = self.get_light_state() light_state = self.get_light_state()
if not self.is_on: if not self.is_on:
return(light_state['dft_on_state']['brightness']) return light_state['dft_on_state']['brightness']
else: else:
return(light_state['brightness']) return light_state['brightness']
@brightness.setter @brightness.setter
def brightness(self, brightness): def brightness(self, brightness):
@ -205,10 +205,40 @@ class SmartBulb(SmartDevice):
return self.BULB_STATE_ON return self.BULB_STATE_ON
return self.BULB_STATE_OFF return self.BULB_STATE_OFF
@property
def state_information(self):
"""
Return bulb-specific state information.
:return: Bulb information dict, keys in user-presentable form.
:rtype: dict
"""
info = {
'Brightness': self.brightness,
'Is dimmable': self.is_dimmable,
}
if self.is_variable_color_temp:
info["Color temperature"] = self.color_temp
if self.is_color:
info["HSV"] = self.hsv
return info
@property @property
def is_on(self): def is_on(self):
return self.state == self.BULB_STATE_ON return self.state == self.BULB_STATE_ON
def turn_off(self):
"""
Turn the bulb off.
"""
self.state = self.BULB_STATE_OFF
def turn_on(self):
"""
Turn the bulb on.
"""
self.state = self.BULB_STATE_ON
@state.setter @state.setter
def state(self, bulb_state): def state(self, bulb_state):
""" """
@ -218,9 +248,6 @@ class SmartBulb(SmartDevice):
BULB_STATE_ON BULB_STATE_ON
BULB_STATE_OFF BULB_STATE_OFF
""" """
print(bulb_state)
print(self.BULB_STATE_ON)
print(self.BULB_STATE_OFF)
if bulb_state == self.BULB_STATE_ON: if bulb_state == self.BULB_STATE_ON:
bulb_state = 1 bulb_state = 1
elif bulb_state == self.BULB_STATE_OFF: elif bulb_state == self.BULB_STATE_OFF:

View File

@ -30,12 +30,6 @@ class SmartPlug(SmartDevice):
SWITCH_STATE_OFF = 'OFF' SWITCH_STATE_OFF = 'OFF'
SWITCH_STATE_UNKNOWN = 'UNKNOWN' SWITCH_STATE_UNKNOWN = 'UNKNOWN'
# possible device features
FEATURE_ENERGY_METER = 'ENE'
FEATURE_TIMER = 'TIM'
ALL_FEATURES = (FEATURE_ENERGY_METER, FEATURE_TIMER)
def __init__(self, ip_address, protocol=None): def __init__(self, ip_address, protocol=None):
SmartDevice.__init__(self, ip_address, protocol) SmartDevice.__init__(self, ip_address, protocol)
self.emeter_type = "emeter" self.emeter_type = "emeter"
@ -108,33 +102,6 @@ class SmartPlug(SmartDevice):
""" """
self._query_helper("system", "set_relay_state", {"state": 0}) self._query_helper("system", "set_relay_state", {"state": 0})
@property
def has_emeter(self):
"""
Checks feature list for energey meter support.
:return: True if energey meter is available
False if energymeter is missing
"""
return SmartPlug.FEATURE_ENERGY_METER in self.features
@property
def features(self):
"""
Returns features of the devices
:return: list of features
:rtype: list
"""
features = self.sys_info['feature'].split(':')
for feature in features:
if feature not in SmartPlug.ALL_FEATURES:
_LOGGER.warning("Unknown feature %s on device %s.",
feature, self.model)
return features
@property @property
def led(self): def led(self):
""" """
@ -145,6 +112,13 @@ class SmartPlug(SmartDevice):
""" """
return bool(1 - self.sys_info["led_off"]) return bool(1 - self.sys_info["led_off"])
@property
def state_information(self):
return {
'LED state': self.led,
'On since': self.on_since
}
@led.setter @led.setter
def led(self, state): def led(self, state):
""" """