mirror of
https://github.com/python-kasa/python-kasa.git
synced 2024-12-23 03:33:35 +00:00
Refactor split smartdevice tests to test_{iot,smart}device (#822)
This commit is contained in:
parent
41e58252f7
commit
48ac39e6d8
@ -3,7 +3,9 @@ import pytest
|
|||||||
from kasa.smart.modules import HumiditySensor
|
from kasa.smart.modules import HumiditySensor
|
||||||
from kasa.tests.device_fixtures import parametrize
|
from kasa.tests.device_fixtures import parametrize
|
||||||
|
|
||||||
humidity = parametrize("has humidity", component_filter="humidity", protocol_filter={"SMART.CHILD"})
|
humidity = parametrize(
|
||||||
|
"has humidity", component_filter="humidity", protocol_filter={"SMART.CHILD"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@humidity
|
@humidity
|
||||||
|
@ -3,7 +3,9 @@ import pytest
|
|||||||
from kasa.smart.modules import TemperatureSensor
|
from kasa.smart.modules import TemperatureSensor
|
||||||
from kasa.tests.device_fixtures import parametrize
|
from kasa.tests.device_fixtures import parametrize
|
||||||
|
|
||||||
temperature = parametrize("has temperature", component_filter="temperature", protocol_filter={"SMART.CHILD"})
|
temperature = parametrize(
|
||||||
|
"has temperature", component_filter="temperature", protocol_filter={"SMART.CHILD"}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@temperature
|
@temperature
|
||||||
|
@ -24,7 +24,7 @@ from .conftest import (
|
|||||||
variable_temp,
|
variable_temp,
|
||||||
variable_temp_iot,
|
variable_temp_iot,
|
||||||
)
|
)
|
||||||
from .test_smartdevice import SYSINFO_SCHEMA
|
from .test_iotdevice import SYSINFO_SCHEMA
|
||||||
|
|
||||||
|
|
||||||
@bulb
|
@bulb
|
||||||
@ -370,3 +370,10 @@ SYSINFO_SCHEMA_BULB = SYSINFO_SCHEMA.extend(
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bulb
|
||||||
|
def test_device_type_bulb(dev):
|
||||||
|
if dev.is_light_strip:
|
||||||
|
pytest.skip("bulb has also lightstrips to test the api")
|
||||||
|
assert dev.device_type == DeviceType.Bulb
|
||||||
|
121
kasa/tests/test_device.py
Normal file
121
kasa/tests/test_device.py
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
"""Tests for all devices."""
|
||||||
|
import importlib
|
||||||
|
import inspect
|
||||||
|
import pkgutil
|
||||||
|
import sys
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import kasa
|
||||||
|
from kasa import Credentials, Device, DeviceConfig
|
||||||
|
from kasa.iot import IotDevice
|
||||||
|
from kasa.smart import SmartChildDevice, SmartDevice
|
||||||
|
|
||||||
|
|
||||||
|
def _get_subclasses(of_class):
|
||||||
|
package = sys.modules["kasa"]
|
||||||
|
subclasses = set()
|
||||||
|
for _, modname, _ in pkgutil.iter_modules(package.__path__):
|
||||||
|
importlib.import_module("." + modname, package="kasa")
|
||||||
|
module = sys.modules["kasa." + modname]
|
||||||
|
for name, obj in inspect.getmembers(module):
|
||||||
|
if (
|
||||||
|
inspect.isclass(obj)
|
||||||
|
and issubclass(obj, of_class)
|
||||||
|
and module.__package__ != "kasa"
|
||||||
|
):
|
||||||
|
subclasses.add((module.__package__ + "." + name, obj))
|
||||||
|
return subclasses
|
||||||
|
|
||||||
|
|
||||||
|
device_classes = pytest.mark.parametrize(
|
||||||
|
"device_class_name_obj", _get_subclasses(Device), ids=lambda t: t[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_alias(dev):
|
||||||
|
test_alias = "TEST1234"
|
||||||
|
original = dev.alias
|
||||||
|
|
||||||
|
assert isinstance(original, str)
|
||||||
|
await dev.set_alias(test_alias)
|
||||||
|
await dev.update()
|
||||||
|
assert dev.alias == test_alias
|
||||||
|
|
||||||
|
await dev.set_alias(original)
|
||||||
|
await dev.update()
|
||||||
|
assert dev.alias == original
|
||||||
|
|
||||||
|
|
||||||
|
@device_classes
|
||||||
|
async def test_device_class_ctors(device_class_name_obj):
|
||||||
|
"""Make sure constructor api not broken for new and existing SmartDevices."""
|
||||||
|
host = "127.0.0.2"
|
||||||
|
port = 1234
|
||||||
|
credentials = Credentials("foo", "bar")
|
||||||
|
config = DeviceConfig(host, port_override=port, credentials=credentials)
|
||||||
|
klass = device_class_name_obj[1]
|
||||||
|
if issubclass(klass, SmartChildDevice):
|
||||||
|
parent = SmartDevice(host, config=config)
|
||||||
|
dev = klass(
|
||||||
|
parent, {"dummy": "info", "device_id": "dummy"}, {"dummy": "components"}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
dev = klass(host, config=config)
|
||||||
|
assert dev.host == host
|
||||||
|
assert dev.port == port
|
||||||
|
assert dev.credentials == credentials
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_device_with_timeout():
|
||||||
|
"""Make sure timeout is passed to the protocol."""
|
||||||
|
host = "127.0.0.1"
|
||||||
|
dev = IotDevice(host, config=DeviceConfig(host, timeout=100))
|
||||||
|
assert dev.protocol._transport._timeout == 100
|
||||||
|
dev = SmartDevice(host, config=DeviceConfig(host, timeout=100))
|
||||||
|
assert dev.protocol._transport._timeout == 100
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_thin_wrapper():
|
||||||
|
"""Make sure thin wrapper is created with the correct device type."""
|
||||||
|
mock = Mock()
|
||||||
|
config = DeviceConfig(
|
||||||
|
host="test_host",
|
||||||
|
port_override=1234,
|
||||||
|
timeout=100,
|
||||||
|
credentials=Credentials("username", "password"),
|
||||||
|
)
|
||||||
|
with patch("kasa.device_factory.connect", return_value=mock) as connect:
|
||||||
|
dev = await Device.connect(config=config)
|
||||||
|
assert dev is mock
|
||||||
|
|
||||||
|
connect.assert_called_once_with(
|
||||||
|
host=None,
|
||||||
|
config=config,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"device_class, use_class", kasa.deprecated_smart_devices.items()
|
||||||
|
)
|
||||||
|
def test_deprecated_devices(device_class, use_class):
|
||||||
|
package_name = ".".join(use_class.__module__.split(".")[:-1])
|
||||||
|
msg = f"{device_class} is deprecated, use {use_class.__name__} from package {package_name} instead"
|
||||||
|
with pytest.deprecated_call(match=msg):
|
||||||
|
getattr(kasa, device_class)
|
||||||
|
packages = package_name.split(".")
|
||||||
|
module = __import__(packages[0])
|
||||||
|
for _ in packages[1:]:
|
||||||
|
module = importlib.import_module(package_name, package=module.__name__)
|
||||||
|
getattr(module, use_class.__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"exceptions_class, use_class", kasa.deprecated_exceptions.items()
|
||||||
|
)
|
||||||
|
def test_deprecated_exceptions(exceptions_class, use_class):
|
||||||
|
msg = f"{exceptions_class} is deprecated, use {use_class.__name__} instead"
|
||||||
|
with pytest.deprecated_call(match=msg):
|
||||||
|
getattr(kasa, exceptions_class)
|
||||||
|
getattr(kasa, use_class.__name__)
|
@ -1,5 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from kasa import DeviceType
|
||||||
from kasa.iot import IotDimmer
|
from kasa.iot import IotDimmer
|
||||||
|
|
||||||
from .conftest import dimmer, handle_turn_on, turn_on
|
from .conftest import dimmer, handle_turn_on, turn_on
|
||||||
@ -132,3 +133,8 @@ async def test_set_dimmer_transition_invalid(dev):
|
|||||||
for invalid_transition in [-1, 0, 0.5]:
|
for invalid_transition in [-1, 0, 0.5]:
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
await dev.set_dimmer_transition(1, invalid_transition)
|
await dev.set_dimmer_transition(1, invalid_transition)
|
||||||
|
|
||||||
|
|
||||||
|
@dimmer
|
||||||
|
def test_device_type_dimmer(dev):
|
||||||
|
assert dev.device_type == DeviceType.Dimmer
|
||||||
|
259
kasa/tests/test_iotdevice.py
Normal file
259
kasa/tests/test_iotdevice.py
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
"""Module for common iotdevice tests."""
|
||||||
|
import re
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from voluptuous import (
|
||||||
|
REMOVE_EXTRA,
|
||||||
|
All,
|
||||||
|
Any,
|
||||||
|
Boolean,
|
||||||
|
In,
|
||||||
|
Invalid,
|
||||||
|
Optional,
|
||||||
|
Range,
|
||||||
|
Schema,
|
||||||
|
)
|
||||||
|
|
||||||
|
from kasa import KasaException
|
||||||
|
from kasa.iot import IotDevice
|
||||||
|
|
||||||
|
from .conftest import handle_turn_on, turn_on
|
||||||
|
from .device_fixtures import device_iot, has_emeter_iot, no_emeter_iot
|
||||||
|
from .fakeprotocol_iot import FakeIotProtocol
|
||||||
|
|
||||||
|
TZ_SCHEMA = Schema(
|
||||||
|
{"zone_str": str, "dst_offset": int, "index": All(int, Range(min=0)), "tz_str": str}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def check_mac(x):
|
||||||
|
if re.match("[0-9a-f]{2}([-:])[0-9a-f]{2}(\\1[0-9a-f]{2}){4}$", x.lower()):
|
||||||
|
return x
|
||||||
|
raise Invalid(x)
|
||||||
|
|
||||||
|
|
||||||
|
SYSINFO_SCHEMA = Schema(
|
||||||
|
{
|
||||||
|
"active_mode": In(["schedule", "none", "count_down"]),
|
||||||
|
"alias": str,
|
||||||
|
"dev_name": str,
|
||||||
|
"deviceId": str,
|
||||||
|
"feature": str,
|
||||||
|
"fwId": str,
|
||||||
|
"hwId": str,
|
||||||
|
"hw_ver": str,
|
||||||
|
"icon_hash": str,
|
||||||
|
"led_off": Boolean,
|
||||||
|
"latitude": Any(All(float, Range(min=-90, max=90)), 0, None),
|
||||||
|
"latitude_i": Any(
|
||||||
|
All(int, Range(min=-900000, max=900000)),
|
||||||
|
All(float, Range(min=-900000, max=900000)),
|
||||||
|
0,
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
"longitude": Any(All(float, Range(min=-180, max=180)), 0, None),
|
||||||
|
"longitude_i": Any(
|
||||||
|
All(int, Range(min=-18000000, max=18000000)),
|
||||||
|
All(float, Range(min=-18000000, max=18000000)),
|
||||||
|
0,
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
"mac": check_mac,
|
||||||
|
"model": str,
|
||||||
|
"oemId": str,
|
||||||
|
"on_time": int,
|
||||||
|
"relay_state": int,
|
||||||
|
"rssi": Any(int, None), # rssi can also be positive, see #54
|
||||||
|
"sw_ver": str,
|
||||||
|
"type": str,
|
||||||
|
"mic_type": str,
|
||||||
|
"updating": Boolean,
|
||||||
|
# these are available on hs220
|
||||||
|
"brightness": int,
|
||||||
|
"preferred_state": [
|
||||||
|
{"brightness": All(int, Range(min=0, max=100)), "index": int}
|
||||||
|
],
|
||||||
|
"next_action": {"type": int},
|
||||||
|
"child_num": Optional(Any(None, int)),
|
||||||
|
"children": Optional(list),
|
||||||
|
},
|
||||||
|
extra=REMOVE_EXTRA,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@device_iot
|
||||||
|
async def test_state_info(dev):
|
||||||
|
assert isinstance(dev.state_information, dict)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.requires_dummy
|
||||||
|
@device_iot
|
||||||
|
async def test_invalid_connection(mocker, dev):
|
||||||
|
with mocker.patch.object(
|
||||||
|
FakeIotProtocol, "query", side_effect=KasaException
|
||||||
|
), pytest.raises(KasaException):
|
||||||
|
await dev.update()
|
||||||
|
|
||||||
|
|
||||||
|
@has_emeter_iot
|
||||||
|
async def test_initial_update_emeter(dev, mocker):
|
||||||
|
"""Test that the initial update performs second query if emeter is available."""
|
||||||
|
dev._last_update = None
|
||||||
|
dev._legacy_features = set()
|
||||||
|
spy = mocker.spy(dev.protocol, "query")
|
||||||
|
await dev.update()
|
||||||
|
# Devices with small buffers may require 3 queries
|
||||||
|
expected_queries = 2 if dev.max_device_response_size > 4096 else 3
|
||||||
|
assert spy.call_count == expected_queries + len(dev.children)
|
||||||
|
|
||||||
|
|
||||||
|
@no_emeter_iot
|
||||||
|
async def test_initial_update_no_emeter(dev, mocker):
|
||||||
|
"""Test that the initial update performs second query if emeter is available."""
|
||||||
|
dev._last_update = None
|
||||||
|
dev._legacy_features = set()
|
||||||
|
spy = mocker.spy(dev.protocol, "query")
|
||||||
|
await dev.update()
|
||||||
|
# 2 calls are necessary as some devices crash on unexpected modules
|
||||||
|
# See #105, #120, #161
|
||||||
|
assert spy.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
@device_iot
|
||||||
|
async def test_query_helper(dev):
|
||||||
|
with pytest.raises(KasaException):
|
||||||
|
await dev._query_helper("test", "testcmd", {})
|
||||||
|
# TODO check for unwrapping?
|
||||||
|
|
||||||
|
|
||||||
|
@device_iot
|
||||||
|
@turn_on
|
||||||
|
async def test_state(dev, turn_on):
|
||||||
|
await handle_turn_on(dev, turn_on)
|
||||||
|
orig_state = dev.is_on
|
||||||
|
if orig_state:
|
||||||
|
await dev.turn_off()
|
||||||
|
await dev.update()
|
||||||
|
assert not dev.is_on
|
||||||
|
assert dev.is_off
|
||||||
|
|
||||||
|
await dev.turn_on()
|
||||||
|
await dev.update()
|
||||||
|
assert dev.is_on
|
||||||
|
assert not dev.is_off
|
||||||
|
else:
|
||||||
|
await dev.turn_on()
|
||||||
|
await dev.update()
|
||||||
|
assert dev.is_on
|
||||||
|
assert not dev.is_off
|
||||||
|
|
||||||
|
await dev.turn_off()
|
||||||
|
await dev.update()
|
||||||
|
assert not dev.is_on
|
||||||
|
assert dev.is_off
|
||||||
|
|
||||||
|
|
||||||
|
@device_iot
|
||||||
|
@turn_on
|
||||||
|
async def test_on_since(dev, turn_on):
|
||||||
|
await handle_turn_on(dev, turn_on)
|
||||||
|
orig_state = dev.is_on
|
||||||
|
if "on_time" not in dev.sys_info and not dev.is_strip:
|
||||||
|
assert dev.on_since is None
|
||||||
|
elif orig_state:
|
||||||
|
assert isinstance(dev.on_since, datetime)
|
||||||
|
else:
|
||||||
|
assert dev.on_since is None
|
||||||
|
|
||||||
|
|
||||||
|
@device_iot
|
||||||
|
async def test_time(dev):
|
||||||
|
assert isinstance(await dev.get_time(), datetime)
|
||||||
|
|
||||||
|
|
||||||
|
@device_iot
|
||||||
|
async def test_timezone(dev):
|
||||||
|
TZ_SCHEMA(await dev.get_timezone())
|
||||||
|
|
||||||
|
|
||||||
|
@device_iot
|
||||||
|
async def test_hw_info(dev):
|
||||||
|
SYSINFO_SCHEMA(dev.hw_info)
|
||||||
|
|
||||||
|
|
||||||
|
@device_iot
|
||||||
|
async def test_location(dev):
|
||||||
|
SYSINFO_SCHEMA(dev.location)
|
||||||
|
|
||||||
|
|
||||||
|
@device_iot
|
||||||
|
async def test_rssi(dev):
|
||||||
|
SYSINFO_SCHEMA({"rssi": dev.rssi}) # wrapping for vol
|
||||||
|
|
||||||
|
|
||||||
|
@device_iot
|
||||||
|
async def test_mac(dev):
|
||||||
|
SYSINFO_SCHEMA({"mac": dev.mac}) # wrapping for val
|
||||||
|
|
||||||
|
|
||||||
|
@device_iot
|
||||||
|
async def test_representation(dev):
|
||||||
|
pattern = re.compile("<DeviceType\..+ at .+? - .*? \(.+?\)>")
|
||||||
|
assert pattern.match(str(dev))
|
||||||
|
|
||||||
|
|
||||||
|
@device_iot
|
||||||
|
async def test_children(dev):
|
||||||
|
"""Make sure that children property is exposed by every device."""
|
||||||
|
if dev.is_strip:
|
||||||
|
assert len(dev.children) > 0
|
||||||
|
else:
|
||||||
|
assert len(dev.children) == 0
|
||||||
|
|
||||||
|
|
||||||
|
@device_iot
|
||||||
|
async def test_modules_preserved(dev: IotDevice):
|
||||||
|
"""Make modules that are not being updated are preserved between updates."""
|
||||||
|
dev._last_update["some_module_not_being_updated"] = "should_be_kept"
|
||||||
|
await dev.update()
|
||||||
|
assert dev._last_update["some_module_not_being_updated"] == "should_be_kept"
|
||||||
|
|
||||||
|
|
||||||
|
@device_iot
|
||||||
|
async def test_internal_state(dev):
|
||||||
|
"""Make sure the internal state returns the last update results."""
|
||||||
|
assert dev.internal_state == dev._last_update
|
||||||
|
|
||||||
|
|
||||||
|
@device_iot
|
||||||
|
async def test_features(dev):
|
||||||
|
"""Make sure features is always accessible."""
|
||||||
|
sysinfo = dev._last_update["system"]["get_sysinfo"]
|
||||||
|
if "feature" in sysinfo:
|
||||||
|
assert dev._legacy_features == set(sysinfo["feature"].split(":"))
|
||||||
|
else:
|
||||||
|
assert dev._legacy_features == set()
|
||||||
|
|
||||||
|
|
||||||
|
@device_iot
|
||||||
|
async def test_max_device_response_size(dev):
|
||||||
|
"""Make sure every device return has a set max response size."""
|
||||||
|
assert dev.max_device_response_size > 0
|
||||||
|
|
||||||
|
|
||||||
|
@device_iot
|
||||||
|
async def test_estimated_response_sizes(dev):
|
||||||
|
"""Make sure every module has an estimated response size set."""
|
||||||
|
for mod in dev.modules.values():
|
||||||
|
assert mod.estimated_query_response_size > 0
|
||||||
|
|
||||||
|
|
||||||
|
@device_iot
|
||||||
|
async def test_modules_not_supported(dev: IotDevice):
|
||||||
|
"""Test that unsupported modules do not break the device."""
|
||||||
|
for module in dev.modules.values():
|
||||||
|
assert module.is_supported is not None
|
||||||
|
await dev.update()
|
||||||
|
for module in dev.modules.values():
|
||||||
|
assert module.is_supported is not None
|
@ -71,3 +71,8 @@ async def test_effects_lightstrip_set_effect_transition(
|
|||||||
async def test_effects_lightstrip_has_effects(dev: IotLightStrip):
|
async def test_effects_lightstrip_has_effects(dev: IotLightStrip):
|
||||||
assert dev.has_effects is True
|
assert dev.has_effects is True
|
||||||
assert dev.effect_list
|
assert dev.effect_list
|
||||||
|
|
||||||
|
|
||||||
|
@lightstrip
|
||||||
|
def test_device_type_lightstrip(dev):
|
||||||
|
assert dev.device_type == DeviceType.LightStrip
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
from kasa import DeviceType
|
from kasa import DeviceType
|
||||||
|
|
||||||
from .conftest import plug_iot, plug_smart, switch_smart, wallswitch_iot
|
from .conftest import plug, plug_iot, plug_smart, switch_smart, wallswitch_iot
|
||||||
from .test_smartdevice import SYSINFO_SCHEMA
|
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
|
||||||
@ -76,3 +76,8 @@ async def test_switch_device_info(dev):
|
|||||||
assert (
|
assert (
|
||||||
dev.device_type == DeviceType.WallSwitch or dev.device_type == DeviceType.Dimmer
|
dev.device_type == DeviceType.WallSwitch or dev.device_type == DeviceType.Dimmer
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@plug
|
||||||
|
def test_device_type_plug(dev):
|
||||||
|
assert dev.device_type == DeviceType.Plug
|
||||||
|
@ -1,317 +1,16 @@
|
|||||||
import importlib
|
"""Tests for SMART devices."""
|
||||||
import inspect
|
|
||||||
import logging
|
import logging
|
||||||
import pkgutil
|
from unittest.mock import patch
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from datetime import datetime
|
|
||||||
from unittest.mock import Mock, patch
|
|
||||||
|
|
||||||
import pytest # type: ignore # https://github.com/pytest-dev/pytest/issues/3342
|
import pytest # type: ignore # https://github.com/pytest-dev/pytest/issues/3342
|
||||||
from voluptuous import (
|
|
||||||
REMOVE_EXTRA,
|
|
||||||
All,
|
|
||||||
Any,
|
|
||||||
Boolean,
|
|
||||||
In,
|
|
||||||
Invalid,
|
|
||||||
Optional,
|
|
||||||
Range,
|
|
||||||
Schema,
|
|
||||||
)
|
|
||||||
|
|
||||||
import kasa
|
from kasa import KasaException
|
||||||
from kasa import Credentials, Device, DeviceConfig, KasaException
|
|
||||||
from kasa.device_type import DeviceType
|
|
||||||
from kasa.exceptions import SmartErrorCode
|
from kasa.exceptions import SmartErrorCode
|
||||||
from kasa.iot import IotDevice
|
from kasa.smart import SmartDevice
|
||||||
from kasa.smart import SmartChildDevice, SmartDevice
|
|
||||||
|
|
||||||
from .conftest import (
|
from .conftest import (
|
||||||
bulb,
|
|
||||||
device_iot,
|
|
||||||
device_smart,
|
device_smart,
|
||||||
dimmer,
|
|
||||||
handle_turn_on,
|
|
||||||
has_emeter_iot,
|
|
||||||
lightstrip,
|
|
||||||
no_emeter_iot,
|
|
||||||
plug,
|
|
||||||
strip,
|
|
||||||
turn_on,
|
|
||||||
)
|
)
|
||||||
from .fakeprotocol_iot import FakeIotProtocol
|
|
||||||
|
|
||||||
|
|
||||||
def _get_subclasses(of_class):
|
|
||||||
package = sys.modules["kasa"]
|
|
||||||
subclasses = set()
|
|
||||||
for _, modname, _ in pkgutil.iter_modules(package.__path__):
|
|
||||||
importlib.import_module("." + modname, package="kasa")
|
|
||||||
module = sys.modules["kasa." + modname]
|
|
||||||
for name, obj in inspect.getmembers(module):
|
|
||||||
if (
|
|
||||||
inspect.isclass(obj)
|
|
||||||
and issubclass(obj, of_class)
|
|
||||||
and module.__package__ != "kasa"
|
|
||||||
):
|
|
||||||
subclasses.add((module.__package__ + "." + name, obj))
|
|
||||||
return subclasses
|
|
||||||
|
|
||||||
|
|
||||||
device_classes = pytest.mark.parametrize(
|
|
||||||
"device_class_name_obj", _get_subclasses(Device), ids=lambda t: t[0]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@device_iot
|
|
||||||
async def test_state_info(dev):
|
|
||||||
assert isinstance(dev.state_information, dict)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.requires_dummy
|
|
||||||
@device_iot
|
|
||||||
async def test_invalid_connection(dev):
|
|
||||||
with patch.object(
|
|
||||||
FakeIotProtocol, "query", side_effect=KasaException
|
|
||||||
), pytest.raises(KasaException):
|
|
||||||
await dev.update()
|
|
||||||
|
|
||||||
|
|
||||||
@has_emeter_iot
|
|
||||||
async def test_initial_update_emeter(dev, mocker):
|
|
||||||
"""Test that the initial update performs second query if emeter is available."""
|
|
||||||
dev._last_update = None
|
|
||||||
dev._legacy_features = set()
|
|
||||||
spy = mocker.spy(dev.protocol, "query")
|
|
||||||
await dev.update()
|
|
||||||
# Devices with small buffers may require 3 queries
|
|
||||||
expected_queries = 2 if dev.max_device_response_size > 4096 else 3
|
|
||||||
assert spy.call_count == expected_queries + len(dev.children)
|
|
||||||
|
|
||||||
|
|
||||||
@no_emeter_iot
|
|
||||||
async def test_initial_update_no_emeter(dev, mocker):
|
|
||||||
"""Test that the initial update performs second query if emeter is available."""
|
|
||||||
dev._last_update = None
|
|
||||||
dev._legacy_features = set()
|
|
||||||
spy = mocker.spy(dev.protocol, "query")
|
|
||||||
await dev.update()
|
|
||||||
# 2 calls are necessary as some devices crash on unexpected modules
|
|
||||||
# See #105, #120, #161
|
|
||||||
assert spy.call_count == 2
|
|
||||||
|
|
||||||
|
|
||||||
@device_iot
|
|
||||||
async def test_query_helper(dev):
|
|
||||||
with pytest.raises(KasaException):
|
|
||||||
await dev._query_helper("test", "testcmd", {})
|
|
||||||
# TODO check for unwrapping?
|
|
||||||
|
|
||||||
|
|
||||||
@device_iot
|
|
||||||
@turn_on
|
|
||||||
async def test_state(dev, turn_on):
|
|
||||||
await handle_turn_on(dev, turn_on)
|
|
||||||
orig_state = dev.is_on
|
|
||||||
if orig_state:
|
|
||||||
await dev.turn_off()
|
|
||||||
await dev.update()
|
|
||||||
assert not dev.is_on
|
|
||||||
assert dev.is_off
|
|
||||||
|
|
||||||
await dev.turn_on()
|
|
||||||
await dev.update()
|
|
||||||
assert dev.is_on
|
|
||||||
assert not dev.is_off
|
|
||||||
else:
|
|
||||||
await dev.turn_on()
|
|
||||||
await dev.update()
|
|
||||||
assert dev.is_on
|
|
||||||
assert not dev.is_off
|
|
||||||
|
|
||||||
await dev.turn_off()
|
|
||||||
await dev.update()
|
|
||||||
assert not dev.is_on
|
|
||||||
assert dev.is_off
|
|
||||||
|
|
||||||
|
|
||||||
@device_iot
|
|
||||||
async def test_alias(dev):
|
|
||||||
test_alias = "TEST1234"
|
|
||||||
original = dev.alias
|
|
||||||
|
|
||||||
assert isinstance(original, str)
|
|
||||||
await dev.set_alias(test_alias)
|
|
||||||
await dev.update()
|
|
||||||
assert dev.alias == test_alias
|
|
||||||
|
|
||||||
await dev.set_alias(original)
|
|
||||||
await dev.update()
|
|
||||||
assert dev.alias == original
|
|
||||||
|
|
||||||
|
|
||||||
@device_iot
|
|
||||||
@turn_on
|
|
||||||
async def test_on_since(dev, turn_on):
|
|
||||||
await handle_turn_on(dev, turn_on)
|
|
||||||
orig_state = dev.is_on
|
|
||||||
if "on_time" not in dev.sys_info and not dev.is_strip:
|
|
||||||
assert dev.on_since is None
|
|
||||||
elif orig_state:
|
|
||||||
assert isinstance(dev.on_since, datetime)
|
|
||||||
else:
|
|
||||||
assert dev.on_since is None
|
|
||||||
|
|
||||||
|
|
||||||
@device_iot
|
|
||||||
async def test_time(dev):
|
|
||||||
assert isinstance(await dev.get_time(), datetime)
|
|
||||||
|
|
||||||
|
|
||||||
@device_iot
|
|
||||||
async def test_timezone(dev):
|
|
||||||
TZ_SCHEMA(await dev.get_timezone())
|
|
||||||
|
|
||||||
|
|
||||||
@device_iot
|
|
||||||
async def test_hw_info(dev):
|
|
||||||
SYSINFO_SCHEMA(dev.hw_info)
|
|
||||||
|
|
||||||
|
|
||||||
@device_iot
|
|
||||||
async def test_location(dev):
|
|
||||||
SYSINFO_SCHEMA(dev.location)
|
|
||||||
|
|
||||||
|
|
||||||
@device_iot
|
|
||||||
async def test_rssi(dev):
|
|
||||||
SYSINFO_SCHEMA({"rssi": dev.rssi}) # wrapping for vol
|
|
||||||
|
|
||||||
|
|
||||||
@device_iot
|
|
||||||
async def test_mac(dev):
|
|
||||||
SYSINFO_SCHEMA({"mac": dev.mac}) # wrapping for val
|
|
||||||
|
|
||||||
|
|
||||||
@device_iot
|
|
||||||
async def test_representation(dev):
|
|
||||||
import re
|
|
||||||
|
|
||||||
pattern = re.compile("<DeviceType\..+ at .+? - .*? \(.+?\)>")
|
|
||||||
assert pattern.match(str(dev))
|
|
||||||
|
|
||||||
|
|
||||||
@strip
|
|
||||||
def test_children_api(dev):
|
|
||||||
"""Test the child device API."""
|
|
||||||
first = dev.children[0]
|
|
||||||
first_by_get_child_device = dev.get_child_device(first.device_id)
|
|
||||||
assert first == first_by_get_child_device
|
|
||||||
|
|
||||||
|
|
||||||
@device_iot
|
|
||||||
async def test_children(dev):
|
|
||||||
"""Make sure that children property is exposed by every device."""
|
|
||||||
if dev.is_strip:
|
|
||||||
assert len(dev.children) > 0
|
|
||||||
else:
|
|
||||||
assert len(dev.children) == 0
|
|
||||||
|
|
||||||
|
|
||||||
@device_iot
|
|
||||||
async def test_internal_state(dev):
|
|
||||||
"""Make sure the internal state returns the last update results."""
|
|
||||||
assert dev.internal_state == dev._last_update
|
|
||||||
|
|
||||||
|
|
||||||
@device_iot
|
|
||||||
async def test_features(dev):
|
|
||||||
"""Make sure features is always accessible."""
|
|
||||||
sysinfo = dev._last_update["system"]["get_sysinfo"]
|
|
||||||
if "feature" in sysinfo:
|
|
||||||
assert dev._legacy_features == set(sysinfo["feature"].split(":"))
|
|
||||||
else:
|
|
||||||
assert dev._legacy_features == set()
|
|
||||||
|
|
||||||
|
|
||||||
@device_iot
|
|
||||||
async def test_max_device_response_size(dev):
|
|
||||||
"""Make sure every device return has a set max response size."""
|
|
||||||
assert dev.max_device_response_size > 0
|
|
||||||
|
|
||||||
|
|
||||||
@device_iot
|
|
||||||
async def test_estimated_response_sizes(dev):
|
|
||||||
"""Make sure every module has an estimated response size set."""
|
|
||||||
for mod in dev.modules.values():
|
|
||||||
assert mod.estimated_query_response_size > 0
|
|
||||||
|
|
||||||
|
|
||||||
@device_classes
|
|
||||||
async def test_device_class_ctors(device_class_name_obj):
|
|
||||||
"""Make sure constructor api not broken for new and existing SmartDevices."""
|
|
||||||
host = "127.0.0.2"
|
|
||||||
port = 1234
|
|
||||||
credentials = Credentials("foo", "bar")
|
|
||||||
config = DeviceConfig(host, port_override=port, credentials=credentials)
|
|
||||||
klass = device_class_name_obj[1]
|
|
||||||
if issubclass(klass, SmartChildDevice):
|
|
||||||
parent = SmartDevice(host, config=config)
|
|
||||||
dev = klass(
|
|
||||||
parent, {"dummy": "info", "device_id": "dummy"}, {"dummy": "components"}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
dev = klass(host, config=config)
|
|
||||||
assert dev.host == host
|
|
||||||
assert dev.port == port
|
|
||||||
assert dev.credentials == credentials
|
|
||||||
|
|
||||||
|
|
||||||
@device_iot
|
|
||||||
async def test_modules_preserved(dev: IotDevice):
|
|
||||||
"""Make modules that are not being updated are preserved between updates."""
|
|
||||||
dev._last_update["some_module_not_being_updated"] = "should_be_kept"
|
|
||||||
await dev.update()
|
|
||||||
assert dev._last_update["some_module_not_being_updated"] == "should_be_kept"
|
|
||||||
|
|
||||||
|
|
||||||
async def test_create_smart_device_with_timeout():
|
|
||||||
"""Make sure timeout is passed to the protocol."""
|
|
||||||
host = "127.0.0.1"
|
|
||||||
dev = IotDevice(host, config=DeviceConfig(host, timeout=100))
|
|
||||||
assert dev.protocol._transport._timeout == 100
|
|
||||||
dev = SmartDevice(host, config=DeviceConfig(host, timeout=100))
|
|
||||||
assert dev.protocol._transport._timeout == 100
|
|
||||||
|
|
||||||
|
|
||||||
async def test_create_thin_wrapper():
|
|
||||||
"""Make sure thin wrapper is created with the correct device type."""
|
|
||||||
mock = Mock()
|
|
||||||
config = DeviceConfig(
|
|
||||||
host="test_host",
|
|
||||||
port_override=1234,
|
|
||||||
timeout=100,
|
|
||||||
credentials=Credentials("username", "password"),
|
|
||||||
)
|
|
||||||
with patch("kasa.device_factory.connect", return_value=mock) as connect:
|
|
||||||
dev = await Device.connect(config=config)
|
|
||||||
assert dev is mock
|
|
||||||
|
|
||||||
connect.assert_called_once_with(
|
|
||||||
host=None,
|
|
||||||
config=config,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@device_iot
|
|
||||||
async def test_modules_not_supported(dev: IotDevice):
|
|
||||||
"""Test that unsupported modules do not break the device."""
|
|
||||||
for module in dev.modules.values():
|
|
||||||
assert module.is_supported is not None
|
|
||||||
await dev.update()
|
|
||||||
for module in dev.modules.values():
|
|
||||||
assert module.is_supported is not None
|
|
||||||
|
|
||||||
|
|
||||||
@device_smart
|
@device_smart
|
||||||
@ -336,110 +35,3 @@ async def test_update_no_device_info(dev: SmartDevice):
|
|||||||
KasaException, match=msg
|
KasaException, match=msg
|
||||||
):
|
):
|
||||||
await dev.update()
|
await dev.update()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"device_class, use_class", kasa.deprecated_smart_devices.items()
|
|
||||||
)
|
|
||||||
def test_deprecated_devices(device_class, use_class):
|
|
||||||
package_name = ".".join(use_class.__module__.split(".")[:-1])
|
|
||||||
msg = f"{device_class} is deprecated, use {use_class.__name__} from package {package_name} instead"
|
|
||||||
with pytest.deprecated_call(match=msg):
|
|
||||||
getattr(kasa, device_class)
|
|
||||||
packages = package_name.split(".")
|
|
||||||
module = __import__(packages[0])
|
|
||||||
for _ in packages[1:]:
|
|
||||||
module = importlib.import_module(package_name, package=module.__name__)
|
|
||||||
getattr(module, use_class.__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"exceptions_class, use_class", kasa.deprecated_exceptions.items()
|
|
||||||
)
|
|
||||||
def test_deprecated_exceptions(exceptions_class, use_class):
|
|
||||||
msg = f"{exceptions_class} is deprecated, use {use_class.__name__} instead"
|
|
||||||
with pytest.deprecated_call(match=msg):
|
|
||||||
getattr(kasa, exceptions_class)
|
|
||||||
getattr(kasa, use_class.__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def check_mac(x):
|
|
||||||
if re.match("[0-9a-f]{2}([-:])[0-9a-f]{2}(\\1[0-9a-f]{2}){4}$", x.lower()):
|
|
||||||
return x
|
|
||||||
raise Invalid(x)
|
|
||||||
|
|
||||||
|
|
||||||
TZ_SCHEMA = Schema(
|
|
||||||
{"zone_str": str, "dst_offset": int, "index": All(int, Range(min=0)), "tz_str": str}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
SYSINFO_SCHEMA = Schema(
|
|
||||||
{
|
|
||||||
"active_mode": In(["schedule", "none", "count_down"]),
|
|
||||||
"alias": str,
|
|
||||||
"dev_name": str,
|
|
||||||
"deviceId": str,
|
|
||||||
"feature": str,
|
|
||||||
"fwId": str,
|
|
||||||
"hwId": str,
|
|
||||||
"hw_ver": str,
|
|
||||||
"icon_hash": str,
|
|
||||||
"led_off": Boolean,
|
|
||||||
"latitude": Any(All(float, Range(min=-90, max=90)), 0, None),
|
|
||||||
"latitude_i": Any(
|
|
||||||
All(int, Range(min=-900000, max=900000)),
|
|
||||||
All(float, Range(min=-900000, max=900000)),
|
|
||||||
0,
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
"longitude": Any(All(float, Range(min=-180, max=180)), 0, None),
|
|
||||||
"longitude_i": Any(
|
|
||||||
All(int, Range(min=-18000000, max=18000000)),
|
|
||||||
All(float, Range(min=-18000000, max=18000000)),
|
|
||||||
0,
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
"mac": check_mac,
|
|
||||||
"model": str,
|
|
||||||
"oemId": str,
|
|
||||||
"on_time": int,
|
|
||||||
"relay_state": int,
|
|
||||||
"rssi": Any(int, None), # rssi can also be positive, see #54
|
|
||||||
"sw_ver": str,
|
|
||||||
"type": str,
|
|
||||||
"mic_type": str,
|
|
||||||
"updating": Boolean,
|
|
||||||
# these are available on hs220
|
|
||||||
"brightness": int,
|
|
||||||
"preferred_state": [
|
|
||||||
{"brightness": All(int, Range(min=0, max=100)), "index": int}
|
|
||||||
],
|
|
||||||
"next_action": {"type": int},
|
|
||||||
"child_num": Optional(Any(None, int)),
|
|
||||||
"children": Optional(list),
|
|
||||||
},
|
|
||||||
extra=REMOVE_EXTRA,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dimmer
|
|
||||||
def test_device_type_dimmer(dev):
|
|
||||||
assert dev.device_type == DeviceType.Dimmer
|
|
||||||
|
|
||||||
|
|
||||||
@bulb
|
|
||||||
def test_device_type_bulb(dev):
|
|
||||||
if dev.is_light_strip:
|
|
||||||
pytest.skip("bulb has also lightstrips to test the api")
|
|
||||||
assert dev.device_type == DeviceType.Bulb
|
|
||||||
|
|
||||||
|
|
||||||
@plug
|
|
||||||
def test_device_type_plug(dev):
|
|
||||||
assert dev.device_type == DeviceType.Plug
|
|
||||||
|
|
||||||
|
|
||||||
@lightstrip
|
|
||||||
def test_device_type_lightstrip(dev):
|
|
||||||
assert dev.device_type == DeviceType.LightStrip
|
|
||||||
|
@ -131,3 +131,11 @@ async def test_all_binary_states(dev):
|
|||||||
# original state map should be restored
|
# original state map should be restored
|
||||||
for index, state in dev.is_on.items():
|
for index, state in dev.is_on.items():
|
||||||
assert state == state_map[index]
|
assert state == state_map[index]
|
||||||
|
|
||||||
|
|
||||||
|
@strip
|
||||||
|
def test_children_api(dev):
|
||||||
|
"""Test the child device API."""
|
||||||
|
first = dev.children[0]
|
||||||
|
first_by_get_child_device = dev.get_child_device(first.device_id)
|
||||||
|
assert first == first_by_get_child_device
|
||||||
|
Loading…
Reference in New Issue
Block a user