Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Fix iot light effect brightness #1092

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 31, 2024
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
9 changes: 8 additions & 1 deletion kasa/iot/iotbulb.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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"])

Expand Down
25 changes: 16 additions & 9 deletions kasa/iot/modules/lighteffect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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.
Expand Down Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions kasa/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down
13 changes: 11 additions & 2 deletions kasa/smart/modules/lightstripeffect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
26 changes: 20 additions & 6 deletions kasa/tests/fakeprotocol_iot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand All @@ -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"]] = (
Expand Down
9 changes: 5 additions & 4 deletions kasa/tests/fakeprotocol_smart.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
27 changes: 12 additions & 15 deletions kasa/tests/smart/modules/test_light_strip_effect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
28 changes: 28 additions & 0 deletions kasa/tests/test_common_modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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()
Expand Down
Loading