Use _get_device_info methods for smart and iot devs in devtools (#1265)

This commit is contained in:
Steven B.
2024-11-18 14:53:11 +00:00
committed by GitHub
parent 9d46996e9b
commit e209d40a6d
20 changed files with 386 additions and 168 deletions

View File

@@ -15,6 +15,7 @@ from kasa.transports.basetransport import BaseTransport
from .device_fixtures import * # noqa: F403
from .discovery_fixtures import * # noqa: F403
from .fixtureinfo import fixture_info # noqa: F401
# Parametrize tests to run with device both on and off
turn_on = pytest.mark.parametrize("turn_on", [True, False])

View File

@@ -188,11 +188,12 @@ def parametrize(
data_root_filter=None,
device_type_filter=None,
ids=None,
fixture_name="dev",
):
if ids is None:
ids = idgenerator
return pytest.mark.parametrize(
"dev",
fixture_name,
filter_fixtures(
desc,
model_filter=model_filter,
@@ -407,22 +408,28 @@ async def _discover_update_and_close(ip, username, password) -> Device:
return await _update_and_close(d)
async def get_device_for_fixture(fixture_data: FixtureInfo) -> Device:
async def get_device_for_fixture(
fixture_data: FixtureInfo, *, verbatim=False, update_after_init=True
) -> Device:
# if the wanted file is not an absolute path, prepend the fixtures directory
d = device_for_fixture_name(fixture_data.name, fixture_data.protocol)(
host="127.0.0.123"
)
if fixture_data.protocol in {"SMART", "SMART.CHILD"}:
d.protocol = FakeSmartProtocol(fixture_data.data, fixture_data.name)
d.protocol = FakeSmartProtocol(
fixture_data.data, fixture_data.name, verbatim=verbatim
)
elif fixture_data.protocol == "SMARTCAMERA":
d.protocol = FakeSmartCameraProtocol(fixture_data.data, fixture_data.name)
d.protocol = FakeSmartCameraProtocol(
fixture_data.data, fixture_data.name, verbatim=verbatim
)
else:
d.protocol = FakeIotProtocol(fixture_data.data)
d.protocol = FakeIotProtocol(fixture_data.data, verbatim=verbatim)
discovery_data = None
if "discovery_result" in fixture_data.data:
discovery_data = {"result": fixture_data.data["discovery_result"]}
discovery_data = fixture_data.data["discovery_result"]
elif "system" in fixture_data.data:
discovery_data = {
"system": {"get_sysinfo": fixture_data.data["system"]["get_sysinfo"]}
@@ -431,7 +438,8 @@ async def get_device_for_fixture(fixture_data: FixtureInfo) -> Device:
if discovery_data: # Child devices do not have discovery info
d.update_from_discover_info(discovery_data)
await _update_and_close(d)
if update_after_init:
await _update_and_close(d)
return d

View File

@@ -177,9 +177,9 @@ MOTION_MODULE = {
class FakeIotProtocol(IotProtocol):
def __init__(self, info, fixture_name=None):
def __init__(self, info, fixture_name=None, *, verbatim=False):
super().__init__(
transport=FakeIotTransport(info, fixture_name),
transport=FakeIotTransport(info, fixture_name, verbatim=verbatim),
)
async def query(self, request, retry_count: int = 3):
@@ -189,21 +189,33 @@ class FakeIotProtocol(IotProtocol):
class FakeIotTransport(BaseTransport):
def __init__(self, info, fixture_name=None):
def __init__(self, info, fixture_name=None, *, verbatim=False):
super().__init__(config=DeviceConfig("127.0.0.123"))
info = copy.deepcopy(info)
self.discovery_data = info
self.fixture_name = fixture_name
self.writer = None
self.reader = None
self.verbatim = verbatim
# When True verbatim will bypass any extra processing of missing
# methods and is used to test the fixture creation itself.
if verbatim:
self.proto = copy.deepcopy(info)
else:
self.proto = self._build_fake_proto(info)
@staticmethod
def _build_fake_proto(info):
"""Create an internal protocol with extra data not in the fixture."""
proto = copy.deepcopy(FakeIotTransport.baseproto)
for target in info:
# print("target %s" % target)
if target != "discovery_result":
for cmd in info[target]:
# print("initializing tgt %s cmd %s" % (target, cmd))
proto[target][cmd] = info[target][cmd]
# if we have emeter support, we need to add the missing pieces
for module in ["emeter", "smartlife.iot.common.emeter"]:
if (
@@ -223,10 +235,7 @@ class FakeIotTransport(BaseTransport):
dummy_data = emeter_commands[module][etype]
# print("got %s %s from dummy: %s" % (module, etype, dummy_data))
proto[module][etype] = dummy_data
# print("initialized: %s" % proto[module])
self.proto = proto
return proto
@property
def default_port(self) -> int:
@@ -421,8 +430,20 @@ class FakeIotTransport(BaseTransport):
}
async def send(self, request, port=9999):
proto = self.proto
if not self.verbatim:
return await self._send(request, port)
# Simply return whatever is in the fixture
response = {}
for target in request:
if target in self.proto:
response.update({target: self.proto[target]})
else:
response.update({"err_msg": "module not support"})
return copy.deepcopy(response)
async def _send(self, request, port=9999):
proto = self.proto
# collect child ids from context
try:
child_ids = request["context"]["child_ids"]

View File

@@ -11,9 +11,11 @@ from kasa.transports.basetransport import BaseTransport
class FakeSmartProtocol(SmartProtocol):
def __init__(self, info, fixture_name, *, is_child=False):
def __init__(self, info, fixture_name, *, is_child=False, verbatim=False):
super().__init__(
transport=FakeSmartTransport(info, fixture_name, is_child=is_child),
transport=FakeSmartTransport(
info, fixture_name, is_child=is_child, verbatim=verbatim
),
)
async def query(self, request, retry_count: int = 3):
@@ -34,6 +36,7 @@ class FakeSmartTransport(BaseTransport):
fix_incomplete_fixture_lists=True,
is_child=False,
get_child_fixtures=True,
verbatim=False,
):
super().__init__(
config=DeviceConfig(
@@ -64,6 +67,13 @@ class FakeSmartTransport(BaseTransport):
self.warn_fixture_missing_methods = warn_fixture_missing_methods
self.fix_incomplete_fixture_lists = fix_incomplete_fixture_lists
# When True verbatim will bypass any extra processing of missing
# methods and is used to test the fixture creation itself.
self.verbatim = verbatim
if verbatim:
self.warn_fixture_missing_methods = False
self.fix_incomplete_fixture_lists = False
@property
def default_port(self):
"""Default port for the transport."""
@@ -444,10 +454,10 @@ class FakeSmartTransport(BaseTransport):
return await self._handle_control_child(request_dict["params"])
params = request_dict.get("params", {})
if method == "component_nego" or method[:4] == "get_":
if method in {"component_nego", "qs_component_nego"} or method[:4] == "get_":
if method in info:
result = copy.deepcopy(info[method])
if "start_index" in result and "sum" in result:
if result and "start_index" in result and "sum" in result:
list_key = next(
iter([key for key in result if isinstance(result[key], list)])
)
@@ -473,6 +483,12 @@ class FakeSmartTransport(BaseTransport):
]
return {"result": result, "error_code": 0}
if self.verbatim:
return {
"error_code": SmartErrorCode.PARAMS_ERROR.value,
"method": method,
}
if (
# FIXTURE_MISSING is for service calls not in place when
# SMART fixtures started to be generated

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
import copy
from json import loads as json_loads
from typing import Any
from kasa import Credentials, DeviceConfig, SmartProtocol
from kasa.protocols.smartcameraprotocol import SmartCameraProtocol
@@ -11,9 +12,11 @@ from .fakeprotocol_smart import FakeSmartTransport
class FakeSmartCameraProtocol(SmartCameraProtocol):
def __init__(self, info, fixture_name, *, is_child=False):
def __init__(self, info, fixture_name, *, is_child=False, verbatim=False):
super().__init__(
transport=FakeSmartCameraTransport(info, fixture_name, is_child=is_child),
transport=FakeSmartCameraTransport(
info, fixture_name, is_child=is_child, verbatim=verbatim
),
)
async def query(self, request, retry_count: int = 3):
@@ -30,6 +33,7 @@ class FakeSmartCameraTransport(BaseTransport):
*,
list_return_size=10,
is_child=False,
verbatim=False,
):
super().__init__(
config=DeviceConfig(
@@ -41,6 +45,9 @@ class FakeSmartCameraTransport(BaseTransport):
),
)
self.fixture_name = fixture_name
# When True verbatim will bypass any extra processing of missing
# methods and is used to test the fixture creation itself.
self.verbatim = verbatim
if not is_child:
self.info = copy.deepcopy(info)
self.child_protocols = FakeSmartTransport._get_child_protocols(
@@ -70,11 +77,11 @@ class FakeSmartCameraTransport(BaseTransport):
responses = []
for request in params["requests"]:
response = await self._send_request(request) # type: ignore[arg-type]
response["method"] = request["method"] # type: ignore[index]
responses.append(response)
# Devices do not continue after error
if response["error_code"] != 0:
break
response["method"] = request["method"] # type: ignore[index]
responses.append(response)
return {"result": {"responses": responses}, "error_code": 0}
else:
return await self._send_request(request_dict)
@@ -129,6 +136,15 @@ class FakeSmartCameraTransport(BaseTransport):
],
}
@staticmethod
def _get_second_key(request_dict: dict[str, Any]) -> str:
assert (
len(request_dict) == 2
), f"Unexpected dict {request_dict}, should be length 2"
it = iter(request_dict)
next(it, None)
return next(it)
async def _send_request(self, request_dict: dict):
method = request_dict["method"]
@@ -175,6 +191,14 @@ class FakeSmartCameraTransport(BaseTransport):
return {"error_code": -1}
break
return {"error_code": 0}
elif method == "get":
module = self._get_second_key(request_dict)
get_method = f"get_{module}"
if get_method in info:
result = copy.deepcopy(info[get_method]["get"])
return {**result, "error_code": 0}
else:
return {"error_code": -1}
elif method[:3] == "get":
params = request_dict.get("params")
if method in info:

View File

@@ -1,13 +1,16 @@
from __future__ import annotations
import copy
import glob
import json
import os
from pathlib import Path
from typing import Iterable, NamedTuple
from kasa.device_factory import _get_device_type_from_sys_info
import pytest
from kasa.device_type import DeviceType
from kasa.iot import IotDevice
from kasa.smart.smartdevice import SmartDevice
from kasa.smartcamera.smartcamera import SmartCamera
@@ -171,7 +174,10 @@ def filter_fixtures(
in device_type
)
elif fixture_data.protocol == "IOT":
return _get_device_type_from_sys_info(fixture_data.data) in device_type
return (
IotDevice._get_device_type_from_sys_info(fixture_data.data)
in device_type
)
elif fixture_data.protocol == "SMARTCAMERA":
info = fixture_data.data["getDeviceInfo"]["device_info"]["basic_info"]
return SmartCamera._get_device_type_from_sysinfo(info) in device_type
@@ -206,3 +212,14 @@ def filter_fixtures(
print(f"\t{value.name}")
filtered.sort()
return filtered
@pytest.fixture(
params=filter_fixtures("all fixture infos"),
ids=idgenerator,
)
def fixture_info(request, mocker):
"""Return raw discovery file contents as JSON. Used for discovery tests."""
fixture_info = request.param
fixture_data = copy.deepcopy(fixture_info.data)
return FixtureInfo(fixture_info.name, fixture_info.protocol, fixture_data)

View File

@@ -19,9 +19,9 @@ from kasa import (
)
from kasa.device_factory import (
Device,
IotDevice,
SmartCamera,
SmartDevice,
_get_device_type_from_sys_info,
connect,
get_device_class_from_family,
get_protocol,
@@ -182,12 +182,12 @@ async def test_device_types(dev: Device):
res = SmartCamera._get_device_type_from_sysinfo(dev.sys_info)
elif isinstance(dev, SmartDevice):
assert dev._discovery_info
device_type = cast(str, dev._discovery_info["result"]["device_type"])
device_type = cast(str, dev._discovery_info["device_type"])
res = SmartDevice._get_device_type_from_components(
list(dev._components.keys()), device_type
)
else:
res = _get_device_type_from_sys_info(dev._last_update)
res = IotDevice._get_device_type_from_sys_info(dev._last_update)
assert dev.device_type == res

103
tests/test_devtools.py Normal file
View File

@@ -0,0 +1,103 @@
"""Module for dump_devinfo tests."""
import pytest
from devtools.dump_devinfo import get_legacy_fixture, get_smart_fixtures
from kasa.iot import IotDevice
from kasa.protocols import IotProtocol
from kasa.smart import SmartDevice
from kasa.smartcamera import SmartCamera
from .conftest import (
FixtureInfo,
get_device_for_fixture,
parametrize,
)
smart_fixtures = parametrize(
"smart fixtures", protocol_filter={"SMART"}, fixture_name="fixture_info"
)
smartcamera_fixtures = parametrize(
"smartcamera fixtures", protocol_filter={"SMARTCAMERA"}, fixture_name="fixture_info"
)
iot_fixtures = parametrize(
"iot fixtures", protocol_filter={"IOT"}, fixture_name="fixture_info"
)
async def test_fixture_names(fixture_info: FixtureInfo):
"""Test that device info gets the right fixture names."""
if fixture_info.protocol in {"SMARTCAMERA"}:
device_info = SmartCamera._get_device_info(
fixture_info.data, fixture_info.data.get("discovery_result")
)
elif fixture_info.protocol in {"SMART"}:
device_info = SmartDevice._get_device_info(
fixture_info.data, fixture_info.data.get("discovery_result")
)
elif fixture_info.protocol in {"SMART.CHILD"}:
device_info = SmartDevice._get_device_info(fixture_info.data, None)
else:
device_info = IotDevice._get_device_info(fixture_info.data, None)
region = f"({device_info.region})" if device_info.region else ""
expected = f"{device_info.long_name}{region}_{device_info.hardware_version}_{device_info.firmware_version}.json"
assert fixture_info.name == expected
@smart_fixtures
async def test_smart_fixtures(fixture_info: FixtureInfo):
"""Test that smart fixtures are created the same."""
dev = await get_device_for_fixture(fixture_info, verbatim=True)
assert isinstance(dev, SmartDevice)
if dev.children:
pytest.skip("Test not currently implemented for devices with children.")
fixtures = await get_smart_fixtures(
dev.protocol,
discovery_info=fixture_info.data.get("discovery_result"),
batch_size=5,
)
fixture_result = fixtures[0]
assert fixture_info.data == fixture_result.data
@smartcamera_fixtures
async def test_smartcamera_fixtures(fixture_info: FixtureInfo):
"""Test that smartcamera fixtures are created the same."""
dev = await get_device_for_fixture(fixture_info, verbatim=True)
assert isinstance(dev, SmartCamera)
if dev.children:
pytest.skip("Test not currently implemented for devices with children.")
fixtures = await get_smart_fixtures(
dev.protocol,
discovery_info=fixture_info.data.get("discovery_result"),
batch_size=5,
)
fixture_result = fixtures[0]
assert fixture_info.data == fixture_result.data
@iot_fixtures
async def test_iot_fixtures(fixture_info: FixtureInfo):
"""Test that iot fixtures are created the same."""
# Iot fixtures often do not have enough data to perform a device update()
# without missing info being added to suppress the update
dev = await get_device_for_fixture(
fixture_info, verbatim=True, update_after_init=False
)
assert isinstance(dev.protocol, IotProtocol)
fixture = await get_legacy_fixture(
dev.protocol, discovery_info=fixture_info.data.get("discovery_result")
)
fixture_result = fixture
created_fixture = {
key: val for key, val in fixture_result.data.items() if "err_code" not in val
}
saved_fixture = {
key: val for key, val in fixture_info.data.items() if "err_code" not in val
}
assert saved_fixture == created_fixture

View File

@@ -39,7 +39,7 @@ from kasa.discover import (
json_dumps,
)
from kasa.exceptions import AuthenticationError, UnsupportedDeviceError
from kasa.iot import IotDevice
from kasa.iot import IotDevice, IotPlug
from kasa.transports.aestransport import AesEncyptionSession
from kasa.transports.xortransport import XorEncryption, XorTransport
@@ -119,10 +119,11 @@ async def test_type_detection_lightstrip(dev: Device):
assert d.device_type == DeviceType.LightStrip
async def test_type_unknown():
async def test_type_unknown(caplog):
invalid_info = {"system": {"get_sysinfo": {"type": "nosuchtype"}}}
with pytest.raises(UnsupportedDeviceError):
Discover._get_device_class(invalid_info)
assert Discover._get_device_class(invalid_info) is IotPlug
msg = "Unknown device type nosuchtype, falling back to plug"
assert msg in caplog.text
@pytest.mark.parametrize("custom_port", [123, None])
@@ -266,7 +267,6 @@ INVALIDS = [
"Unable to find the device type field",
{"system": {"get_sysinfo": {"missing_type": 1}}},
),
("Unknown device type: foo", {"system": {"get_sysinfo": {"type": "foo"}}}),
]