Follow main package structure for tests (#1317)

* Transport tests under tests/transports/
* Protocol tests under tests/protocols/
* IOT tests under iot/
* Plus some minor cleanups, most code changes are related to splitting
up smart & iot tests
This commit is contained in:
Teemu R. 2024-11-28 17:56:20 +01:00 committed by GitHub
parent 6adb2b5c28
commit fcb604e435
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 393 additions and 392 deletions

View File

@ -217,6 +217,9 @@ no_emeter = parametrize(
model_filter=ALL_DEVICES - WITH_EMETER, model_filter=ALL_DEVICES - WITH_EMETER,
protocol_filter={"SMART", "IOT"}, protocol_filter={"SMART", "IOT"},
) )
has_emeter_smart = parametrize(
"has emeter smart", model_filter=WITH_EMETER_SMART, protocol_filter={"SMART"}
)
has_emeter_iot = parametrize( has_emeter_iot = parametrize(
"has emeter iot", model_filter=WITH_EMETER_IOT, protocol_filter={"IOT"} "has emeter iot", model_filter=WITH_EMETER_IOT, protocol_filter={"IOT"}
) )

View File

@ -12,13 +12,9 @@ from voluptuous import (
from kasa import Device, DeviceType, EmeterStatus, Module from kasa import Device, DeviceType, EmeterStatus, Module
from kasa.interfaces.energy import Energy from kasa.interfaces.energy import Energy
from kasa.iot import IotDevice, IotStrip from kasa.iot import IotStrip
from kasa.iot.modules.emeter import Emeter from kasa.iot.modules.emeter import Emeter
from kasa.smart import SmartDevice from tests.conftest import has_emeter_iot, no_emeter_iot
from kasa.smart.modules import Energy as SmartEnergyModule
from kasa.smart.smartmodule import SmartModule
from .conftest import has_emeter, has_emeter_iot, no_emeter
CURRENT_CONSUMPTION_SCHEMA = Schema( CURRENT_CONSUMPTION_SCHEMA = Schema(
Any( Any(
@ -40,30 +36,23 @@ CURRENT_CONSUMPTION_SCHEMA = Schema(
) )
@no_emeter @no_emeter_iot
async def test_no_emeter(dev): async def test_no_emeter(dev):
assert not dev.has_emeter assert not dev.has_emeter
with pytest.raises(AttributeError): with pytest.raises(AttributeError):
await dev.get_emeter_realtime() await dev.get_emeter_realtime()
# Only iot devices support the historical stats so other
# devices will not implement the methods below with pytest.raises(AttributeError):
if isinstance(dev, IotDevice): await dev.get_emeter_daily()
with pytest.raises(AttributeError): with pytest.raises(AttributeError):
await dev.get_emeter_daily() await dev.get_emeter_monthly()
with pytest.raises(AttributeError): with pytest.raises(AttributeError):
await dev.get_emeter_monthly() await dev.erase_emeter_stats()
with pytest.raises(AttributeError):
await dev.erase_emeter_stats()
@has_emeter @has_emeter_iot
async def test_get_emeter_realtime(dev): async def test_get_emeter_realtime(dev):
if isinstance(dev, SmartDevice):
mod = SmartEnergyModule(dev, str(Module.Energy))
if not await mod._check_supported():
pytest.skip(f"Energy module not supported for {dev}.")
emeter = dev.modules[Module.Energy] emeter = dev.modules[Module.Energy]
current_emeter = await emeter.get_status() current_emeter = await emeter.get_status()
@ -136,7 +125,7 @@ async def test_emeter_status(dev):
@pytest.mark.skip("not clearing your stats..") @pytest.mark.skip("not clearing your stats..")
@has_emeter @has_emeter_iot
async def test_erase_emeter_stats(dev): async def test_erase_emeter_stats(dev):
emeter = dev.modules[Module.Energy] emeter = dev.modules[Module.Energy]
@ -191,37 +180,22 @@ async def test_emeter_daily():
assert emeter.consumption_today == 0.500 assert emeter.consumption_today == 0.500
@has_emeter @has_emeter_iot
async def test_supported(dev: Device): async def test_supported(dev: Device):
if isinstance(dev, SmartDevice):
mod = SmartEnergyModule(dev, str(Module.Energy))
if not await mod._check_supported():
pytest.skip(f"Energy module not supported for {dev}.")
energy_module = dev.modules.get(Module.Energy) energy_module = dev.modules.get(Module.Energy)
assert energy_module assert energy_module
if isinstance(dev, IotDevice): info = (
info = ( dev._last_update
dev._last_update if not isinstance(dev, IotStrip)
if not isinstance(dev, IotStrip) else dev.children[0].internal_state
else dev.children[0].internal_state )
) emeter = info[energy_module._module]["get_realtime"]
emeter = info[energy_module._module]["get_realtime"] has_total = "total" in emeter or "total_wh" in emeter
has_total = "total" in emeter or "total_wh" in emeter has_voltage_current = "voltage" in emeter or "voltage_mv" in emeter
has_voltage_current = "voltage" in emeter or "voltage_mv" in emeter assert energy_module.supports(Energy.ModuleFeature.CONSUMPTION_TOTAL) is has_total
assert ( assert (
energy_module.supports(Energy.ModuleFeature.CONSUMPTION_TOTAL) is has_total energy_module.supports(Energy.ModuleFeature.VOLTAGE_CURRENT)
) is has_voltage_current
assert ( )
energy_module.supports(Energy.ModuleFeature.VOLTAGE_CURRENT) assert energy_module.supports(Energy.ModuleFeature.PERIODIC_STATS) is True
is has_voltage_current
)
assert energy_module.supports(Energy.ModuleFeature.PERIODIC_STATS) is True
else:
assert isinstance(energy_module, SmartModule)
assert energy_module.supports(Energy.ModuleFeature.CONSUMPTION_TOTAL) is False
assert energy_module.supports(Energy.ModuleFeature.PERIODIC_STATS) is False
if energy_module.supported_version < 2:
assert energy_module.supports(Energy.ModuleFeature.VOLTAGE_CURRENT) is False
else:
assert energy_module.supports(Energy.ModuleFeature.VOLTAGE_CURRENT) is True

320
tests/iot/test_iotbulb.py Normal file
View File

@ -0,0 +1,320 @@
from __future__ import annotations
import re
import pytest
from voluptuous import (
All,
Boolean,
Optional,
Range,
Schema,
)
from kasa import Device, IotLightPreset, KasaException, LightState, Module
from kasa.iot import IotBulb, IotDimmer
from kasa.iot.modules import LightPreset as IotLightPresetModule
from tests.conftest import (
bulb_iot,
color_bulb_iot,
dimmable_iot,
handle_turn_on,
non_dimmable_iot,
turn_on,
variable_temp_iot,
)
from tests.iot.test_iotdevice import SYSINFO_SCHEMA
@bulb_iot
async def test_bulb_sysinfo(dev: Device):
assert dev.sys_info is not None
SYSINFO_SCHEMA_BULB(dev.sys_info)
assert dev.model is not None
@bulb_iot
async def test_light_state_without_update(dev: IotBulb, monkeypatch):
monkeypatch.setitem(dev._last_update["system"]["get_sysinfo"], "light_state", None)
with pytest.raises(KasaException):
print(dev.light_state)
@bulb_iot
async def test_get_light_state(dev: IotBulb):
LIGHT_STATE_SCHEMA(await dev.get_light_state())
@color_bulb_iot
async def test_set_hsv_transition(dev: IotBulb, mocker):
set_light_state = mocker.patch("kasa.iot.IotBulb._set_light_state")
light = dev.modules.get(Module.Light)
assert light
await light.set_hsv(10, 10, 100, transition=1000)
set_light_state.assert_called_with(
{"hue": 10, "saturation": 10, "brightness": 100, "color_temp": 0},
transition=1000,
)
@bulb_iot
async def test_light_set_state(dev: IotBulb, mocker):
"""Testing setting LightState on the light module."""
light = dev.modules.get(Module.Light)
assert light
set_light_state = mocker.spy(dev, "_set_light_state")
state = LightState(light_on=True)
await light.set_state(state)
set_light_state.assert_called_with({"on_off": 1}, transition=None)
state = LightState(light_on=False)
await light.set_state(state)
set_light_state.assert_called_with({"on_off": 0}, transition=None)
@variable_temp_iot
async def test_set_color_temp_transition(dev: IotBulb, mocker):
set_light_state = mocker.patch("kasa.iot.IotBulb._set_light_state")
light = dev.modules.get(Module.Light)
assert light
await light.set_color_temp(2700, transition=100)
set_light_state.assert_called_with({"color_temp": 2700}, transition=100)
@variable_temp_iot
@pytest.mark.xdist_group(name="caplog")
async def test_unknown_temp_range(dev: IotBulb, monkeypatch, caplog):
monkeypatch.setitem(dev._sys_info, "model", "unknown bulb")
light = dev.modules.get(Module.Light)
assert light
assert light.valid_temperature_range == (2700, 5000)
assert "Unknown color temperature range, fallback to 2700-5000" in caplog.text
@dimmable_iot
@turn_on
async def test_dimmable_brightness(dev: IotBulb, turn_on):
assert isinstance(dev, IotBulb | IotDimmer)
light = dev.modules.get(Module.Light)
assert light
await handle_turn_on(dev, turn_on)
assert dev._is_dimmable
await light.set_brightness(50)
await dev.update()
assert light.brightness == 50
await light.set_brightness(10)
await dev.update()
assert light.brightness == 10
with pytest.raises(TypeError, match="Brightness must be an integer"):
await light.set_brightness("foo") # type: ignore[arg-type]
@bulb_iot
async def test_turn_on_transition(dev: IotBulb, mocker):
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)
await dev.turn_off(transition=100)
set_light_state.assert_called_with({"on_off": 0}, transition=100)
@bulb_iot
async def test_dimmable_brightness_transition(dev: IotBulb, mocker):
set_light_state = mocker.patch("kasa.iot.IotBulb._set_light_state")
light = dev.modules.get(Module.Light)
assert light
await light.set_brightness(10, transition=1000)
set_light_state.assert_called_with({"brightness": 10, "on_off": 1}, transition=1000)
@dimmable_iot
async def test_invalid_brightness(dev: IotBulb):
assert dev._is_dimmable
light = dev.modules.get(Module.Light)
assert light
with pytest.raises(
ValueError,
match=re.escape("Invalid brightness value: 110 (valid range: 0-100%)"),
):
await light.set_brightness(110)
with pytest.raises(
ValueError,
match=re.escape("Invalid brightness value: -100 (valid range: 0-100%)"),
):
await light.set_brightness(-100)
@non_dimmable_iot
async def test_non_dimmable(dev: IotBulb):
assert not dev._is_dimmable
light = dev.modules.get(Module.Light)
assert light
with pytest.raises(KasaException):
assert light.brightness == 0
with pytest.raises(KasaException):
await light.set_brightness(100)
@bulb_iot
async def test_ignore_default_not_set_without_color_mode_change_turn_on(
dev: IotBulb, mocker
):
query_helper = mocker.patch("kasa.iot.IotBulb._query_helper")
# When turning back without settings, ignore default to restore the state
await dev.turn_on()
args, kwargs = query_helper.call_args_list[0]
assert args[2] == {"on_off": 1, "ignore_default": 0}
await dev.turn_off()
args, kwargs = query_helper.call_args_list[1]
assert args[2] == {"on_off": 0, "ignore_default": 1}
@bulb_iot
async def test_list_presets(dev: IotBulb):
light_preset = dev.modules.get(Module.LightPreset)
assert light_preset
assert isinstance(light_preset, IotLightPresetModule)
presets = light_preset._deprecated_presets
# Light strip devices may list some light effects along with normal presets but these
# are handled by the LightEffect module so exclude preferred states with id
raw_presets = [
pstate for pstate in dev.sys_info["preferred_state"] if "id" not in pstate
]
assert len(presets) == len(raw_presets)
for preset, raw in zip(presets, raw_presets, strict=False):
assert preset.index == raw["index"]
assert preset.brightness == raw["brightness"]
assert preset.hue == raw["hue"]
assert preset.saturation == raw["saturation"]
assert preset.color_temp == raw["color_temp"]
@bulb_iot
async def test_modify_preset(dev: IotBulb, mocker):
"""Verify that modifying preset calls the and exceptions are raised properly."""
if (
not (light_preset := dev.modules.get(Module.LightPreset))
or not light_preset._deprecated_presets
):
pytest.skip("Some strips do not support presets")
assert isinstance(light_preset, IotLightPresetModule)
data: dict[str, int | None] = {
"index": 0,
"brightness": 10,
"hue": 0,
"saturation": 0,
"color_temp": 0,
}
preset = IotLightPreset(**data) # type: ignore[call-arg, arg-type]
assert preset.index == 0
assert preset.brightness == 10
assert preset.hue == 0
assert preset.saturation == 0
assert preset.color_temp == 0
await light_preset._deprecated_save_preset(preset)
await dev.update()
assert light_preset._deprecated_presets[0].brightness == 10
with pytest.raises(KasaException):
await light_preset._deprecated_save_preset(
IotLightPreset(index=5, hue=0, brightness=0, saturation=0, color_temp=0) # type: ignore[call-arg]
)
@bulb_iot
@pytest.mark.parametrize(
("preset", "payload"),
[
(
IotLightPreset(index=0, hue=0, brightness=1, saturation=0), # type: ignore[call-arg]
{"index": 0, "hue": 0, "brightness": 1, "saturation": 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},
),
],
)
async def test_modify_preset_payloads(dev: IotBulb, preset, payload, mocker):
"""Test that modify preset payloads ignore none values."""
if (
not (light_preset := dev.modules.get(Module.LightPreset))
or not light_preset._deprecated_presets
):
pytest.skip("Some strips do not support presets")
query_helper = mocker.patch("kasa.iot.IotBulb._query_helper")
await light_preset._deprecated_save_preset(preset)
query_helper.assert_called_with(dev.LIGHT_SERVICE, "set_preferred_state", payload)
LIGHT_STATE_SCHEMA = Schema(
{
"brightness": All(int, Range(min=0, max=100)),
"color_temp": int,
"hue": All(int, Range(min=0, max=360)),
"mode": str,
"on_off": Boolean,
"saturation": All(int, Range(min=0, max=100)),
"length": Optional(int),
"transition": Optional(int),
"dft_on_state": Optional(
{
"brightness": All(int, Range(min=0, max=100)),
"color_temp": All(int, Range(min=0, max=9000)),
"hue": All(int, Range(min=0, max=360)),
"mode": str,
"saturation": All(int, Range(min=0, max=100)),
"groups": Optional(list[int]),
}
),
"err_code": int,
}
)
SYSINFO_SCHEMA_BULB = SYSINFO_SCHEMA.extend(
{
"ctrl_protocols": Optional(dict),
"description": Optional(str), # Seen on LBxxx, similar to dev_name
"dev_state": str,
"disco_ver": str,
"heapsize": int,
"is_color": Boolean,
"is_dimmable": Boolean,
"is_factory": Boolean,
"is_variable_color_temp": Boolean,
"light_state": LIGHT_STATE_SCHEMA,
"preferred_state": [
{
"brightness": All(int, Range(min=0, max=100)),
"color_temp": int,
"hue": All(int, Range(min=0, max=360)),
"index": int,
"saturation": All(int, Range(min=0, max=100)),
}
],
}
)
@bulb_iot
async def test_turn_on_behaviours(dev: IotBulb):
behavior = await dev.get_turn_on_behavior()
assert behavior

View File

@ -19,10 +19,9 @@ from voluptuous import (
from kasa import DeviceType, KasaException, Module from kasa import DeviceType, KasaException, Module
from kasa.iot import IotDevice from kasa.iot import IotDevice
from kasa.iot.iotmodule import _merge_dict from kasa.iot.iotmodule import _merge_dict
from tests.conftest import get_device_for_fixture_protocol, handle_turn_on, turn_on
from .conftest import get_device_for_fixture_protocol, handle_turn_on, turn_on from tests.device_fixtures import device_iot, has_emeter_iot, no_emeter_iot
from .device_fixtures import device_iot, has_emeter_iot, no_emeter_iot from tests.fakeprotocol_iot import FakeIotProtocol
from .fakeprotocol_iot import FakeIotProtocol
TZ_SCHEMA = Schema( TZ_SCHEMA = Schema(
{"zone_str": str, "dst_offset": int, "index": All(int, Range(min=0)), "tz_str": str} {"zone_str": str, "dst_offset": int, "index": All(int, Range(min=0)), "tz_str": str}

View File

@ -2,8 +2,7 @@ import pytest
from kasa import DeviceType, Module from kasa import DeviceType, Module
from kasa.iot import IotDimmer from kasa.iot import IotDimmer
from tests.conftest import dimmer_iot, handle_turn_on, turn_on
from .conftest import dimmer_iot, handle_turn_on, turn_on
@dimmer_iot @dimmer_iot

View File

@ -3,8 +3,7 @@ import pytest
from kasa import DeviceType, Module from kasa import DeviceType, Module
from kasa.iot import IotLightStrip from kasa.iot import IotLightStrip
from kasa.iot.modules import LightEffect from kasa.iot.modules import LightEffect
from tests.conftest import lightstrip_iot
from .conftest import lightstrip_iot
@lightstrip_iot @lightstrip_iot

View File

View File

@ -29,8 +29,8 @@ from kasa.transports.basetransport import BaseTransport
from kasa.transports.klaptransport import KlapTransport, KlapTransportV2 from kasa.transports.klaptransport import KlapTransport, KlapTransportV2
from kasa.transports.xortransport import XorEncryption, XorTransport from kasa.transports.xortransport import XorEncryption, XorTransport
from .conftest import device_iot from ..conftest import device_iot
from .fakeprotocol_iot import FakeIotTransport from ..fakeprotocol_iot import FakeIotTransport
@pytest.mark.parametrize( @pytest.mark.parametrize(

View File

@ -12,8 +12,8 @@ from kasa.exceptions import (
from kasa.protocols.smartprotocol import SmartProtocol, _ChildProtocolWrapper from kasa.protocols.smartprotocol import SmartProtocol, _ChildProtocolWrapper
from kasa.smart import SmartDevice from kasa.smart import SmartDevice
from .conftest import device_smart from ..conftest import device_smart
from .fakeprotocol_smart import FakeSmartTransport from ..fakeprotocol_smart import FakeSmartTransport
DUMMY_QUERY = {"foobar": {"foo": "bar", "bar": "foo"}} DUMMY_QUERY = {"foobar": {"foo": "bar", "bar": "foo"}}
DUMMY_MULTIPLE_QUERY = { DUMMY_MULTIPLE_QUERY = {

View File

@ -0,0 +1,21 @@
import pytest
from kasa import Module, SmartDevice
from kasa.interfaces.energy import Energy
from kasa.smart.modules import Energy as SmartEnergyModule
from tests.conftest import has_emeter_smart
@has_emeter_smart
async def test_supported(dev: SmartDevice):
energy_module = dev.modules.get(Module.Energy)
if not energy_module:
pytest.skip(f"Energy module not supported for {dev}.")
assert isinstance(energy_module, SmartEnergyModule)
assert energy_module.supports(Energy.ModuleFeature.CONSUMPTION_TOTAL) is False
assert energy_module.supports(Energy.ModuleFeature.PERIODIC_STATS) is False
if energy_module.supported_version < 2:
assert energy_module.supports(Energy.ModuleFeature.VOLTAGE_CURRENT) is False
else:
assert energy_module.supports(Energy.ModuleFeature.VOLTAGE_CURRENT) is True

View File

@ -17,12 +17,12 @@ from kasa.protocols.smartprotocol import _ChildProtocolWrapper
from kasa.smart import SmartDevice from kasa.smart import SmartDevice
from kasa.smart.modules.energy import Energy from kasa.smart.modules.energy import Energy
from kasa.smart.smartmodule import SmartModule from kasa.smart.smartmodule import SmartModule
from tests.conftest import (
from .conftest import (
device_smart, device_smart,
get_device_for_fixture_protocol, get_device_for_fixture_protocol,
get_parent_and_child_modules, get_parent_and_child_modules,
) )
from tests.device_fixtures import variable_temp_smart
@device_smart @device_smart
@ -435,3 +435,10 @@ async def test_smartdevice_cloud_connection(dev: SmartDevice, mocker: MockerFixt
): ):
await new_dev.update() await new_dev.update()
assert new_dev.is_cloud_connected is False assert new_dev.is_cloud_connected is False
@variable_temp_smart
async def test_smart_temp_range(dev: Device):
light = dev.modules.get(Module.Light)
assert light
assert light.valid_temperature_range

View File

@ -1,44 +1,16 @@
from __future__ import annotations from __future__ import annotations
import re
import pytest import pytest
from voluptuous import (
All,
Boolean,
Optional,
Range,
Schema,
)
from kasa import Device, DeviceType, IotLightPreset, KasaException, LightState, Module from kasa import Device, DeviceType, KasaException, Module
from kasa.iot import IotBulb, IotDimmer from tests.conftest import handle_turn_on, turn_on
from kasa.iot.modules import LightPreset as IotLightPresetModule from tests.device_fixtures import (
from .conftest import (
bulb, bulb,
bulb_iot,
color_bulb, color_bulb,
color_bulb_iot,
dimmable_iot,
handle_turn_on,
non_color_bulb, non_color_bulb,
non_dimmable_iot,
non_variable_temp, non_variable_temp,
turn_on,
variable_temp, variable_temp,
variable_temp_iot,
variable_temp_smart,
) )
from .test_iotdevice import SYSINFO_SCHEMA
@bulb_iot
async def test_bulb_sysinfo(dev: Device):
assert dev.sys_info is not None
SYSINFO_SCHEMA_BULB(dev.sys_info)
assert dev.model is not None
@bulb @bulb
@ -47,18 +19,6 @@ async def test_state_attributes(dev: Device):
assert isinstance(dev.state_information["Cloud connection"], bool) assert isinstance(dev.state_information["Cloud connection"], bool)
@bulb_iot
async def test_light_state_without_update(dev: IotBulb, monkeypatch):
monkeypatch.setitem(dev._last_update["system"]["get_sysinfo"], "light_state", None)
with pytest.raises(KasaException):
print(dev.light_state)
@bulb_iot
async def test_get_light_state(dev: IotBulb):
LIGHT_STATE_SCHEMA(await dev.get_light_state())
@color_bulb @color_bulb
@turn_on @turn_on
async def test_hsv(dev: Device, turn_on): async def test_hsv(dev: Device, turn_on):
@ -81,35 +41,6 @@ async def test_hsv(dev: Device, turn_on):
assert brightness == 1 assert brightness == 1
@color_bulb_iot
async def test_set_hsv_transition(dev: IotBulb, mocker):
set_light_state = mocker.patch("kasa.iot.IotBulb._set_light_state")
light = dev.modules.get(Module.Light)
assert light
await light.set_hsv(10, 10, 100, transition=1000)
set_light_state.assert_called_with(
{"hue": 10, "saturation": 10, "brightness": 100, "color_temp": 0},
transition=1000,
)
@bulb_iot
async def test_light_set_state(dev: IotBulb, mocker):
"""Testing setting LightState on the light module."""
light = dev.modules.get(Module.Light)
assert light
set_light_state = mocker.spy(dev, "_set_light_state")
state = LightState(light_on=True)
await light.set_state(state)
set_light_state.assert_called_with({"on_off": 1}, transition=None)
state = LightState(light_on=False)
await light.set_state(state)
set_light_state.assert_called_with({"on_off": 0}, transition=None)
@color_bulb @color_bulb
@turn_on @turn_on
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -221,33 +152,6 @@ async def test_try_set_colortemp(dev: Device, turn_on):
assert light.color_temp == 2700 assert light.color_temp == 2700
@variable_temp_iot
async def test_set_color_temp_transition(dev: IotBulb, mocker):
set_light_state = mocker.patch("kasa.iot.IotBulb._set_light_state")
light = dev.modules.get(Module.Light)
assert light
await light.set_color_temp(2700, transition=100)
set_light_state.assert_called_with({"color_temp": 2700}, transition=100)
@variable_temp_iot
@pytest.mark.xdist_group(name="caplog")
async def test_unknown_temp_range(dev: IotBulb, monkeypatch, caplog):
monkeypatch.setitem(dev._sys_info, "model", "unknown bulb")
light = dev.modules.get(Module.Light)
assert light
assert light.valid_temperature_range == (2700, 5000)
assert "Unknown color temperature range, fallback to 2700-5000" in caplog.text
@variable_temp_smart
async def test_smart_temp_range(dev: Device):
light = dev.modules.get(Module.Light)
assert light
assert light.valid_temperature_range
@variable_temp @variable_temp
async def test_out_of_range_temperature(dev: Device): async def test_out_of_range_temperature(dev: Device):
light = dev.modules.get(Module.Light) light = dev.modules.get(Module.Light)
@ -276,231 +180,6 @@ async def test_non_variable_temp(dev: Device):
print(light.color_temp) print(light.color_temp)
@dimmable_iot
@turn_on
async def test_dimmable_brightness(dev: IotBulb, turn_on):
assert isinstance(dev, IotBulb | IotDimmer)
light = dev.modules.get(Module.Light)
assert light
await handle_turn_on(dev, turn_on)
assert dev._is_dimmable
await light.set_brightness(50)
await dev.update()
assert light.brightness == 50
await light.set_brightness(10)
await dev.update()
assert light.brightness == 10
with pytest.raises(TypeError, match="Brightness must be an integer"):
await light.set_brightness("foo") # type: ignore[arg-type]
@bulb_iot
async def test_turn_on_transition(dev: IotBulb, mocker):
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)
await dev.turn_off(transition=100)
set_light_state.assert_called_with({"on_off": 0}, transition=100)
@bulb_iot
async def test_dimmable_brightness_transition(dev: IotBulb, mocker):
set_light_state = mocker.patch("kasa.iot.IotBulb._set_light_state")
light = dev.modules.get(Module.Light)
assert light
await light.set_brightness(10, transition=1000)
set_light_state.assert_called_with({"brightness": 10, "on_off": 1}, transition=1000)
@dimmable_iot
async def test_invalid_brightness(dev: IotBulb):
assert dev._is_dimmable
light = dev.modules.get(Module.Light)
assert light
with pytest.raises(
ValueError,
match=re.escape("Invalid brightness value: 110 (valid range: 0-100%)"),
):
await light.set_brightness(110)
with pytest.raises(
ValueError,
match=re.escape("Invalid brightness value: -100 (valid range: 0-100%)"),
):
await light.set_brightness(-100)
@non_dimmable_iot
async def test_non_dimmable(dev: IotBulb):
assert not dev._is_dimmable
light = dev.modules.get(Module.Light)
assert light
with pytest.raises(KasaException):
assert light.brightness == 0
with pytest.raises(KasaException):
await light.set_brightness(100)
@bulb_iot
async def test_ignore_default_not_set_without_color_mode_change_turn_on(
dev: IotBulb, mocker
):
query_helper = mocker.patch("kasa.iot.IotBulb._query_helper")
# When turning back without settings, ignore default to restore the state
await dev.turn_on()
args, kwargs = query_helper.call_args_list[0]
assert args[2] == {"on_off": 1, "ignore_default": 0}
await dev.turn_off()
args, kwargs = query_helper.call_args_list[1]
assert args[2] == {"on_off": 0, "ignore_default": 1}
@bulb_iot
async def test_list_presets(dev: IotBulb):
light_preset = dev.modules.get(Module.LightPreset)
assert light_preset
assert isinstance(light_preset, IotLightPresetModule)
presets = light_preset._deprecated_presets
# Light strip devices may list some light effects along with normal presets but these
# are handled by the LightEffect module so exclude preferred states with id
raw_presets = [
pstate for pstate in dev.sys_info["preferred_state"] if "id" not in pstate
]
assert len(presets) == len(raw_presets)
for preset, raw in zip(presets, raw_presets, strict=False):
assert preset.index == raw["index"]
assert preset.brightness == raw["brightness"]
assert preset.hue == raw["hue"]
assert preset.saturation == raw["saturation"]
assert preset.color_temp == raw["color_temp"]
@bulb_iot
async def test_modify_preset(dev: IotBulb, mocker):
"""Verify that modifying preset calls the and exceptions are raised properly."""
if (
not (light_preset := dev.modules.get(Module.LightPreset))
or not light_preset._deprecated_presets
):
pytest.skip("Some strips do not support presets")
assert isinstance(light_preset, IotLightPresetModule)
data: dict[str, int | None] = {
"index": 0,
"brightness": 10,
"hue": 0,
"saturation": 0,
"color_temp": 0,
}
preset = IotLightPreset(**data) # type: ignore[call-arg, arg-type]
assert preset.index == 0
assert preset.brightness == 10
assert preset.hue == 0
assert preset.saturation == 0
assert preset.color_temp == 0
await light_preset._deprecated_save_preset(preset)
await dev.update()
assert light_preset._deprecated_presets[0].brightness == 10
with pytest.raises(KasaException):
await light_preset._deprecated_save_preset(
IotLightPreset(index=5, hue=0, brightness=0, saturation=0, color_temp=0) # type: ignore[call-arg]
)
@bulb_iot
@pytest.mark.parametrize(
("preset", "payload"),
[
(
IotLightPreset(index=0, hue=0, brightness=1, saturation=0), # type: ignore[call-arg]
{"index": 0, "hue": 0, "brightness": 1, "saturation": 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},
),
],
)
async def test_modify_preset_payloads(dev: IotBulb, preset, payload, mocker):
"""Test that modify preset payloads ignore none values."""
if (
not (light_preset := dev.modules.get(Module.LightPreset))
or not light_preset._deprecated_presets
):
pytest.skip("Some strips do not support presets")
query_helper = mocker.patch("kasa.iot.IotBulb._query_helper")
await light_preset._deprecated_save_preset(preset)
query_helper.assert_called_with(dev.LIGHT_SERVICE, "set_preferred_state", payload)
LIGHT_STATE_SCHEMA = Schema(
{
"brightness": All(int, Range(min=0, max=100)),
"color_temp": int,
"hue": All(int, Range(min=0, max=360)),
"mode": str,
"on_off": Boolean,
"saturation": All(int, Range(min=0, max=100)),
"length": Optional(int),
"transition": Optional(int),
"dft_on_state": Optional(
{
"brightness": All(int, Range(min=0, max=100)),
"color_temp": All(int, Range(min=0, max=9000)),
"hue": All(int, Range(min=0, max=360)),
"mode": str,
"saturation": All(int, Range(min=0, max=100)),
"groups": Optional(list[int]),
}
),
"err_code": int,
}
)
SYSINFO_SCHEMA_BULB = SYSINFO_SCHEMA.extend(
{
"ctrl_protocols": Optional(dict),
"description": Optional(str), # Seen on LBxxx, similar to dev_name
"dev_state": str,
"disco_ver": str,
"heapsize": int,
"is_color": Boolean,
"is_dimmable": Boolean,
"is_factory": Boolean,
"is_variable_color_temp": Boolean,
"light_state": LIGHT_STATE_SCHEMA,
"preferred_state": [
{
"brightness": All(int, Range(min=0, max=100)),
"color_temp": int,
"hue": All(int, Range(min=0, max=360)),
"index": int,
"saturation": All(int, Range(min=0, max=100)),
}
],
}
)
@bulb @bulb
def test_device_type_bulb(dev: Device): def test_device_type_bulb(dev: Device):
assert dev.device_type in {DeviceType.Bulb, DeviceType.LightStrip} assert dev.device_type in {DeviceType.Bulb, DeviceType.LightStrip}
@bulb_iot
async def test_turn_on_behaviours(dev: IotBulb):
behavior = await dev.get_turn_on_behavior()
assert behavior

View File

@ -1,9 +1,9 @@
import pytest import pytest
from kasa import DeviceType from kasa import DeviceType
from tests.iot.test_iotdevice import SYSINFO_SCHEMA
from .conftest import plug, plug_iot, plug_smart, switch_smart, wallswitch_iot from .conftest import plug, plug_iot, plug_smart, switch_smart, wallswitch_iot
from .test_iotdevice import SYSINFO_SCHEMA
# these schemas should go to the mainlib as # these schemas should go to the mainlib as
# they can be useful when adding support for new features/devices # they can be useful when adding support for new features/devices

View File