diff --git a/kasa/smart/modules/lighttransition.py b/kasa/smart/modules/lighttransition.py index e623108f..14fe302d 100644 --- a/kasa/smart/modules/lighttransition.py +++ b/kasa/smart/modules/lighttransition.py @@ -39,6 +39,7 @@ class LightTransition(SmartModule): _on_state: _State _off_state: _State _enabled: bool + _change_transition: bool | None = None def __init__(self, device: SmartDevice, module: str) -> None: super().__init__(device, module) @@ -91,12 +92,29 @@ class LightTransition(SmartModule): ) ) + # Fading between states while the light is already on is a separate + # flag from the on/off fades, and only v4 reports it. + if self._change_transition is not None: + self._add_feature( + Feature( + self._device, + id="smooth_transition_change", + name="Smooth transition on change", + container=self, + attribute_getter="change_transition", + attribute_setter="set_change_transition", + icon=icon, + type=Feature.Type.Switch, + ) + ) + async def _post_update_hook(self) -> None: """Update the states.""" # Assumes any device with state in sysinfo supports on and off and # has maximum values for both. # v2 adds separate on & off states # v3 adds max_duration except for ks240 which is v2 but supports it + # v4 adds change_state if not self._supports_on_and_off: self._enabled = self.data["enable"] return @@ -138,6 +156,9 @@ class LightTransition(SmartModule): "max_duration": off_max, } + if (change_state := self.data.get("change_state")) is not None: + self._change_transition = change_state["enable"] + @allow_update_after async def set_enabled(self, enable: bool) -> dict: """Enable gradual on/off.""" @@ -169,6 +190,23 @@ class LightTransition(SmartModule): """Return True if gradual on/off is enabled.""" return self._enabled + @property + def change_transition(self) -> bool: + """Return True if fading between states is enabled.""" + if self._change_transition is None: + raise KasaException( + f"Device does not support state change transitions for " + f"{self.REQUIRED_COMPONENT} v{self.supported_version}" + ) + return self._change_transition + + @allow_update_after + async def set_change_transition(self, enable: bool) -> dict: + """Enable fading between states while the light is on.""" + return await self.call( + "set_on_off_gradually_info", {"change_state": {"enable": enable}} + ) + @property def turn_on_transition(self) -> int: """Return transition time for turning the light on. diff --git a/tests/fakeprotocol_smart.py b/tests/fakeprotocol_smart.py index 257e07ea..c4f23256 100644 --- a/tests/fakeprotocol_smart.py +++ b/tests/fakeprotocol_smart.py @@ -480,6 +480,12 @@ class FakeSmartTransport(BaseTransport): info["get_on_off_gradually_info"]["off_state"]["duration"] = ( off_state["duration"] ) + elif (change_state := params.get("change_state")) and ( + "get_on_off_gradually_info" in info + ): + info["get_on_off_gradually_info"]["change_state"]["enable"] = change_state[ + "enable" + ] return {"error_code": 0} def _set_dynamic_light_effect(self, info, params): diff --git a/tests/smart/modules/test_lighttransition.py b/tests/smart/modules/test_lighttransition.py index 2ef5a9e7..5cbadaa9 100644 --- a/tests/smart/modules/test_lighttransition.py +++ b/tests/smart/modules/test_lighttransition.py @@ -1,6 +1,7 @@ +import pytest from pytest_mock import MockerFixture -from kasa import Feature, Module +from kasa import Feature, KasaException, Module from kasa.smart import SmartDevice from ...device_fixtures import get_parent_and_child_modules, parametrize @@ -20,6 +21,11 @@ light_transition_gt_v1 = parametrize( ), protocol_filter={"SMART"}, ) +light_transition_any = parametrize( + "has light transition", + component_filter="on_off_gradually", + protocol_filter={"SMART"}, +) @light_transition_v1 @@ -79,3 +85,33 @@ async def test_module_gt_v1(dev: SmartDevice, mocker: MockerFixture) -> None: await light_transition.set_turn_off_transition(0) await dev.update() assert light_transition.enabled is False + + +@light_transition_any +async def test_change_transition(dev: SmartDevice, mocker: MockerFixture) -> None: + """Test the state change transition flag, which only newer firmware reports.""" + light_transition = next(get_parent_and_child_modules(dev, Module.LightTransition)) + supported = isinstance(light_transition.data.get("change_state"), dict) + + if not supported: + assert "smooth_transition_change" not in light_transition._module_features + with pytest.raises(KasaException, match="does not support state change"): + _ = light_transition.change_transition + return + + feat = light_transition._module_features["smooth_transition_change"] + assert feat.type is Feature.Type.Switch + assert feat.value == light_transition.change_transition + + call_spy = mocker.spy(light_transition, "call") + await light_transition.set_change_transition(True) + call_spy.assert_called_once_with( + "set_on_off_gradually_info", {"change_state": {"enable": True}} + ) + + await dev.update() + assert light_transition.change_transition is True + + await light_transition.set_change_transition(False) + await dev.update() + assert light_transition.change_transition is False