Merge remote-tracking branch 'upstream/master' into feat/device_update

This commit is contained in:
sdb9696
2024-05-14 18:35:23 +01:00
83 changed files with 3312 additions and 933 deletions

View File

@@ -0,0 +1,28 @@
import pytest
from kasa import Module, SmartDevice
from kasa.tests.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)

View File

@@ -1,8 +1,8 @@
import pytest
from pytest_mock import MockerFixture
from kasa import Module
from kasa.smart import SmartDevice
from kasa.smart.modules import FanModule
from kasa.tests.device_fixtures import parametrize
fan = parametrize("has fan", component_filter="fan_control", protocol_filter={"SMART"})
@@ -11,10 +11,10 @@ fan = parametrize("has fan", component_filter="fan_control", protocol_filter={"S
@fan
async def test_fan_speed(dev: SmartDevice, mocker: MockerFixture):
"""Test fan speed feature."""
fan = dev.get_module(FanModule)
fan = dev.modules.get(Module.Fan)
assert fan
level_feature = fan._module_features["fan_speed_level"]
level_feature = dev.features["fan_speed_level"]
assert (
level_feature.minimum_value
<= level_feature.value
@@ -36,9 +36,9 @@ async def test_fan_speed(dev: SmartDevice, mocker: MockerFixture):
@fan
async def test_sleep_mode(dev: SmartDevice, mocker: MockerFixture):
"""Test sleep mode feature."""
fan = dev.get_module(FanModule)
fan = dev.modules.get(Module.Fan)
assert fan
sleep_feature = fan._module_features["fan_sleep_mode"]
sleep_feature = dev.features["fan_sleep_mode"]
assert isinstance(sleep_feature.value, bool)
call = mocker.spy(fan, "call")
@@ -52,29 +52,28 @@ async def test_sleep_mode(dev: SmartDevice, mocker: MockerFixture):
@fan
async def test_fan_interface(dev: SmartDevice, mocker: MockerFixture):
async def test_fan_module(dev: SmartDevice, mocker: MockerFixture):
"""Test fan speed on device interface."""
assert isinstance(dev, SmartDevice)
fan = dev.get_module(FanModule)
fan = dev.modules.get(Module.Fan)
assert fan
device = fan._device
assert device.is_fan
await device.set_fan_speed_level(1)
await fan.set_fan_speed_level(1)
await dev.update()
assert device.fan_speed_level == 1
assert fan.fan_speed_level == 1
assert device.is_on
await device.set_fan_speed_level(4)
await fan.set_fan_speed_level(4)
await dev.update()
assert device.fan_speed_level == 4
assert fan.fan_speed_level == 4
await device.set_fan_speed_level(0)
await fan.set_fan_speed_level(0)
await dev.update()
assert not device.is_on
with pytest.raises(ValueError):
await device.set_fan_speed_level(-1)
await fan.set_fan_speed_level(-1)
with pytest.raises(ValueError):
await device.set_fan_speed_level(5)
await fan.set_fan_speed_level(5)

View File

@@ -0,0 +1,113 @@
from __future__ import annotations
import asyncio
import logging
import pytest
from pytest_mock import MockerFixture
from kasa import Module
from kasa.smart import SmartDevice
from kasa.smart.modules.firmware import DownloadState, Firmware
from kasa.tests.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),
("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
if not isinstance(fw, Firmware): # TODO needed while common interface still TBD
return
if not dev.is_cloud_connected:
pytest.skip("Device is not cloud connected, skipping test")
if fw.supported_version < required_version:
pytest.skip("Feature %s requires newer version" % feature)
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
if not isinstance(fw, Firmware): # TODO needed while common interface still TBD
return
if dev.is_cloud_connected:
assert isinstance(fw.update_available, bool)
else:
assert fw.update_available is None
@firmware
async def test_firmware_update(
dev: SmartDevice, mocker: MockerFixture, caplog: pytest.LogCaptureFixture
):
"""Test updating firmware."""
caplog.set_level(logging.INFO)
fw = dev.modules.get(Module.Firmware)
assert fw
if not isinstance(fw, Firmware): # TODO needed while common interface still TBD
return
upgrade_time = 5
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()
await fw.update(progress_cb=cb_mock)
# 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)

View File

@@ -23,6 +23,6 @@ async def test_humidity_features(dev, feature, type):
prop = getattr(humidity, feature)
assert isinstance(prop, type)
feat = humidity._module_features[feature]
feat = dev.features[feature]
assert feat.value == prop
assert isinstance(feat.value, type)

View File

@@ -1,13 +1,12 @@
from __future__ import annotations
from itertools import chain
from typing import cast
import pytest
from pytest_mock import MockerFixture
from kasa import Device, Feature
from kasa.smart.modules import LightEffectModule
from kasa import Device, Feature, Module
from kasa.smart.modules import LightEffect
from kasa.tests.device_fixtures import parametrize
light_effect = parametrize(
@@ -18,10 +17,10 @@ light_effect = parametrize(
@light_effect
async def test_light_effect(dev: Device, mocker: MockerFixture):
"""Test light effect."""
light_effect = cast(LightEffectModule, dev.modules.get("LightEffectModule"))
assert light_effect
light_effect = dev.modules.get(Module.LightEffect)
assert isinstance(light_effect, LightEffect)
feature = light_effect._module_features["light_effect"]
feature = dev.features["light_effect"]
assert feature.type == Feature.Type.Choice
call = mocker.spy(light_effect, "call")
@@ -29,7 +28,7 @@ async def test_light_effect(dev: Device, mocker: MockerFixture):
assert feature.choices
for effect in chain(reversed(feature.choices), feature.choices):
await light_effect.set_effect(effect)
enable = effect != LightEffectModule.LIGHT_EFFECTS_OFF
enable = effect != LightEffect.LIGHT_EFFECTS_OFF
params: dict[str, bool | str] = {"enable": enable}
if enable:
params["id"] = light_effect._scenes_names_to_id[effect]

View File

@@ -29,7 +29,7 @@ async def test_temperature_features(dev, feature, type):
prop = getattr(temp_module, feature)
assert isinstance(prop, type)
feat = temp_module._module_features[feature]
feat = dev.features[feature]
assert feat.value == prop
assert isinstance(feat.value, type)
@@ -42,6 +42,6 @@ async def test_temperature_warning(dev):
assert hasattr(temp_module, "temperature_warning")
assert isinstance(temp_module.temperature_warning, bool)
feat = temp_module._module_features["temperature_warning"]
feat = dev.features["temperature_warning"]
assert feat.value == temp_module.temperature_warning
assert isinstance(feat.value, bool)

View File

@@ -28,7 +28,7 @@ async def test_temperature_control_features(dev, feature, type):
prop = getattr(temp_module, feature)
assert isinstance(prop, type)
feat = temp_module._module_features[feature]
feat = dev.features[feature]
assert feat.value == prop
assert isinstance(feat.value, type)

View File

@@ -25,7 +25,7 @@ async def test_waterleak_properties(dev, feature, prop_name, type):
prop = getattr(waterleak, prop_name)
assert isinstance(prop, type)
feat = waterleak._module_features[feature]
feat = dev.features[feature]
assert feat.value == prop
assert isinstance(feat.value, type)