diff --git a/pyHS100/__init__.py b/pyHS100/__init__.py index e8345dc7..51a64d35 100644 --- a/pyHS100/__init__.py +++ b/pyHS100/__init__.py @@ -2,4 +2,4 @@ from .smartplug import SmartPlug from .pyHS100 import SmartPlugException, SmartDevice from .smartbulb import SmartBulb -from .protocol import TPLinkSmartHomeProtocol \ No newline at end of file +from .protocol import TPLinkSmartHomeProtocol diff --git a/pyHS100/cli.py b/pyHS100/cli.py index afad864f..2ab9a2b3 100644 --- a/pyHS100/cli.py +++ b/pyHS100/cli.py @@ -9,16 +9,18 @@ if sys.version_info < (3, 4): sys.version_info) 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.option('--ip', envvar="PYHS100_IP", required=False) @click.option('--debug/--normal', default=False) +@click.option('--bulb', default=False, is_flag=True) @click.pass_context -def cli(ctx, ip, debug): +def cli(ctx, ip, debug, bulb): """A cli tool for controlling TP-Link smart home plugs.""" if debug: logging.basicConfig(level=logging.DEBUG) @@ -32,8 +34,11 @@ def cli(ctx, ip, debug): click.echo("You must specify the IP!") sys.exit(-1) - plug = SmartPlug(ip) - ctx.obj = plug + if bulb: + dev = SmartBulb(ip) + else: + dev = SmartPlug(ip) + ctx.obj = dev if ctx.invoked_subcommand is None: ctx.invoke(state) @@ -50,29 +55,31 @@ def discover(timeout): @cli.command() @pass_dev -def sysinfo(plug): +def sysinfo(dev): """Print out full system information.""" click.echo(click.style("== System info ==", bold=True)) - click.echo(pformat(plug.sys_info)) + click.echo(pformat(dev.sys_info)) @cli.command() @pass_dev @click.pass_context -def state(ctx, plug): +def state(ctx, dev): """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)) - click.echo(click.style("Device state: %s" % plug.state, - fg="green" if plug.is_on else "red")) - click.echo("LED state: %s" % plug.led) - click.echo("Time: %s" % plug.time) - click.echo("On since: %s" % plug.on_since) - click.echo("Hardware: %s" % plug.hw_info["hw_ver"]) - click.echo("Software: %s" % plug.hw_info["sw_ver"]) - click.echo("MAC (rssi): %s (%s)" % (plug.mac, plug.rssi)) - click.echo("Location: %s" % plug.location) + click.echo(click.style("Device state: %s" % "ON" if dev.is_on else "OFF", + fg="green" if dev.is_on else "red")) + for k, v in dev.state_information.items(): + click.echo("%s: %s" % (k, v)) + click.echo(click.style("== Generic information ==", bold=True)) + click.echo("Time: %s" % dev.time) + click.echo("Hardware: %s" % dev.hw_info["hw_ver"]) + click.echo("Software: %s" % dev.hw_info["sw_ver"]) + click.echo("MAC (rssi): %s (%s)" % (dev.mac, dev.rssi)) + click.echo("Location: %s" % dev.location) + ctx.invoke(emeter) @@ -83,37 +90,74 @@ def state(ctx, plug): @click.option('--month', type=Datetime(format='%Y-%m'), default=None, required=False) @click.option('--erase', is_flag=True) -def emeter(plug, year, month, erase): +def emeter(dev, year, month, erase): """Query emeter for historical consumption.""" click.echo(click.style("== Emeter ==", bold=True)) - if not plug.has_emeter: + if not dev.has_emeter: click.echo("Device has no emeter") return if erase: click.echo("Erasing emeter statistics..") - plug.erase_emeter_stats() + dev.erase_emeter_stats() return - click.echo("Current state: %s" % plug.get_emeter_realtime()) + click.echo("Current state: %s" % dev.get_emeter_realtime()) if 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: 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() @click.argument('state', type=bool, required=False) @pass_dev -def led(plug, state): - """Get or set led state.""" +def led(dev, state): + """Get or set led state. (Plug only)""" if state is not None: click.echo("Turning led to %s" % state) - plug.led = state + dev.led = state else: - click.echo("LED state: %s" % plug.led) + click.echo("LED state: %s" % dev.led) @cli.command() diff --git a/pyHS100/pyHS100.py b/pyHS100/pyHS100.py index cc7b7b9d..8336e9c5 100644 --- a/pyHS100/pyHS100.py +++ b/pyHS100/pyHS100.py @@ -16,6 +16,7 @@ http://www.apache.org/licenses/LICENSE-2.0 import datetime import logging import socket +import enum from .protocol import TPLinkSmartHomeProtocol @@ -29,7 +30,20 @@ class SmartPlugException(Exception): pass +class DeviceType(enum.Enum): + Unknown = -1, + Plug = 0, + Switch = 1 + Bulb = 2 + + 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): """ Create a new SmartDevice instance, identified through its IP address. @@ -78,32 +92,43 @@ class SmartDevice(object): 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 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() - @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): """ Retrieve system information. @@ -402,3 +427,45 @@ class SmartDevice(object): return response['power_mw'] else: 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.") diff --git a/pyHS100/smartbulb.py b/pyHS100/smartbulb.py index b2fb9271..d71e2871 100644 --- a/pyHS100/smartbulb.py +++ b/pyHS100/smartbulb.py @@ -106,7 +106,7 @@ class SmartBulb(SmartDevice): saturation = light_state['saturation'] value = int(light_state['brightness'] * 255 / 100) - return(hue, saturation, value) + return hue, saturation, value @hsv.setter def hsv(self, state): @@ -139,9 +139,9 @@ class SmartBulb(SmartDevice): light_state = self.get_light_state() if not self.is_on: - return(light_state['dft_on_state']['color_temp']) + return light_state['dft_on_state']['color_temp'] else: - return(light_state['color_temp']) + return light_state['color_temp'] @color_temp.setter def color_temp(self, temp): @@ -171,9 +171,9 @@ class SmartBulb(SmartDevice): light_state = self.get_light_state() if not self.is_on: - return(light_state['dft_on_state']['brightness']) + return light_state['dft_on_state']['brightness'] else: - return(light_state['brightness']) + return light_state['brightness'] @brightness.setter def brightness(self, brightness): @@ -205,10 +205,40 @@ class SmartBulb(SmartDevice): return self.BULB_STATE_ON 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 def is_on(self): 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 def state(self, bulb_state): """ @@ -218,9 +248,6 @@ class SmartBulb(SmartDevice): BULB_STATE_ON BULB_STATE_OFF """ - print(bulb_state) - print(self.BULB_STATE_ON) - print(self.BULB_STATE_OFF) if bulb_state == self.BULB_STATE_ON: bulb_state = 1 elif bulb_state == self.BULB_STATE_OFF: diff --git a/pyHS100/smartplug.py b/pyHS100/smartplug.py index d3fa9b63..264b50e6 100644 --- a/pyHS100/smartplug.py +++ b/pyHS100/smartplug.py @@ -30,12 +30,6 @@ class SmartPlug(SmartDevice): SWITCH_STATE_OFF = 'OFF' 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): SmartDevice.__init__(self, ip_address, protocol) self.emeter_type = "emeter" @@ -108,33 +102,6 @@ class SmartPlug(SmartDevice): """ 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 def led(self): """ @@ -145,6 +112,13 @@ class SmartPlug(SmartDevice): """ 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 def led(self, state): """