mirror of
https://github.com/python-kasa/python-kasa.git
synced 2024-12-23 03:33:35 +00:00
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:
parent
86f14642c8
commit
09e8948790
100
pyHS100/cli.py
100
pyHS100/cli.py
@ -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()
|
||||||
|
@ -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.")
|
||||||
|
@ -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:
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
Loading…
Reference in New Issue
Block a user