Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 43 additions & 23 deletions kasa/smartbulb.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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).
Expand Down Expand Up @@ -122,21 +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

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
Expand Down Expand Up @@ -200,7 +219,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, %, %)
Expand All @@ -214,7 +233,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):
Expand All @@ -224,7 +243,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.

Expand All @@ -247,15 +266,16 @@ 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:
self._raise_for_invalid_brightness(value)
light_state["brightness"] = value

return await self.set_light_state(light_state, transition=transition)

@property # type: ignore
Expand Down Expand Up @@ -284,7 +304,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}
Expand Down
9 changes: 5 additions & 4 deletions kasa/tests/test_bulb.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down