Create common interfaces for remaining device types (#895)

Introduce common module interfaces across smart and iot devices and provide better typing implementation for getting modules to support this.
This commit is contained in:
Steven B
2024-05-10 19:29:28 +01:00
committed by GitHub
parent 7d4dc4c710
commit 9473d97ad2
33 changed files with 673 additions and 220 deletions

View File

@@ -189,6 +189,11 @@ class FakeSmartTransport(BaseTransport):
if "current_rule_id" in info["get_dynamic_light_effect_rules"]:
del info["get_dynamic_light_effect_rules"]["current_rule_id"]
def _set_led_info(self, info, params):
"""Set or remove values as per the device behaviour."""
info["get_led_info"]["led_status"] = params["led_rule"] != "never"
info["get_led_info"]["led_rule"] = params["led_rule"]
def _send_request(self, request_dict: dict):
method = request_dict["method"]
params = request_dict["params"]
@@ -218,7 +223,9 @@ class FakeSmartTransport(BaseTransport):
# SMART fixtures started to be generated
missing_result := self.FIXTURE_MISSING_MAP.get(method)
) and missing_result[0] in self.components:
result = copy.deepcopy(missing_result[1])
# Copy to info so it will work with update methods
info[method] = copy.deepcopy(missing_result[1])
result = copy.deepcopy(info[method])
retval = {"result": result, "error_code": 0}
else:
# PARAMS error returned for KS240 when get_device_usage called
@@ -239,6 +246,9 @@ class FakeSmartTransport(BaseTransport):
elif method == "set_dynamic_light_effect_rule_enable":
self._set_light_effect(info, params)
return {"error_code": 0}
elif method == "set_led_info":
self._set_led_info(info, params)
return {"error_code": 0}
elif method[:4] == "set_":
target_method = f"get_{method[4:]}"
info[target_method].update(params)

View File

@@ -10,7 +10,7 @@ brightness = parametrize("brightness smart", component_filter="brightness")
@brightness
async def test_brightness_component(dev: SmartDevice):
"""Test brightness feature."""
brightness = dev.get_module("Brightness")
brightness = dev.modules.get("Brightness")
assert brightness
assert isinstance(dev, SmartDevice)
assert "brightness" in dev._components

View File

