mirror of
https://github.com/python-kasa/python-kasa.git
synced 2025-01-22 12:47:05 +00:00
Add smartcam child device support for smartcam hubs (#1413)
This commit is contained in:
parent
a211cc0af5
commit
589d15091a
@ -54,7 +54,8 @@ from kasa.protocols.smartcamprotocol import (
|
||||
from kasa.protocols.smartprotocol import REDACTORS as SMART_REDACTORS
|
||||
from kasa.protocols.smartprotocol import SmartProtocol, _ChildProtocolWrapper
|
||||
from kasa.smart import SmartChildDevice, SmartDevice
|
||||
from kasa.smartcam import SmartCamDevice
|
||||
from kasa.smartcam import SmartCamChild, SmartCamDevice
|
||||
from kasa.smartcam.smartcamchild import CHILD_INFO_FROM_PARENT
|
||||
|
||||
Call = namedtuple("Call", "module method")
|
||||
FixtureResult = namedtuple("FixtureResult", "filename, folder, data, protocol_suffix")
|
||||
@ -62,11 +63,13 @@ FixtureResult = namedtuple("FixtureResult", "filename, folder, data, protocol_su
|
||||
SMART_FOLDER = "tests/fixtures/smart/"
|
||||
SMARTCAM_FOLDER = "tests/fixtures/smartcam/"
|
||||
SMART_CHILD_FOLDER = "tests/fixtures/smart/child/"
|
||||
SMARTCAM_CHILD_FOLDER = "tests/fixtures/smartcam/child/"
|
||||
IOT_FOLDER = "tests/fixtures/iot/"
|
||||
|
||||
SMART_PROTOCOL_SUFFIX = "SMART"
|
||||
SMARTCAM_SUFFIX = "SMARTCAM"
|
||||
SMART_CHILD_SUFFIX = "SMART.CHILD"
|
||||
SMARTCAM_CHILD_SUFFIX = "SMARTCAM.CHILD"
|
||||
IOT_SUFFIX = "IOT"
|
||||
|
||||
NO_GIT_FIXTURE_FOLDER = "kasa-fixtures"
|
||||
@ -844,9 +847,8 @@ async def get_smart_test_calls(protocol: SmartProtocol):
|
||||
return test_calls, successes
|
||||
|
||||
|
||||
def get_smart_child_fixture(response):
|
||||
def get_smart_child_fixture(response, model_info, folder, suffix):
|
||||
"""Get a seperate fixture for the child device."""
|
||||
model_info = SmartDevice._get_device_info(response, None)
|
||||
hw_version = model_info.hardware_version
|
||||
fw_version = model_info.firmware_version
|
||||
model = model_info.long_name
|
||||
@ -855,12 +857,68 @@ def get_smart_child_fixture(response):
|
||||
save_filename = f"{model}_{hw_version}_{fw_version}"
|
||||
return FixtureResult(
|
||||
filename=save_filename,
|
||||
folder=SMART_CHILD_FOLDER,
|
||||
folder=folder,
|
||||
data=response,
|
||||
protocol_suffix=SMART_CHILD_SUFFIX,
|
||||
protocol_suffix=suffix,
|
||||
)
|
||||
|
||||
|
||||
def scrub_child_device_ids(
|
||||
main_response: dict, child_responses: dict
|
||||
) -> dict[str, str]:
|
||||
"""Scrub all the child device ids in the responses."""
|
||||
# Make the scrubbed id map
|
||||
scrubbed_child_id_map = {
|
||||
device_id: f"SCRUBBED_CHILD_DEVICE_ID_{index + 1}"
|
||||
for index, device_id in enumerate(child_responses.keys())
|
||||
if device_id != ""
|
||||
}
|
||||
|
||||
for child_id, response in child_responses.items():
|
||||
scrubbed_child_id = scrubbed_child_id_map[child_id]
|
||||
# scrub the device id in the child's get info response
|
||||
# The checks for the device_id will ensure we can get a fixture
|
||||
# even if the data is unexpectedly not available although it should
|
||||
# always be there
|
||||
if "get_device_info" in response and "device_id" in response["get_device_info"]:
|
||||
response["get_device_info"]["device_id"] = scrubbed_child_id
|
||||
elif (
|
||||
basic_info := response.get("getDeviceInfo", {})
|
||||
.get("device_info", {})
|
||||
.get("basic_info")
|
||||
) and "dev_id" in basic_info:
|
||||
basic_info["dev_id"] = scrubbed_child_id
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Cannot find device id in child get device info: %s", child_id
|
||||
)
|
||||
|
||||
# Scrub the device ids in the parent for smart protocol
|
||||
if gc := main_response.get("get_child_device_component_list"):
|
||||
for child in gc["child_component_list"]:
|
||||
device_id = child["device_id"]
|
||||
child["device_id"] = scrubbed_child_id_map[device_id]
|
||||
for child in main_response["get_child_device_list"]["child_device_list"]:
|
||||
device_id = child["device_id"]
|
||||
child["device_id"] = scrubbed_child_id_map[device_id]
|
||||
|
||||
# Scrub the device ids in the parent for the smart camera protocol
|
||||
if gc := main_response.get("getChildDeviceComponentList"):
|
||||
for child in gc["child_component_list"]:
|
||||
device_id = child["device_id"]
|
||||
child["device_id"] = scrubbed_child_id_map[device_id]
|
||||
for child in main_response["getChildDeviceList"]["child_device_list"]:
|
||||
if device_id := child.get("device_id"):
|
||||
child["device_id"] = scrubbed_child_id_map[device_id]
|
||||
continue
|
||||
elif dev_id := child.get("dev_id"):
|
||||
child["dev_id"] = scrubbed_child_id_map[dev_id]
|
||||
continue
|
||||
_LOGGER.error("Could not find a device id for the child device: %s", child)
|
||||
|
||||
return scrubbed_child_id_map
|
||||
|
||||
|
||||
async def get_smart_fixtures(
|
||||
protocol: SmartProtocol,
|
||||
*,
|
||||
@ -917,21 +975,19 @@ async def get_smart_fixtures(
|
||||
finally:
|
||||
await protocol.close()
|
||||
|
||||
# Put all the successes into a dict[child_device_id or "", successes[]]
|
||||
device_requests: dict[str, list[SmartCall]] = {}
|
||||
for success in successes:
|
||||
device_request = device_requests.setdefault(success.child_device_id, [])
|
||||
device_request.append(success)
|
||||
|
||||
scrubbed_device_ids = {
|
||||
device_id: f"SCRUBBED_CHILD_DEVICE_ID_{index}"
|
||||
for index, device_id in enumerate(device_requests.keys())
|
||||
if device_id != ""
|
||||
}
|
||||
|
||||
final = await _make_final_calls(
|
||||
protocol, device_requests[""], "All successes", batch_size, child_device_id=""
|
||||
)
|
||||
fixture_results = []
|
||||
|
||||
# Make the final child calls
|
||||
child_responses = {}
|
||||
for child_device_id, requests in device_requests.items():
|
||||
if child_device_id == "":
|
||||
continue
|
||||
@ -942,55 +998,82 @@ async def get_smart_fixtures(
|
||||
batch_size,
|
||||
child_device_id=child_device_id,
|
||||
)
|
||||
child_responses[child_device_id] = response
|
||||
|
||||
scrubbed = scrubbed_device_ids[child_device_id]
|
||||
if "get_device_info" in response and "device_id" in response["get_device_info"]:
|
||||
response["get_device_info"]["device_id"] = scrubbed
|
||||
# If the child is a different model to the parent create a seperate fixture
|
||||
if "get_device_info" in final:
|
||||
parent_model = final["get_device_info"]["model"]
|
||||
elif "getDeviceInfo" in final:
|
||||
parent_model = final["getDeviceInfo"]["device_info"]["basic_info"][
|
||||
"device_model"
|
||||
]
|
||||
# scrub the child ids
|
||||
scrubbed_child_id_map = scrub_child_device_ids(final, child_responses)
|
||||
|
||||
# Redact data from the main device response. _wrap_redactors ensure we do
|
||||
# not redact the scrubbed child device ids and replaces REDACTED_partial_id
|
||||
# with zeros
|
||||
final = redact_data(final, _wrap_redactors(SMART_REDACTORS))
|
||||
|
||||
# smart cam child devices provide more information in getChildDeviceList on the
|
||||
# parent than they return when queried directly for getDeviceInfo so we will store
|
||||
# it in the child fixture.
|
||||
if smart_cam_child_list := final.get("getChildDeviceList"):
|
||||
child_infos_on_parent = {
|
||||
info["device_id"]: info
|
||||
for info in smart_cam_child_list["child_device_list"]
|
||||
}
|
||||
|
||||
for child_id, response in child_responses.items():
|
||||
scrubbed_child_id = scrubbed_child_id_map[child_id]
|
||||
|
||||
# Get the parent model for checking whether to create a seperate child fixture
|
||||
if model := final.get("get_device_info", {}).get("model"):
|
||||
parent_model = model
|
||||
elif (
|
||||
device_model := final.get("getDeviceInfo", {})
|
||||
.get("device_info", {})
|
||||
.get("basic_info", {})
|
||||
.get("device_model")
|
||||
):
|
||||
parent_model = device_model
|
||||
else:
|
||||
raise KasaException("Cannot determine parent device model.")
|
||||
parent_model = None
|
||||
_LOGGER.error("Cannot determine parent device model.")
|
||||
|
||||
# different model smart child device
|
||||
if (
|
||||
"component_nego" in response
|
||||
and "get_device_info" in response
|
||||
and (child_model := response["get_device_info"].get("model"))
|
||||
(child_model := response.get("get_device_info", {}).get("model"))
|
||||
and parent_model
|
||||
and child_model != parent_model
|
||||
):
|
||||
response = redact_data(response, _wrap_redactors(SMART_REDACTORS))
|
||||
fixture_results.append(get_smart_child_fixture(response))
|
||||
model_info = SmartDevice._get_device_info(response, None)
|
||||
fixture_results.append(
|
||||
get_smart_child_fixture(
|
||||
response, model_info, SMART_CHILD_FOLDER, SMART_CHILD_SUFFIX
|
||||
)
|
||||
)
|
||||
# different model smartcam child device
|
||||
elif (
|
||||
(
|
||||
child_model := response.get("getDeviceInfo", {})
|
||||
.get("device_info", {})
|
||||
.get("basic_info", {})
|
||||
.get("device_model")
|
||||
)
|
||||
and parent_model
|
||||
and child_model != parent_model
|
||||
):
|
||||
response = redact_data(response, _wrap_redactors(SMART_REDACTORS))
|
||||
# There is more info in the childDeviceList on the parent
|
||||
# particularly the region is needed here.
|
||||
child_info_from_parent = child_infos_on_parent[scrubbed_child_id]
|
||||
response[CHILD_INFO_FROM_PARENT] = child_info_from_parent
|
||||
model_info = SmartCamChild._get_device_info(response, None)
|
||||
fixture_results.append(
|
||||
get_smart_child_fixture(
|
||||
response, model_info, SMARTCAM_CHILD_FOLDER, SMARTCAM_CHILD_SUFFIX
|
||||
)
|
||||
)
|
||||
# same model child device
|
||||
else:
|
||||
cd = final.setdefault("child_devices", {})
|
||||
cd[scrubbed] = response
|
||||
cd[scrubbed_child_id] = response
|
||||
|
||||
# Scrub the device ids in the parent for smart protocol
|
||||
if gc := final.get("get_child_device_component_list"):
|
||||
for child in gc["child_component_list"]:
|
||||
device_id = child["device_id"]
|
||||
child["device_id"] = scrubbed_device_ids[device_id]
|
||||
for child in final["get_child_device_list"]["child_device_list"]:
|
||||
device_id = child["device_id"]
|
||||
child["device_id"] = scrubbed_device_ids[device_id]
|
||||
|
||||
# Scrub the device ids in the parent for the smart camera protocol
|
||||
if gc := final.get("getChildDeviceComponentList"):
|
||||
for child in gc["child_component_list"]:
|
||||
device_id = child["device_id"]
|
||||
child["device_id"] = scrubbed_device_ids[device_id]
|
||||
for child in final["getChildDeviceList"]["child_device_list"]:
|
||||
if device_id := child.get("device_id"):
|
||||
child["device_id"] = scrubbed_device_ids[device_id]
|
||||
continue
|
||||
elif dev_id := child.get("dev_id"):
|
||||
child["dev_id"] = scrubbed_device_ids[dev_id]
|
||||
continue
|
||||
_LOGGER.error("Could not find a device for the child device: %s", child)
|
||||
|
||||
final = redact_data(final, _wrap_redactors(SMART_REDACTORS))
|
||||
discovery_result = None
|
||||
if discovery_info:
|
||||
final["discovery_result"] = redact_data(
|
||||
|
@ -13,7 +13,7 @@ from typing import Any, NamedTuple
|
||||
from kasa.device_type import DeviceType
|
||||
from kasa.iot import IotDevice
|
||||
from kasa.smart import SmartDevice
|
||||
from kasa.smartcam import SmartCamDevice
|
||||
from kasa.smartcam import SmartCamChild, SmartCamDevice
|
||||
|
||||
|
||||
class SupportedVersion(NamedTuple):
|
||||
@ -49,6 +49,7 @@ IOT_FOLDER = "tests/fixtures/iot/"
|
||||
SMART_FOLDER = "tests/fixtures/smart/"
|
||||
SMART_CHILD_FOLDER = "tests/fixtures/smart/child"
|
||||
SMARTCAM_FOLDER = "tests/fixtures/smartcam/"
|
||||
SMARTCAM_CHILD_FOLDER = "tests/fixtures/smartcam/child"
|
||||
|
||||
|
||||
def generate_supported(args):
|
||||
@ -66,6 +67,7 @@ def generate_supported(args):
|
||||
_get_supported_devices(supported, SMART_FOLDER, SmartDevice)
|
||||
_get_supported_devices(supported, SMART_CHILD_FOLDER, SmartDevice)
|
||||
_get_supported_devices(supported, SMARTCAM_FOLDER, SmartCamDevice)
|
||||
_get_supported_devices(supported, SMARTCAM_CHILD_FOLDER, SmartCamChild)
|
||||
|
||||
readme_updated = _update_supported_file(
|
||||
README_FILENAME, _supported_summary(supported), print_diffs
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""Package for supporting tapo-branded cameras."""
|
||||
|
||||
from .smartcamchild import SmartCamChild
|
||||
from .smartcamdevice import SmartCamDevice
|
||||
|
||||
__all__ = ["SmartCamDevice"]
|
||||
__all__ = ["SmartCamDevice", "SmartCamChild"]
|
||||
|
115
kasa/smartcam/smartcamchild.py
Normal file
115
kasa/smartcam/smartcamchild.py
Normal file
@ -0,0 +1,115 @@
|
||||
"""Child device implementation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from ..device import DeviceInfo
|
||||
from ..device_type import DeviceType
|
||||
from ..deviceconfig import DeviceConfig
|
||||
from ..protocols.smartcamprotocol import _ChildCameraProtocolWrapper
|
||||
from ..protocols.smartprotocol import SmartProtocol
|
||||
from ..smart.smartchilddevice import SmartChildDevice
|
||||
from ..smart.smartdevice import ComponentsRaw, SmartDevice
|
||||
from .smartcamdevice import SmartCamDevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# SmartCamChild devices have a different info format from getChildDeviceInfo
|
||||
# than when querying getDeviceInfo directly on the child.
|
||||
# As _get_device_info is also called by dump_devtools and generate_supported
|
||||
# this key will be expected by _get_device_info
|
||||
CHILD_INFO_FROM_PARENT = "child_info_from_parent"
|
||||
|
||||
|
||||
class SmartCamChild(SmartChildDevice, SmartCamDevice):
|
||||
"""Presentation of a child device.
|
||||
|
||||
This wraps the protocol communications and sets internal data for the child.
|
||||
"""
|
||||
|
||||
CHILD_DEVICE_TYPE_MAP = {
|
||||
"camera": DeviceType.Camera,
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: SmartDevice,
|
||||
info: dict,
|
||||
component_info_raw: ComponentsRaw,
|
||||
*,
|
||||
config: DeviceConfig | None = None,
|
||||
protocol: SmartProtocol | None = None,
|
||||
) -> None:
|
||||
_protocol = protocol or _ChildCameraProtocolWrapper(
|
||||
info["device_id"], parent.protocol
|
||||
)
|
||||
super().__init__(parent, info, component_info_raw, protocol=_protocol)
|
||||
self._child_info_from_parent: dict = {}
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device info.
|
||||
|
||||
Child device does not have it info and components in _last_update so
|
||||
this overrides the base implementation to call _get_device_info with
|
||||
info and components combined as they would be in _last_update.
|
||||
"""
|
||||
return self._get_device_info(
|
||||
{
|
||||
CHILD_INFO_FROM_PARENT: self._child_info_from_parent,
|
||||
},
|
||||
None,
|
||||
)
|
||||
|
||||
def _map_child_info_from_parent(self, device_info: dict) -> dict:
|
||||
return {
|
||||
"model": device_info["device_model"],
|
||||
"device_type": device_info["device_type"],
|
||||
"alias": device_info["alias"],
|
||||
"fw_ver": device_info["sw_ver"],
|
||||
"hw_ver": device_info["hw_ver"],
|
||||
"mac": device_info["mac"],
|
||||
"hwId": device_info.get("hw_id"),
|
||||
"oem_id": device_info["oem_id"],
|
||||
"device_id": device_info["device_id"],
|
||||
}
|
||||
|
||||
def _update_internal_state(self, info: dict[str, Any]) -> None:
|
||||
"""Update the internal info state.
|
||||
|
||||
This is used by the parent to push updates to its children.
|
||||
"""
|
||||
# smartcam children have info with different keys to their own
|
||||
# getDeviceInfo queries
|
||||
self._child_info_from_parent = info
|
||||
|
||||
# self._info will have the values normalized across smart and smartcam
|
||||
# devices
|
||||
self._info = self._map_child_info_from_parent(info)
|
||||
|
||||
@staticmethod
|
||||
def _get_device_info(
|
||||
info: dict[str, Any], discovery_info: dict[str, Any] | None
|
||||
) -> DeviceInfo:
|
||||
"""Get model information for a device."""
|
||||
if not (cifp := info.get(CHILD_INFO_FROM_PARENT)):
|
||||
return SmartCamDevice._get_device_info(info, discovery_info)
|
||||
|
||||
model = cifp["device_model"]
|
||||
device_type = SmartCamDevice._get_device_type_from_sysinfo(cifp)
|
||||
fw_version_full = cifp["sw_ver"]
|
||||
firmware_version, firmware_build = fw_version_full.split(" ", maxsplit=1)
|
||||
return DeviceInfo(
|
||||
short_name=model,
|
||||
long_name=model,
|
||||
brand="tapo",
|
||||
device_family=cifp["device_type"],
|
||||
device_type=device_type,
|
||||
hardware_version=cifp["hw_ver"],
|
||||
firmware_version=firmware_version,
|
||||
firmware_build=firmware_build,
|
||||
requires_auth=True,
|
||||
region=cifp.get("region"),
|
||||
)
|
@ -63,6 +63,13 @@ class SmartCamDevice(SmartDevice):
|
||||
info = self._try_get_response(info_resp, "getDeviceInfo")
|
||||
self._info = self._map_info(info["device_info"])
|
||||
|
||||
def _update_internal_state(self, info: dict[str, Any]) -> None:
|
||||
"""Update the internal info state.
|
||||
|
||||
This is used by the parent to push updates to its children.
|
||||
"""
|
||||
self._info = self._map_info(info)
|
||||
|
||||
def _update_children_info(self) -> None:
|
||||
"""Update the internal child device info from the parent info."""
|
||||
if child_info := self._try_get_response(
|
||||
@ -99,6 +106,27 @@ class SmartCamDevice(SmartDevice):
|
||||
last_update=initial_response,
|
||||
)
|
||||
|
||||
async def _initialize_smartcam_child(
|
||||
self, info: dict, child_components_raw: ComponentsRaw
|
||||
) -> SmartDevice:
|
||||
"""Initialize a smart child device attached to a smartcam device."""
|
||||
child_id = info["device_id"]
|
||||
child_protocol = _ChildCameraProtocolWrapper(child_id, self.protocol)
|
||||
|
||||
last_update = {"getDeviceInfo": {"device_info": {"basic_info": info}}}
|
||||
app_component_list = {
|
||||
"app_component_list": child_components_raw["component_list"]
|
||||
}
|
||||
from .smartcamchild import SmartCamChild
|
||||
|
||||
return await SmartCamChild.create(
|
||||
parent=self,
|
||||
child_info=info,
|
||||
child_components_raw=app_component_list,
|
||||
protocol=child_protocol,
|
||||
last_update=last_update,
|
||||
)
|
||||
|
||||
async def _initialize_children(self) -> None:
|
||||
"""Initialize children for hubs."""
|
||||
child_info_query = {
|
||||
@ -113,17 +141,27 @@ class SmartCamDevice(SmartDevice):
|
||||
for child in resp["getChildDeviceComponentList"]["child_component_list"]
|
||||
}
|
||||
children = {}
|
||||
from .smartcamchild import SmartCamChild
|
||||
|
||||
for info in resp["getChildDeviceList"]["child_device_list"]:
|
||||
if (
|
||||
(category := info.get("category"))
|
||||
and category in SmartChildDevice.CHILD_DEVICE_TYPE_MAP
|
||||
and (child_id := info.get("device_id"))
|
||||
and (child_components := smart_children_components.get(child_id))
|
||||
):
|
||||
# Smart
|
||||
if category in SmartChildDevice.CHILD_DEVICE_TYPE_MAP:
|
||||
children[child_id] = await self._initialize_smart_child(
|
||||
info, child_components
|
||||
)
|
||||
else:
|
||||
continue
|
||||
# Smartcam
|
||||
if category in SmartCamChild.CHILD_DEVICE_TYPE_MAP:
|
||||
children[child_id] = await self._initialize_smartcam_child(
|
||||
info, child_components
|
||||
)
|
||||
continue
|
||||
|
||||
_LOGGER.debug("Child device type not supported: %s", info)
|
||||
|
||||
self._children = children
|
||||
|
@ -335,7 +335,7 @@ device_smartcam = parametrize("devices smartcam", protocol_filter={"SMARTCAM"})
|
||||
camera_smartcam = parametrize(
|
||||
"camera smartcam",
|
||||
device_type_filter=[DeviceType.Camera],
|
||||
protocol_filter={"SMARTCAM"},
|
||||
protocol_filter={"SMARTCAM", "SMARTCAM.CHILD"},
|
||||
)
|
||||
hub_smartcam = parametrize(
|
||||
"hub smartcam",
|
||||
@ -377,7 +377,7 @@ check_categories()
|
||||
def device_for_fixture_name(model, protocol):
|
||||
if protocol in {"SMART", "SMART.CHILD"}:
|
||||
return SmartDevice
|
||||
elif protocol == "SMARTCAM":
|
||||
elif protocol in {"SMARTCAM", "SMARTCAM.CHILD"}:
|
||||
return SmartCamDevice
|
||||
else:
|
||||
for d in STRIPS_IOT:
|
||||
@ -434,7 +434,7 @@ async def get_device_for_fixture(
|
||||
d.protocol = FakeSmartProtocol(
|
||||
fixture_data.data, fixture_data.name, verbatim=verbatim
|
||||
)
|
||||
elif fixture_data.protocol == "SMARTCAM":
|
||||
elif fixture_data.protocol in {"SMARTCAM", "SMARTCAM.CHILD"}:
|
||||
d.protocol = FakeSmartCamProtocol(
|
||||
fixture_data.data, fixture_data.name, verbatim=verbatim
|
||||
)
|
||||
|
@ -7,6 +7,8 @@ import pytest
|
||||
from kasa import Credentials, DeviceConfig, SmartProtocol
|
||||
from kasa.exceptions import SmartErrorCode
|
||||
from kasa.smart import SmartChildDevice
|
||||
from kasa.smartcam import SmartCamChild
|
||||
from kasa.smartcam.smartcamchild import CHILD_INFO_FROM_PARENT
|
||||
from kasa.transports.basetransport import BaseTransport
|
||||
|
||||
|
||||
@ -227,16 +229,20 @@ class FakeSmartTransport(BaseTransport):
|
||||
# imported here to avoid circular import
|
||||
from .conftest import filter_fixtures
|
||||
|
||||
def try_get_child_fixture_info(child_dev_info):
|
||||
def try_get_child_fixture_info(child_dev_info, protocol):
|
||||
hw_version = child_dev_info["hw_ver"]
|
||||
sw_version = child_dev_info["fw_ver"]
|
||||
sw_version = child_dev_info.get("sw_ver", child_dev_info.get("fw_ver"))
|
||||
sw_version = sw_version.split(" ")[0]
|
||||
model = child_dev_info["model"]
|
||||
region = child_dev_info.get("specs", "XX")
|
||||
child_fixture_name = f"{model}({region})_{hw_version}_{sw_version}"
|
||||
model = child_dev_info.get("device_model", child_dev_info.get("model"))
|
||||
assert sw_version
|
||||
assert model
|
||||
|
||||
region = child_dev_info.get("specs", child_dev_info.get("region"))
|
||||
region = f"({region})" if region else ""
|
||||
child_fixture_name = f"{model}{region}_{hw_version}_{sw_version}"
|
||||
child_fixtures = filter_fixtures(
|
||||
"Child fixture",
|
||||
protocol_filter={"SMART.CHILD"},
|
||||
protocol_filter={protocol},
|
||||
model_filter={child_fixture_name},
|
||||
)
|
||||
if child_fixtures:
|
||||
@ -249,7 +255,9 @@ class FakeSmartTransport(BaseTransport):
|
||||
and (category := child_info.get("category"))
|
||||
and category in SmartChildDevice.CHILD_DEVICE_TYPE_MAP
|
||||
):
|
||||
if fixture_info_tuple := try_get_child_fixture_info(child_info):
|
||||
if fixture_info_tuple := try_get_child_fixture_info(
|
||||
child_info, "SMART.CHILD"
|
||||
):
|
||||
child_fixture = copy.deepcopy(fixture_info_tuple.data)
|
||||
child_fixture["get_device_info"]["device_id"] = device_id
|
||||
found_child_fixture_infos.append(child_fixture["get_device_info"])
|
||||
@ -270,9 +278,32 @@ class FakeSmartTransport(BaseTransport):
|
||||
pytest.fixtures_missing_methods.setdefault( # type: ignore[attr-defined]
|
||||
parent_fixture_name, set()
|
||||
).add("child_devices")
|
||||
elif (
|
||||
(device_id := child_info.get("device_id"))
|
||||
and (category := child_info.get("category"))
|
||||
and category in SmartCamChild.CHILD_DEVICE_TYPE_MAP
|
||||
and (
|
||||
fixture_info_tuple := try_get_child_fixture_info(
|
||||
child_info, "SMARTCAM.CHILD"
|
||||
)
|
||||
)
|
||||
):
|
||||
from .fakeprotocol_smartcam import FakeSmartCamProtocol
|
||||
|
||||
child_fixture = copy.deepcopy(fixture_info_tuple.data)
|
||||
child_fixture["getDeviceInfo"]["device_info"]["basic_info"][
|
||||
"dev_id"
|
||||
] = device_id
|
||||
child_fixture[CHILD_INFO_FROM_PARENT]["device_id"] = device_id
|
||||
# We copy the child device info to the parent getChildDeviceInfo
|
||||
# list for smartcam children in order for updates to work.
|
||||
found_child_fixture_infos.append(child_fixture[CHILD_INFO_FROM_PARENT])
|
||||
child_protocols[device_id] = FakeSmartCamProtocol(
|
||||
child_fixture, fixture_info_tuple.name, is_child=True
|
||||
)
|
||||
else:
|
||||
warn(
|
||||
f"Child is a cameraprotocol which needs to be implemented {child_info}",
|
||||
f"Child is a protocol which needs to be implemented {child_info}",
|
||||
stacklevel=2,
|
||||
)
|
||||
# Replace parent child infos with the infos from the child fixtures so
|
||||
|
@ -6,6 +6,7 @@ from typing import Any
|
||||
|
||||
from kasa import Credentials, DeviceConfig, SmartProtocol
|
||||
from kasa.protocols.smartcamprotocol import SmartCamProtocol
|
||||
from kasa.smartcam.smartcamchild import CHILD_INFO_FROM_PARENT
|
||||
from kasa.transports.basetransport import BaseTransport
|
||||
|
||||
from .fakeprotocol_smart import FakeSmartTransport
|
||||
@ -125,10 +126,26 @@ class FakeSmartCamTransport(BaseTransport):
|
||||
|
||||
@staticmethod
|
||||
def _get_param_set_value(info: dict, set_keys: list[str], value):
|
||||
cifp = info.get(CHILD_INFO_FROM_PARENT)
|
||||
|
||||
for key in set_keys[:-1]:
|
||||
info = info[key]
|
||||
info[set_keys[-1]] = value
|
||||
|
||||
if (
|
||||
cifp
|
||||
and set_keys[0] == "getDeviceInfo"
|
||||
and (
|
||||
child_info_parent_key
|
||||
:= FakeSmartCamTransport.CHILD_INFO_SETTER_MAP.get(set_keys[-1])
|
||||
)
|
||||
):
|
||||
cifp[child_info_parent_key] = value
|
||||
|
||||
CHILD_INFO_SETTER_MAP = {
|
||||
"device_alias": "alias",
|
||||
}
|
||||
|
||||
FIXTURE_MISSING_MAP = {
|
||||
"getMatterSetupInfo": (
|
||||
"matter",
|
||||
|
@ -60,11 +60,19 @@ SUPPORTED_SMARTCAM_DEVICES = [
|
||||
)
|
||||
]
|
||||
|
||||
SUPPORTED_SMARTCAM_CHILD_DEVICES = [
|
||||
(device, "SMARTCAM.CHILD")
|
||||
for device in glob.glob(
|
||||
os.path.dirname(os.path.abspath(__file__)) + "/fixtures/smartcam/child/*.json"
|
||||
)
|
||||
]
|
||||
|
||||
SUPPORTED_DEVICES = (
|
||||
SUPPORTED_IOT_DEVICES
|
||||
+ SUPPORTED_SMART_DEVICES
|
||||
+ SUPPORTED_SMART_CHILD_DEVICES
|
||||
+ SUPPORTED_SMARTCAM_DEVICES
|
||||
+ SUPPORTED_SMARTCAM_CHILD_DEVICES
|
||||
)
|
||||
|
||||
|
||||
@ -82,14 +90,8 @@ def get_fixture_infos() -> list[FixtureInfo]:
|
||||
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:
|
||||
with open(file) as f:
|
||||
data = json.load(f)
|
||||
|
||||
fixture_name = p.name
|
||||
@ -188,7 +190,7 @@ def filter_fixtures(
|
||||
IotDevice._get_device_type_from_sys_info(fixture_data.data)
|
||||
in device_type
|
||||
)
|
||||
elif fixture_data.protocol == "SMARTCAM":
|
||||
elif fixture_data.protocol in {"SMARTCAM", "SMARTCAM.CHILD"}:
|
||||
info = fixture_data.data["getDeviceInfo"]["device_info"]["basic_info"]
|
||||
return SmartCamDevice._get_device_type_from_sysinfo(info) in device_type
|
||||
return False
|
||||
|
@ -10,7 +10,13 @@ import pytest
|
||||
|
||||
from kasa import Credentials, Device, DeviceType, Module, StreamResolution
|
||||
|
||||
from ...conftest import camera_smartcam, device_smartcam
|
||||
from ...conftest import device_smartcam, parametrize
|
||||
|
||||
not_child_camera_smartcam = parametrize(
|
||||
"not child camera smartcam",
|
||||
device_type_filter=[DeviceType.Camera],
|
||||
protocol_filter={"SMARTCAM"},
|
||||
)
|
||||
|
||||
|
||||
@device_smartcam
|
||||
@ -24,7 +30,7 @@ async def test_state(dev: Device):
|
||||
assert dev.is_on is not state
|
||||
|
||||
|
||||
@camera_smartcam
|
||||
@not_child_camera_smartcam
|
||||
async def test_stream_rtsp_url(dev: Device):
|
||||
camera_module = dev.modules.get(Module.Camera)
|
||||
assert camera_module
|
||||
@ -84,7 +90,7 @@ async def test_stream_rtsp_url(dev: Device):
|
||||
assert url is None
|
||||
|
||||
|
||||
@camera_smartcam
|
||||
@not_child_camera_smartcam
|
||||
async def test_onvif_url(dev: Device):
|
||||
"""Test the onvif url."""
|
||||
camera_module = dev.modules.get(Module.Camera)
|
||||
|
@ -52,12 +52,12 @@ async def test_alias(dev):
|
||||
async def test_hub(dev):
|
||||
assert dev.children
|
||||
for child in dev.children:
|
||||
assert "Cloud" in child.modules
|
||||
assert child.modules["Cloud"].data
|
||||
assert child.modules
|
||||
assert child.device_info
|
||||
|
||||
assert child.alias
|
||||
await child.update()
|
||||
assert "Time" not in child.modules
|
||||
assert child.time
|
||||
assert child.device_id
|
||||
|
||||
|
||||
@device_smartcam
|
||||
|
@ -31,7 +31,7 @@ from kasa.iot.iottimezone import (
|
||||
)
|
||||
from kasa.iot.modules import IotLightPreset
|
||||
from kasa.smart import SmartChildDevice, SmartDevice
|
||||
from kasa.smartcam import SmartCamDevice
|
||||
from kasa.smartcam import SmartCamChild, SmartCamDevice
|
||||
|
||||
|
||||
def _get_subclasses(of_class):
|
||||
@ -84,13 +84,24 @@ async def test_device_class_ctors(device_class_name_obj):
|
||||
credentials = Credentials("foo", "bar")
|
||||
config = DeviceConfig(host, port_override=port, credentials=credentials)
|
||||
klass = device_class_name_obj[1]
|
||||
if issubclass(klass, SmartChildDevice):
|
||||
if issubclass(klass, SmartChildDevice | SmartCamChild):
|
||||
parent = SmartDevice(host, config=config)
|
||||
smartcam_required = {
|
||||
"device_model": "foo",
|
||||
"device_type": "SMART.TAPODOORBELL",
|
||||
"alias": "Foo",
|
||||
"sw_ver": "1.1",
|
||||
"hw_ver": "1.0",
|
||||
"mac": "1.2.3.4",
|
||||
"hwId": "hw_id",
|
||||
"oem_id": "oem_id",
|
||||
}
|
||||
dev = klass(
|
||||
parent,
|
||||
{"dummy": "info", "device_id": "dummy"},
|
||||
{"dummy": "info", "device_id": "dummy", **smartcam_required},
|
||||
{
|
||||
"component_list": [{"id": "device", "ver_code": 1}],
|
||||
"app_component_list": [{"name": "device", "version": 1}],
|
||||
},
|
||||
)
|
||||
else:
|
||||
@ -108,13 +119,24 @@ async def test_device_class_repr(device_class_name_obj):
|
||||
credentials = Credentials("foo", "bar")
|
||||
config = DeviceConfig(host, port_override=port, credentials=credentials)
|
||||
klass = device_class_name_obj[1]
|
||||
if issubclass(klass, SmartChildDevice):
|
||||
if issubclass(klass, SmartChildDevice | SmartCamChild):
|
||||
parent = SmartDevice(host, config=config)
|
||||
smartcam_required = {
|
||||
"device_model": "foo",
|
||||
"device_type": "SMART.TAPODOORBELL",
|
||||
"alias": "Foo",
|
||||
"sw_ver": "1.1",
|
||||
"hw_ver": "1.0",
|
||||
"mac": "1.2.3.4",
|
||||
"hwId": "hw_id",
|
||||
"oem_id": "oem_id",
|
||||
}
|
||||
dev = klass(
|
||||
parent,
|
||||
{"dummy": "info", "device_id": "dummy"},
|
||||
{"dummy": "info", "device_id": "dummy", **smartcam_required},
|
||||
{
|
||||
"component_list": [{"id": "device", "ver_code": 1}],
|
||||
"app_component_list": [{"name": "device", "version": 1}],
|
||||
},
|
||||
)
|
||||
else:
|
||||
@ -132,11 +154,14 @@ async def test_device_class_repr(device_class_name_obj):
|
||||
SmartChildDevice: DeviceType.Unknown,
|
||||
SmartDevice: DeviceType.Unknown,
|
||||
SmartCamDevice: DeviceType.Camera,
|
||||
SmartCamChild: DeviceType.Camera,
|
||||
}
|
||||
type_ = CLASS_TO_DEFAULT_TYPE[klass]
|
||||
child_repr = "<DeviceType.Unknown(child) of <DeviceType.Unknown at 127.0.0.2 - update() needed>>"
|
||||
not_child_repr = f"<{type_} at 127.0.0.2 - update() needed>"
|
||||
expected_repr = child_repr if klass is SmartChildDevice else not_child_repr
|
||||
expected_repr = (
|
||||
child_repr if klass in {SmartChildDevice, SmartCamChild} else not_child_repr
|
||||
)
|
||||
assert repr(dev) == expected_repr
|
||||
|
||||
|
||||
|
@ -4,11 +4,18 @@ import copy
|
||||
|
||||
import pytest
|
||||
|
||||
from devtools.dump_devinfo import get_legacy_fixture, get_smart_fixtures
|
||||
from devtools.dump_devinfo import (
|
||||
_wrap_redactors,
|
||||
get_legacy_fixture,
|
||||
get_smart_fixtures,
|
||||
)
|
||||
from kasa.iot import IotDevice
|
||||
from kasa.protocols import IotProtocol
|
||||
from kasa.protocols.protocol import redact_data
|
||||
from kasa.protocols.smartprotocol import REDACTORS as SMART_REDACTORS
|
||||
from kasa.smart import SmartDevice
|
||||
from kasa.smartcam import SmartCamDevice
|
||||
from kasa.smartcam.smartcamchild import CHILD_INFO_FROM_PARENT
|
||||
|
||||
from .conftest import (
|
||||
FixtureInfo,
|
||||
@ -113,6 +120,18 @@ async def test_smartcam_fixtures(fixture_info: FixtureInfo):
|
||||
saved_fixture_data = {
|
||||
key: val for key, val in saved_fixture_data.items() if val != -1001
|
||||
}
|
||||
|
||||
# Remove the child info from parent from the comparison because the
|
||||
# child may have been created by a different parent fixture
|
||||
saved_fixture_data.pop(CHILD_INFO_FROM_PARENT, None)
|
||||
created_cifp = created_child_fixture.data.pop(CHILD_INFO_FROM_PARENT, None)
|
||||
|
||||
# Still check that the created child info from parent was redacted.
|
||||
# only smartcam children generate child_info_from_parent
|
||||
if created_cifp:
|
||||
redacted_cifp = redact_data(created_cifp, _wrap_redactors(SMART_REDACTORS))
|
||||
assert created_cifp == redacted_cifp
|
||||
|
||||
assert saved_fixture_data == created_child_fixture.data
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user