diff --git a/kasa/iot/iotbulb.py b/kasa/iot/iotbulb.py index 26c73096a..ed2d0a3ce 100644 --- a/kasa/iot/iotbulb.py +++ b/kasa/iot/iotbulb.py @@ -364,7 +364,7 @@ def _hsv(self) -> HSV: hue = light_state["hue"] saturation = light_state["saturation"] - value = light_state["brightness"] + value = self._brightness return HSV(hue, saturation, value) @@ -454,6 +454,13 @@ def _brightness(self) -> int: if not self._is_dimmable: # pragma: no cover raise KasaException("Bulb is not dimmable.") + # If the device supports effects and one is active, we get the brightness + # from the effect. This is not required when setting the brightness as + # the device handles it via set_light_state + if ( + light_effect := self.modules.get(Module.IotLightEffect) + ) is not None and light_effect.effect != light_effect.LIGHT_EFFECTS_OFF: + return light_effect.brightness light_state = self.light_state return int(light_state["brightness"]) diff --git a/kasa/iot/modules/lighteffect.py b/kasa/iot/modules/lighteffect.py index 8f855bcf2..3a13f6806 100644 --- a/kasa/iot/modules/lighteffect.py +++ b/kasa/iot/modules/lighteffect.py @@ -3,7 +3,6 @@ from __future__ import annotations from ...interfaces.lighteffect import LightEffect as LightEffectInterface -from ...module import Module from ..effects import EFFECT_MAPPING_V1, EFFECT_NAMES_V1 from ..iotmodule import IotModule @@ -29,6 +28,11 @@ def effect(self) -> str: return self.LIGHT_EFFECTS_OFF + @property + def brightness(self) -> int: + """Return light effect brightness.""" + return self.data["lighting_effect_state"]["brightness"] + @property def effect_list(self) -> list[str]: """Return built-in effects list. @@ -60,18 +64,21 @@ async def set_effect( :param int transition: The wanted transition time """ if effect == self.LIGHT_EFFECTS_OFF: - light_module = self._device.modules[Module.Light] - effect_off_state = light_module.state - if brightness is not None: - effect_off_state.brightness = brightness - if transition is not None: - effect_off_state.transition = transition - await light_module.set_state(effect_off_state) + if self.effect in EFFECT_MAPPING_V1: + # TODO: We could query get_lighting_effect here to + # get the custom effect although not sure how to find + # custom effects + effect_dict = EFFECT_MAPPING_V1[self.effect] + else: + effect_dict = EFFECT_MAPPING_V1["Aurora"] + effect_dict = {**effect_dict} + effect_dict["enable"] = 0 + await self.set_custom_effect(effect_dict) elif effect not in EFFECT_MAPPING_V1: raise ValueError(f"The effect {effect} is not a built in effect.") else: effect_dict = EFFECT_MAPPING_V1[effect] - + effect_dict = {**effect_dict} if brightness is not None: effect_dict["brightness"] = brightness if transition is not None: diff --git a/kasa/module.py b/kasa/module.py index fe370603c..faf17c4d3 100644 --- a/kasa/module.py +++ b/kasa/module.py @@ -116,6 +116,7 @@ class Module(ABC): SmartLightEffect: Final[ModuleName[smart.SmartLightEffect]] = ModuleName( "LightEffect" ) + IotLightEffect: Final[ModuleName[iot.LightEffect]] = ModuleName("LightEffect") TemperatureSensor: Final[ModuleName[smart.TemperatureSensor]] = ModuleName( "TemperatureSensor" ) diff --git a/kasa/smart/modules/lightstripeffect.py b/kasa/smart/modules/lightstripeffect.py index f75620686..3b0ff7da5 100644 --- a/kasa/smart/modules/lightstripeffect.py +++ b/kasa/smart/modules/lightstripeffect.py @@ -106,14 +106,23 @@ async def set_effect( """ brightness_module = self._device.modules[Module.Brightness] if effect == self.LIGHT_EFFECTS_OFF: - state = self._device.modules[Module.Light].state - await self._device.modules[Module.Light].set_state(state) + if self.effect in self._effect_mapping: + # TODO: We could query get_lighting_effect here to + # get the custom effect although not sure how to find + # custom effects + effect_dict = self._effect_mapping[self.effect] + else: + effect_dict = self._effect_mapping["Aurora"] + effect_dict = {**effect_dict} + effect_dict["enable"] = 0 + await self.set_custom_effect(effect_dict) return if effect not in self._effect_mapping: raise ValueError(f"The effect {effect} is not a built in effect.") else: effect_dict = self._effect_mapping[effect] + effect_dict = {**effect_dict} # Use explicitly given brightness if brightness is not None: diff --git a/kasa/tests/fakeprotocol_iot.py b/kasa/tests/fakeprotocol_iot.py index 9c5f655c4..0a5433206 100644 --- a/kasa/tests/fakeprotocol_iot.py +++ b/kasa/tests/fakeprotocol_iot.py @@ -292,6 +292,26 @@ def set_lighting_effect(self, effect, *args): self.proto["system"]["get_sysinfo"]["lighting_effect_state"] = dict(effect) def transition_light_state(self, state_changes, *args): + # Setting the light state on a device will turn off any active lighting effects. + # Unless it's just the brightness in which case it will update the brightness for + # the lighting effect + if lighting_effect_state := self.proto["system"]["get_sysinfo"].get( + "lighting_effect_state" + ): + if ( + "hue" in state_changes + or "saturation" in state_changes + or "color_temp" in state_changes + ): + lighting_effect_state["enable"] = 0 + elif ( + lighting_effect_state["enable"] == 1 + and state_changes.get("on_off") != 0 + and (brightness := state_changes.get("brightness")) + ): + lighting_effect_state["brightness"] = brightness + return + _LOGGER.debug("Setting light state to %s", state_changes) light_state = self.proto["system"]["get_sysinfo"]["light_state"] @@ -317,12 +337,6 @@ def transition_light_state(self, state_changes, *args): _LOGGER.debug("New light state: %s", new_state) self.proto["system"]["get_sysinfo"]["light_state"] = new_state - # Setting the light state on a device will turn off any active lighting effects. - if lighting_effect_state := self.proto["system"]["get_sysinfo"].get( - "lighting_effect_state" - ): - lighting_effect_state["enable"] = 0 - def set_preferred_state(self, new_state, *args): """Implement set_preferred_state.""" self.proto["system"]["get_sysinfo"]["preferred_state"][new_state["index"]] = ( diff --git a/kasa/tests/fakeprotocol_smart.py b/kasa/tests/fakeprotocol_smart.py index 40465b6f7..6c9423ecc 100644 --- a/kasa/tests/fakeprotocol_smart.py +++ b/kasa/tests/fakeprotocol_smart.py @@ -271,13 +271,14 @@ def _set_edit_dynamic_light_effect_rule(self, info, params): def _set_light_strip_effect(self, info, params): """Set or remove values as per the device behaviour.""" - info["get_device_info"]["lighting_effect"]["enable"] = params["enable"] - info["get_device_info"]["lighting_effect"]["name"] = params["name"] - info["get_device_info"]["lighting_effect"]["id"] = params["id"] # Brightness is not always available if (brightness := params.get("brightness")) is not None: info["get_device_info"]["lighting_effect"]["brightness"] = brightness - info["get_lighting_effect"] = copy.deepcopy(params) + if "enable" in params: + info["get_device_info"]["lighting_effect"]["enable"] = params["enable"] + info["get_device_info"]["lighting_effect"]["name"] = params["name"] + info["get_device_info"]["lighting_effect"]["id"] = params["id"] + info["get_lighting_effect"] = copy.deepcopy(params) def _set_led_info(self, info, params): """Set or remove values as per the device behaviour.""" diff --git a/kasa/tests/smart/modules/test_light_strip_effect.py b/kasa/tests/smart/modules/test_light_strip_effect.py index 92ef2202c..283d294d2 100644 --- a/kasa/tests/smart/modules/test_light_strip_effect.py +++ b/kasa/tests/smart/modules/test_light_strip_effect.py @@ -30,26 +30,23 @@ async def test_light_strip_effect(dev: Device, mocker: MockerFixture): call = mocker.spy(light_effect, "call") - light = dev.modules[Module.Light] - light_call = mocker.spy(light, "call") - assert feature.choices == light_effect.effect_list assert feature.choices for effect in chain(reversed(feature.choices), feature.choices): + if effect == LightEffect.LIGHT_EFFECTS_OFF: + off_effect = ( + light_effect.effect + if light_effect.effect in light_effect._effect_mapping + else "Aurora" + ) await light_effect.set_effect(effect) - if effect == LightEffect.LIGHT_EFFECTS_OFF: - light_call.assert_called() - continue - - # Start with the current effect data - params = light_effect.data["lighting_effect"] - enable = effect != LightEffect.LIGHT_EFFECTS_OFF - params["enable"] = enable - if enable: - params = light_effect._effect_mapping[effect] - params["enable"] = enable - params["brightness"] = brightness.brightness # use the existing brightness + if effect != LightEffect.LIGHT_EFFECTS_OFF: + params = {**light_effect._effect_mapping[effect]} + else: + params = {**light_effect._effect_mapping[off_effect]} + params["enable"] = 0 + params["brightness"] = brightness.brightness # use the existing brightness call.assert_called_with("set_lighting_effect", params) diff --git a/kasa/tests/test_common_modules.py b/kasa/tests/test_common_modules.py index beed8e8ba..03671109f 100644 --- a/kasa/tests/test_common_modules.py +++ b/kasa/tests/test_common_modules.py @@ -124,6 +124,31 @@ async def test_light_effect_module(dev: Device, mocker: MockerFixture): call.assert_not_called() +@light_effect +async def test_light_effect_brightness(dev: Device, mocker: MockerFixture): + """Test that light module uses light_effect for brightness when active.""" + light_module = dev.modules[Module.Light] + + light_effect = dev.modules[Module.LightEffect] + + await light_effect.set_effect(light_effect.LIGHT_EFFECTS_OFF) + await light_module.set_brightness(50) + await dev.update() + assert light_effect.effect == light_effect.LIGHT_EFFECTS_OFF + assert light_module.brightness == 50 + await light_effect.set_effect(light_effect.effect_list[1]) + await dev.update() + # assert light_module.brightness == 100 + + await light_module.set_brightness(75) + await dev.update() + assert light_module.brightness == 75 + + await light_effect.set_effect(light_effect.LIGHT_EFFECTS_OFF) + await dev.update() + assert light_module.brightness == 50 + + @dimmable async def test_light_brightness(dev: Device): """Test brightness setter and getter.""" @@ -153,6 +178,9 @@ async def test_light_set_state(dev: Device): assert isinstance(dev, Device) light = next(get_parent_and_child_modules(dev, Module.Light)) assert light + # For fixtures that have a light effect active switch off + if light_effect := light._device.modules.get(Module.LightEffect): + await light_effect.set_effect(light_effect.LIGHT_EFFECTS_OFF) await light.set_state(LightState(light_on=False)) await dev.update()