mirror of
https://github.com/python-kasa/python-kasa.git
synced 2025-08-09 20:24:02 +00:00
Use _get_device_info methods for smart and iot devs in devtools (#1265)
This commit is contained in:
@@ -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])
|
||||
|
@@ -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
|
||||
|
||||
|
||||
|
@@ -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"]
|
||||
|
@@ -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
|
||||
|
@@ -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:
|
||||
|
@@ -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)
|
||||
|
@@ -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
103
tests/test_devtools.py
Normal 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
|
@@ -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"}}}),
|
||||
]
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user