2018-09-05 20:44:44 +00:00
|
|
|
from pyHS100 import SmartDevice, SmartDeviceException
|
2018-06-17 17:01:18 +00:00
|
|
|
import re
|
2017-09-18 16:13:06 +00:00
|
|
|
from typing import Any, Dict, Optional, Tuple
|
2017-04-14 12:24:58 +00:00
|
|
|
|
2018-06-17 17:01:18 +00:00
|
|
|
TPLINK_KELVIN = {'LB130': (2500, 9000),
|
|
|
|
'LB120': (2700, 6500),
|
|
|
|
'LB230': (2500, 9000),
|
2019-01-26 20:12:22 +00:00
|
|
|
'KB130': (2500, 9000),
|
|
|
|
'KL130': (2500, 9000),
|
|
|
|
'KL120': (2700, 6500)}
|
2018-06-17 17:01:18 +00:00
|
|
|
|
2017-04-14 12:24:58 +00:00
|
|
|
|
|
|
|
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
|
2017-09-14 18:47:06 +00:00
|
|
|
p.hsv = (180, 100, 100)
|
2017-04-14 12:24:58 +00:00
|
|
|
# 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)
|
|
|
|
|
2017-08-05 13:49:56 +00:00
|
|
|
Errors reported by the device are raised as SmartDeviceExceptions,
|
2017-04-14 12:24:58 +00:00
|
|
|
and should be handled by the user of the library.
|
|
|
|
|
|
|
|
"""
|
|
|
|
# bulb states
|
|
|
|
BULB_STATE_ON = 'ON'
|
|
|
|
BULB_STATE_OFF = 'OFF'
|
|
|
|
|
2017-09-18 16:13:06 +00:00
|
|
|
def __init__(self,
|
2018-01-17 21:03:19 +00:00
|
|
|
host: str,
|
2017-09-18 16:13:06 +00:00
|
|
|
protocol: 'TPLinkSmartHomeProtocol' = None) -> None:
|
2018-01-17 21:03:19 +00:00
|
|
|
SmartDevice.__init__(self, host, protocol)
|
2017-04-14 12:24:58 +00:00
|
|
|
self.emeter_type = "smartlife.iot.common.emeter"
|
|
|
|
|
|
|
|
@property
|
2017-09-18 16:13:06 +00:00
|
|
|
def is_color(self) -> bool:
|
2017-04-14 12:24:58 +00:00
|
|
|
"""
|
|
|
|
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
|
2017-09-18 16:13:06 +00:00
|
|
|
def is_dimmable(self) -> bool:
|
2017-04-14 12:24:58 +00:00
|
|
|
"""
|
|
|
|
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
|
2017-09-18 16:13:06 +00:00
|
|
|
def is_variable_color_temp(self) -> bool:
|
2017-04-14 12:24:58 +00:00
|
|
|
"""
|
|
|
|
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'])
|
|
|
|
|
2018-06-17 17:01:18 +00:00
|
|
|
@property
|
|
|
|
def valid_temperature_range(self) -> Tuple[int, int]:
|
|
|
|
"""
|
|
|
|
Returns the white temperature range (in Kelvin)
|
|
|
|
depending on the bulb model
|
|
|
|
|
|
|
|
:return: White temperature range in Kelvin (minimun, maximum)
|
|
|
|
:rtype: tuple
|
|
|
|
"""
|
|
|
|
if not self.is_variable_color_temp:
|
|
|
|
return (0, 0)
|
|
|
|
for model, temp_range in TPLINK_KELVIN.items():
|
|
|
|
if re.match(model, self.sys_info['model']):
|
|
|
|
return temp_range
|
|
|
|
return (0, 0)
|
|
|
|
|
2017-09-18 16:13:06 +00:00
|
|
|
def get_light_state(self) -> Dict:
|
2017-04-14 12:24:58 +00:00
|
|
|
return self._query_helper("smartlife.iot.smartbulb.lightingservice",
|
|
|
|
"get_light_state")
|
|
|
|
|
2017-09-18 16:13:06 +00:00
|
|
|
def set_light_state(self, state: Dict) -> Dict:
|
2017-04-14 12:24:58 +00:00
|
|
|
return self._query_helper("smartlife.iot.smartbulb.lightingservice",
|
|
|
|
"transition_light_state", state)
|
|
|
|
|
|
|
|
@property
|
2017-09-18 16:13:06 +00:00
|
|
|
def hsv(self) -> Optional[Tuple[int, int, int]]:
|
2017-04-14 12:24:58 +00:00
|
|
|
"""
|
|
|
|
Returns the current HSV state of the bulb, if supported
|
|
|
|
|
2017-09-16 16:57:42 +00:00
|
|
|
:return: hue, saturation and value (degrees, %, %)
|
2017-04-14 12:24:58 +00:00
|
|
|
: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']
|
2018-09-05 20:44:44 +00:00
|
|
|
value = light_state['dft_on_state']['brightness']
|
2017-04-14 12:24:58 +00:00
|
|
|
else:
|
|
|
|
hue = light_state['hue']
|
|
|
|
saturation = light_state['saturation']
|
2018-09-05 20:44:44 +00:00
|
|
|
value = light_state['brightness']
|
2017-04-14 12:24:58 +00:00
|
|
|
|
2017-04-24 17:28:22 +00:00
|
|
|
return hue, saturation, value
|
2017-04-14 12:24:58 +00:00
|
|
|
|
|
|
|
@hsv.setter
|
2017-09-18 16:13:06 +00:00
|
|
|
def hsv(self, state: Tuple[int, int, int]):
|
2017-04-14 12:24:58 +00:00
|
|
|
"""
|
|
|
|
Sets new HSV, if supported
|
|
|
|
|
2017-09-14 18:47:06 +00:00
|
|
|
:param tuple state: hue, saturation and value (degrees, %, %)
|
2017-04-14 12:24:58 +00:00
|
|
|
"""
|
|
|
|
if not self.is_color:
|
|
|
|
return None
|
|
|
|
|
2019-01-16 20:50:48 +00:00
|
|
|
if not isinstance(state[0], int) or not (0 <= state[0] <= 360):
|
2018-09-05 20:44:44 +00:00
|
|
|
raise SmartDeviceException(
|
|
|
|
'Invalid hue value: {} '
|
2019-01-16 20:50:48 +00:00
|
|
|
'(valid range: 0-360)'.format(state[0]))
|
2018-09-05 20:44:44 +00:00
|
|
|
|
|
|
|
if not isinstance(state[1], int) or not (0 <= state[1] <= 100):
|
|
|
|
raise SmartDeviceException(
|
|
|
|
'Invalid saturation value: {} '
|
|
|
|
'(valid range: 0-100%)'.format(state[1]))
|
|
|
|
|
|
|
|
if not isinstance(state[2], int) or not (0 <= state[2] <= 100):
|
|
|
|
raise SmartDeviceException(
|
|
|
|
'Invalid brightness value: {} '
|
|
|
|
'(valid range: 0-100%)'.format(state[2]))
|
|
|
|
|
2017-04-14 12:24:58 +00:00
|
|
|
light_state = {
|
|
|
|
"hue": state[0],
|
|
|
|
"saturation": state[1],
|
2018-09-05 20:44:44 +00:00
|
|
|
"brightness": state[2],
|
2017-04-14 12:24:58 +00:00
|
|
|
"color_temp": 0
|
|
|
|
}
|
2017-09-18 16:13:06 +00:00
|
|
|
self.set_light_state(light_state)
|
2017-04-14 12:24:58 +00:00
|
|
|
|
|
|
|
@property
|
2017-09-18 16:13:06 +00:00
|
|
|
def color_temp(self) -> Optional[int]:
|
2017-04-14 12:24:58 +00:00
|
|
|
"""
|
|
|
|
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:
|
2017-09-18 16:13:06 +00:00
|
|
|
return int(light_state['dft_on_state']['color_temp'])
|
2017-04-14 12:24:58 +00:00
|
|
|
else:
|
2017-09-18 16:13:06 +00:00
|
|
|
return int(light_state['color_temp'])
|
2017-04-14 12:24:58 +00:00
|
|
|
|
|
|
|
@color_temp.setter
|
2017-09-18 16:13:06 +00:00
|
|
|
def color_temp(self, temp: int) -> None:
|
2017-04-14 12:24:58 +00:00
|
|
|
"""
|
|
|
|
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
|
|
|
|
|
2018-06-17 17:01:18 +00:00
|
|
|
if temp < self.valid_temperature_range[0] or \
|
|
|
|
temp > self.valid_temperature_range[1]:
|
|
|
|
raise ValueError("Temperature should be between {} "
|
|
|
|
"and {}".format(*self.valid_temperature_range))
|
|
|
|
|
2017-04-14 12:24:58 +00:00
|
|
|
light_state = {
|
|
|
|
"color_temp": temp,
|
|
|
|
}
|
2017-09-18 16:13:06 +00:00
|
|
|
self.set_light_state(light_state)
|
2017-04-14 12:24:58 +00:00
|
|
|
|
|
|
|
@property
|
2017-09-18 16:13:06 +00:00
|
|
|
def brightness(self) -> Optional[int]:
|
2017-04-14 12:24:58 +00:00
|
|
|
"""
|
|
|
|
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:
|
2017-09-18 16:13:06 +00:00
|
|
|
return int(light_state['dft_on_state']['brightness'])
|
2017-04-14 12:24:58 +00:00
|
|
|
else:
|
2017-09-18 16:13:06 +00:00
|
|
|
return int(light_state['brightness'])
|
2017-04-14 12:24:58 +00:00
|
|
|
|
|
|
|
@brightness.setter
|
2017-09-18 16:13:06 +00:00
|
|
|
def brightness(self, brightness: int) -> None:
|
2017-04-14 12:24:58 +00:00
|
|
|
"""
|
|
|
|
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,
|
|
|
|
}
|
2017-09-18 16:13:06 +00:00
|
|
|
self.set_light_state(light_state)
|
2017-04-14 12:24:58 +00:00
|
|
|
|
|
|
|
@property
|
2017-09-18 16:13:06 +00:00
|
|
|
def state(self) -> str:
|
2017-04-14 12:24:58 +00:00
|
|
|
"""
|
|
|
|
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
|
|
|
|
|
2017-09-18 16:13:06 +00:00
|
|
|
@state.setter
|
|
|
|
def state(self, bulb_state: str) -> None:
|
|
|
|
"""
|
|
|
|
Set the new bulb state
|
|
|
|
|
|
|
|
:param bulb_state: one of
|
|
|
|
BULB_STATE_ON
|
|
|
|
BULB_STATE_OFF
|
|
|
|
"""
|
|
|
|
if bulb_state == self.BULB_STATE_ON:
|
|
|
|
new_state = 1
|
|
|
|
elif bulb_state == self.BULB_STATE_OFF:
|
|
|
|
new_state = 0
|
|
|
|
else:
|
|
|
|
raise ValueError
|
|
|
|
|
|
|
|
light_state = {
|
|
|
|
"on_off": new_state,
|
|
|
|
}
|
|
|
|
self.set_light_state(light_state)
|
|
|
|
|
2017-04-24 17:28:22 +00:00
|
|
|
@property
|
2017-09-18 16:13:06 +00:00
|
|
|
def state_information(self) -> Dict[str, Any]:
|
2017-04-24 17:28:22 +00:00
|
|
|
"""
|
|
|
|
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,
|
2017-09-18 16:13:06 +00:00
|
|
|
} # type: Dict[str, Any]
|
2017-04-24 17:28:22 +00:00
|
|
|
if self.is_variable_color_temp:
|
|
|
|
info["Color temperature"] = self.color_temp
|
|
|
|
if self.is_color:
|
|
|
|
info["HSV"] = self.hsv
|
|
|
|
|
|
|
|
return info
|
|
|
|
|
2017-04-14 12:24:58 +00:00
|
|
|
@property
|
2017-09-18 16:13:06 +00:00
|
|
|
def is_on(self) -> bool:
|
|
|
|
return bool(self.state == self.BULB_STATE_ON)
|
2017-04-14 12:24:58 +00:00
|
|
|
|
2017-09-18 16:13:06 +00:00
|
|
|
def turn_off(self) -> None:
|
2017-04-24 17:28:22 +00:00
|
|
|
"""
|
|
|
|
Turn the bulb off.
|
|
|
|
"""
|
|
|
|
self.state = self.BULB_STATE_OFF
|
|
|
|
|
2017-09-18 16:13:06 +00:00
|
|
|
def turn_on(self) -> None:
|
2017-04-24 17:28:22 +00:00
|
|
|
"""
|
|
|
|
Turn the bulb on.
|
|
|
|
"""
|
|
|
|
self.state = self.BULB_STATE_ON
|
|
|
|
|
2017-04-14 12:24:58 +00:00
|
|
|
@property
|
2017-09-18 16:13:06 +00:00
|
|
|
def has_emeter(self) -> bool:
|
2017-04-14 12:24:58 +00:00
|
|
|
return True
|