mirror of
https://github.com/python-kasa/python-kasa.git
synced 2024-12-22 19:23:34 +00:00
Refactor and drop py2 support (#49)
* move is_off property to SmartDevice, implement is_on for bulb and use it * refactor by moving smartbulb and smartplug to their own classes * drop python2 compatibility, make flake8 happy * travis: remove 2.7, add 3.6
This commit is contained in:
parent
c9662fcb7b
commit
86f14642c8
@ -1,9 +1,9 @@
|
||||
sudo: false
|
||||
language: python
|
||||
python:
|
||||
- "2.7"
|
||||
- "3.4"
|
||||
- "3.5"
|
||||
- "3.6"
|
||||
install: pip install tox-travis
|
||||
script: tox
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
# flake8: noqa
|
||||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
from pyHS100.pyHS100 import SmartBulb, SmartPlug, TPLinkSmartHomeProtocol, SmartPlugException
|
||||
from .smartplug import SmartPlug
|
||||
from .pyHS100 import SmartPlugException, SmartDevice
|
||||
from .smartbulb import SmartBulb
|
||||
from .protocol import TPLinkSmartHomeProtocol
|
@ -4,7 +4,12 @@ import logging
|
||||
from click_datetime import Datetime
|
||||
from pprint import pformat
|
||||
|
||||
from pyHS100 import SmartPlug, TPLinkSmartHomeProtocol
|
||||
if sys.version_info < (3, 4):
|
||||
print("To use this script you need python 3.4 or newer! got %s" %
|
||||
sys.version_info)
|
||||
sys.exit(1)
|
||||
|
||||
from pyHS100 import SmartPlug, TPLinkSmartHomeProtocol # noqa: E402
|
||||
|
||||
pass_dev = click.make_pass_decorator(SmartPlug)
|
||||
|
||||
@ -24,7 +29,7 @@ def cli(ctx, ip, debug):
|
||||
return
|
||||
|
||||
if ip is None:
|
||||
click.echo("You must specify the IP either by --ip or setting PYHS100_IP environment variable!")
|
||||
click.echo("You must specify the IP!")
|
||||
sys.exit(-1)
|
||||
|
||||
plug = SmartPlug(ip)
|
||||
|
@ -1,6 +1,3 @@
|
||||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import json
|
||||
import socket
|
||||
import struct
|
||||
|
@ -13,21 +13,11 @@ Stroetmann which is licensed under the Apache License, Version 2.0.
|
||||
You may obtain a copy of the license at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
"""
|
||||
|
||||
# python2 compatibility
|
||||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
from future.utils import raise_from
|
||||
try:
|
||||
basestring
|
||||
except NameError:
|
||||
basestring = str
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import socket
|
||||
|
||||
from pyHS100.protocol import TPLinkSmartHomeProtocol
|
||||
from .protocol import TPLinkSmartHomeProtocol
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -53,32 +43,35 @@ class SmartDevice(object):
|
||||
protocol = TPLinkSmartHomeProtocol()
|
||||
self.protocol = protocol
|
||||
|
||||
def _query_helper(self, target, cmd, arg={}):
|
||||
def _query_helper(self, target, cmd, arg=None):
|
||||
"""
|
||||
Helper returning unwrapped result object and doing error handling.
|
||||
|
||||
:param target: Target system {system, time, emeter, ..}
|
||||
:param cmd: Command to execute
|
||||
:param arg: JSON object passed as parameter to the command, defaults to {}
|
||||
:param arg: JSON object passed as parameter to the command
|
||||
:return: Unwrapped result for the call.
|
||||
:rtype: dict
|
||||
:raises SmartPlugException: if command was not executed correctly
|
||||
"""
|
||||
|
||||
if arg is None:
|
||||
arg = {}
|
||||
try:
|
||||
response = self.protocol.query(
|
||||
host=self.ip_address,
|
||||
request={target: {cmd: arg}}
|
||||
)
|
||||
except Exception as ex:
|
||||
raise_from(SmartPlugException(), ex)
|
||||
raise SmartPlugException('Communication error') from ex
|
||||
|
||||
if target not in response:
|
||||
raise SmartPlugException("No required {} in response: {}".format(target, response))
|
||||
raise SmartPlugException("No required {} in response: {}"
|
||||
.format(target, response))
|
||||
|
||||
result = response[target]
|
||||
if "err_code" in result and result["err_code"] != 0:
|
||||
raise SmartPlugException("Error on {}.{}: {}".format(target, cmd, result))
|
||||
raise SmartPlugException("Error on {}.{}: {}"
|
||||
.format(target, cmd, result))
|
||||
|
||||
result = result[cmd]
|
||||
del result["err_code"]
|
||||
@ -90,6 +83,27 @@ class SmartDevice(object):
|
||||
# TODO use volyptuous
|
||||
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.
|
||||
@ -167,9 +181,10 @@ class SmartDevice(object):
|
||||
:raises NotImplementedError: when not implemented
|
||||
:raises SmartPlugError: on error
|
||||
"""
|
||||
raise NotImplementedError("Values for this call are unknown at this point.")
|
||||
raise NotImplementedError()
|
||||
# here just for the sake of completeness
|
||||
# self._query_helper("system", "set_dev_icon", {"icon": "", "hash": ""})
|
||||
# self._query_helper("system",
|
||||
# "set_dev_icon", {"icon": "", "hash": ""})
|
||||
# self.initialize()
|
||||
|
||||
@property
|
||||
@ -198,7 +213,10 @@ class SmartDevice(object):
|
||||
:raises SmartPlugException: on error
|
||||
"""
|
||||
raise NotImplementedError("Fails with err_code == 0 with HS110.")
|
||||
""" here just for the sake of completeness / if someone figures out why it doesn't work.
|
||||
"""
|
||||
here just for the sake of completeness.
|
||||
if someone figures out why it doesn't work,
|
||||
please create a PR :-)
|
||||
ts_obj = {
|
||||
"index": self.timezone["index"],
|
||||
"hour": ts.hour,
|
||||
@ -384,407 +402,3 @@ class SmartDevice(object):
|
||||
return response['power_mw']
|
||||
else:
|
||||
return response['power']
|
||||
|
||||
|
||||
class SmartBulb(SmartDevice):
|
||||
"""Representation of a TP-Link Smart Bulb.
|
||||
|
||||
Usage example when used as library:
|
||||
p = SmartBulb("192.168.1.105")
|
||||
# print the devices alias
|
||||
print(p.alias)
|
||||
# change state of bulb
|
||||
p.state = "ON"
|
||||
p.state = "OFF"
|
||||
# query and print current state of plug
|
||||
print(p.state)
|
||||
# check whether the bulb supports color changes
|
||||
if p.is_color:
|
||||
# set the color to an HSV tuple
|
||||
p.hsv = (100, 0, 255)
|
||||
# get the current HSV value
|
||||
print(p.hsv)
|
||||
# check whether the bulb supports setting color temperature
|
||||
if p.is_variable_color_temp:
|
||||
# set the color temperature in Kelvin
|
||||
p.color_temp = 3000
|
||||
# get the current color temperature
|
||||
print(p.color_temp)
|
||||
# check whether the bulb is dimmable
|
||||
if p.is_dimmable:
|
||||
# set the bulb to 50% brightness
|
||||
p.brightness = 50
|
||||
# check the current brightness
|
||||
print(p.brightness)
|
||||
|
||||
Errors reported by the device are raised as SmartPlugExceptions,
|
||||
and should be handled by the user of the library.
|
||||
|
||||
"""
|
||||
# bulb states
|
||||
BULB_STATE_ON = 'ON'
|
||||
BULB_STATE_OFF = 'OFF'
|
||||
|
||||
def __init__(self, ip_address, protocol=None):
|
||||
SmartDevice.__init__(self, ip_address, protocol)
|
||||
self.emeter_type = "smartlife.iot.common.emeter"
|
||||
self.emeter_units = True
|
||||
|
||||
@property
|
||||
def is_color(self):
|
||||
"""
|
||||
Whether the bulb supports color changes
|
||||
|
||||
:return: True if the bulb supports color changes, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
return bool(self.sys_info['is_color'])
|
||||
|
||||
@property
|
||||
def is_dimmable(self):
|
||||
"""
|
||||
Whether the bulb supports brightness changes
|
||||
|
||||
:return: True if the bulb supports brightness changes, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
return bool(self.sys_info['is_dimmable'])
|
||||
|
||||
@property
|
||||
def is_variable_color_temp(self):
|
||||
"""
|
||||
Whether the bulb supports color temperature changes
|
||||
|
||||
:return: True if the bulb supports color temperature changes, False
|
||||
otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
return bool(self.sys_info['is_variable_color_temp'])
|
||||
|
||||
def get_light_state(self):
|
||||
return self._query_helper("smartlife.iot.smartbulb.lightingservice",
|
||||
"get_light_state")
|
||||
|
||||
def set_light_state(self, state):
|
||||
return self._query_helper("smartlife.iot.smartbulb.lightingservice",
|
||||
"transition_light_state", state)
|
||||
|
||||
@property
|
||||
def hsv(self):
|
||||
"""
|
||||
Returns the current HSV state of the bulb, if supported
|
||||
|
||||
:return: tuple containing current hue, saturation and value (0-255)
|
||||
:rtype: tuple
|
||||
"""
|
||||
|
||||
if not self.is_color:
|
||||
return None
|
||||
|
||||
light_state = self.get_light_state()
|
||||
if light_state['on_off'] == 0:
|
||||
hue = light_state['dft_on_state']['hue']
|
||||
saturation = light_state['dft_on_state']['saturation']
|
||||
value = int(light_state['dft_on_state']['brightness'] * 255 / 100)
|
||||
else:
|
||||
hue = light_state['hue']
|
||||
saturation = light_state['saturation']
|
||||
value = int(light_state['brightness'] * 255 / 100)
|
||||
|
||||
return(hue, saturation, value)
|
||||
|
||||
@hsv.setter
|
||||
def hsv(self, state):
|
||||
"""
|
||||
Sets new HSV, if supported
|
||||
|
||||
:param tuple state: hue, saturation and value (0-255 each)
|
||||
"""
|
||||
if not self.is_color:
|
||||
return None
|
||||
|
||||
light_state = {
|
||||
"hue": state[0],
|
||||
"saturation": state[1],
|
||||
"brightness": int(state[2] * 100 / 255),
|
||||
"color_temp": 0
|
||||
}
|
||||
return self.set_light_state(light_state)
|
||||
|
||||
@property
|
||||
def color_temp(self):
|
||||
"""
|
||||
Color temperature of the device, if supported
|
||||
|
||||
:return: Color temperature in Kelvin
|
||||
:rtype: int
|
||||
"""
|
||||
if not self.is_variable_color_temp:
|
||||
return None
|
||||
|
||||
light_state = self.get_light_state()
|
||||
if light_state['on_off'] == 0:
|
||||
return(light_state['dft_on_state']['color_temp'])
|
||||
else:
|
||||
return(light_state['color_temp'])
|
||||
|
||||
@color_temp.setter
|
||||
def color_temp(self, temp):
|
||||
"""
|
||||
Set the color temperature of the device, if supported
|
||||
|
||||
:param int temp: The new color temperature, in Kelvin
|
||||
"""
|
||||
if not self.is_variable_color_temp:
|
||||
return None
|
||||
|
||||
light_state = {
|
||||
"color_temp": temp,
|
||||
}
|
||||
return self.set_light_state(light_state)
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""
|
||||
Current brightness of the device, if supported
|
||||
|
||||
:return: brightness in percent
|
||||
:rtype: int
|
||||
"""
|
||||
if not self.is_dimmable:
|
||||
return None
|
||||
|
||||
light_state = self.get_light_state()
|
||||
if light_state['on_off'] == 0:
|
||||
return(light_state['dft_on_state']['brightness'])
|
||||
else:
|
||||
return(light_state['brightness'])
|
||||
|
||||
@brightness.setter
|
||||
def brightness(self, brightness):
|
||||
"""
|
||||
Set the current brightness of the device, if supported
|
||||
|
||||
:param int brightness: brightness in percent
|
||||
"""
|
||||
if not self.is_dimmable:
|
||||
return None
|
||||
|
||||
light_state = {
|
||||
"brightness": brightness,
|
||||
}
|
||||
return self.set_light_state(light_state)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""
|
||||
Retrieve the bulb state
|
||||
|
||||
:returns: one of
|
||||
BULB_STATE_ON
|
||||
BULB_STATE_OFF
|
||||
:rtype: str
|
||||
"""
|
||||
light_state = self.get_light_state()
|
||||
if light_state['on_off']:
|
||||
return self.BULB_STATE_ON
|
||||
return self.BULB_STATE_OFF
|
||||
|
||||
@state.setter
|
||||
def state(self, bulb_state):
|
||||
"""
|
||||
Set the new bulb state
|
||||
|
||||
:param bulb_state: one of
|
||||
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:
|
||||
bulb_state = 0
|
||||
else:
|
||||
raise ValueError
|
||||
|
||||
light_state = {
|
||||
"on_off": bulb_state,
|
||||
}
|
||||
return self.set_light_state(light_state)
|
||||
|
||||
@property
|
||||
def has_emeter(self):
|
||||
return True
|
||||
|
||||
|
||||
class SmartPlug(SmartDevice):
|
||||
"""Representation of a TP-Link Smart Switch.
|
||||
|
||||
Usage example when used as library:
|
||||
p = SmartPlug("192.168.1.105")
|
||||
# print the devices alias
|
||||
print(p.alias)
|
||||
# change state of plug
|
||||
p.state = "ON"
|
||||
p.state = "OFF"
|
||||
# query and print current state of plug
|
||||
print(p.state)
|
||||
|
||||
Errors reported by the device are raised as SmartPlugExceptions,
|
||||
and should be handled by the user of the library.
|
||||
|
||||
Note:
|
||||
The library references the same structure as defined for the D-Link Switch
|
||||
"""
|
||||
# switch states
|
||||
SWITCH_STATE_ON = 'ON'
|
||||
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"
|
||||
self.emeter_units = False
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""
|
||||
Retrieve the switch state
|
||||
|
||||
:returns: one of
|
||||
SWITCH_STATE_ON
|
||||
SWITCH_STATE_OFF
|
||||
SWITCH_STATE_UNKNOWN
|
||||
:rtype: str
|
||||
"""
|
||||
relay_state = self.sys_info['relay_state']
|
||||
|
||||
if relay_state == 0:
|
||||
return SmartPlug.SWITCH_STATE_OFF
|
||||
elif relay_state == 1:
|
||||
return SmartPlug.SWITCH_STATE_ON
|
||||
else:
|
||||
_LOGGER.warning("Unknown state %s returned.", relay_state)
|
||||
return SmartPlug.SWITCH_STATE_UNKNOWN
|
||||
|
||||
@state.setter
|
||||
def state(self, value):
|
||||
"""
|
||||
Set the new switch state
|
||||
|
||||
:param value: one of
|
||||
SWITCH_STATE_ON
|
||||
SWITCH_STATE_OFF
|
||||
:raises ValueError: on invalid state
|
||||
:raises SmartPlugException: on error
|
||||
|
||||
"""
|
||||
if not isinstance(value, basestring):
|
||||
raise ValueError("State must be str, not of %s.", type(value))
|
||||
elif value.upper() == SmartPlug.SWITCH_STATE_ON:
|
||||
self.turn_on()
|
||||
elif value.upper() == SmartPlug.SWITCH_STATE_OFF:
|
||||
self.turn_off()
|
||||
else:
|
||||
raise ValueError("State %s is not valid.", value)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""
|
||||
Returns whether device is on.
|
||||
|
||||
:return: True if device is on, False otherwise
|
||||
"""
|
||||
return bool(self.sys_info['relay_state'])
|
||||
|
||||
@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):
|
||||
"""
|
||||
Turn the switch on.
|
||||
|
||||
:raises SmartPlugException: on error
|
||||
"""
|
||||
self._query_helper("system", "set_relay_state", {"state": 1})
|
||||
|
||||
def turn_off(self):
|
||||
"""
|
||||
Turn the switch off.
|
||||
|
||||
:raises SmartPlugException: on error
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Returns the state of the led.
|
||||
|
||||
:return: True if led is on, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
return bool(1 - self.sys_info["led_off"])
|
||||
|
||||
@led.setter
|
||||
def led(self, state):
|
||||
"""
|
||||
Sets the state of the led (night mode)
|
||||
|
||||
:param bool state: True to set led on, False to set led off
|
||||
:raises SmartPlugException: on error
|
||||
"""
|
||||
self._query_helper("system", "set_led_off", {"off": int(not state)})
|
||||
|
||||
@property
|
||||
def on_since(self):
|
||||
"""
|
||||
Returns pretty-printed on-time
|
||||
|
||||
:return: datetime for on since
|
||||
:rtype: datetime
|
||||
"""
|
||||
return datetime.datetime.now() - \
|
||||
datetime.timedelta(seconds=self.sys_info["on_time"])
|
||||
|
238
pyHS100/smartbulb.py
Normal file
238
pyHS100/smartbulb.py
Normal file
@ -0,0 +1,238 @@
|
||||
from .pyHS100 import SmartDevice
|
||||
|
||||
|
||||
class SmartBulb(SmartDevice):
|
||||
"""Representation of a TP-Link Smart Bulb.
|
||||
|
||||
Usage example when used as library:
|
||||
p = SmartBulb("192.168.1.105")
|
||||
# print the devices alias
|
||||
print(p.alias)
|
||||
# change state of bulb
|
||||
p.state = "ON"
|
||||
p.state = "OFF"
|
||||
# query and print current state of plug
|
||||
print(p.state)
|
||||
# check whether the bulb supports color changes
|
||||
if p.is_color:
|
||||
# set the color to an HSV tuple
|
||||
p.hsv = (100, 0, 255)
|
||||
# get the current HSV value
|
||||
print(p.hsv)
|
||||
# check whether the bulb supports setting color temperature
|
||||
if p.is_variable_color_temp:
|
||||
# set the color temperature in Kelvin
|
||||
p.color_temp = 3000
|
||||
# get the current color temperature
|
||||
print(p.color_temp)
|
||||
# check whether the bulb is dimmable
|
||||
if p.is_dimmable:
|
||||
# set the bulb to 50% brightness
|
||||
p.brightness = 50
|
||||
# check the current brightness
|
||||
print(p.brightness)
|
||||
|
||||
Errors reported by the device are raised as SmartPlugExceptions,
|
||||
and should be handled by the user of the library.
|
||||
|
||||
"""
|
||||
# bulb states
|
||||
BULB_STATE_ON = 'ON'
|
||||
BULB_STATE_OFF = 'OFF'
|
||||
|
||||
def __init__(self, ip_address, protocol=None):
|
||||
SmartDevice.__init__(self, ip_address, protocol)
|
||||
self.emeter_type = "smartlife.iot.common.emeter"
|
||||
self.emeter_units = True
|
||||
|
||||
@property
|
||||
def is_color(self):
|
||||
"""
|
||||
Whether the bulb supports color changes
|
||||
|
||||
:return: True if the bulb supports color changes, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
return bool(self.sys_info['is_color'])
|
||||
|
||||
@property
|
||||
def is_dimmable(self):
|
||||
"""
|
||||
Whether the bulb supports brightness changes
|
||||
|
||||
:return: True if the bulb supports brightness changes, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
return bool(self.sys_info['is_dimmable'])
|
||||
|
||||
@property
|
||||
def is_variable_color_temp(self):
|
||||
"""
|
||||
Whether the bulb supports color temperature changes
|
||||
|
||||
:return: True if the bulb supports color temperature changes, False
|
||||
otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
return bool(self.sys_info['is_variable_color_temp'])
|
||||
|
||||
def get_light_state(self):
|
||||
return self._query_helper("smartlife.iot.smartbulb.lightingservice",
|
||||
"get_light_state")
|
||||
|
||||
def set_light_state(self, state):
|
||||
return self._query_helper("smartlife.iot.smartbulb.lightingservice",
|
||||
"transition_light_state", state)
|
||||
|
||||
@property
|
||||
def hsv(self):
|
||||
"""
|
||||
Returns the current HSV state of the bulb, if supported
|
||||
|
||||
:return: tuple containing current hue, saturation and value (0-255)
|
||||
:rtype: tuple
|
||||
"""
|
||||
|
||||
if not self.is_color:
|
||||
return None
|
||||
|
||||
light_state = self.get_light_state()
|
||||
if not self.is_on:
|
||||
hue = light_state['dft_on_state']['hue']
|
||||
saturation = light_state['dft_on_state']['saturation']
|
||||
value = int(light_state['dft_on_state']['brightness'] * 255 / 100)
|
||||
else:
|
||||
hue = light_state['hue']
|
||||
saturation = light_state['saturation']
|
||||
value = int(light_state['brightness'] * 255 / 100)
|
||||
|
||||
return(hue, saturation, value)
|
||||
|
||||
@hsv.setter
|
||||
def hsv(self, state):
|
||||
"""
|
||||
Sets new HSV, if supported
|
||||
|
||||
:param tuple state: hue, saturation and value (0-255 each)
|
||||
"""
|
||||
if not self.is_color:
|
||||
return None
|
||||
|
||||
light_state = {
|
||||
"hue": state[0],
|
||||
"saturation": state[1],
|
||||
"brightness": int(state[2] * 100 / 255),
|
||||
"color_temp": 0
|
||||
}
|
||||
return self.set_light_state(light_state)
|
||||
|
||||
@property
|
||||
def color_temp(self):
|
||||
"""
|
||||
Color temperature of the device, if supported
|
||||
|
||||
:return: Color temperature in Kelvin
|
||||
:rtype: int
|
||||
"""
|
||||
if not self.is_variable_color_temp:
|
||||
return None
|
||||
|
||||
light_state = self.get_light_state()
|
||||
if not self.is_on:
|
||||
return(light_state['dft_on_state']['color_temp'])
|
||||
else:
|
||||
return(light_state['color_temp'])
|
||||
|
||||
@color_temp.setter
|
||||
def color_temp(self, temp):
|
||||
"""
|
||||
Set the color temperature of the device, if supported
|
||||
|
||||
:param int temp: The new color temperature, in Kelvin
|
||||
"""
|
||||
if not self.is_variable_color_temp:
|
||||
return None
|
||||
|
||||
light_state = {
|
||||
"color_temp": temp,
|
||||
}
|
||||
return self.set_light_state(light_state)
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
"""
|
||||
Current brightness of the device, if supported
|
||||
|
||||
:return: brightness in percent
|
||||
:rtype: int
|
||||
"""
|
||||
if not self.is_dimmable:
|
||||
return None
|
||||
|
||||
light_state = self.get_light_state()
|
||||
if not self.is_on:
|
||||
return(light_state['dft_on_state']['brightness'])
|
||||
else:
|
||||
return(light_state['brightness'])
|
||||
|
||||
@brightness.setter
|
||||
def brightness(self, brightness):
|
||||
"""
|
||||
Set the current brightness of the device, if supported
|
||||
|
||||
:param int brightness: brightness in percent
|
||||
"""
|
||||
if not self.is_dimmable:
|
||||
return None
|
||||
|
||||
light_state = {
|
||||
"brightness": brightness,
|
||||
}
|
||||
return self.set_light_state(light_state)
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""
|
||||
Retrieve the bulb state
|
||||
|
||||
:returns: one of
|
||||
BULB_STATE_ON
|
||||
BULB_STATE_OFF
|
||||
:rtype: str
|
||||
"""
|
||||
light_state = self.get_light_state()
|
||||
if light_state['on_off']:
|
||||
return self.BULB_STATE_ON
|
||||
return self.BULB_STATE_OFF
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
return self.state == self.BULB_STATE_ON
|
||||
|
||||
@state.setter
|
||||
def state(self, bulb_state):
|
||||
"""
|
||||
Set the new bulb state
|
||||
|
||||
:param bulb_state: one of
|
||||
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:
|
||||
bulb_state = 0
|
||||
else:
|
||||
raise ValueError
|
||||
|
||||
light_state = {
|
||||
"on_off": bulb_state,
|
||||
}
|
||||
return self.set_light_state(light_state)
|
||||
|
||||
@property
|
||||
def has_emeter(self):
|
||||
return True
|
167
pyHS100/smartplug.py
Normal file
167
pyHS100/smartplug.py
Normal file
@ -0,0 +1,167 @@
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
from .pyHS100 import SmartDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SmartPlug(SmartDevice):
|
||||
"""Representation of a TP-Link Smart Switch.
|
||||
|
||||
Usage example when used as library:
|
||||
p = SmartPlug("192.168.1.105")
|
||||
# print the devices alias
|
||||
print(p.alias)
|
||||
# change state of plug
|
||||
p.state = "ON"
|
||||
p.state = "OFF"
|
||||
# query and print current state of plug
|
||||
print(p.state)
|
||||
|
||||
Errors reported by the device are raised as SmartPlugExceptions,
|
||||
and should be handled by the user of the library.
|
||||
|
||||
Note:
|
||||
The library references the same structure as defined for the D-Link Switch
|
||||
"""
|
||||
# switch states
|
||||
SWITCH_STATE_ON = 'ON'
|
||||
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"
|
||||
self.emeter_units = False
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""
|
||||
Retrieve the switch state
|
||||
|
||||
:returns: one of
|
||||
SWITCH_STATE_ON
|
||||
SWITCH_STATE_OFF
|
||||
SWITCH_STATE_UNKNOWN
|
||||
:rtype: str
|
||||
"""
|
||||
relay_state = self.sys_info['relay_state']
|
||||
|
||||
if relay_state == 0:
|
||||
return SmartPlug.SWITCH_STATE_OFF
|
||||
elif relay_state == 1:
|
||||
return SmartPlug.SWITCH_STATE_ON
|
||||
else:
|
||||
_LOGGER.warning("Unknown state %s returned.", relay_state)
|
||||
return SmartPlug.SWITCH_STATE_UNKNOWN
|
||||
|
||||
@state.setter
|
||||
def state(self, value):
|
||||
"""
|
||||
Set the new switch state
|
||||
|
||||
:param value: one of
|
||||
SWITCH_STATE_ON
|
||||
SWITCH_STATE_OFF
|
||||
:raises ValueError: on invalid state
|
||||
:raises SmartPlugException: on error
|
||||
|
||||
"""
|
||||
if not isinstance(value, str):
|
||||
raise ValueError("State must be str, not of %s.", type(value))
|
||||
elif value.upper() == SmartPlug.SWITCH_STATE_ON:
|
||||
self.turn_on()
|
||||
elif value.upper() == SmartPlug.SWITCH_STATE_OFF:
|
||||
self.turn_off()
|
||||
else:
|
||||
raise ValueError("State %s is not valid.", value)
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
"""
|
||||
Returns whether device is on.
|
||||
|
||||
:return: True if device is on, False otherwise
|
||||
"""
|
||||
return bool(self.sys_info['relay_state'])
|
||||
|
||||
def turn_on(self):
|
||||
"""
|
||||
Turn the switch on.
|
||||
|
||||
:raises SmartPlugException: on error
|
||||
"""
|
||||
self._query_helper("system", "set_relay_state", {"state": 1})
|
||||
|
||||
def turn_off(self):
|
||||
"""
|
||||
Turn the switch off.
|
||||
|
||||
:raises SmartPlugException: on error
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
Returns the state of the led.
|
||||
|
||||
:return: True if led is on, False otherwise
|
||||
:rtype: bool
|
||||
"""
|
||||
return bool(1 - self.sys_info["led_off"])
|
||||
|
||||
@led.setter
|
||||
def led(self, state):
|
||||
"""
|
||||
Sets the state of the led (night mode)
|
||||
|
||||
:param bool state: True to set led on, False to set led off
|
||||
:raises SmartPlugException: on error
|
||||
"""
|
||||
self._query_helper("system", "set_led_off", {"off": int(not state)})
|
||||
|
||||
@property
|
||||
def on_since(self):
|
||||
"""
|
||||
Returns pretty-printed on-time
|
||||
|
||||
:return: datetime for on since
|
||||
:rtype: datetime
|
||||
"""
|
||||
return datetime.datetime.now() - \
|
||||
datetime.timedelta(seconds=self.sys_info["on_time"])
|
@ -1,5 +1,5 @@
|
||||
from pyHS100.protocol import TPLinkSmartHomeProtocol
|
||||
from pyHS100 import SmartPlugException
|
||||
from ..protocol import TPLinkSmartHomeProtocol
|
||||
from .. import SmartPlugException
|
||||
import logging
|
||||
|
||||
|
||||
|
@ -1,22 +1,13 @@
|
||||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from unittest import TestCase, skip, skipIf
|
||||
from voluptuous import Schema, Invalid, All, Range
|
||||
from functools import partial
|
||||
|
||||
from pyHS100 import SmartBulb, SmartPlugException
|
||||
from pyHS100.tests.fakes import FakeTransportProtocol, sysinfo_lb130
|
||||
from .. import SmartBulb, SmartPlugException
|
||||
from .fakes import FakeTransportProtocol, sysinfo_lb130
|
||||
|
||||
BULB_IP = '192.168.250.186'
|
||||
SKIP_STATE_TESTS = False
|
||||
|
||||
# python2 compatibility
|
||||
try:
|
||||
basestring
|
||||
except NameError:
|
||||
basestring = str
|
||||
|
||||
|
||||
def check_int_bool(x):
|
||||
if x != 0 and x != 1:
|
||||
@ -37,18 +28,18 @@ class TestSmartBulb(TestCase):
|
||||
# as well as to check that faked devices are operating properly.
|
||||
sysinfo_schema = Schema({
|
||||
'active_mode': check_mode,
|
||||
'alias': basestring,
|
||||
'alias': str,
|
||||
'ctrl_protocols': {
|
||||
'name': basestring,
|
||||
'version': basestring,
|
||||
'name': str,
|
||||
'version': str,
|
||||
},
|
||||
'description': basestring,
|
||||
'dev_state': basestring,
|
||||
'deviceId': basestring,
|
||||
'disco_ver': basestring,
|
||||
'description': str,
|
||||
'dev_state': str,
|
||||
'deviceId': str,
|
||||
'disco_ver': str,
|
||||
'heapsize': int,
|
||||
'hwId': basestring,
|
||||
'hw_ver': basestring,
|
||||
'hwId': str,
|
||||
'hw_ver': str,
|
||||
'is_color': check_int_bool,
|
||||
'is_dimmable': check_int_bool,
|
||||
'is_factory': bool,
|
||||
@ -57,14 +48,14 @@ class TestSmartBulb(TestCase):
|
||||
'brightness': All(int, Range(min=0, max=100)),
|
||||
'color_temp': int,
|
||||
'hue': All(int, Range(min=0, max=255)),
|
||||
'mode': basestring,
|
||||
'mode': str,
|
||||
'on_off': check_int_bool,
|
||||
'saturation': All(int, Range(min=0, max=255)),
|
||||
},
|
||||
'mic_mac': basestring,
|
||||
'mic_type': basestring,
|
||||
'model': basestring,
|
||||
'oemId': basestring,
|
||||
'mic_mac': str,
|
||||
'mic_type': str,
|
||||
'model': str,
|
||||
'oemId': str,
|
||||
'preferred_state': [{
|
||||
'brightness': All(int, Range(min=0, max=100)),
|
||||
'color_temp': int,
|
||||
@ -73,7 +64,7 @@ class TestSmartBulb(TestCase):
|
||||
'saturation': All(int, Range(min=0, max=255)),
|
||||
}],
|
||||
'rssi': All(int, Range(max=0)),
|
||||
'sw_ver': basestring,
|
||||
'sw_ver': str,
|
||||
})
|
||||
|
||||
current_consumption_schema = Schema({
|
||||
@ -81,10 +72,10 @@ class TestSmartBulb(TestCase):
|
||||
})
|
||||
|
||||
tz_schema = Schema({
|
||||
'zone_str': basestring,
|
||||
'zone_str': str,
|
||||
'dst_offset': int,
|
||||
'index': All(int, Range(min=0)),
|
||||
'tz_str': basestring,
|
||||
'tz_str': str,
|
||||
})
|
||||
|
||||
def setUp(self):
|
||||
@ -185,7 +176,7 @@ class TestSmartBulb(TestCase):
|
||||
def test_alias(self):
|
||||
test_alias = "TEST1234"
|
||||
original = self.bulb.alias
|
||||
self.assertTrue(isinstance(original, basestring))
|
||||
self.assertTrue(isinstance(original, str))
|
||||
self.bulb.alias = test_alias
|
||||
self.assertEqual(self.bulb.alias, test_alias)
|
||||
self.bulb.alias = original
|
||||
|
@ -1,8 +1,5 @@
|
||||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from unittest import TestCase
|
||||
from pyHS100.protocol import TPLinkSmartHomeProtocol
|
||||
from ..protocol import TPLinkSmartHomeProtocol
|
||||
import json
|
||||
|
||||
|
||||
|
@ -1,24 +1,15 @@
|
||||
from __future__ import absolute_import
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from unittest import TestCase, skip, skipIf
|
||||
from voluptuous import Schema, Invalid, All, Range
|
||||
from functools import partial
|
||||
import datetime
|
||||
import re
|
||||
|
||||
from pyHS100 import SmartPlug, SmartPlugException
|
||||
from pyHS100.tests.fakes import FakeTransportProtocol, sysinfo_hs110
|
||||
from .. import SmartPlug, SmartPlugException
|
||||
from .fakes import FakeTransportProtocol, sysinfo_hs110
|
||||
|
||||
PLUG_IP = '192.168.250.186'
|
||||
SKIP_STATE_TESTS = False
|
||||
|
||||
# python2 compatibility
|
||||
try:
|
||||
basestring
|
||||
except NameError:
|
||||
basestring = str
|
||||
|
||||
|
||||
def check_int_bool(x):
|
||||
if x != 0 and x != 1:
|
||||
@ -45,25 +36,25 @@ class TestSmartPlug(TestCase):
|
||||
# as well as to check that faked devices are operating properly.
|
||||
sysinfo_schema = Schema({
|
||||
'active_mode': check_mode,
|
||||
'alias': basestring,
|
||||
'dev_name': basestring,
|
||||
'deviceId': basestring,
|
||||
'feature': basestring,
|
||||
'fwId': basestring,
|
||||
'hwId': basestring,
|
||||
'hw_ver': basestring,
|
||||
'icon_hash': basestring,
|
||||
'alias': str,
|
||||
'dev_name': str,
|
||||
'deviceId': str,
|
||||
'feature': str,
|
||||
'fwId': str,
|
||||
'hwId': str,
|
||||
'hw_ver': str,
|
||||
'icon_hash': str,
|
||||
'latitude': All(float, Range(min=-90, max=90)),
|
||||
'led_off': check_int_bool,
|
||||
'longitude': All(float, Range(min=-180, max=180)),
|
||||
'mac': check_mac,
|
||||
'model': basestring,
|
||||
'oemId': basestring,
|
||||
'model': str,
|
||||
'oemId': str,
|
||||
'on_time': int,
|
||||
'relay_state': int,
|
||||
'rssi': All(int, Range(max=0)),
|
||||
'sw_ver': basestring,
|
||||
'type': basestring,
|
||||
'sw_ver': str,
|
||||
'type': str,
|
||||
'updating': check_int_bool,
|
||||
})
|
||||
|
||||
@ -75,10 +66,10 @@ class TestSmartPlug(TestCase):
|
||||
})
|
||||
|
||||
tz_schema = Schema({
|
||||
'zone_str': basestring,
|
||||
'zone_str': str,
|
||||
'dst_offset': int,
|
||||
'index': All(int, Range(min=0)),
|
||||
'tz_str': basestring,
|
||||
'tz_str': str,
|
||||
})
|
||||
|
||||
def setUp(self):
|
||||
@ -195,7 +186,7 @@ class TestSmartPlug(TestCase):
|
||||
def test_alias(self):
|
||||
test_alias = "TEST1234"
|
||||
original = self.plug.alias
|
||||
self.assertTrue(isinstance(original, basestring))
|
||||
self.assertTrue(isinstance(original, str))
|
||||
self.plug.alias = test_alias
|
||||
self.assertEqual(self.plug.alias, test_alias)
|
||||
self.plug.alias = original
|
||||
|
2
setup.py
2
setup.py
@ -8,7 +8,7 @@ setup(name='pyHS100',
|
||||
author_email='sean@gadgetreactor.com',
|
||||
license='GPLv3',
|
||||
packages=['pyHS100'],
|
||||
install_requires=['click', 'click-datetime', 'future'],
|
||||
install_requires=['click', 'click-datetime'],
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'pyhs100=pyHS100.cli:cli',
|
||||
|
Loading…
Reference in New Issue
Block a user