mirror of
				https://github.com/python-kasa/python-kasa.git
				synced 2025-11-04 06:32:07 +00:00 
			
		
		
		
	Refactor split smartdevice tests to test_{iot,smart}device (#822)
This commit is contained in:
		@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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):
 | 
			
		||||
    assert dev.has_effects is True
 | 
			
		||||
    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 .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
 | 
			
		||||
 
 | 
			
		||||
@@ -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("<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
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user