mirror of
https://github.com/python-kasa/python-kasa.git
synced 2024-12-22 11:13:34 +00:00
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:
parent
6adb2b5c28
commit
fcb604e435
@ -217,6 +217,9 @@ no_emeter = parametrize(
|
||||
model_filter=ALL_DEVICES - WITH_EMETER,
|
||||
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", model_filter=WITH_EMETER_IOT, protocol_filter={"IOT"}
|
||||
)
|
||||
|
@ -12,13 +12,9 @@ from voluptuous import (
|
||||
|
||||
from kasa import Device, DeviceType, EmeterStatus, Module
|
||||
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.smart import SmartDevice
|
||||
from kasa.smart.modules import Energy as SmartEnergyModule
|
||||
from kasa.smart.smartmodule import SmartModule
|
||||
|
||||
from .conftest import has_emeter, has_emeter_iot, no_emeter
|
||||
from tests.conftest import has_emeter_iot, no_emeter_iot
|
||||
|
||||
CURRENT_CONSUMPTION_SCHEMA = Schema(
|
||||
Any(
|
||||
@ -40,15 +36,13 @@ CURRENT_CONSUMPTION_SCHEMA = Schema(
|
||||
)
|
||||
|
||||
|
||||
@no_emeter
|
||||
@no_emeter_iot
|
||||
async def test_no_emeter(dev):
|
||||
assert not dev.has_emeter
|
||||
|
||||
with pytest.raises(AttributeError):
|
||||
await dev.get_emeter_realtime()
|
||||
# Only iot devices support the historical stats so other
|
||||
# devices will not implement the methods below
|
||||
if isinstance(dev, IotDevice):
|
||||
|
||||
with pytest.raises(AttributeError):
|
||||
await dev.get_emeter_daily()
|
||||
with pytest.raises(AttributeError):
|
||||
@ -57,13 +51,8 @@ async def test_no_emeter(dev):
|
||||
await dev.erase_emeter_stats()
|
||||
|
||||
|
||||
@has_emeter
|
||||
@has_emeter_iot
|
||||
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]
|
||||
|
||||
current_emeter = await emeter.get_status()
|
||||
@ -136,7 +125,7 @@ async def test_emeter_status(dev):
|
||||
|
||||
|
||||
@pytest.mark.skip("not clearing your stats..")
|
||||
@has_emeter
|
||||
@has_emeter_iot
|
||||
async def test_erase_emeter_stats(dev):
|
||||
emeter = dev.modules[Module.Energy]
|
||||
|
||||
@ -191,16 +180,11 @@ async def test_emeter_daily():
|
||||
assert emeter.consumption_today == 0.500
|
||||
|
||||
|
||||
@has_emeter
|
||||
@has_emeter_iot
|
||||
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)
|
||||
assert energy_module
|
||||
|
||||
if isinstance(dev, IotDevice):
|
||||
info = (
|
||||
dev._last_update
|
||||
if not isinstance(dev, IotStrip)
|
||||
@ -209,19 +193,9 @@ async def test_supported(dev: Device):
|
||||
emeter = info[energy_module._module]["get_realtime"]
|
||||
has_total = "total" in emeter or "total_wh" 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 energy_module.supports(Energy.ModuleFeature.CONSUMPTION_TOTAL) is has_total
|
||||
assert (
|
||||
energy_module.supports(Energy.ModuleFeature.VOLTAGE_CURRENT)
|
||||
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
320
tests/iot/test_iotbulb.py
Normal 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
|
@ -19,10 +19,9 @@ from voluptuous import (
|
||||
from kasa import DeviceType, KasaException, Module
|
||||
from kasa.iot import IotDevice
|
||||
from kasa.iot.iotmodule import _merge_dict
|
||||
|
||||
from .conftest import get_device_for_fixture_protocol, handle_turn_on, turn_on
|
||||
from .device_fixtures import device_iot, has_emeter_iot, no_emeter_iot
|
||||
from .fakeprotocol_iot import FakeIotProtocol
|
||||
from tests.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 tests.fakeprotocol_iot import FakeIotProtocol
|
||||
|
||||
TZ_SCHEMA = Schema(
|
||||
{"zone_str": str, "dst_offset": int, "index": All(int, Range(min=0)), "tz_str": str}
|
@ -2,8 +2,7 @@ import pytest
|
||||
|
||||
from kasa import DeviceType, Module
|
||||
from kasa.iot import IotDimmer
|
||||
|
||||
from .conftest import dimmer_iot, handle_turn_on, turn_on
|
||||
from tests.conftest import dimmer_iot, handle_turn_on, turn_on
|
||||
|
||||
|
||||
@dimmer_iot
|
@ -3,8 +3,7 @@ import pytest
|
||||
from kasa import DeviceType, Module
|
||||
from kasa.iot import IotLightStrip
|
||||
from kasa.iot.modules import LightEffect
|
||||
|
||||
from .conftest import lightstrip_iot
|
||||
from tests.conftest import lightstrip_iot
|
||||
|
||||
|
||||
@lightstrip_iot
|
0
tests/protocols/__init__.py
Normal file
0
tests/protocols/__init__.py
Normal file
@ -29,8 +29,8 @@ from kasa.transports.basetransport import BaseTransport
|
||||
from kasa.transports.klaptransport import KlapTransport, KlapTransportV2
|
||||
from kasa.transports.xortransport import XorEncryption, XorTransport
|
||||
|
||||
from .conftest import device_iot
|
||||
from .fakeprotocol_iot import FakeIotTransport
|
||||
from ..conftest import device_iot
|
||||
from ..fakeprotocol_iot import FakeIotTransport
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
@ -12,8 +12,8 @@ from kasa.exceptions import (
|
||||
from kasa.protocols.smartprotocol import SmartProtocol, _ChildProtocolWrapper
|
||||
from kasa.smart import SmartDevice
|
||||
|
||||
from .conftest import device_smart
|
||||
from .fakeprotocol_smart import FakeSmartTransport
|
||||
from ..conftest import device_smart
|
||||
from ..fakeprotocol_smart import FakeSmartTransport
|
||||
|
||||
DUMMY_QUERY = {"foobar": {"foo": "bar", "bar": "foo"}}
|
||||
DUMMY_MULTIPLE_QUERY = {
|
21
tests/smart/modules/test_energy.py
Normal file
21
tests/smart/modules/test_energy.py
Normal 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
|
@ -17,12 +17,12 @@ from kasa.protocols.smartprotocol import _ChildProtocolWrapper
|
||||
from kasa.smart import SmartDevice
|
||||
from kasa.smart.modules.energy import Energy
|
||||
from kasa.smart.smartmodule import SmartModule
|
||||
|
||||
from .conftest import (
|
||||
from tests.conftest import (
|
||||
device_smart,
|
||||
get_device_for_fixture_protocol,
|
||||
get_parent_and_child_modules,
|
||||
)
|
||||
from tests.device_fixtures import variable_temp_smart
|
||||
|
||||
|
||||
@device_smart
|
||||
@ -435,3 +435,10 @@ async def test_smartdevice_cloud_connection(dev: SmartDevice, mocker: MockerFixt
|
||||
):
|
||||
await new_dev.update()
|
||||
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
|
@ -1,44 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
import pytest
|
||||
from voluptuous import (
|
||||
All,
|
||||
Boolean,
|
||||
Optional,
|
||||
Range,
|
||||
Schema,
|
||||
)
|
||||
|
||||
from kasa import Device, DeviceType, IotLightPreset, KasaException, LightState, Module
|
||||
from kasa.iot import IotBulb, IotDimmer
|
||||
from kasa.iot.modules import LightPreset as IotLightPresetModule
|
||||
|
||||
from .conftest import (
|
||||
from kasa import Device, DeviceType, KasaException, Module
|
||||
from tests.conftest import handle_turn_on, turn_on
|
||||
from tests.device_fixtures import (
|
||||
bulb,
|
||||
bulb_iot,
|
||||
color_bulb,
|
||||
color_bulb_iot,
|
||||
dimmable_iot,
|
||||
handle_turn_on,
|
||||
non_color_bulb,
|
||||
non_dimmable_iot,
|
||||
non_variable_temp,
|
||||
turn_on,
|
||||
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
|
||||
@ -47,18 +19,6 @@ async def test_state_attributes(dev: Device):
|
||||
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
|
||||
@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
|
||||
|
||||
|
||||
@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
|
||||
@turn_on
|
||||
@pytest.mark.parametrize(
|
||||
@ -221,33 +152,6 @@ async def test_try_set_colortemp(dev: Device, turn_on):
|
||||
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
|
||||
async def test_out_of_range_temperature(dev: Device):
|
||||
light = dev.modules.get(Module.Light)
|
||||
@ -276,231 +180,6 @@ async def test_non_variable_temp(dev: Device):
|
||||
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
|
||||
def test_device_type_bulb(dev: Device):
|
||||
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
|
||||
|
@ -1,9 +1,9 @@
|
||||
import pytest
|
||||
|
||||
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 .test_iotdevice import SYSINFO_SCHEMA
|
||||
|
||||
# these schemas should go to the mainlib as
|
||||
# they can be useful when adding support for new features/devices
|
||||
|
0
tests/transports/__init__.py
Normal file
0
tests/transports/__init__.py
Normal file
Loading…
Reference in New Issue
Block a user