mirror of
https://github.com/python-kasa/python-kasa.git
synced 2024-12-23 03:33:35 +00:00
Add transition support for SmartDimmer (#69)
* Adds a transition param to set_brightness(), turn_on(), and turn_off() that specifies the duration in milliseconds that the dimmer switch will take to transition to the new state. * Fixes bug where set_brightness(0) was allowed even though the dimmer does not support it. Now brightness values of 0 are coerced to 1 to be consistent with bulbs (which do support brightness values of 0).
This commit is contained in:
parent
9dc0cbaece
commit
dd073fa8c8
@ -23,6 +23,8 @@ class SmartDimmer(SmartPlug):
|
|||||||
Refer to SmartPlug for the full API.
|
Refer to SmartPlug for the full API.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
DIMMER_SERVICE = "smartlife.iot.dimmer"
|
||||||
|
|
||||||
def __init__(self, host: str) -> None:
|
def __init__(self, host: str) -> None:
|
||||||
super().__init__(host)
|
super().__init__(host)
|
||||||
self._device_type = DeviceType.Dimmer
|
self._device_type = DeviceType.Dimmer
|
||||||
@ -41,19 +43,83 @@ class SmartDimmer(SmartPlug):
|
|||||||
return int(sys_info["brightness"])
|
return int(sys_info["brightness"])
|
||||||
|
|
||||||
@requires_update
|
@requires_update
|
||||||
async def set_brightness(self, value: int):
|
async def set_brightness(self, brightness: int, *, transition: int = None):
|
||||||
"""Set the new dimmer brightness level in percentage."""
|
"""Set the new dimmer brightness level in percentage.
|
||||||
|
|
||||||
|
:param int transition: transition duration in milliseconds.
|
||||||
|
Using a transition will cause the dimmer to turn on.
|
||||||
|
"""
|
||||||
if not self.is_dimmable:
|
if not self.is_dimmable:
|
||||||
raise SmartDeviceException("Device is not dimmable.")
|
raise SmartDeviceException("Device is not dimmable.")
|
||||||
|
|
||||||
if not isinstance(value, int):
|
if not isinstance(brightness, int):
|
||||||
raise ValueError("Brightness must be integer, " "not of %s.", type(value))
|
raise ValueError(
|
||||||
elif 0 <= value <= 100:
|
"Brightness must be integer, " "not of %s.", type(brightness)
|
||||||
return await self._query_helper(
|
)
|
||||||
"smartlife.iot.dimmer", "set_brightness", {"brightness": value}
|
|
||||||
|
if not 0 <= brightness <= 100:
|
||||||
|
raise ValueError("Brightness value %s is not valid." % brightness)
|
||||||
|
|
||||||
|
# Dimmers do not support a brightness of 0, but bulbs do.
|
||||||
|
# Coerce 0 to 1 to maintain the same interface between dimmers and bulbs.
|
||||||
|
if brightness == 0:
|
||||||
|
brightness = 1
|
||||||
|
|
||||||
|
if transition is not None:
|
||||||
|
return await self.set_dimmer_transition(brightness, transition)
|
||||||
|
|
||||||
|
return await self._query_helper(
|
||||||
|
self.DIMMER_SERVICE, "set_brightness", {"brightness": brightness}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def turn_off(self, *, transition: int = None):
|
||||||
|
"""Turn the bulb off.
|
||||||
|
|
||||||
|
:param int transition: transition duration in milliseconds.
|
||||||
|
"""
|
||||||
|
if transition is not None:
|
||||||
|
return await self.set_dimmer_transition(brightness=0, transition=transition)
|
||||||
|
|
||||||
|
return await super().turn_off()
|
||||||
|
|
||||||
|
@requires_update
|
||||||
|
async def turn_on(self, *, transition: int = None):
|
||||||
|
"""Turn the bulb on.
|
||||||
|
|
||||||
|
:param int transition: transition duration in milliseconds.
|
||||||
|
"""
|
||||||
|
if transition is not None:
|
||||||
|
return await self.set_dimmer_transition(
|
||||||
|
brightness=self.brightness, transition=transition
|
||||||
|
)
|
||||||
|
|
||||||
|
return await super().turn_on()
|
||||||
|
|
||||||
|
async def set_dimmer_transition(self, brightness: int, transition: int):
|
||||||
|
"""Turn the bulb on to brightness percentage over transition milliseconds.
|
||||||
|
|
||||||
|
A brightness value of 0 will turn off the dimmer.
|
||||||
|
"""
|
||||||
|
if not isinstance(brightness, int):
|
||||||
|
raise ValueError(
|
||||||
|
"Brightness must be integer, " "not of %s.", type(brightness)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not 0 <= brightness <= 100:
|
||||||
|
raise ValueError("Brightness value %s is not valid." % brightness)
|
||||||
|
|
||||||
|
if not isinstance(transition, int):
|
||||||
|
raise ValueError(
|
||||||
|
"Transition must be integer, " "not of %s.", type(transition)
|
||||||
|
)
|
||||||
|
if transition <= 0:
|
||||||
|
raise ValueError("Transition value %s is not valid." % transition)
|
||||||
|
|
||||||
|
return await self._query_helper(
|
||||||
|
self.DIMMER_SERVICE,
|
||||||
|
"set_dimmer_transition",
|
||||||
|
{"brightness": brightness, "duration": transition},
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
raise ValueError("Brightness value %s is not valid." % value)
|
|
||||||
|
|
||||||
@property # type: ignore
|
@property # type: ignore
|
||||||
@requires_update
|
@requires_update
|
||||||
|
@ -315,6 +315,15 @@ class FakeTransportProtocol(TPLinkSmartHomeProtocol):
|
|||||||
_LOGGER.debug("Setting brightness to %s", x)
|
_LOGGER.debug("Setting brightness to %s", x)
|
||||||
self.proto["system"]["get_sysinfo"]["brightness"] = x["brightness"]
|
self.proto["system"]["get_sysinfo"]["brightness"] = x["brightness"]
|
||||||
|
|
||||||
|
def set_hs220_dimmer_transition(self, x, *args):
|
||||||
|
_LOGGER.debug("Setting dimmer transition to %s", x)
|
||||||
|
brightness = x["brightness"]
|
||||||
|
if brightness == 0:
|
||||||
|
self.proto["system"]["get_sysinfo"]["relay_state"] = 0
|
||||||
|
else:
|
||||||
|
self.proto["system"]["get_sysinfo"]["relay_state"] = 1
|
||||||
|
self.proto["system"]["get_sysinfo"]["brightness"] = x["brightness"]
|
||||||
|
|
||||||
def transition_light_state(self, x, *args):
|
def transition_light_state(self, x, *args):
|
||||||
_LOGGER.debug("Setting light state to %s", x)
|
_LOGGER.debug("Setting light state to %s", x)
|
||||||
light_state = self.proto["system"]["get_sysinfo"]["light_state"]
|
light_state = self.proto["system"]["get_sysinfo"]["light_state"]
|
||||||
@ -392,7 +401,10 @@ class FakeTransportProtocol(TPLinkSmartHomeProtocol):
|
|||||||
"set_timezone": None,
|
"set_timezone": None,
|
||||||
},
|
},
|
||||||
# HS220 brightness, different setter and getter
|
# HS220 brightness, different setter and getter
|
||||||
"smartlife.iot.dimmer": {"set_brightness": set_hs220_brightness},
|
"smartlife.iot.dimmer": {
|
||||||
|
"set_brightness": set_hs220_brightness,
|
||||||
|
"set_dimmer_transition": set_hs220_dimmer_transition,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
async def query(self, host, request, port=9999):
|
async def query(self, host, request, port=9999):
|
||||||
|
134
kasa/tests/test_dimmer.py
Normal file
134
kasa/tests/test_dimmer.py
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from kasa import SmartDimmer
|
||||||
|
|
||||||
|
from .conftest import dimmer, handle_turn_on, turn_on
|
||||||
|
|
||||||
|
|
||||||
|
@dimmer
|
||||||
|
@turn_on
|
||||||
|
async def test_set_brightness(dev, turn_on):
|
||||||
|
await handle_turn_on(dev, turn_on)
|
||||||
|
|
||||||
|
await dev.set_brightness(99)
|
||||||
|
assert dev.brightness == 99
|
||||||
|
assert dev.is_on == turn_on
|
||||||
|
|
||||||
|
await dev.set_brightness(0)
|
||||||
|
assert dev.brightness == 1
|
||||||
|
assert dev.is_on == turn_on
|
||||||
|
|
||||||
|
|
||||||
|
@dimmer
|
||||||
|
@turn_on
|
||||||
|
async def test_set_brightness_transition(dev, turn_on, mocker):
|
||||||
|
await handle_turn_on(dev, turn_on)
|
||||||
|
query_helper = mocker.spy(SmartDimmer, "_query_helper")
|
||||||
|
|
||||||
|
await dev.set_brightness(99, transition=1000)
|
||||||
|
|
||||||
|
assert dev.brightness == 99
|
||||||
|
assert dev.is_on
|
||||||
|
query_helper.assert_called_with(
|
||||||
|
mocker.ANY,
|
||||||
|
"smartlife.iot.dimmer",
|
||||||
|
"set_dimmer_transition",
|
||||||
|
{"brightness": 99, "duration": 1000},
|
||||||
|
)
|
||||||
|
|
||||||
|
await dev.set_brightness(0, transition=1000)
|
||||||
|
assert dev.brightness == 1
|
||||||
|
|
||||||
|
|
||||||
|
@dimmer
|
||||||
|
async def test_set_brightness_invalid(dev):
|
||||||
|
for invalid_brightness in [-1, 101, 0.5]:
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
await dev.set_brightness(invalid_brightness)
|
||||||
|
|
||||||
|
for invalid_transition in [-1, 0, 0.5]:
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
await dev.set_brightness(1, transition=invalid_transition)
|
||||||
|
|
||||||
|
|
||||||
|
@dimmer
|
||||||
|
async def test_turn_on_transition(dev, mocker):
|
||||||
|
query_helper = mocker.spy(SmartDimmer, "_query_helper")
|
||||||
|
original_brightness = dev.brightness
|
||||||
|
|
||||||
|
await dev.turn_on(transition=1000)
|
||||||
|
|
||||||
|
assert dev.is_on
|
||||||
|
assert dev.brightness == original_brightness
|
||||||
|
query_helper.assert_called_with(
|
||||||
|
mocker.ANY,
|
||||||
|
"smartlife.iot.dimmer",
|
||||||
|
"set_dimmer_transition",
|
||||||
|
{"brightness": original_brightness, "duration": 1000},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dimmer
|
||||||
|
async def test_turn_off_transition(dev, mocker):
|
||||||
|
await handle_turn_on(dev, True)
|
||||||
|
query_helper = mocker.spy(SmartDimmer, "_query_helper")
|
||||||
|
original_brightness = dev.brightness
|
||||||
|
|
||||||
|
await dev.turn_off(transition=1000)
|
||||||
|
|
||||||
|
assert dev.is_off
|
||||||
|
assert dev.brightness == original_brightness
|
||||||
|
query_helper.assert_called_with(
|
||||||
|
mocker.ANY,
|
||||||
|
"smartlife.iot.dimmer",
|
||||||
|
"set_dimmer_transition",
|
||||||
|
{"brightness": 0, "duration": 1000},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dimmer
|
||||||
|
@turn_on
|
||||||
|
async def test_set_dimmer_transition(dev, turn_on, mocker):
|
||||||
|
await handle_turn_on(dev, turn_on)
|
||||||
|
query_helper = mocker.spy(SmartDimmer, "_query_helper")
|
||||||
|
|
||||||
|
await dev.set_dimmer_transition(99, 1000)
|
||||||
|
|
||||||
|
assert dev.is_on
|
||||||
|
assert dev.brightness == 99
|
||||||
|
query_helper.assert_called_with(
|
||||||
|
mocker.ANY,
|
||||||
|
"smartlife.iot.dimmer",
|
||||||
|
"set_dimmer_transition",
|
||||||
|
{"brightness": 99, "duration": 1000},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dimmer
|
||||||
|
@turn_on
|
||||||
|
async def test_set_dimmer_transition_to_off(dev, turn_on, mocker):
|
||||||
|
await handle_turn_on(dev, turn_on)
|
||||||
|
original_brightness = dev.brightness
|
||||||
|
query_helper = mocker.spy(SmartDimmer, "_query_helper")
|
||||||
|
|
||||||
|
await dev.set_dimmer_transition(0, 1000)
|
||||||
|
|
||||||
|
assert dev.is_off
|
||||||
|
assert dev.brightness == original_brightness
|
||||||
|
query_helper.assert_called_with(
|
||||||
|
mocker.ANY,
|
||||||
|
"smartlife.iot.dimmer",
|
||||||
|
"set_dimmer_transition",
|
||||||
|
{"brightness": 0, "duration": 1000},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dimmer
|
||||||
|
async def test_set_dimmer_transition_invalid(dev):
|
||||||
|
for invalid_brightness in [-1, 101, 0.5]:
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
await dev.set_dimmer_transition(invalid_brightness, 1000)
|
||||||
|
|
||||||
|
for invalid_transition in [-1, 0, 0.5]:
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
await dev.set_dimmer_transition(1, invalid_transition)
|
Loading…
Reference in New Issue
Block a user