From 8b56fddeb30ba44e35d0fb4af6de79c6d36e68fc Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Tue, 21 Sep 2021 00:26:24 +0200 Subject: [PATCH 1/2] 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 --- kasa/smartbulb.py | 58 +++++++++++++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/kasa/smartbulb.py b/kasa/smartbulb.py index 3625cf567..8843b1748 100644 --- a/kasa/smartbulb.py +++ b/kasa/smartbulb.py @@ -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, Tuple, cast from kasa.smartdevice import ( DeviceType, @@ -9,18 +10,36 @@ 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. @@ -134,9 +153,8 @@ def valid_temperature_range(self) -> Tuple[int, int]: if re.match(model, sys_info["model"]): return temp_range - raise SmartDeviceException( - "Unknown color temperature range, please open an issue on github" - ) + _LOGGER.error("Unknown color temperature range, please open an issue on github") + return ColorTempRange(2000, 5000) @property # type: ignore @requires_update @@ -200,7 +218,7 @@ async def set_light_state(self, state: Dict, *, transition: int = None) -> Dict: @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 +232,7 @@ def hsv(self) -> Tuple[int, int, int]: 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 +242,7 @@ def _raise_for_invalid_brightness(self, value): @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 +265,15 @@ async def set_hsv( "(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: + light_state["brightness"] = value + return await self.set_light_state(light_state, transition=transition) @property # type: ignore @@ -284,7 +302,7 @@ async def set_color_temp( 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} From 88277ad5e85ab26ec83d282c56f96a3ed083330f Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Tue, 21 Sep 2021 01:02:13 +0200 Subject: [PATCH 2/2] Fix tests, change fallback range to 2700-5000 --- kasa/smartbulb.py | 14 ++++++++------ kasa/tests/test_bulb.py | 9 +++++---- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/kasa/smartbulb.py b/kasa/smartbulb.py index 8843b1748..74d5c1cf9 100644 --- a/kasa/smartbulb.py +++ b/kasa/smartbulb.py @@ -1,7 +1,7 @@ """Module for bulbs (LB*, KL*, KB*).""" import logging import re -from typing import Any, Dict, NamedTuple, Tuple, cast +from typing import Any, Dict, NamedTuple, cast from kasa.smartdevice import ( DeviceType, @@ -88,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 @@ -99,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). @@ -141,20 +141,21 @@ def is_variable_color_temp(self) -> bool: @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 - _LOGGER.error("Unknown color temperature range, please open an issue on github") - return ColorTempRange(2000, 5000) + _LOGGER.warning("Unknown color temperature range, fallback to 2700-5000") + return ColorTempRange(2700, 5000) @property # type: ignore @requires_update @@ -272,6 +273,7 @@ async def set_hsv( } 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) diff --git a/kasa/tests/test_bulb.py b/kasa/tests/test_bulb.py index c7beb1abb..28fcd4cb7 100644 --- a/kasa/tests/test_bulb.py +++ b/kasa/tests/test_bulb.py @@ -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): - monkeypatch.setitem(dev._sys_info, "model", "unknown bulb") - dev.valid_temperature_range() +async def test_unknown_temp_range(dev, monkeypatch, caplog): + monkeypatch.setitem(dev._sys_info, "model", "unknown bulb") + + assert dev.valid_temperature_range == (2700, 5000) + assert "Unknown color temperature range, fallback to 2700-5000" in caplog.text @variable_temp