python-kasa/tests/fixtureinfo.py

232 lines
7.5 KiB
Python

from __future__ import annotations
import copy
import glob
import json
import os
from collections.abc import Iterable
from pathlib import Path
from typing import NamedTuple
import pytest
from kasa.device_type import DeviceType
from kasa.iot import IotDevice
from kasa.smart.smartdevice import SmartDevice
from kasa.smartcam import SmartCamDevice
class FixtureInfo(NamedTuple):
name: str
protocol: str
data: dict
class ComponentFilter(NamedTuple):
component_name: str
minimum_version: int = 0
maximum_version: int | None = None
FixtureInfo.__hash__ = lambda self: hash((self.name, self.protocol)) # type: ignore[attr-defined, method-assign]
FixtureInfo.__eq__ = lambda x, y: hash(x) == hash(y) # type: ignore[method-assign]
SUPPORTED_IOT_DEVICES = [
(device, "IOT")
for device in glob.glob(
os.path.dirname(os.path.abspath(__file__)) + "/fixtures/iot/*.json"
)
]
SUPPORTED_SMART_DEVICES = [
(device, "SMART")
for device in glob.glob(
os.path.dirname(os.path.abspath(__file__)) + "/fixtures/smart/*.json"
)
]
SUPPORTED_SMART_CHILD_DEVICES = [
(device, "SMART.CHILD")
for device in glob.glob(
os.path.dirname(os.path.abspath(__file__)) + "/fixtures/smart/child/*.json"
)
]
SUPPORTED_SMARTCAM_DEVICES = [
(device, "SMARTCAM")
for device in glob.glob(
os.path.dirname(os.path.abspath(__file__)) + "/fixtures/smartcam/*.json"
)
]
SUPPORTED_DEVICES = (
SUPPORTED_IOT_DEVICES
+ SUPPORTED_SMART_DEVICES
+ SUPPORTED_SMART_CHILD_DEVICES
+ SUPPORTED_SMARTCAM_DEVICES
)
def idgenerator(paramtuple: FixtureInfo):
try:
return paramtuple.name + (
"" if paramtuple.protocol == "IOT" else "-" + paramtuple.protocol
)
except: # TODO: HACK as idgenerator is now used by default # noqa: E722
return None
def get_fixture_info() -> list[FixtureInfo]:
"""Return raw discovery file contents as JSON. Used for discovery tests."""
fixture_data = []
for file, protocol in SUPPORTED_DEVICES:
p = Path(file)
folder = Path(__file__).parent / "fixtures"
if protocol == "SMART":
folder = folder / "smart"
if protocol == "SMART.CHILD":
folder = folder / "smart/child"
p = folder / file
with open(p) as f:
data = json.load(f)
fixture_name = p.name
fixture_data.append(
FixtureInfo(data=data, protocol=protocol, name=fixture_name)
)
return fixture_data
FIXTURE_DATA: list[FixtureInfo] = get_fixture_info()
def filter_fixtures(
desc,
*,
data_root_filter: str | None = None,
protocol_filter: set[str] | None = None,
model_filter: set[str] | None = None,
model_startswith_filter: str | None = None,
component_filter: str | ComponentFilter | None = None,
device_type_filter: Iterable[DeviceType] | None = None,
fixture_list: list[FixtureInfo] = FIXTURE_DATA,
):
"""Filter the fixtures based on supplied parameters.
data_root_filter: return fixtures containing the supplied top
level key, i.e. discovery_result
protocol_filter: set of protocols to match, IOT, SMART, SMART.CHILD
model_filter: set of device models to match
component_filter: filter SMART fixtures that have the provided
component in component_nego details.
"""
def _model_match(fixture_data: FixtureInfo, model_filter: set[str]):
if isinstance(model_filter, str):
model_filter = {model_filter}
assert isinstance(model_filter, set), "model filter must be a set"
model_filter_list = [mf for mf in model_filter]
if (
len(model_filter_list) == 1
and (model := model_filter_list[0])
and len(model.split("_")) == 3
):
# filter string includes hw and fw, return exact match
return fixture_data.name == f"{model}.json"
file_model_region = fixture_data.name.split("_")[0]
file_model = file_model_region.split("(")[0]
return file_model in model_filter
def _model_startswith_match(fixture_data: FixtureInfo, starts_with: str):
return fixture_data.name.startswith(starts_with)
def _component_match(
fixture_data: FixtureInfo, component_filter: str | ComponentFilter
):
components = {}
if component_nego := fixture_data.data.get("component_nego"):
components = {
component["id"]: component["ver_code"]
for component in component_nego["component_list"]
}
if get_app_component_list := fixture_data.data.get("getAppComponentList"):
components = {
component["name"]: component["version"]
for component in get_app_component_list["app_component"][
"app_component_list"
]
}
if not components:
return False
if isinstance(component_filter, str):
return component_filter in components
else:
return (
(ver_code := components.get(component_filter.component_name))
and ver_code >= component_filter.minimum_version
and (
component_filter.maximum_version is None
or ver_code <= component_filter.maximum_version
)
)
def _device_type_match(fixture_data: FixtureInfo, device_type):
if fixture_data.protocol in {"SMART", "SMART.CHILD"}:
info = fixture_data.data["get_device_info"]
component_nego = fixture_data.data["component_nego"]
components = [
component["id"] for component in component_nego["component_list"]
]
return (
SmartDevice._get_device_type_from_components(components, info["type"])
in device_type
)
elif fixture_data.protocol == "IOT":
return (
IotDevice._get_device_type_from_sys_info(fixture_data.data)
in device_type
)
elif fixture_data.protocol == "SMARTCAM":
info = fixture_data.data["getDeviceInfo"]["device_info"]["basic_info"]
return SmartCamDevice._get_device_type_from_sysinfo(info) in device_type
return False
filtered = []
if protocol_filter is None:
protocol_filter = {"IOT", "SMART", "SMARTCAM"}
for fixture_data in fixture_list:
if data_root_filter and data_root_filter not in fixture_data.data:
continue
if fixture_data.protocol not in protocol_filter:
continue
if model_filter is not None and not _model_match(fixture_data, model_filter):
continue
if model_startswith_filter is not None and not _model_startswith_match(
fixture_data, model_startswith_filter
):
continue
if component_filter and not _component_match(fixture_data, component_filter):
continue
if device_type_filter and not _device_type_match(
fixture_data, device_type_filter
):
continue
filtered.append(fixture_data)
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)