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
84 changes: 75 additions & 9 deletions kasa/smartdimmer.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ class SmartDimmer(SmartPlug):
Refer to SmartPlug for the full API.
"""

DIMMER_SERVICE = "smartlife.iot.dimmer"

def __init__(self, host: str) -> None:
super().__init__(host)
self._device_type = DeviceType.Dimmer
Expand All @@ -41,19 +43,83 @@ def brightness(self) -> int:
return int(sys_info["brightness"])

@requires_update
async def set_brightness(self, value: int):
"""Set the new dimmer brightness level in percentage."""
async def set_brightness(self, brightness: int, *, transition: int = None):
"""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:
raise SmartDeviceException("Device is not dimmable.")

if not isinstance(value, int):
raise ValueError("Brightness must be integer, " "not of %s.", type(value))
elif 0 <= value <= 100:
return await self._query_helper(
"smartlife.iot.dimmer", "set_brightness", {"brightness": value}
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)

# 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)
)
else:
raise ValueError("Brightness value %s is not valid." % value)
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},
)

@property # type: ignore
@requires_update
Expand Down
14 changes: 13 additions & 1 deletion kasa/tests/newfakes.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,15 @@ def set_hs220_brightness(self, x, *args):
_LOGGER.debug("Setting brightness to %s", x)
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):
_LOGGER.debug("Setting light state to %s", x)
light_state = self.proto["system"]["get_sysinfo"]["light_state"]
Expand Down Expand Up @@ -392,7 +401,10 @@ def light_state(self, x, *args):
"set_timezone": None,
},
# 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):
Expand Down
134 changes: 134 additions & 0 deletions kasa/tests/test_dimmer.py
Original file line number Diff line number Diff line change
@@ -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)