8000 Fix iot light effect brightness (#1092) · python-kasa/python-kasa@31ec27c · GitHub
[go: up one dir, main page]

Skip to content

Commit 31ec27c

Browse files
authored
Fix iot light effect brightness (#1092)
Fixes issue where the brightness of the `iot` light effect is set properly on the light effect but read back incorrectly from the light.
1 parent cb0077f commit 31ec27c

File tree

8 files changed

+101
-37
lines changed

8 files changed

+101
-37
lines changed

kasa/iot/iotbulb.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ def _hsv(self) -> HSV:
365365

366366
hue = light_state["hue"]
367367
saturation = light_state["saturation"]
368-
value = light_state["brightness"]
368+
value = self._brightness
369369

370370
return HSV(hue, saturation, value)
371371

@@ -455,6 +455,13 @@ def _brightness(self) -> int:
455455
if not self._is_dimmable: # pragma: no cover
456456
raise KasaException("Bulb is not dimmable.")
457457

458+
# If the device supports effects and one is active, we get the brightness
459+
# from the effect. This is not required when setting the brightness as
460+
# the device handles it via set_light_state
461+
if (
462+
light_effect := self.modules.get(Module.IotLightEffect)
463+
) is not None and light_effect.effect != light_effect.LIGHT_EFFECTS_OFF:
464+
return light_effect.brightness
458465
light_state = self.light_state
459466
return int(light_state["brightness"])
460467

kasa/iot/modules/lighteffect.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from __future__ import annotations
44

55
from ...interfaces.lighteffect import LightEffect as LightEffectInterface
6-
from ...module import Module
76
from ..effects import EFFECT_MAPPING_V1, EFFECT_NAMES_V1
87
from ..iotmodule import IotModule
98

@@ -29,6 +28,11 @@ def effect(self) -> str:
2928

3029
return self.LIGHT_EFFECTS_OFF
3130

31+
@property
32+
def brightness(self) -> int:
33+
"""Return light effect brightness."""
34+
return self.data["lighting_effect_state"]["brightness"]
35+
3236
@property
3337
def effect_list(self) -> list[str]:
3438
"""Return built-in effects list.
@@ -60,18 +64,21 @@ async def set_effect(
6064
:param int transition: The wanted transition time
6165
"""
6266
if effect == self.LIGHT_EFFECTS_OFF:
63-
light_module = self._device.modules[Module.Light]
64-
effect_off_state = light_module.state
65-
if brightness is not None:
66-
effect_off_state.brightness = brightness
67-
if transition is not None:
68-
effect_off_state.transition = transition
69-
await light_module.set_state(effect_off_state)
67+
if self.effect in EFFECT_MAPPING_V1:
68+
# TODO: We could query get_lighting_effect here to
69+
# get the custom effect although not sure how to find
70+
# custom effects
71+
effect_dict = EFFECT_MAPPING_V1[self.effect]
72+
else:
73+
effect_dict = EFFECT_MAPPING_V1["Aurora"]
74+
effect_dict = {**effect_dict}
75+
effect_dict["enable"] = 0
76+
await self.set_custom_effect(effect_dict)
7077
elif effect not in EFFECT_MAPPING_V1:
7178
raise ValueError(f"The effect {effect} is not a built in effect.")
7279
else:
7380
effect_dict = EFFECT_MAPPING_V1[effect]
74-
81+
effect_dict = {**effect_dict}
7582
if brightness is not None:
7683
effect_dict["brightness"] = brightness
7784
if transition is not None:

kasa/module.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ class Module(ABC):
116116
SmartLightEffect: Final[ModuleName[smart.SmartLightEffect]] = ModuleName(
117117
"LightEffect"
118118
)
119+
IotLightEffect: Final[ModuleName[iot.LightEffect]] = ModuleName("LightEffect")
119120
TemperatureSensor: Final[ModuleName[smart.TemperatureSensor]] = ModuleName(
120121
"TemperatureSensor"
121122
)

kasa/smart/modules/lightstripeffect.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,14 +106,23 @@ async def set_effect(
106106
"""
107107
brightness_module = self._device.modules[Module. F438 Brightness]
108108
if effect == self.LIGHT_EFFECTS_OFF:
109-
state = self._device.modules[Module.Light].state
110-
await self._device.modules[Module.Light].set_state(state)
109+
if self.effect in self._effect_mapping:
110+
# TODO: We could query get_lighting_effect here to
111+
# get the custom effect although not sure how to find
112+
# custom effects
113+
effect_dict = self._effect_mapping[self.effect]
114+
else:
115+
effect_dict = self._effect_mapping["Aurora"]
116+
effect_dict = {**effect_dict}
117+
effect_dict["enable"] = 0
118+
await self.set_custom_effect(effect_dict)
111119
return
112120

113121
if effect not in self._effect_mapping:
114122
raise ValueError(f"The effect {effect} is not a built in effect.")
115123
else:
116124
effect_dict = self._effect_mapping[effect]
125+
effect_dict = {**effect_dict}
117126

118127
# Use explicitly given brightness
119128
if brightness is not None:

kasa/tests/fakeprotocol_iot.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,26 @@ def set_lighting_effect(self, effect, *args):
292292
self.proto["system"]["get_sysinfo"]["lighting_effect_state"] = dict(effect)
293293

294294
def transition_light_state(self, state_changes, *args):
295+
# Setting the light state on a device will turn off any active lighting effects.
296+
# Unless it's just the brightness in which case it will update the brightness for
297+
# the lighting effect
298+
if lighting_effect_state := self.proto["system"]["get_sysinfo"].get(
299+
"lighting_effect_state"
300+
):
301+
if (
302+
"hue" in state_changes
303+
or "saturation" in state_changes
304+
or "color_temp" in state_changes
305+
):
306+
lighting_effect_state["enable"] = 0
307+
elif (
308+
lighting_effect_state["enable"] == 1
309+
and state_changes.get("on_off") != 0
310+
and (brightness := state_changes.get("brightness"))
311+
):
312+
lighting_effect_state["brightness"] = brightness
313+
return
314+
295315
_LOGGER.debug("Setting light state to %s", state_changes)
296316
light_state = self.proto["system"]["get_sysinfo"]["light_state"]
297317

@@ -317,12 +337,6 @@ def transition_light_state(self, state_changes, *args):
317337
_LOGGER.debug("New light state: %s", new_state)
318338
self.proto["system"]["get_sysinfo"]["light_state"] = new_state
319339

320-
# Setting the light state on a device will turn off any active lighting effects.
321-
if lighting_effect_state := self.proto["system"]["get_sysinfo"].get(
322-
"lighting_effect_state"
323-
):
324-
lighting_effect_state["enable"] = 0
325-
326340
def set_preferred_state(self, new_state, *args):
327341
"""Implement set_preferred_state."""
328342
self.proto["system"]["get_sysinfo"]["preferred_state"][new_state["index"]] = (

kasa/tests/fakeprotocol_smart.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -271,13 +271,14 @@ def _set_edit_dynamic_light_effect_rule(self, info, params):
271271

272272
def _set_light_strip_effect(self, info, params):
273273
"""Set or remove values as per the device behaviour."""
274-
info["get_device_info"]["lighting_effect"]["enable"] = params["enable"]
275-
info["get_device_info"]["lighting_effect"]["name"] = params["name"]
276-
info["get_device_info"]["lighting_effect"]["id"] = params["id"]
277274
# Brightness is not always available
278275
if (brightness := params.get("brightness")) is not None:
279276
info["get_device_info"]["lighting_effect"]["brightness"] = brightness
280-
info["get_lighting_effect"] = copy.deepcopy(params)
277+
if "enable" in params:
278+
info["get_device_info"]["lighting_effect"]["enable"] = params["enable"]
279+
info["get_device_info"]["lighting_effect"]["name"] = params["name"]
280+
info["get_device_info"]["lighting_effect"]["id"] = params["id"]
281+
info["get_lighting_effect"] = copy.deepcopy(params)
281282

282283
def _set_led_info(self, info, params):
283284
"""Set or remove values as per the device behaviour."""

kasa/tests/smart/modules/test_light_strip_effect.py

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,26 +30,23 @@ async def test_light_strip_effect(dev: Device, mocker: MockerFixture):
3030

3131
call = mocker.spy(light_effect, "call")
3232

33-
light = dev.modules[Module.Light]
34-
light_call = mocker.spy(light, "call")
35-
3633
assert feature.choices == light_effect.effect_list
3734
assert feature.choices
3835
for effect in chain(reversed(feature.choices), feature.choices):
36+
if effect == LightEffect.LIGHT_EFFECTS_OFF:
37+
off_effect = (
38+
light_effect.effect
39+
if light_effect.effect in light_effect._effect_mapping
40+
else "Aurora"
41+
)
3942
await light_effect.set_effect(effect)
4043

41-
if effect == LightEffect.LIGHT_EFFECTS_OFF:
42-
light_call.assert_called()
43-
continue
44-
45-
# Start with the current effect data
46-
params = light_effect.data["lighting_effect"]
47-
enable = effect != LightEffect.LIGHT_EFFECTS_OFF
48-
params["enable"] = enable
49-
if enable:
50-
params = light_effect._effect_mapping[effect]
51-
params["enable"] = enable
52-
params["brightness"] = brightness.brightness # use the existing brightness
44+
if effect != LightEffect.LIGHT_EFFECTS_OFF:
45+
params = {**light_effect._effect_mapping[effect]}
46+
else:
47+
params = {**light_effect._effect_mapping[off_effect]}
48+
params["enable"] = 0
49+
params["brightness"] = brightness.brightness # use the existing brightness
5350

5451
call.assert_called_with("set_lighting_effect", params)
5552

kasa/tests/test_common_modules.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,31 @@ async def test_light_effect_module(dev: Device, mocker: MockerFixture):
133133
call.assert_not_called()
134134

135135

136+
@light_effect
137+
async def test_light_effect_brightness(dev: Device, mocker: MockerFixture):
138+
"""Test that light module uses light_effect for brightness when active."""
139+
light_module = dev.modules[Module.Light]
140+
141+
light_effect = dev.modules[Module.LightEffect]
142+
143+
await light_effect.set_effect(light_effect.LIGHT_EFFECTS_OFF)
144+
await light_module.set_brightness(50)
145+
await dev.update()
146+
assert light_effect.effect == light_effect.LIGHT_EFFECTS_OFF
147+
assert light_module.brightness == 50
148+
await light_effect.set_effect(light_effect.effect_list[1])
149+
await dev.update()
150+
# assert light_module.brightness == 100
151+
152+
await light_module.set_brightness(75)
153+
await dev.update()
154+
assert light_module.brightness == 75
155+
156+
await light_effect.set_effect(light_effect.LIGHT_EFFECTS_OFF)
157+
await dev.update()
158+
assert light_module.brightness == 50
159+
160+
136161
@dimmable
137162
async def test_light_brightness(dev: Device):
138163
"""Test brightness setter and getter."""
@@ -201,6 +226,9 @@ async def test_light_set_state(dev: Device):
201226
assert isinstance(dev, Device)
202227
light = next(get_parent_and_child_modules(dev, Module.Light))
203228
assert light
229+
# For fixtures that have a light effect active switch off
230+
if light_effect := light._device.modules.get(Module.LightEffect):
231+
await light_effect.set_effect(light_effect.LIGHT_EFFECTS_OFF)
204232

205233
await light.set_state(LightState(light_on=False))
206234
await dev.update()

0 commit comments

Comments
 (0)
0