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*)."""
import logging
import re
from typing import Any, Dict, Tuple, cast
from typing import Any, Dict, NamedTuple, cast
from kasa.smartdevice import (
DeviceType,
@ -9,18 +10,36 @@ from kasa.smartdevice import (
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 = {
"LB130": (2500, 9000),
"LB120": (2700, 6500),
"LB230": (2500, 9000),
"KB130": (2500, 9000),
"KL130": (2500, 9000),
"KL125": (2500, 6500),
r"KL120\(EU\)": (2700, 6500),
r"KL120\(US\)": (2700, 5000),
r"KL430": (2500, 9000),
"LB130": ColorTempRange(2500, 9000),
"LB120": ColorTempRange(2700, 6500),
"LB230": ColorTempRange(2500, 9000),
"KB130": ColorTempRange(2500, 9000),
"KL130": ColorTempRange(2500, 9000),
"KL125": ColorTempRange(2500, 6500),
r"KL120\(EU\)": ColorTempRange(2700, 6500),
r"KL120\(US\)": ColorTempRange(2700, 5000),
r"KL430": ColorTempRange(2500, 9000),
}
_LOGGER = logging.getLogger(__name__)
class SmartBulb(SmartDevice):
"""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:
>>> bulb.valid_temperature_range
(2500, 9000)
ColorTempRange(min=2500, max=9000)
>>> asyncio.run(bulb.set_color_temp(3000))
>>> asyncio.run(bulb.update())
>>> bulb.color_temp
@ -80,7 +99,7 @@ class SmartBulb(SmartDevice):
>>> asyncio.run(bulb.set_hsv(180, 100, 80))
>>> asyncio.run(bulb.update())
>>> 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.
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
@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: White temperature range in Kelvin (minimum, maximum)
"""
if not self.is_variable_color_temp:
raise SmartDeviceException("Color temperature not supported")
for model, temp_range in TPLINK_KELVIN.items():
sys_info = self.sys_info
if re.match(model, sys_info["model"]):
return temp_range
raise SmartDeviceException(
"Unknown color temperature range, please open an issue on github"
)
_LOGGER.warning("Unknown color temperature range, fallback to 2700-5000")
return ColorTempRange(2700, 5000)
@property # type: ignore
@requires_update
@ -200,7 +219,7 @@ class SmartBulb(SmartDevice):
@property # type: ignore
@requires_update
def hsv(self) -> Tuple[int, int, int]:
def hsv(self) -> HSV:
"""Return the current HSV state of the bulb.
:return: hue, saturation and value (degrees, %, %)
@ -214,7 +233,7 @@ class SmartBulb(SmartDevice):
saturation = light_state["saturation"]
value = light_state["brightness"]
return hue, saturation, value
return HSV(hue, saturation, value)
def _raise_for_invalid_brightness(self, value):
if not isinstance(value, int) or not (0 <= value <= 100):
@ -224,7 +243,7 @@ class SmartBulb(SmartDevice):
@requires_update
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:
"""Set new HSV.
@ -247,15 +266,16 @@ class SmartBulb(SmartDevice):
"(valid range: 0-100%)".format(saturation)
)
self._raise_for_invalid_brightness(value)
light_state = {
"hue": hue,
"saturation": saturation,
"brightness": value,
"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)
@property # type: ignore
@ -284,7 +304,7 @@ class SmartBulb(SmartDevice):
if temp < valid_temperature_range[0] or temp > valid_temperature_range[1]:
raise ValueError(
"Temperature should be between {} "
"and {}".format(*valid_temperature_range)
"and {}, was {}".format(*valid_temperature_range, temp)
)
light_state = {"color_temp": temp}

View File

@ -148,10 +148,11 @@ async def test_set_color_temp_transition(dev, mocker):
@variable_temp
async def test_unknown_temp_range(dev, monkeypatch):
with pytest.raises(SmartDeviceException):
async def test_unknown_temp_range(dev, monkeypatch, caplog):
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