@@ -1,7 +1,6 @@
import pytest
from kasa import SmartDevice
from kasa.smart.modules import ContactSensor
from kasa import Module, SmartDevice
from kasa.tests.device_fixtures import parametrize
contact = parametrize(
@@ -18,7 +17,7 @@ contact = parametrize(
)
async def test_contact_features(dev: SmartDevice, feature, type):
"""Test that features are registered and work as expected."""
contact = dev.get_module(ContactSensor)
contact = dev.modules.get(Module.ContactSensor)
assert contact is not None
prop = getattr(contact, feature)

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,7 +11,7 @@ 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"]
@@ -36,7 +36,7 @@ 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"]
assert isinstance(sleep_feature.value, bool)
@@ -55,7 +55,7 @@ async def test_sleep_mode(dev: SmartDevice, mocker: MockerFixture):
async def test_fan_interface(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

View File

@@ -6,8 +6,8 @@ import logging
import pytest
from pytest_mock import MockerFixture
from kasa import Module
from kasa.smart import SmartDevice
from kasa.smart.modules import Firmware
from kasa.smart.modules.firmware import DownloadState
from kasa.tests.device_fixtures import parametrize
@@ -31,7 +31,7 @@ async def test_firmware_features(
dev: SmartDevice, feature, prop_name, type, required_version, mocker: MockerFixture
):
"""Test light effect."""
fw = dev.get_module(Firmware)
fw = dev.modules.get(Module.Firmware)
assert fw
if not dev.is_cloud_connected:
@@ -51,7 +51,7 @@ async def test_firmware_features(
@firmware
async def test_update_available_without_cloud(dev: SmartDevice):
"""Test that update_available returns None when disconnected."""
fw = dev.get_module(Firmware)
fw = dev.modules.get(Module.Firmware)
assert fw
if dev.is_cloud_connected:
@@ -67,7 +67,7 @@ async def test_firmware_update(
"""Test updating firmware."""
caplog.set_level(logging.INFO)
fw = dev.get_module(Firmware)
fw = dev.modules.get(Module.Firmware)
assert fw
upgrade_time = 5

View File

@@ -1,12 +1,11 @@
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 import Device, Feature, Module
from kasa.smart.modules import LightEffectModule
from kasa.tests.device_fixtures import parametrize
@@ -18,8 +17,8 @@ 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, LightEffectModule)
feature = light_effect._module_features["light_effect"]
assert feature.type == Feature.Type.Choice

View File

@@ -0,0 +1,95 @@
import pytest
from pytest_mock import MockerFixture
from kasa import Device, Module
from kasa.tests.device_fixtures import (
lightstrip,
parametrize,
parametrize_combine,
plug_iot,
)
led_smart = parametrize(
"has led smart", component_filter="led", protocol_filter={"SMART"}
)
led = parametrize_combine([led_smart, plug_iot])
light_effect_smart = parametrize(
"has light effect smart", component_filter="light_effect", protocol_filter={"SMART"}
)
light_effect = parametrize_combine([light_effect_smart, lightstrip])
@led
async def test_led_module(dev: Device, mocker: MockerFixture):
"""Test fan speed feature."""
led_module = dev.modules.get(Module.Led)
assert led_module
feat = led_module._module_features["led"]
call = mocker.spy(led_module, "call")
await led_module.set_led(True)
assert call.call_count == 1
await dev.update()
assert led_module.led is True
assert feat.value is True
await led_module.set_led(False)
assert call.call_count == 2
await dev.update()
assert led_module.led is False
assert feat.value is False
await feat.set_value(True)
assert call.call_count == 3
await dev.update()
assert feat.value is True
assert led_module.led is True
@light_effect
async def test_light_effect_module(dev: Device, mocker: MockerFixture):
"""Test fan speed feature."""
light_effect_module = dev.modules[Module.LightEffect]
assert light_effect_module
feat = light_effect_module._module_features["light_effect"]
call = mocker.spy(light_effect_module, "call")
effect_list = light_effect_module.effect_list
assert "Off" in effect_list
assert effect_list.index("Off") == 0
assert len(effect_list) > 1
assert effect_list == feat.choices
assert light_effect_module.has_custom_effects is not None
await light_effect_module.set_effect("Off")
assert call.call_count == 1
await dev.update()
assert light_effect_module.effect == "Off"
assert feat.value == "Off"
second_effect = effect_list[1]
await light_effect_module.set_effect(second_effect)
assert call.call_count == 2
await dev.update()
assert light_effect_module.effect == second_effect
assert feat.value == second_effect
last_effect = effect_list[len(effect_list) - 1]
await light_effect_module.set_effect(last_effect)
assert call.call_count == 3
await dev.update()
assert light_effect_module.effect == last_effect
assert feat.value == last_effect
# Test feature set
await feat.set_value(second_effect)
assert call.call_count == 4
await dev.update()
assert light_effect_module.effect == second_effect
assert feat.value == second_effect
with pytest.raises(ValueError):
await light_effect_module.set_effect("foobar")
assert call.call_count == 4

View File

@@ -16,7 +16,7 @@ from voluptuous import (
Schema,
)
from kasa import KasaException
from kasa import KasaException, Module
from kasa.iot import IotDevice
from .conftest import get_device_for_fixture_protocol, handle_turn_on, turn_on
@@ -261,27 +261,26 @@ async def test_modules_not_supported(dev: IotDevice):
async def test_get_modules():
"""Test get_modules for child and parent modules."""
"""Test getting modules for child and parent modules."""
dummy_device = await get_device_for_fixture_protocol(
"HS100(US)_2.0_1.5.6.json", "IOT"
)
from kasa.iot.modules import Cloud
from kasa.smart.modules import CloudModule
# Modules on device
module = dummy_device.get_module("Cloud")
module = dummy_device.modules.get("cloud")
assert module
assert module._device == dummy_device
assert isinstance(module, Cloud)
module = dummy_device.get_module(Cloud)
module = dummy_device.modules.get(Module.IotCloud)
assert module
assert module._device == dummy_device
assert isinstance(module, Cloud)
# Invalid modules
module = dummy_device.get_module("DummyModule")
module = dummy_device.modules.get("DummyModule")
assert module is None
module = dummy_device.get_module(CloudModule)
module = dummy_device.modules.get(Module.Cloud)
assert module is None

View File

@@ -1,7 +1,6 @@
import pytest
from kasa import DeviceType
from kasa.exceptions import KasaException
from kasa.iot import IotLightStrip
from .conftest import lightstrip
@@ -23,7 +22,7 @@ async def test_lightstrip_effect(dev: IotLightStrip):
@lightstrip
async def test_effects_lightstrip_set_effect(dev: IotLightStrip):
with pytest.raises(KasaException):
with pytest.raises(ValueError):
await dev.set_effect("Not real")
await dev.set_effect("Candy Cane")

View File

@@ -9,7 +9,7 @@ from unittest.mock import patch
import pytest
from pytest_mock import MockerFixture
from kasa import KasaException
from kasa import KasaException, Module
from kasa.exceptions import SmartErrorCode
from kasa.smart import SmartDevice
@@ -123,40 +123,39 @@ async def test_update_module_queries(dev: SmartDevice, mocker: MockerFixture):
async def test_get_modules():
"""Test get_modules for child and parent modules."""
"""Test getting modules for child and parent modules."""
dummy_device = await get_device_for_fixture_protocol(
"KS240(US)_1.0_1.0.5.json", "SMART"
)
from kasa.iot.modules import AmbientLight
from kasa.smart.modules import CloudModule, FanModule
from kasa.smart.modules import CloudModule
# Modules on device
module = dummy_device.get_module("CloudModule")
module = dummy_device.modules.get("CloudModule")
assert module
assert module._device == dummy_device
assert isinstance(module, CloudModule)
module = dummy_device.get_module(CloudModule)
module = dummy_device.modules.get(Module.Cloud)
assert module
assert module._device == dummy_device
assert isinstance(module, CloudModule)
# Modules on child
module = dummy_device.get_module("FanModule")
module = dummy_device.modules.get("FanModule")
assert module
assert module._device != dummy_device
assert module._device._parent == dummy_device
module = dummy_device.get_module(FanModule)
module = dummy_device.modules.get(Module.Fan)
assert module
assert module._device != dummy_device
assert module._device._parent == dummy_device
# Invalid modules
module = dummy_device.get_module("DummyModule")
module = dummy_device.modules.get("DummyModule")
assert module is None
module = dummy_device.get_module(AmbientLight)
module = dummy_device.modules.get(Module.IotAmbientLight)
assert module is None