mirror of
https://github.com/python-kasa/python-kasa.git
synced 2025-08-06 10:44:04 +00:00
Add light presets common module to devices. (#907)
Adds light preset common module for switching to presets and saving presets. Deprecates the `presets` attribute and `save_preset` method from the `bulb` interface in favour of the modular approach. Allows setting preset for `iot` which was not previously supported.
This commit is contained in:
@@ -157,6 +157,8 @@ class FakeSmartTransport(BaseTransport):
|
||||
elif child_method == "set_device_info":
|
||||
info.update(child_params)
|
||||
return {"error_code": 0}
|
||||
elif child_method == "set_preset_rules":
|
||||
return self._set_child_preset_rules(info, child_params)
|
||||
elif (
|
||||
# FIXTURE_MISSING is for service calls not in place when
|
||||
# SMART fixtures started to be generated
|
||||
@@ -205,6 +207,30 @@ class FakeSmartTransport(BaseTransport):
|
||||
info["get_led_info"]["led_status"] = params["led_rule"] != "never"
|
||||
info["get_led_info"]["led_rule"] = params["led_rule"]
|
||||
|
||||
def _set_preset_rules(self, info, params):
|
||||
"""Set or remove values as per the device behaviour."""
|
||||
if "brightness" not in info["get_preset_rules"]:
|
||||
return {"error_code": SmartErrorCode.PARAMS_ERROR}
|
||||
info["get_preset_rules"]["brightness"] = params["brightness"]
|
||||
return {"error_code": 0}
|
||||
|
||||
def _set_child_preset_rules(self, info, params):
|
||||
"""Set or remove values as per the device behaviour."""
|
||||
# So far the only child device with light preset (KS240) has the
|
||||
# data available to read in the device_info. If a child device
|
||||
# appears that doesn't have this this will need to be extended.
|
||||
if "preset_state" not in info:
|
||||
return {"error_code": SmartErrorCode.PARAMS_ERROR}
|
||||
info["preset_state"] = [{"brightness": b} for b in params["brightness"]]
|
||||
return {"error_code": 0}
|
||||
|
||||
def _edit_preset_rules(self, info, params):
|
||||
"""Set or remove values as per the device behaviour."""
|
||||
if "states" not in info["get_preset_rules"] is None:
|
||||
return {"error_code": SmartErrorCode.PARAMS_ERROR}
|
||||
info["get_preset_rules"]["states"][params["index"]] = params["state"]
|
||||
return {"error_code": 0}
|
||||
|
||||
def _send_request(self, request_dict: dict):
|
||||
method = request_dict["method"]
|
||||
params = request_dict["params"]
|
||||
@@ -276,6 +302,10 @@ class FakeSmartTransport(BaseTransport):
|
||||
elif method == "set_led_info":
|
||||
self._set_led_info(info, params)
|
||||
return {"error_code": 0}
|
||||
elif method == "set_preset_rules":
|
||||
return self._set_preset_rules(info, params)
|
||||
elif method == "edit_preset_rules":
|
||||
return self._edit_preset_rules(info, params)
|
||||
elif method[:4] == "set_":
|
||||
target_method = f"get_{method[4:]}"
|
||||
info[target_method].update(params)
|
||||
|
@@ -1,3 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from voluptuous import (
|
||||
All,
|
||||
@@ -7,7 +9,7 @@ from voluptuous import (
|
||||
Schema,
|
||||
)
|
||||
|
||||
from kasa import Device, DeviceType, KasaException, LightPreset, Module
|
||||
from kasa import Device, DeviceType, IotLightPreset, KasaException, Module
|
||||
from kasa.iot import IotBulb, IotDimmer
|
||||
|
||||
from .conftest import (
|
||||
@@ -85,7 +87,7 @@ async def test_hsv(dev: Device, turn_on):
|
||||
|
||||
@color_bulb_iot
|
||||
async def test_set_hsv_transition(dev: IotBulb, mocker):
|
||||
set_light_state = mocker.patch("kasa.iot.IotBulb.set_light_state")
|
||||
set_light_state = mocker.patch("kasa.iot.IotBulb._set_light_state")
|
||||
await dev.set_hsv(10, 10, 100, transition=1000)
|
||||
|
||||
set_light_state.assert_called_with(
|
||||
@@ -158,7 +160,7 @@ async def test_try_set_colortemp(dev: Device, turn_on):
|
||||
|
||||
@variable_temp_iot
|
||||
async def test_set_color_temp_transition(dev: IotBulb, mocker):
|
||||
set_light_state = mocker.patch("kasa.iot.IotBulb.set_light_state")
|
||||
set_light_state = mocker.patch("kasa.iot.IotBulb._set_light_state")
|
||||
await dev.set_color_temp(2700, transition=100)
|
||||
|
||||
set_light_state.assert_called_with({"color_temp": 2700}, transition=100)
|
||||
@@ -224,7 +226,7 @@ async def test_dimmable_brightness(dev: IotBulb, turn_on):
|
||||
|
||||
@bulb_iot
|
||||
async def test_turn_on_transition(dev: IotBulb, mocker):
|
||||
set_light_state = mocker.patch("kasa.iot.IotBulb.set_light_state")
|
||||
set_light_state = mocker.patch("kasa.iot.IotBulb._set_light_state")
|
||||
await dev.turn_on(transition=1000)
|
||||
|
||||
set_light_state.assert_called_with({"on_off": 1}, transition=1000)
|
||||
@@ -236,7 +238,7 @@ async def test_turn_on_transition(dev: IotBulb, mocker):
|
||||
|
||||
@bulb_iot
|
||||
async def test_dimmable_brightness_transition(dev: IotBulb, mocker):
|
||||
set_light_state = mocker.patch("kasa.iot.IotBulb.set_light_state")
|
||||
set_light_state = mocker.patch("kasa.iot.IotBulb._set_light_state")
|
||||
await dev.set_brightness(10, transition=1000)
|
||||
|
||||
set_light_state.assert_called_with({"brightness": 10}, transition=1000)
|
||||
@@ -297,14 +299,14 @@ async def test_modify_preset(dev: IotBulb, mocker):
|
||||
if not dev.presets:
|
||||
pytest.skip("Some strips do not support presets")
|
||||
|
||||
data = {
|
||||
data: dict[str, int | None] = {
|
||||
"index": 0,
|
||||
"brightness": 10,
|
||||
"hue": 0,
|
||||
"saturation": 0,
|
||||
"color_temp": 0,
|
||||
}
|
||||
preset = LightPreset(**data)
|
||||
preset = IotLightPreset(**data) # type: ignore[call-arg, arg-type]
|
||||
|
||||
assert preset.index == 0
|
||||
assert preset.brightness == 10
|
||||
@@ -318,7 +320,7 @@ async def test_modify_preset(dev: IotBulb, mocker):
|
||||
|
||||
with pytest.raises(KasaException):
|
||||
await dev.save_preset(
|
||||
LightPreset(index=5, hue=0, brightness=0, saturation=0, color_temp=0)
|
||||
IotLightPreset(index=5, hue=0, brightness=0, saturation=0, color_temp=0) # type: ignore[call-arg]
|
||||
)
|
||||
|
||||
|
||||
@@ -327,11 +329,11 @@ async def test_modify_preset(dev: IotBulb, mocker):
|
||||
("preset", "payload"),
|
||||
[
|
||||
(
|
||||
LightPreset(index=0, hue=0, brightness=1, saturation=0),
|
||||
IotLightPreset(index=0, hue=0, brightness=1, saturation=0), # type: ignore[call-arg]
|
||||
{"index": 0, "hue": 0, "brightness": 1, "saturation": 0},
|
||||
),
|
||||
(
|
||||
LightPreset(index=0, brightness=1, id="testid", mode=2, custom=0),
|
||||
IotLightPreset(index=0, brightness=1, id="testid", mode=2, custom=0), # type: ignore[call-arg]
|
||||
{"index": 0, "brightness": 1, "id": "testid", "mode": 2, "custom": 0},
|
||||
),
|
||||
],
|
||||
|
@@ -1,8 +1,9 @@
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from kasa import Device, Module
|
||||
from kasa import Device, LightState, Module
|
||||
from kasa.tests.device_fixtures import (
|
||||
bulb_iot,
|
||||
dimmable_iot,
|
||||
dimmer_iot,
|
||||
lightstrip_iot,
|
||||
@@ -33,6 +34,12 @@ dimmable_smart = parametrize(
|
||||
)
|
||||
dimmable = parametrize_combine([dimmable_smart, dimmer_iot, dimmable_iot])
|
||||
|
||||
light_preset_smart = parametrize(
|
||||
"has light preset smart", component_filter="preset", protocol_filter={"SMART"}
|
||||
)
|
||||
|
||||
light_preset = parametrize_combine([light_preset_smart, bulb_iot])
|
||||
|
||||
|
||||
@led
|
||||
async def test_led_module(dev: Device, mocker: MockerFixture):
|
||||
@@ -130,3 +137,80 @@ async def test_light_brightness(dev: Device):
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
await light.set_brightness(feature.maximum_value + 10)
|
||||
|
||||
|
||||
@light_preset
|
||||
async def test_light_preset_module(dev: Device, mocker: MockerFixture):
|
||||
"""Test light preset module."""
|
||||
preset_mod = dev.modules[Module.LightPreset]
|
||||
assert preset_mod
|
||||
light_mod = dev.modules[Module.Light]
|
||||
assert light_mod
|
||||
feat = dev.features["light_preset"]
|
||||
|
||||
call = mocker.spy(light_mod, "set_state")
|
||||
preset_list = preset_mod.preset_list
|
||||
assert "Not set" in preset_list
|
||||
assert preset_list.index("Not set") == 0
|
||||
assert preset_list == feat.choices
|
||||
|
||||
assert preset_mod.has_save_preset is True
|
||||
|
||||
await light_mod.set_brightness(33) # Value that should not be a preset
|
||||
assert call.call_count == 0
|
||||
await dev.update()
|
||||
assert preset_mod.preset == "Not set"
|
||||
assert feat.value == "Not set"
|
||||
|
||||
if len(preset_list) == 1:
|
||||
return
|
||||
|
||||
second_preset = preset_list[1]
|
||||
await preset_mod.set_preset(second_preset)
|
||||
assert call.call_count == 1
|
||||
await dev.update()
|
||||
assert preset_mod.preset == second_preset
|
||||
assert feat.value == second_preset
|
||||
|
||||
last_preset = preset_list[len(preset_list) - 1]
|
||||
await preset_mod.set_preset(last_preset)
|
||||
assert call.call_count == 2
|
||||
await dev.update()
|
||||
assert preset_mod.preset == last_preset
|
||||
assert feat.value == last_preset
|
||||
|
||||
# Test feature set
|
||||
await feat.set_value(second_preset)
|
||||
assert call.call_count == 3
|
||||
await dev.update()
|
||||
assert preset_mod.preset == second_preset
|
||||
assert feat.value == second_preset
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
await preset_mod.set_preset("foobar")
|
||||
assert call.call_count == 3
|
||||
|
||||
|
||||
@light_preset
|
||||
async def test_light_preset_save(dev: Device, mocker: MockerFixture):
|
||||
"""Test saving a new preset value."""
|
||||
preset_mod = dev.modules[Module.LightPreset]
|
||||
assert preset_mod
|
||||
preset_list = preset_mod.preset_list
|
||||
if len(preset_list) == 1:
|
||||
return
|
||||
|
||||
second_preset = preset_list[1]
|
||||
if preset_mod.preset_states_list[0].hue is None:
|
||||
new_preset = LightState(brightness=52)
|
||||
else:
|
||||
new_preset = LightState(brightness=52, color_temp=3000, hue=20, saturation=30)
|
||||
await preset_mod.save_preset(second_preset, new_preset)
|
||||
await dev.update()
|
||||
new_preset_state = preset_mod.preset_states_list[0]
|
||||
assert (
|
||||
new_preset_state.brightness == new_preset.brightness
|
||||
and new_preset_state.hue == new_preset.hue
|
||||
and new_preset_state.saturation == new_preset.saturation
|
||||
and new_preset_state.color_temp == new_preset.color_temp
|
||||
)
|
||||
|
@@ -1,5 +1,7 @@
|
||||
"""Tests for all devices."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import inspect
|
||||
import pkgutil
|
||||
@@ -11,6 +13,7 @@ import pytest
|
||||
import kasa
|
||||
from kasa import Credentials, Device, DeviceConfig, DeviceType, KasaException, Module
|
||||
from kasa.iot import IotDevice
|
||||
from kasa.iot.modules import IotLightPreset
|
||||
from kasa.smart import SmartChildDevice, SmartDevice
|
||||
|
||||
|
||||
@@ -238,3 +241,28 @@ async def test_deprecated_other_attributes(dev: Device):
|
||||
|
||||
await _test_attribute(dev, "led", bool(led_module), "Led")
|
||||
await _test_attribute(dev, "set_led", bool(led_module), "Led", True)
|
||||
|
||||
|
||||
async def test_deprecated_light_preset_attributes(dev: Device):
|
||||
preset = dev.modules.get(Module.LightPreset)
|
||||
|
||||
exc: type[AttributeError] | type[KasaException] | None = (
|
||||
AttributeError if not preset else None
|
||||
)
|
||||
await _test_attribute(dev, "presets", bool(preset), "LightPreset", will_raise=exc)
|
||||
|
||||
exc = None
|
||||
# deprecated save_preset not implemented for smart devices as it's unlikely anyone
|
||||
# has an existing reliance on this for the newer devices.
|
||||
if not preset or isinstance(dev, SmartDevice):
|
||||
exc = AttributeError
|
||||
elif len(preset.preset_states_list) == 0:
|
||||
exc = KasaException
|
||||
await _test_attribute(
|
||||
dev,
|
||||
"save_preset",
|
||||
bool(preset),
|
||||
"LightPreset",
|
||||
IotLightPreset(index=0, hue=100, brightness=100, saturation=0, color_temp=0), # type: ignore[call-arg]
|
||||
will_raise=exc,
|
||||
)
|
||||
|
Reference in New Issue
Block a user