mirror of
https://github.com/python-kasa/python-kasa.git
synced 2025-08-06 18:54:08 +00:00
Move tests folder to top level of project (#1242)
This commit is contained in:
0
tests/smart/modules/__init__.py
Normal file
0
tests/smart/modules/__init__.py
Normal file
104
tests/smart/modules/test_autooff.py
Normal file
104
tests/smart/modules/test_autooff.py
Normal file
@@ -0,0 +1,104 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from kasa import Module
|
||||
from kasa.smart import SmartDevice
|
||||
|
||||
from ...device_fixtures import get_parent_and_child_modules, parametrize
|
||||
|
||||
autooff = parametrize(
|
||||
"has autooff", component_filter="auto_off", protocol_filter={"SMART"}
|
||||
)
|
||||
|
||||
|
||||
@autooff
|
||||
@pytest.mark.parametrize(
|
||||
("feature", "prop_name", "type"),
|
||||
[
|
||||
("auto_off_enabled", "enabled", bool),
|
||||
("auto_off_minutes", "delay", int),
|
||||
("auto_off_at", "auto_off_at", Optional[datetime]),
|
||||
],
|
||||
)
|
||||
@pytest.mark.skipif(
|
||||
sys.version_info < (3, 10),
|
||||
reason="Subscripted generics cannot be used with class and instance checks",
|
||||
)
|
||||
async def test_autooff_features(
|
||||
dev: SmartDevice, feature: str, prop_name: str, type: type
|
||||
):
|
||||
"""Test that features are registered and work as expected."""
|
||||
autooff = next(get_parent_and_child_modules(dev, Module.AutoOff))
|
||||
assert autooff is not None
|
||||
|
||||
prop = getattr(autooff, prop_name)
|
||||
assert isinstance(prop, type)
|
||||
|
||||
feat = autooff._device.features[feature]
|
||||
assert feat.value == prop
|
||||
assert isinstance(feat.value, type)
|
||||
|
||||
|
||||
@autooff
|
||||
async def test_settings(dev: SmartDevice, mocker: MockerFixture):
|
||||
"""Test autooff settings."""
|
||||
autooff = next(get_parent_and_child_modules(dev, Module.AutoOff))
|
||||
assert autooff
|
||||
|
||||
enabled = autooff._device.features["auto_off_enabled"]
|
||||
assert autooff.enabled == enabled.value
|
||||
|
||||
delay = autooff._device.features["auto_off_minutes"]
|
||||
assert autooff.delay == delay.value
|
||||
|
||||
call = mocker.spy(autooff, "call")
|
||||
new_state = True
|
||||
|
||||
await autooff.set_enabled(new_state)
|
||||
call.assert_called_with(
|
||||
"set_auto_off_config", {"enable": new_state, "delay_min": delay.value}
|
||||
)
|
||||
call.reset_mock()
|
||||
await dev.update()
|
||||
|
||||
new_delay = 123
|
||||
|
||||
await autooff.set_delay(new_delay)
|
||||
|
||||
call.assert_called_with(
|
||||
"set_auto_off_config", {"enable": new_state, "delay_min": new_delay}
|
||||
)
|
||||
|
||||
await dev.update()
|
||||
|
||||
assert autooff.enabled == new_state
|
||||
assert autooff.delay == new_delay
|
||||
|
||||
|
||||
@autooff
|
||||
@pytest.mark.parametrize("is_timer_active", [True, False])
|
||||
async def test_auto_off_at(
|
||||
dev: SmartDevice, mocker: MockerFixture, is_timer_active: bool
|
||||
):
|
||||
"""Test auto-off at sensor."""
|
||||
autooff = next(get_parent_and_child_modules(dev, Module.AutoOff))
|
||||
assert autooff
|
||||
|
||||
autooff_at = autooff._device.features["auto_off_at"]
|
||||
|
||||
mocker.patch.object(
|
||||
type(autooff),
|
||||
"is_timer_active",
|
||||
new_callable=mocker.PropertyMock,
|
||||
return_value=is_timer_active,
|
||||
)
|
||||
if is_timer_active:
|
||||
assert isinstance(autooff_at.value, datetime)
|
||||
else:
|
||||
assert autooff_at.value is None
|
44
tests/smart/modules/test_childprotection.py
Normal file
44
tests/smart/modules/test_childprotection.py
Normal file
@@ -0,0 +1,44 @@
|
||||
import pytest
|
||||
|
||||
from kasa import Module
|
||||
from kasa.smart.modules import ChildProtection
|
||||
|
||||
from ...device_fixtures import parametrize
|
||||
|
||||
child_protection = parametrize(
|
||||
"has child protection",
|
||||
component_filter="child_protection",
|
||||
protocol_filter={"SMART.CHILD"},
|
||||
)
|
||||
|
||||
|
||||
@child_protection
|
||||
@pytest.mark.parametrize(
|
||||
("feature", "prop_name", "type"),
|
||||
[
|
||||
("child_lock", "enabled", bool),
|
||||
],
|
||||
)
|
||||
async def test_features(dev, feature, prop_name, type):
|
||||
"""Test that features are registered and work as expected."""
|
||||
protect: ChildProtection = dev.modules[Module.ChildProtection]
|
||||
assert protect is not None
|
||||
|
||||
prop = getattr(protect, prop_name)
|
||||
assert isinstance(prop, type)
|
||||
|
||||
feat = protect._device.features[feature]
|
||||
assert feat.value == prop
|
||||
assert isinstance(feat.value, type)
|
||||
|
||||
|
||||
@child_protection
|
||||
async def test_enabled(dev):
|
||||
"""Test the API."""
|
||||
protect: ChildProtection = dev.modules[Module.ChildProtection]
|
||||
assert protect is not None
|
||||
|
||||
assert isinstance(protect.enabled, bool)
|
||||
await protect.set_enabled(False)
|
||||
await dev.update()
|
||||
assert protect.enabled is False
|
29
tests/smart/modules/test_contact.py
Normal file
29
tests/smart/modules/test_contact.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import pytest
|
||||
|
||||
from kasa import Module, SmartDevice
|
||||
|
||||
from ...device_fixtures import parametrize
|
||||
|
||||
contact = parametrize(
|
||||
"is contact sensor", model_filter="T110", protocol_filter={"SMART.CHILD"}
|
||||
)
|
||||
|
||||
|
||||
@contact
|
||||
@pytest.mark.parametrize(
|
||||
("feature", "type"),
|
||||
[
|
||||
("is_open", bool),
|
||||
],
|
||||
)
|
||||
async def test_contact_features(dev: SmartDevice, feature, type):
|
||||
"""Test that features are registered and work as expected."""
|
||||
contact = dev.modules.get(Module.ContactSensor)
|
||||
assert contact is not None
|
||||
|
||||
prop = getattr(contact, feature)
|
||||
assert isinstance(prop, type)
|
||||
|
||||
feat = dev.features[feature]
|
||||
assert feat.value == prop
|
||||
assert isinstance(feat.value, type)
|
84
tests/smart/modules/test_fan.py
Normal file
84
tests/smart/modules/test_fan.py
Normal file
@@ -0,0 +1,84 @@
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from kasa import Module
|
||||
from kasa.smart import SmartDevice
|
||||
|
||||
from ...device_fixtures import get_parent_and_child_modules, parametrize
|
||||
|
||||
fan = parametrize("has fan", component_filter="fan_control", protocol_filter={"SMART"})
|
||||
|
||||
|
||||
@fan
|
||||
async def test_fan_speed(dev: SmartDevice, mocker: MockerFixture):
|
||||
"""Test fan speed feature."""
|
||||
fan = next(get_parent_and_child_modules(dev, Module.Fan))
|
||||
assert fan
|
||||
level_feature = fan._module_features["fan_speed_level"]
|
||||
assert (
|
||||
level_feature.minimum_value
|
||||
<= level_feature.value
|
||||
<= level_feature.maximum_value
|
||||
)
|
||||
|
||||
call = mocker.spy(fan, "call")
|
||||
await fan.set_fan_speed_level(3)
|
||||
call.assert_called_with(
|
||||
"set_device_info", {"device_on": True, "fan_speed_level": 3}
|
||||
)
|
||||
|
||||
await dev.update()
|
||||
|
||||
assert fan.fan_speed_level == 3
|
||||
assert level_feature.value == 3
|
||||
|
||||
|
||||
@fan
|
||||
async def test_sleep_mode(dev: SmartDevice, mocker: MockerFixture):
|
||||
"""Test sleep mode feature."""
|
||||
fan = next(get_parent_and_child_modules(dev, Module.Fan))
|
||||
assert fan
|
||||
sleep_feature = fan._module_features["fan_sleep_mode"]
|
||||
assert isinstance(sleep_feature.value, bool)
|
||||
|
||||
call = mocker.spy(fan, "call")
|
||||
await fan.set_sleep_mode(True)
|
||||
call.assert_called_with("set_device_info", {"fan_sleep_mode_on": True})
|
||||
|
||||
await dev.update()
|
||||
|
||||
assert fan.sleep_mode is True
|
||||
assert sleep_feature.value is True
|
||||
|
||||
|
||||
@fan
|
||||
async def test_fan_module(dev: SmartDevice, mocker: MockerFixture):
|
||||
"""Test fan speed on device interface."""
|
||||
assert isinstance(dev, SmartDevice)
|
||||
fan = next(get_parent_and_child_modules(dev, Module.Fan))
|
||||
assert fan
|
||||
device = fan._device
|
||||
|
||||
await fan.set_fan_speed_level(1)
|
||||
await dev.update()
|
||||
assert fan.fan_speed_level == 1
|
||||
assert device.is_on
|
||||
|
||||
# Check that if the device is off the speed level is 0.
|
||||
await device.set_state(False)
|
||||
await dev.update()
|
||||
assert fan.fan_speed_level == 0
|
||||
|
||||
await fan.set_fan_speed_level(4)
|
||||
await dev.update()
|
||||
assert fan.fan_speed_level == 4
|
||||
|
||||
await fan.set_fan_speed_level(0)
|
||||
await dev.update()
|
||||
assert not device.is_on
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid level"):
|
||||
await fan.set_fan_speed_level(-1)
|
||||
|
||||
with pytest.raises(ValueError, match="Invalid level"):
|
||||
await fan.set_fan_speed_level(5)
|
150
tests/smart/modules/test_firmware.py
Normal file
150
tests/smart/modules/test_firmware.py
Normal file
@@ -0,0 +1,150 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from contextlib import nullcontext
|
||||
from typing import TypedDict
|
||||
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from kasa import KasaException, Module
|
||||
from kasa.smart import SmartDevice
|
||||
from kasa.smart.modules.firmware import DownloadState
|
||||
|
||||
from ...device_fixtures import parametrize
|
||||
|
||||
firmware = parametrize(
|
||||
"has firmware", component_filter="firmware", protocol_filter={"SMART"}
|
||||
)
|
||||
|
||||
|
||||
@firmware
|
||||
@pytest.mark.parametrize(
|
||||
("feature", "prop_name", "type", "required_version"),
|
||||
[
|
||||
("auto_update_enabled", "auto_update_enabled", bool, 2),
|
||||
("update_available", "update_available", bool, 1),
|
||||
("current_firmware_version", "current_firmware", str, 1),
|
||||
("available_firmware_version", "latest_firmware", str, 1),
|
||||
],
|
||||
)
|
||||
async def test_firmware_features(
|
||||
dev: SmartDevice, feature, prop_name, type, required_version, mocker: MockerFixture
|
||||
):
|
||||
"""Test light effect."""
|
||||
fw = dev.modules.get(Module.Firmware)
|
||||
assert fw
|
||||
assert fw.firmware_update_info is None
|
||||
|
||||
if not dev.is_cloud_connected:
|
||||
pytest.skip("Device is not cloud connected, skipping test")
|
||||
|
||||
await fw.check_latest_firmware()
|
||||
if fw.supported_version < required_version:
|
||||
pytest.skip(f"Feature {feature} requires newer version")
|
||||
|
||||
prop = getattr(fw, prop_name)
|
||||
assert isinstance(prop, type)
|
||||
|
||||
feat = dev.features[feature]
|
||||
assert feat.value == prop
|
||||
assert isinstance(feat.value, type)
|
||||
|
||||
|
||||
@firmware
|
||||
async def test_update_available_without_cloud(dev: SmartDevice):
|
||||
"""Test that update_available returns None when disconnected."""
|
||||
fw = dev.modules.get(Module.Firmware)
|
||||
assert fw
|
||||
assert fw.firmware_update_info is None
|
||||
|
||||
if dev.is_cloud_connected:
|
||||
await fw.check_latest_firmware()
|
||||
assert isinstance(fw.update_available, bool)
|
||||
else:
|
||||
assert fw.update_available is None
|
||||
|
||||
|
||||
@firmware
|
||||
@pytest.mark.parametrize(
|
||||
("update_available", "expected_result"),
|
||||
[
|
||||
pytest.param(True, nullcontext(), id="available"),
|
||||
pytest.param(False, pytest.raises(KasaException), id="not-available"),
|
||||
],
|
||||
)
|
||||
async def test_firmware_update(
|
||||
dev: SmartDevice,
|
||||
mocker: MockerFixture,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
update_available,
|
||||
expected_result,
|
||||
):
|
||||
"""Test updating firmware."""
|
||||
caplog.set_level(logging.INFO)
|
||||
|
||||
if not dev.is_cloud_connected:
|
||||
pytest.skip("Device is not cloud connected, skipping test")
|
||||
|
||||
fw = dev.modules.get(Module.Firmware)
|
||||
assert fw
|
||||
|
||||
upgrade_time = 5
|
||||
|
||||
class Extras(TypedDict):
|
||||
reboot_time: int
|
||||
upgrade_time: int
|
||||
auto_upgrade: bool
|
||||
|
||||
extras: Extras = {
|
||||
"reboot_time": 5,
|
||||
"upgrade_time": upgrade_time,
|
||||
"auto_upgrade": False,
|
||||
}
|
||||
update_states = [
|
||||
# Unknown 1
|
||||
DownloadState(status=1, download_progress=0, **extras),
|
||||
# Downloading
|
||||
DownloadState(status=2, download_progress=10, **extras),
|
||||
DownloadState(status=2, download_progress=100, **extras),
|
||||
# Flashing
|
||||
DownloadState(status=3, download_progress=100, **extras),
|
||||
DownloadState(status=3, download_progress=100, **extras),
|
||||
# Done
|
||||
DownloadState(status=0, download_progress=100, **extras),
|
||||
]
|
||||
|
||||
asyncio_sleep = asyncio.sleep
|
||||
sleep = mocker.patch("asyncio.sleep")
|
||||
mocker.patch.object(fw, "get_update_state", side_effect=update_states)
|
||||
|
||||
cb_mock = mocker.AsyncMock()
|
||||
|
||||
assert fw.firmware_update_info is None
|
||||
with pytest.raises(KasaException):
|
||||
await fw.update(progress_cb=cb_mock)
|
||||
await fw.check_latest_firmware()
|
||||
assert fw.firmware_update_info is not None
|
||||
|
||||
fw._firmware_update_info.status = 1 if update_available else 0
|
||||
|
||||
with expected_result:
|
||||
await fw.update(progress_cb=cb_mock)
|
||||
|
||||
if not update_available:
|
||||
return
|
||||
|
||||
# This is necessary to allow the eventloop to process the created tasks
|
||||
await asyncio_sleep(0)
|
||||
|
||||
assert "Unhandled state code" in caplog.text
|
||||
assert "Downloading firmware, progress: 10" in caplog.text
|
||||
assert "Flashing firmware, sleeping" in caplog.text
|
||||
assert "Update idle" in caplog.text
|
||||
|
||||
for state in update_states:
|
||||
cb_mock.assert_any_await(state)
|
||||
|
||||
# sleep based on the upgrade_time
|
||||
sleep.assert_any_call(upgrade_time)
|
29
tests/smart/modules/test_humidity.py
Normal file
29
tests/smart/modules/test_humidity.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import pytest
|
||||
|
||||
from kasa.smart.modules import HumiditySensor
|
||||
|
||||
from ...device_fixtures import parametrize
|
||||
|
||||
humidity = parametrize(
|
||||
"has humidity", component_filter="humidity", protocol_filter={"SMART.CHILD"}
|
||||
)
|
||||
|
||||
|
||||
@humidity
|
||||
@pytest.mark.parametrize(
|
||||
("feature", "type"),
|
||||
[
|
||||
("humidity", int),
|
||||
("humidity_warning", bool),
|
||||
],
|
||||
)
|
||||
async def test_humidity_features(dev, feature, type):
|
||||
"""Test that features are registered and work as expected."""
|
||||
humidity: HumiditySensor = dev.modules["HumiditySensor"]
|
||||
|
||||
prop = getattr(humidity, feature)
|
||||
assert isinstance(prop, type)
|
||||
|
||||
feat = dev.features[feature]
|
||||
assert feat.value == prop
|
||||
assert isinstance(feat.value, type)
|
84
tests/smart/modules/test_light_effect.py
Normal file
84
tests/smart/modules/test_light_effect.py
Normal file
@@ -0,0 +1,84 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from itertools import chain
|
||||
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from kasa import Device, Feature, Module
|
||||
from kasa.smart.modules import LightEffect
|
||||
|
||||
from ...device_fixtures import parametrize
|
||||
|
||||
light_effect = parametrize(
|
||||
"has light effect", component_filter="light_effect", protocol_filter={"SMART"}
|
||||
)
|
||||
|
||||
|
||||
@light_effect
|
||||
async def test_light_effect(dev: Device, mocker: MockerFixture):
|
||||
"""Test light effect."""
|
||||
light_effect = dev.modules.get(Module.LightEffect)
|
||||
assert isinstance(light_effect, LightEffect)
|
||||
|
||||
feature = dev.features["light_effect"]
|
||||
assert feature.type == Feature.Type.Choice
|
||||
|
||||
call = mocker.spy(light_effect, "call")
|
||||
assert feature.choices == light_effect.effect_list
|
||||
assert feature.choices
|
||||
for effect in chain(reversed(feature.choices), feature.choices):
|
||||
await light_effect.set_effect(effect)
|
||||
enable = effect != LightEffect.LIGHT_EFFECTS_OFF
|
||||
params: dict[str, bool | str] = {"enable": enable}
|
||||
if enable:
|
||||
params["id"] = light_effect._scenes_names_to_id[effect]
|
||||
call.assert_called_with("set_dynamic_light_effect_rule_enable", params)
|
||||
await dev.update()
|
||||
assert light_effect.effect == effect
|
||||
assert feature.value == effect
|
||||
|
||||
with pytest.raises(ValueError, match="The effect foobar is not a built in effect"):
|
||||
await light_effect.set_effect("foobar")
|
||||
|
||||
|
||||
@light_effect
|
||||
@pytest.mark.parametrize("effect_active", [True, False])
|
||||
async def test_light_effect_brightness(
|
||||
dev: Device, effect_active: bool, mocker: MockerFixture
|
||||
):
|
||||
"""Test that light module uses light_effect for brightness when active."""
|
||||
light_module = dev.modules[Module.Light]
|
||||
|
||||
light_effect = dev.modules[Module.SmartLightEffect]
|
||||
light_effect_set_brightness = mocker.spy(light_effect, "set_brightness")
|
||||
mock_light_effect_call = mocker.patch.object(light_effect, "call")
|
||||
|
||||
brightness = dev.modules[Module.Brightness]
|
||||
brightness_set_brightness = mocker.spy(brightness, "set_brightness")
|
||||
mock_brightness_call = mocker.patch.object(brightness, "call")
|
||||
|
||||
mocker.patch.object(
|
||||
type(light_effect),
|
||||
"is_active",
|
||||
new_callable=mocker.PropertyMock,
|
||||
return_value=effect_active,
|
||||
)
|
||||
if effect_active: # Set the rule L1 active for testing
|
||||
light_effect.data["current_rule_id"] = "L1"
|
||||
|
||||
await light_module.set_brightness(10)
|
||||
|
||||
if effect_active:
|
||||
assert light_effect.is_active
|
||||
assert light_effect.brightness == dev.brightness
|
||||
|
||||
light_effect_set_brightness.assert_called_with(10)
|
||||
mock_light_effect_call.assert_called_with(
|
||||
"edit_dynamic_light_effect_rule", mocker.ANY
|
||||
)
|
||||
else:
|
||||
assert not light_effect.is_active
|
||||
|
||||
brightness_set_brightness.assert_called_with(10)
|
||||
mock_brightness_call.assert_called_with("set_device_info", {"brightness": 10})
|
99
tests/smart/modules/test_light_strip_effect.py
Normal file
99
tests/smart/modules/test_light_strip_effect.py
Normal file
@@ -0,0 +1,99 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from itertools import chain
|
||||
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from kasa import Device, Feature, Module
|
||||
from kasa.smart.modules import LightEffect, LightStripEffect
|
||||
|
||||
from ...device_fixtures import parametrize
|
||||
|
||||
light_strip_effect = parametrize(
|
||||
"has light strip effect",
|
||||
component_filter="light_strip_lighting_effect",
|
||||
protocol_filter={"SMART"},
|
||||
)
|
||||
|
||||
|
||||
@light_strip_effect
|
||||
async def test_light_strip_effect(dev: Device, mocker: MockerFixture):
|
||||
"""Test light strip effect."""
|
||||
light_effect = dev.modules.get(Module.LightEffect)
|
||||
|
||||
assert isinstance(light_effect, LightStripEffect)
|
||||
|
||||
brightness = dev.modules[Module.Brightness]
|
||||
|
||||
feature = dev.features["light_effect"]
|
||||
assert feature.type == Feature.Type.Choice
|
||||
|
||||
call = mocker.spy(light_effect, "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:
|
||||
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)
|
||||
|
||||
await dev.update()
|
||||
assert light_effect.effect == effect
|
||||
assert feature.value == effect
|
||||
|
||||
with pytest.raises(ValueError, match="The effect foobar is not a built in effect"):
|
||||
await light_effect.set_effect("foobar")
|
||||
|
||||
|
||||
@light_strip_effect
|
||||
@pytest.mark.parametrize("effect_active", [True, False])
|
||||
async def test_light_effect_brightness(
|
||||
dev: Device, effect_active: bool, mocker: MockerFixture
|
||||
):
|
||||
"""Test that light module uses light_effect for brightness when active."""
|
||||
light_module = dev.modules[Module.Light]
|
||||
|
||||
light_effect = dev.modules[Module.SmartLightEffect]
|
||||
light_effect_set_brightness = mocker.spy(light_effect, "set_brightness")
|
||||
mock_light_effect_call = mocker.patch.object(light_effect, "call")
|
||||
|
||||
brightness = dev.modules[Module.Brightness]
|
||||
brightness_set_brightness = mocker.spy(brightness, "set_brightness")
|
||||
mock_brightness_call = mocker.patch.object(brightness, "call")
|
||||
|
||||
mocker.patch.object(
|
||||
type(light_effect),
|
||||
"is_active",
|
||||
new_callable=mocker.PropertyMock,
|
||||
return_value=effect_active,
|
||||
)
|
||||
|
||||
await light_module.set_brightness(10)
|
||||
|
||||
if effect_active:
|
||||
assert light_effect.is_active
|
||||
assert light_effect.brightness == dev.brightness
|
||||
|
||||
light_effect_set_brightness.assert_called_with(10)
|
||||
mock_light_effect_call.assert_called_with(
|
||||
"set_lighting_effect", {"brightness": 10, "bAdjusted": True}
|
||||
)
|
||||
else:
|
||||
assert not light_effect.is_active
|
||||
|
||||
brightness_set_brightness.assert_called_with(10)
|
||||
mock_brightness_call.assert_called_with("set_device_info", {"brightness": 10})
|
81
tests/smart/modules/test_lighttransition.py
Normal file
81
tests/smart/modules/test_lighttransition.py
Normal file
@@ -0,0 +1,81 @@
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from kasa import Feature, Module
|
||||
from kasa.smart import SmartDevice
|
||||
|
||||
from ...device_fixtures import get_parent_and_child_modules, parametrize
|
||||
from ...fixtureinfo import ComponentFilter
|
||||
|
||||
light_transition_v1 = parametrize(
|
||||
"has light transition",
|
||||
component_filter=ComponentFilter(
|
||||
component_name="on_off_gradually", maximum_version=1
|
||||
),
|
||||
protocol_filter={"SMART"},
|
||||
)
|
||||
light_transition_gt_v1 = parametrize(
|
||||
"has light transition",
|
||||
component_filter=ComponentFilter(
|
||||
component_name="on_off_gradually", minimum_version=2
|
||||
),
|
||||
protocol_filter={"SMART"},
|
||||
)
|
||||
|
||||
|
||||
@light_transition_v1
|
||||
async def test_module_v1(dev: SmartDevice, mocker: MockerFixture):
|
||||
"""Test light transition module."""
|
||||
assert isinstance(dev, SmartDevice)
|
||||
light_transition = next(get_parent_and_child_modules(dev, Module.LightTransition))
|
||||
assert light_transition
|
||||
assert "smooth_transitions" in light_transition._module_features
|
||||
assert "smooth_transition_on" not in light_transition._module_features
|
||||
assert "smooth_transition_off" not in light_transition._module_features
|
||||
|
||||
await light_transition.set_enabled(True)
|
||||
await dev.update()
|
||||
assert light_transition.enabled is True
|
||||
|
||||
await light_transition.set_enabled(False)
|
||||
await dev.update()
|
||||
assert light_transition.enabled is False
|
||||
|
||||
|
||||
@light_transition_gt_v1
|
||||
async def test_module_gt_v1(dev: SmartDevice, mocker: MockerFixture):
|
||||
"""Test light transition module."""
|
||||
assert isinstance(dev, SmartDevice)
|
||||
light_transition = next(get_parent_and_child_modules(dev, Module.LightTransition))
|
||||
assert light_transition
|
||||
assert "smooth_transitions" not in light_transition._module_features
|
||||
assert "smooth_transition_on" in light_transition._module_features
|
||||
assert "smooth_transition_off" in light_transition._module_features
|
||||
|
||||
await light_transition.set_enabled(True)
|
||||
await dev.update()
|
||||
assert light_transition.enabled is True
|
||||
|
||||
await light_transition.set_enabled(False)
|
||||
await dev.update()
|
||||
assert light_transition.enabled is False
|
||||
|
||||
await light_transition.set_turn_on_transition(5)
|
||||
await dev.update()
|
||||
assert light_transition.turn_on_transition == 5
|
||||
# enabled is true if either on or off is enabled
|
||||
assert light_transition.enabled is True
|
||||
|
||||
await light_transition.set_turn_off_transition(10)
|
||||
await dev.update()
|
||||
assert light_transition.turn_off_transition == 10
|
||||
assert light_transition.enabled is True
|
||||
|
||||
max_on = light_transition._module_features["smooth_transition_on"].maximum_value
|
||||
assert max_on < Feature.DEFAULT_MAX
|
||||
max_off = light_transition._module_features["smooth_transition_off"].maximum_value
|
||||
assert max_off < Feature.DEFAULT_MAX
|
||||
|
||||
await light_transition.set_turn_on_transition(0)
|
||||
await light_transition.set_turn_off_transition(0)
|
||||
await dev.update()
|
||||
assert light_transition.enabled is False
|
29
tests/smart/modules/test_motionsensor.py
Normal file
29
tests/smart/modules/test_motionsensor.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import pytest
|
||||
|
||||
from kasa import Module, SmartDevice
|
||||
|
||||
from ...device_fixtures import parametrize
|
||||
|
||||
motion = parametrize(
|
||||
"is motion sensor", model_filter="T100", protocol_filter={"SMART.CHILD"}
|
||||
)
|
||||
|
||||
|
||||
@motion
|
||||
@pytest.mark.parametrize(
|
||||
("feature", "type"),
|
||||
[
|
||||
("motion_detected", bool),
|
||||
],
|
||||
)
|
||||
async def test_motion_features(dev: SmartDevice, feature, type):
|
||||
"""Test that features are registered and work as expected."""
|
||||
motion = dev.modules.get(Module.MotionSensor)
|
||||
assert motion is not None
|
||||
|
||||
prop = getattr(motion, feature)
|
||||
assert isinstance(prop, type)
|
||||
|
||||
feat = dev.features[feature]
|
||||
assert feat.value == prop
|
||||
assert isinstance(feat.value, type)
|
48
tests/smart/modules/test_temperature.py
Normal file
48
tests/smart/modules/test_temperature.py
Normal file
@@ -0,0 +1,48 @@
|
||||
import pytest
|
||||
|
||||
from kasa.smart.modules import TemperatureSensor
|
||||
|
||||
from ...device_fixtures import parametrize
|
||||
|
||||
temperature = parametrize(
|
||||
"has temperature", component_filter="temperature", protocol_filter={"SMART.CHILD"}
|
||||
)
|
||||
|
||||
temperature_warning = parametrize(
|
||||
"has temperature warning",
|
||||
component_filter="comfort_temperature",
|
||||
protocol_filter={"SMART.CHILD"},
|
||||
)
|
||||
|
||||
|
||||
@temperature
|
||||
@pytest.mark.parametrize(
|
||||
("feature", "type"),
|
||||
[
|
||||
("temperature", float),
|
||||
("temperature_unit", str),
|
||||
],
|
||||
)
|
||||
async def test_temperature_features(dev, feature, type):
|
||||
"""Test that features are registered and work as expected."""
|
||||
temp_module: TemperatureSensor = dev.modules["TemperatureSensor"]
|
||||
|
||||
prop = getattr(temp_module, feature)
|
||||
assert isinstance(prop, type)
|
||||
|
||||
feat = dev.features[feature]
|
||||
assert feat.value == prop
|
||||
assert isinstance(feat.value, type)
|
||||
|
||||
|
||||
@temperature_warning
|
||||
async def test_temperature_warning(dev):
|
||||
"""Test that features are registered and work as expected."""
|
||||
temp_module: TemperatureSensor = dev.modules["TemperatureSensor"]
|
||||
|
||||
assert hasattr(temp_module, "temperature_warning")
|
||||
assert isinstance(temp_module.temperature_warning, bool)
|
||||
|
||||
feat = dev.features["temperature_warning"]
|
||||
assert feat.value == temp_module.temperature_warning
|
||||
assert isinstance(feat.value, bool)
|
155
tests/smart/modules/test_temperaturecontrol.py
Normal file
155
tests/smart/modules/test_temperaturecontrol.py
Normal file
@@ -0,0 +1,155 @@
|
||||
import logging
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from kasa.smart.modules import TemperatureControl
|
||||
from kasa.smart.modules.temperaturecontrol import ThermostatState
|
||||
|
||||
from ...device_fixtures import parametrize, thermostats_smart
|
||||
|
||||
temperature = parametrize(
|
||||
"has temperature control",
|
||||
component_filter="temperature_control",
|
||||
protocol_filter={"SMART.CHILD"},
|
||||
)
|
||||
|
||||
|
||||
@thermostats_smart
|
||||
@pytest.mark.parametrize(
|
||||
("feature", "type"),
|
||||
[
|
||||
("target_temperature", float),
|
||||
("temperature_offset", int),
|
||||
],
|
||||
)
|
||||
async def test_temperature_control_features(dev, feature, type):
|
||||
"""Test that features are registered and work as expected."""
|
||||
temp_module: TemperatureControl = dev.modules["TemperatureControl"]
|
||||
|
||||
prop = getattr(temp_module, feature)
|
||||
assert isinstance(prop, type)
|
||||
|
||||
feat = dev.features[feature]
|
||||
assert feat.value == prop
|
||||
assert isinstance(feat.value, type)
|
||||
|
||||
await feat.set_value(10)
|
||||
await dev.update()
|
||||
assert feat.value == 10
|
||||
|
||||
|
||||
@thermostats_smart
|
||||
async def test_set_temperature_turns_heating_on(dev):
|
||||
"""Test that set_temperature turns heating on."""
|
||||
temp_module: TemperatureControl = dev.modules["TemperatureControl"]
|
||||
|
||||
await temp_module.set_state(False)
|
||||
await dev.update()
|
||||
assert temp_module.state is False
|
||||
assert temp_module.mode is ThermostatState.Off
|
||||
|
||||
await temp_module.set_target_temperature(10)
|
||||
await dev.update()
|
||||
assert temp_module.state is True
|
||||
assert temp_module.mode is ThermostatState.Heating
|
||||
assert temp_module.target_temperature == 10
|
||||
|
||||
|
||||
@thermostats_smart
|
||||
async def test_set_temperature_invalid_values(dev):
|
||||
"""Test that out-of-bounds temperature values raise errors."""
|
||||
temp_module: TemperatureControl = dev.modules["TemperatureControl"]
|
||||
|
||||
with pytest.raises(
|
||||
ValueError, match="Invalid target temperature -1, must be in range"
|
||||
):
|
||||
await temp_module.set_target_temperature(-1)
|
||||
|
||||
with pytest.raises(
|
||||
ValueError, match="Invalid target temperature 100, must be in range"
|
||||
):
|
||||
await temp_module.set_target_temperature(100)
|
||||
|
||||
|
||||
@thermostats_smart
|
||||
async def test_temperature_offset(dev):
|
||||
"""Test the temperature offset API."""
|
||||
temp_module: TemperatureControl = dev.modules["TemperatureControl"]
|
||||
with pytest.raises(
|
||||
ValueError, match=re.escape("Temperature offset must be [-10, 10]")
|
||||
):
|
||||
await temp_module.set_temperature_offset(100)
|
||||
|
||||
with pytest.raises(
|
||||
ValueError, match=re.escape("Temperature offset must be [-10, 10]")
|
||||
):
|
||||
await temp_module.set_temperature_offset(-100)
|
||||
|
||||
await temp_module.set_temperature_offset(5)
|
||||
await dev.update()
|
||||
assert temp_module.temperature_offset == 5
|
||||
|
||||
|
||||
@thermostats_smart
|
||||
@pytest.mark.parametrize(
|
||||
("mode", "states", "frost_protection"),
|
||||
[
|
||||
pytest.param(ThermostatState.Idle, [], False, id="idle has empty"),
|
||||
pytest.param(
|
||||
ThermostatState.Off,
|
||||
["anything"],
|
||||
True,
|
||||
id="any state with frost_protection on means off",
|
||||
),
|
||||
pytest.param(
|
||||
ThermostatState.Heating,
|
||||
["heating"],
|
||||
False,
|
||||
id="heating is heating",
|
||||
),
|
||||
pytest.param(ThermostatState.Unknown, ["invalid"], False, id="unknown state"),
|
||||
],
|
||||
)
|
||||
async def test_thermostat_mode(dev, mode, states, frost_protection):
|
||||
"""Test different thermostat modes."""
|
||||
temp_module: TemperatureControl = dev.modules["TemperatureControl"]
|
||||
|
||||
temp_module.data["frost_protection_on"] = frost_protection
|
||||
temp_module.data["trv_states"] = states
|
||||
|
||||
assert temp_module.state is not frost_protection
|
||||
assert temp_module.mode is mode
|
||||
|
||||
|
||||
@thermostats_smart
|
||||
@pytest.mark.parametrize(
|
||||
("mode", "states", "msg"),
|
||||
[
|
||||
pytest.param(
|
||||
ThermostatState.Heating,
|
||||
["heating", "something else"],
|
||||
"Got multiple states",
|
||||
id="multiple states",
|
||||
),
|
||||
pytest.param(
|
||||
ThermostatState.Unknown, ["foobar"], "Got unknown state", id="unknown state"
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_thermostat_mode_warnings(dev, mode, states, msg, caplog):
|
||||
"""Test thermostat modes that should log a warning."""
|
||||
temp_module: TemperatureControl = dev.modules["TemperatureControl"]
|
||||
caplog.set_level(logging.WARNING)
|
||||
|
||||
temp_module.data["trv_states"] = states
|
||||
assert temp_module.mode is mode
|
||||
assert msg in caplog.text
|
||||
|
||||
|
||||
@thermostats_smart
|
||||
async def test_thermostat_heating_with_low_battery(dev):
|
||||
"""Test that mode is reported correctly with extra states."""
|
||||
temp_module: TemperatureControl = dev.modules["TemperatureControl"]
|
||||
temp_module.data["trv_states"] = ["low_battery", "heating"]
|
||||
assert temp_module.mode is ThermostatState.Heating
|
46
tests/smart/modules/test_waterleak.py
Normal file
46
tests/smart/modules/test_waterleak.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
|
||||
import pytest
|
||||
|
||||
from kasa.smart.modules import WaterleakSensor
|
||||
|
||||
from ...device_fixtures import parametrize
|
||||
|
||||
waterleak = parametrize(
|
||||
"has waterleak", component_filter="sensor_alarm", protocol_filter={"SMART.CHILD"}
|
||||
)
|
||||
|
||||
|
||||
@waterleak
|
||||
@pytest.mark.parametrize(
|
||||
("feature", "prop_name", "type"),
|
||||
[
|
||||
("water_alert", "alert", int),
|
||||
# Can be converted to 'datetime | None' after py3.9 support is dropped
|
||||
("water_alert_timestamp", "alert_timestamp", (datetime, type(None))),
|
||||
("water_leak", "status", Enum),
|
||||
],
|
||||
)
|
||||
async def test_waterleak_properties(dev, feature, prop_name, type):
|
||||
"""Test that features are registered and work as expected."""
|
||||
waterleak: WaterleakSensor = dev.modules["WaterleakSensor"]
|
||||
|
||||
prop = getattr(waterleak, prop_name)
|
||||
assert isinstance(prop, type)
|
||||
|
||||
feat = dev.features[feature]
|
||||
assert feat.value == prop
|
||||
assert isinstance(feat.value, type)
|
||||
|
||||
|
||||
@waterleak
|
||||
async def test_waterleak_features(dev):
|
||||
"""Test waterleak features."""
|
||||
waterleak: WaterleakSensor = dev.modules["WaterleakSensor"]
|
||||
|
||||
assert "water_leak" in dev.features
|
||||
assert dev.features["water_leak"].value == waterleak.status
|
||||
|
||||
assert "water_alert" in dev.features
|
||||
assert dev.features["water_alert"].value == waterleak.alert
|
Reference in New Issue
Block a user