mirror of
https://github.com/python-kasa/python-kasa.git
synced 2026-10-21 04:02:54 +00:00
Expose the light transition state change flag (#1745)
Bulbs and light strips can fade smoothly when their brightness or colour changes, instead of snapping straight to the new value. The Tapo app exposes this as "Smooth Transition". python-kasa cannot read or change it today, so the setting is missing from Home Assistant even though the device supports it. This adds support for this feature as a switch: - read `change_state` in `_post_update_hook` - add a `change_transition` property and `set_change_transition()` - register a `smooth_transition_change` switch feature when the device reports the flag
This commit is contained in:
@@ -39,6 +39,7 @@ class LightTransition(SmartModule):
|
|||||||
_on_state: _State
|
_on_state: _State
|
||||||
_off_state: _State
|
_off_state: _State
|
||||||
_enabled: bool
|
_enabled: bool
|
||||||
|
_change_transition: bool | None = None
|
||||||
|
|
||||||
def __init__(self, device: SmartDevice, module: str) -> None:
|
def __init__(self, device: SmartDevice, module: str) -> None:
|
||||||
super().__init__(device, module)
|
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:
|
async def _post_update_hook(self) -> None:
|
||||||
"""Update the states."""
|
"""Update the states."""
|
||||||
# Assumes any device with state in sysinfo supports on and off and
|
# Assumes any device with state in sysinfo supports on and off and
|
||||||
# has maximum values for both.
|
# has maximum values for both.
|
||||||
# v2 adds separate on & off states
|
# v2 adds separate on & off states
|
||||||
# v3 adds max_duration except for ks240 which is v2 but supports it
|
# v3 adds max_duration except for ks240 which is v2 but supports it
|
||||||
|
# v4 adds change_state
|
||||||
if not self._supports_on_and_off:
|
if not self._supports_on_and_off:
|
||||||
self._enabled = self.data["enable"]
|
self._enabled = self.data["enable"]
|
||||||
return
|
return
|
||||||
@@ -138,6 +156,9 @@ class LightTransition(SmartModule):
|
|||||||
"max_duration": off_max,
|
"max_duration": off_max,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (change_state := self.data.get("change_state")) is not None:
|
||||||
|
self._change_transition = change_state["enable"]
|
||||||
|
|
||||||
@allow_update_after
|
@allow_update_after
|
||||||
async def set_enabled(self, enable: bool) -> dict:
|
async def set_enabled(self, enable: bool) -> dict:
|
||||||
"""Enable gradual on/off."""
|
"""Enable gradual on/off."""
|
||||||
@@ -169,6 +190,23 @@ class LightTransition(SmartModule):
|
|||||||
"""Return True if gradual on/off is enabled."""
|
"""Return True if gradual on/off is enabled."""
|
||||||
return self._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
|
@property
|
||||||
def turn_on_transition(self) -> int:
|
def turn_on_transition(self) -> int:
|
||||||
"""Return transition time for turning the light on.
|
"""Return transition time for turning the light on.
|
||||||
|
|||||||
@@ -480,6 +480,12 @@ class FakeSmartTransport(BaseTransport):
|
|||||||
info["get_on_off_gradually_info"]["off_state"]["duration"] = (
|
info["get_on_off_gradually_info"]["off_state"]["duration"] = (
|
||||||
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}
|
return {"error_code": 0}
|
||||||
|
|
||||||
def _set_dynamic_light_effect(self, info, params):
|
def _set_dynamic_light_effect(self, info, params):
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import pytest
|
||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
|
|
||||||
from kasa import Feature, Module
|
from kasa import Feature, KasaException, Module
|
||||||
from kasa.smart import SmartDevice
|
from kasa.smart import SmartDevice
|
||||||
|
|
||||||
from ...device_fixtures import get_parent_and_child_modules, parametrize
|
from ...device_fixtures import get_parent_and_child_modules, parametrize
|
||||||
@@ -20,6 +21,11 @@ light_transition_gt_v1 = parametrize(
|
|||||||
),
|
),
|
||||||
protocol_filter={"SMART"},
|
protocol_filter={"SMART"},
|
||||||
)
|
)
|
||||||
|
light_transition_any = parametrize(
|
||||||
|
"has light transition",
|
||||||
|
component_filter="on_off_gradually",
|
||||||
|
protocol_filter={"SMART"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@light_transition_v1
|
@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 light_transition.set_turn_off_transition(0)
|
||||||
await dev.update()
|
await dev.update()
|
||||||
assert light_transition.enabled is False
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user