diff --git a/kasa/tests/device_fixtures.py b/kasa/tests/device_fixtures.py index 71cc34bd..9d01a830 100644 --- a/kasa/tests/device_fixtures.py +++ b/kasa/tests/device_fixtures.py @@ -376,7 +376,7 @@ async def get_device_for_fixture(fixture_data: FixtureInfo): "system": {"get_sysinfo": fixture_data.data["system"]["get_sysinfo"]} } - if discovery_data: # Child devices do not have discovery info + if discovery_data: # Child devices do not have discovery info d.update_from_discover_info(discovery_data) await _update_and_close(d) diff --git a/kasa/tests/smart/modules/test_humidity.py b/kasa/tests/smart/modules/test_humidity.py index 99e4702e..bf746f2b 100644 --- a/kasa/tests/smart/modules/test_humidity.py +++ b/kasa/tests/smart/modules/test_humidity.py @@ -3,7 +3,9 @@ import pytest from kasa.smart.modules import HumiditySensor 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 diff --git a/kasa/tests/smart/modules/test_temperature.py b/kasa/tests/smart/modules/test_temperature.py index 649b5bc4..3b9ab50e 100644 --- a/kasa/tests/smart/modules/test_temperature.py +++ b/kasa/tests/smart/modules/test_temperature.py @@ -3,7 +3,9 @@ import pytest from kasa.smart.modules import TemperatureSensor 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 diff --git a/kasa/tests/test_bulb.py b/kasa/tests/test_bulb.py index e8c95dbd..48b5976e 100644 --- a/kasa/tests/test_bulb.py +++ b/kasa/tests/test_bulb.py @@ -24,7 +24,7 @@ from .conftest import ( variable_temp, variable_temp_iot, ) -from .test_smartdevice import SYSINFO_SCHEMA +from .test_iotdevice import SYSINFO_SCHEMA @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 diff --git a/kasa/tests/test_device.py b/kasa/tests/test_device.py new file mode 100644 index 00000000..7ceab8e9 --- /dev/null +++ b/kasa/tests/test_device.py @@ -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__) diff --git a/kasa/tests/test_dimmer.py b/kasa/tests/test_dimmer.py index fafa9544..d63aa453 100644 --- a/kasa/tests/test_dimmer.py +++ b/kasa/tests/test_dimmer.py @@ -1,5 +1,6 @@ import pytest +from kasa import DeviceType from kasa.iot import IotDimmer 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]: with pytest.raises(ValueError): await dev.set_dimmer_transition(1, invalid_transition) + + +@dimmer +def test_device_type_dimmer(dev): + assert dev.device_type == DeviceType.Dimmer diff --git a/kasa/tests/test_iotdevice.py b/kasa/tests/test_iotdevice.py new file mode 100644 index 00000000..b7846e41 --- /dev/null +++ b/kasa/tests/test_iotdevice.py @@ -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("") + 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 diff --git a/kasa/tests/test_lightstrip.py b/kasa/tests/test_lightstrip.py index 123360a4..fcc48dfa 100644 --- a/kasa/tests/test_lightstrip.py +++ b/kasa/tests/test_lightstrip.py @@ -71,3 +71,8 @@ async def test_effects_lightstrip_set_effect_transition( async def test_effects_lightstrip_has_effects(dev: IotLightStrip): assert dev.has_effects is True assert dev.effect_list + + +@lightstrip +def test_device_type_lightstrip(dev): + assert dev.device_type == DeviceType.LightStrip diff --git a/kasa/tests/test_plug.py b/kasa/tests/test_plug.py index 9ccf3d04..8989c975 100644 --- a/kasa/tests/test_plug.py +++ b/kasa/tests/test_plug.py @@ -1,7 +1,7 @@ from kasa import DeviceType -from .conftest import plug_iot, plug_smart, switch_smart, wallswitch_iot -from .test_smartdevice 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 @@ -76,3 +76,8 @@ async def test_switch_device_info(dev): assert ( dev.device_type == DeviceType.WallSwitch or dev.device_type == DeviceType.Dimmer ) + + +@plug +def test_device_type_plug(dev): + assert dev.device_type == DeviceType.Plug diff --git a/kasa/tests/test_smartdevice.py b/kasa/tests/test_smartdevice.py index fdd342ca..a9871fa2 100644 --- a/kasa/tests/test_smartdevice.py +++ b/kasa/tests/test_smartdevice.py @@ -1,317 +1,16 @@ -import importlib -import inspect +"""Tests for SMART devices.""" import logging -import pkgutil -import re -import sys -from datetime import datetime -from unittest.mock import Mock, patch +from unittest.mock import patch 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 Credentials, Device, DeviceConfig, KasaException -from kasa.device_type import DeviceType +from kasa import KasaException from kasa.exceptions import SmartErrorCode -from kasa.iot import IotDevice -from kasa.smart import SmartChildDevice, SmartDevice +from kasa.smart import SmartDevice from .conftest import ( - bulb, - device_iot, 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("") - 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 @@ -336,110 +35,3 @@ async def test_update_no_device_info(dev: SmartDevice): KasaException, match=msg ): 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 diff --git a/kasa/tests/test_strip.py b/kasa/tests/test_strip.py index e7d36f90..e5285acc 100644 --- a/kasa/tests/test_strip.py +++ b/kasa/tests/test_strip.py @@ -131,3 +131,11 @@ async def test_all_binary_states(dev): # original state map should be restored for index, state in dev.is_on.items(): 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