bulb: allow set_hsv without v, add fallback ct range (#200)

* bulb: allow set_hsv without v, add fallback ct range

* add ColorTempRange and HSV named tuples
* add a fallback color temp range if unknown, log a warning
* set_hsv: the value is now optional

* Fix tests, change fallback range to 2700-5000
This commit is contained in:
Teemu R 2021-09-21 13:23:56 +02:00 committed by GitHub
parent 7565d03c8e
commit 2c83d8ee6d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 48 additions and 27 deletions

View File

@ -1,6 +1,7 @@
"""Module for bulbs (LB*, KL*, KB*).""" """Module for bulbs (LB*, KL*, KB*)."""
import logging
import re import re
from typing import Any, Dict, Tuple, cast from typing import Any, Dict, NamedTuple, cast
from kasa.smartdevice import ( from kasa.smartdevice import (
DeviceType, DeviceType,
@ -9,18 +10,36 @@ from kasa.smartdevice import (
requires_update, requires_update,
) )
class ColorTempRange(NamedTuple):
"""Color temperature range."""
min: int
max: int
class HSV(NamedTuple):
"""Hue-saturation-value."""
hue: int
saturation: int
value: int
TPLINK_KELVIN = { TPLINK_KELVIN = {
"LB130": (2500, 9000), "LB130": ColorTempRange(2500, 9000),
"LB120": (2700, 6500), "LB120": ColorTempRange(2700, 6500),
"LB230": (2500, 9000), "LB230": ColorTempRange(2500, 9000),
"KB130": (2500, 9000), "KB130": ColorTempRange(2500, 9000),
"KL130": (2500, 9000), "KL130": ColorTempRange(2500, 9000),
"KL125": (2500, 6500), "KL125": ColorTempRange(2500, 6500),
r"KL120\(EU\)": (2700, 6500), r"KL120\(EU\)": ColorTempRange(2700, 6500),
r"KL120\(US\)": (2700, 5000), r"KL120\(US\)": ColorTempRange(2700, 5000),
r"KL430": (2500, 9000), r"KL430": ColorTempRange(2500, 9000),
} }
_LOGGER = logging.getLogger(__name__)
class SmartBulb(SmartDevice): class SmartBulb(SmartDevice):
"""Representation of a TP-Link Smart Bulb. """Representation of a TP-Link Smart Bulb.
@ -69,7 +88,7 @@ class SmartBulb(SmartDevice):
Bulbs supporting color temperature can be queried to know which range is accepted: Bulbs supporting color temperature can be queried to know which range is accepted:
>>> bulb.valid_temperature_range >>> bulb.valid_temperature_range
(2500, 9000) ColorTempRange(min=2500, max=9000)
>>> asyncio.run(bulb.set_color_temp(3000)) >>> asyncio.run(bulb.set_color_temp(3000))
>>> asyncio.run(bulb.update()) >>> asyncio.run(bulb.update())
>>> bulb.color_temp >>> bulb.color_temp
@ -80,7 +99,7 @@ class SmartBulb(SmartDevice):
>>> asyncio.run(bulb.set_hsv(180, 100, 80)) >>> asyncio.run(bulb.set_hsv(180, 100, 80))
>>> asyncio.run(bulb.update()) >>> asyncio.run(bulb.update())
>>> bulb.hsv >>> bulb.hsv
(180, 100, 80) HSV(hue=180, saturation=100, value=80)
If you don't want to use the default transitions, you can pass `transition` in milliseconds. If you don't want to use the default transitions, you can pass `transition` in milliseconds.
This applies to all transitions (turn_on, turn_off, set_hsv, set_color_temp, set_brightness). This applies to all transitions (turn_on, turn_off, set_hsv, set_color_temp, set_brightness).
@ -122,21 +141,21 @@ class SmartBulb(SmartDevice):
@property # type: ignore @property # type: ignore
@requires_update @requires_update
def valid_temperature_range(self) -> Tuple[int, int]: def valid_temperature_range(self) -> ColorTempRange:
"""Return the device-specific white temperature range (in Kelvin). """Return the device-specific white temperature range (in Kelvin).
:return: White temperature range in Kelvin (minimum, maximum) :return: White temperature range in Kelvin (minimum, maximum)
""" """
if not self.is_variable_color_temp: if not self.is_variable_color_temp:
raise SmartDeviceException("Color temperature not supported") raise SmartDeviceException("Color temperature not supported")
for model, temp_range in TPLINK_KELVIN.items(): for model, temp_range in TPLINK_KELVIN.items():
sys_info = self.sys_info sys_info = self.sys_info
if re.match(model, sys_info["model"]): if re.match(model, sys_info["model"]):
return temp_range return temp_range
raise SmartDeviceException( _LOGGER.warning("Unknown color temperature range, fallback to 2700-5000")
"Unknown color temperature range, please open an issue on github" return ColorTempRange(2700, 5000)
)
@property # type: ignore @property # type: ignore
@requires_update @requires_update
@ -200,7 +219,7 @@ class SmartBulb(SmartDevice):
@property # type: ignore @property # type: ignore
@requires_update @requires_update
def hsv(self) -> Tuple[int, int, int]: def hsv(self) -> HSV:
"""Return the current HSV state of the bulb. """Return the current HSV state of the bulb.
:return: hue, saturation and value (degrees, %, %) :return: hue, saturation and value (degrees, %, %)
@ -214,7 +233,7 @@ class SmartBulb(SmartDevice):
saturation = light_state["saturation"] saturation = light_state["saturation"]
value = light_state["brightness"] value = light_state["brightness"]
return hue, saturation, value return HSV(hue, saturation, value)
def _raise_for_invalid_brightness(self, value): def _raise_for_invalid_brightness(self, value):
if not isinstance(value, int) or not (0 <= value <= 100): if not isinstance(value, int) or not (0 <= value <= 100):
@ -224,7 +243,7 @@ class SmartBulb(SmartDevice):
@requires_update @requires_update
async def set_hsv( async def set_hsv(
self, hue: int, saturation: int, value: int, *, transition: int = None self, hue: int, saturation: int, value: int = None, *, transition: int = None
) -> Dict: ) -> Dict:
"""Set new HSV. """Set new HSV.
@ -247,15 +266,16 @@ class SmartBulb(SmartDevice):
"(valid range: 0-100%)".format(saturation) "(valid range: 0-100%)".format(saturation)
) )
self._raise_for_invalid_brightness(value)
light_state = { light_state = {
"hue": hue, "hue": hue,
"saturation": saturation, "saturation": saturation,
"brightness": value,
"color_temp": 0, "color_temp": 0,
} }
if value is not None:
self._raise_for_invalid_brightness(value)
light_state["brightness"] = value
return await self.set_light_state(light_state, transition=transition) return await self.set_light_state(light_state, transition=transition)
@property # type: ignore @property # type: ignore
@ -284,7 +304,7 @@ class SmartBulb(SmartDevice):
if temp < valid_temperature_range[0] or temp > valid_temperature_range[1]: if temp < valid_temperature_range[0] or temp > valid_temperature_range[1]:
raise ValueError( raise ValueError(
"Temperature should be between {} " "Temperature should be between {} "
"and {}".format(*valid_temperature_range) "and {}, was {}".format(*valid_temperature_range, temp)
) )
light_state = {"color_temp": temp} light_state = {"color_temp": temp}

View File

@ -148,10 +148,11 @@ async def test_set_color_temp_transition(dev, mocker):
@variable_temp @variable_temp
async def test_unknown_temp_range(dev, monkeypatch): async def test_unknown_temp_range(dev, monkeypatch, caplog):
with pytest.raises(SmartDeviceException): monkeypatch.setitem(dev._sys_info, "model", "unknown bulb")
monkeypatch.setitem(dev._sys_info, "model", "unknown bulb")
dev.valid_temperature_range() assert dev.valid_temperature_range == (2700, 5000)
assert "Unknown color temperature range, fallback to 2700-5000" in caplog.text
@variable_temp @variable_temp