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 REDACTORS as SMART_REDACTORS
|
||||||
from kasa.protocols.smartprotocol import SmartProtocol, _ChildProtocolWrapper
|
from kasa.protocols.smartprotocol import SmartProtocol, _ChildProtocolWrapper
|
||||||
from kasa.smart import SmartChildDevice, SmartDevice
|
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")
|
Call = namedtuple("Call", "module method")
|
||||||
FixtureResult = namedtuple("FixtureResult", "filename, folder, data, protocol_suffix")
|
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/"
|
SMART_FOLDER = "tests/fixtures/smart/"
|
||||||
SMARTCAM_FOLDER = "tests/fixtures/smartcam/"
|
SMARTCAM_FOLDER = "tests/fixtures/smartcam/"
|
||||||
SMART_CHILD_FOLDER = "tests/fixtures/smart/child/"
|
SMART_CHILD_FOLDER = "tests/fixtures/smart/child/"
|
||||||
|
SMARTCAM_CHILD_FOLDER = "tests/fixtures/smartcam/child/"
|
||||||
IOT_FOLDER = "tests/fixtures/iot/"
|
IOT_FOLDER = "tests/fixtures/iot/"
|
||||||
|
|
||||||
SMART_PROTOCOL_SUFFIX = "SMART"
|
SMART_PROTOCOL_SUFFIX = "SMART"
|
||||||
SMARTCAM_SUFFIX = "SMARTCAM"
|
SMARTCAM_SUFFIX = "SMARTCAM"
|
||||||
SMART_CHILD_SUFFIX = "SMART.CHILD"
|
SMART_CHILD_SUFFIX = "SMART.CHILD"
|
||||||
|
SMARTCAM_CHILD_SUFFIX = "SMARTCAM.CHILD"
|
||||||
IOT_SUFFIX = "IOT"
|
IOT_SUFFIX = "IOT"
|
||||||
|
|
||||||
NO_GIT_FIXTURE_FOLDER = "kasa-fixtures"
|
NO_GIT_FIXTURE_FOLDER = "kasa-fixtures"
|
||||||
@ -844,9 +847,8 @@ async def get_smart_test_calls(protocol: SmartProtocol):
|
|||||||
return test_calls, successes
|
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."""
|
"""Get a seperate fixture for the child device."""
|
||||||
model_info = SmartDevice._get_device_info(response, None)
|
|
||||||
hw_version = model_info.hardware_version
|
hw_version = model_info.hardware_version
|
||||||
fw_version = model_info.firmware_version
|
fw_version = model_info.firmware_version
|
||||||
model = model_info.long_name
|
model = model_info.long_name
|
||||||
@ -855,12 +857,68 @@ def get_smart_child_fixture(response):
|
|||||||
save_filename = f"{model}_{hw_version}_{fw_version}"
|
save_filename = f"{model}_{hw_version}_{fw_version}"
|
||||||
return FixtureResult(
|
return FixtureResult(
|
||||||
filename=save_filename,
|
filename=save_filename,
|
||||||
folder=SMART_CHILD_FOLDER,
|
folder=folder,
|
||||||
data=response,
|
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(
|
async def get_smart_fixtures(
|
||||||
protocol: SmartProtocol,
|
protocol: SmartProtocol,
|
||||||
*,
|
*,
|
||||||
@ -917,21 +975,19 @@ async def get_smart_fixtures(
|
|||||||
finally:
|
finally:
|
||||||
await protocol.close()
|
await protocol.close()
|
||||||
|
|
||||||
|
# Put all the successes into a dict[child_device_id or "", successes[]]
|
||||||
device_requests: dict[str, list[SmartCall]] = {}
|
device_requests: dict[str, list[SmartCall]] = {}
|
||||||
for success in successes:
|
for success in successes:
|
||||||
device_request = device_requests.setdefault(success.child_device_id, [])
|
device_request = device_requests.setdefault(success.child_device_id, [])
|
||||||
device_request.append(success)
|
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(
|
final = await _make_final_calls(
|
||||||
protocol, device_requests[""], "All successes", batch_size, child_device_id=""
|
protocol, device_requests[""], "All successes", batch_size, child_device_id=""
|
||||||
)
|
)
|
||||||
fixture_results = []
|
fixture_results = []
|
||||||
|
|
||||||
|
# Make the final child calls
|
||||||
|
child_responses = {}
|
||||||
for child_device_id, requests in device_requests.items():
|
for child_device_id, requests in device_requests.items():
|
||||||
if child_device_id == "":
|
if child_device_id == "":
|
||||||
continue
|
continue
|
||||||
@ -942,55 +998,82 @@ async def get_smart_fixtures(
|
|||||||
batch_size,
|
batch_size,
|
||||||
child_device_id=child_device_id,
|
child_device_id=child_device_id,
|
||||||
)
|
)
|
||||||
|
child_responses[child_device_id] = response
|
||||||
|
|
||||||
scrubbed = scrubbed_device_ids[child_device_id]
|
# scrub the child ids
|
||||||
if "get_device_info" in response and "device_id" in response["get_device_info"]:
|
scrubbed_child_id_map = scrub_child_device_ids(final, child_responses)
|
||||||
response["get_device_info"]["device_id"] = scrubbed
|
|
||||||
# If the child is a different model to the parent create a seperate fixture
|
# Redact data from the main device response. _wrap_redactors ensure we do
|
||||||
if "get_device_info" in final:
|
# not redact the scrubbed child device ids and replaces REDACTED_partial_id
|
||||||
parent_model = final["get_device_info"]["model"]
|
# with zeros
|
||||||
elif "getDeviceInfo" in final:
|
final = redact_data(final, _wrap_redactors(SMART_REDACTORS))
|
||||||
parent_model = final["getDeviceInfo"]["device_info"]["basic_info"][
|
|
||||||
"device_model"
|
# 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:
|
else:
|
||||||
raise KasaException("Cannot determine parent device model.")
|
parent_model = None
|
||||||
|
_LOGGER.error("Cannot determine parent device model.")
|
||||||
|
|
||||||
|
# different model smart child device
|
||||||
if (
|
if (
|
||||||
"component_nego" in response
|
(child_model := response.get("get_device_info", {}).get("model"))
|
||||||
and "get_device_info" in response
|
and parent_model
|
||||||
and (child_model := response["get_device_info"].get("model"))
|
|
||||||
and child_model != parent_model
|
and child_model != parent_model
|
||||||
):
|
):
|
||||||
response = redact_data(response, _wrap_redactors(SMART_REDACTORS))
|
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:
|
else:
|
||||||
cd = final.setdefault("child_devices", {})
|
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
|
discovery_result = None
|
||||||
if discovery_info:
|
if discovery_info:
|
||||||
final["discovery_result"] = redact_data(
|
final["discovery_result"] = redact_data(
|
||||||
|
@ -13,7 +13,7 @@ from typing import Any, NamedTuple
|
|||||||
from kasa.device_type import DeviceType
|
from kasa.device_type import DeviceType
|
||||||
from kasa.iot import IotDevice
|
from kasa.iot import IotDevice
|
||||||
from kasa.smart import SmartDevice
|
from kasa.smart import SmartDevice
|
||||||
from kasa.smartcam import SmartCamDevice
|
from kasa.smartcam import SmartCamChild, SmartCamDevice
|
||||||
|
|
||||||
|
|
||||||
class SupportedVersion(NamedTuple):
|
class SupportedVersion(NamedTuple):
|
||||||
@ -49,6 +49,7 @@ IOT_FOLDER = "tests/fixtures/iot/"
|
|||||||
SMART_FOLDER = "tests/fixtures/smart/"
|
SMART_FOLDER = "tests/fixtures/smart/"
|
||||||
SMART_CHILD_FOLDER = "tests/fixtures/smart/child"
|
SMART_CHILD_FOLDER = "tests/fixtures/smart/child"
|
||||||
SMARTCAM_FOLDER = "tests/fixtures/smartcam/"
|
SMARTCAM_FOLDER = "tests/fixtures/smartcam/"
|
||||||
|
SMARTCAM_CHILD_FOLDER = "tests/fixtures/smartcam/child"
|
||||||
|
|
||||||
|
|
||||||
def generate_supported(args):
|
def generate_supported(args):
|
||||||
@ -66,6 +67,7 @@ def generate_supported(args):
|
|||||||
_get_supported_devices(supported, SMART_FOLDER, SmartDevice)
|
_get_supported_devices(supported, SMART_FOLDER, SmartDevice)
|
||||||
_get_supported_devices(supported, SMART_CHILD_FOLDER, SmartDevice)
|
_get_supported_devices(supported, SMART_CHILD_FOLDER, SmartDevice)
|
||||||
_get_supported_devices(supported, SMARTCAM_FOLDER, SmartCamDevice)
|
_get_supported_devices(supported, SMARTCAM_FOLDER, SmartCamDevice)
|
||||||
|
_get_supported_devices(supported, SMARTCAM_CHILD_FOLDER, SmartCamChild)
|
||||||
|
|
||||||
readme_updated = _update_supported_file(
|
readme_updated = _update_supported_file(
|
||||||
README_FILENAME, _supported_summary(supported), print_diffs
|
README_FILENAME, _supported_summary(supported), print_diffs
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""Package for supporting tapo-branded cameras."""
|
"""Package for supporting tapo-branded cameras."""
|
||||||
|
|
||||||
|
from .smartcamchild import SmartCamChild
|
||||||
from .smartcamdevice import SmartCamDevice
|
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")
|
info = self._try_get_response(info_resp, "getDeviceInfo")
|
||||||
self._info = self._map_info(info["device_info"])
|
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:
|
def _update_children_info(self) -> None:
|
||||||
"""Update the internal child device info from the parent info."""
|
"""Update the internal child device info from the parent info."""
|
||||||
if child_info := self._try_get_response(
|
if child_info := self._try_get_response(
|
||||||
@ -99,6 +106,27 @@ class SmartCamDevice(SmartDevice):
|
|||||||
last_update=initial_response,
|
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:
|
async def _initialize_children(self) -> None:
|
||||||
"""Initialize children for hubs."""
|
"""Initialize children for hubs."""
|
||||||
child_info_query = {
|
child_info_query = {
|
||||||
@ -113,18 +141,28 @@ class SmartCamDevice(SmartDevice):
|
|||||||
for child in resp["getChildDeviceComponentList"]["child_component_list"]
|
for child in resp["getChildDeviceComponentList"]["child_component_list"]
|
||||||
}
|
}
|
||||||
children = {}
|
children = {}
|
||||||
|
from .smartcamchild import SmartCamChild
|
||||||
|
|
||||||
for info in resp["getChildDeviceList"]["child_device_list"]:
|
for info in resp["getChildDeviceList"]["child_device_list"]:
|
||||||
if (
|
if (
|
||||||
(category := info.get("category"))
|
(category := info.get("category"))
|
||||||
and category in SmartChildDevice.CHILD_DEVICE_TYPE_MAP
|
|
||||||
and (child_id := info.get("device_id"))
|
and (child_id := info.get("device_id"))
|
||||||
and (child_components := smart_children_components.get(child_id))
|
and (child_components := smart_children_components.get(child_id))
|
||||||
):
|
):
|
||||||
children[child_id] = await self._initialize_smart_child(
|
# Smart
|
||||||
info, child_components
|
if category in SmartChildDevice.CHILD_DEVICE_TYPE_MAP:
|
||||||
)
|
children[child_id] = await self._initialize_smart_child(
|
||||||
else:
|
info, child_components
|
||||||
_LOGGER.debug("Child device type not supported: %s", info)
|
)
|
||||||
|
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
|
self._children = children
|
||||||
|
|
||||||
|
@ -335,7 +335,7 @@ device_smartcam = parametrize("devices smartcam", protocol_filter={"SMARTCAM"})
|
|||||||
camera_smartcam = parametrize(
|
camera_smartcam = parametrize(
|
||||||
"camera smartcam",
|
"camera smartcam",
|
||||||
device_type_filter=[DeviceType.Camera],
|
device_type_filter=[DeviceType.Camera],
|
||||||
protocol_filter={"SMARTCAM"},
|
protocol_filter={"SMARTCAM", "SMARTCAM.CHILD"},
|
||||||
)
|
)
|
||||||
hub_smartcam = parametrize(
|
hub_smartcam = parametrize(
|
||||||
"hub smartcam",
|
"hub smartcam",
|
||||||
@ -377,7 +377,7 @@ check_categories()
|
|||||||
def device_for_fixture_name(model, protocol):
|
def device_for_fixture_name(model, protocol):
|
||||||
if protocol in {"SMART", "SMART.CHILD"}:
|
if protocol in {"SMART", "SMART.CHILD"}:
|
||||||
return SmartDevice
|
return SmartDevice
|
||||||
elif protocol == "SMARTCAM":
|
elif protocol in {"SMARTCAM", "SMARTCAM.CHILD"}:
|
||||||
return SmartCamDevice
|
return SmartCamDevice
|
||||||
else:
|
else:
|
||||||
for d in STRIPS_IOT:
|
for d in STRIPS_IOT:
|
||||||
@ -434,7 +434,7 @@ async def get_device_for_fixture(
|
|||||||
d.protocol = FakeSmartProtocol(
|
d.protocol = FakeSmartProtocol(
|
||||||
fixture_data.data, fixture_data.name, verbatim=verbatim
|
fixture_data.data, fixture_data.name, verbatim=verbatim
|
||||||
)
|
)
|
||||||
elif fixture_data.protocol == "SMARTCAM":
|
elif fixture_data.protocol in {"SMARTCAM", "SMARTCAM.CHILD"}:
|
||||||
d.protocol = FakeSmartCamProtocol(
|
d.protocol = FakeSmartCamProtocol(
|
||||||
fixture_data.data, fixture_data.name, verbatim=verbatim
|
fixture_data.data, fixture_data.name, verbatim=verbatim
|
||||||
)
|
)
|
||||||
|
@ -7,6 +7,8 @@ import pytest
|
|||||||
from kasa import Credentials, DeviceConfig, SmartProtocol
|
from kasa import Credentials, DeviceConfig, SmartProtocol
|
||||||
from kasa.exceptions import SmartErrorCode
|
from kasa.exceptions import SmartErrorCode
|
||||||
from kasa.smart import SmartChildDevice
|
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
|
from kasa.transports.basetransport import BaseTransport
|
||||||
|
|
||||||
|
|
||||||
@ -227,16 +229,20 @@ class FakeSmartTransport(BaseTransport):
|
|||||||
# imported here to avoid circular import
|
# imported here to avoid circular import
|
||||||
from .conftest import filter_fixtures
|
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"]
|
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]
|
sw_version = sw_version.split(" ")[0]
|
||||||
model = child_dev_info["model"]
|
model = child_dev_info.get("device_model", child_dev_info.get("model"))
|
||||||
region = child_dev_info.get("specs", "XX")
|
assert sw_version
|
||||||
child_fixture_name = f"{model}({region})_{hw_version}_{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_fixtures = filter_fixtures(
|
||||||
"Child fixture",
|
"Child fixture",
|
||||||
protocol_filter={"SMART.CHILD"},
|
protocol_filter={protocol},
|
||||||
model_filter={child_fixture_name},
|
model_filter={child_fixture_name},
|
||||||
)
|
)
|
||||||
if child_fixtures:
|
if child_fixtures:
|
||||||
@ -249,7 +255,9 @@ class FakeSmartTransport(BaseTransport):
|
|||||||
and (category := child_info.get("category"))
|
and (category := child_info.get("category"))
|
||||||
and category in SmartChildDevice.CHILD_DEVICE_TYPE_MAP
|
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 = copy.deepcopy(fixture_info_tuple.data)
|
||||||
child_fixture["get_device_info"]["device_id"] = device_id
|
child_fixture["get_device_info"]["device_id"] = device_id
|
||||||
found_child_fixture_infos.append(child_fixture["get_device_info"])
|
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]
|
pytest.fixtures_missing_methods.setdefault( # type: ignore[attr-defined]
|
||||||
parent_fixture_name, set()
|
parent_fixture_name, set()
|
||||||
).add("child_devices")
|
).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:
|
else:
|
||||||
warn(
|
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,
|
stacklevel=2,
|
||||||
)
|
)
|
||||||
# Replace parent child infos with the infos from the child fixtures so
|
# 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 import Credentials, DeviceConfig, SmartProtocol
|
||||||
from kasa.protocols.smartcamprotocol import SmartCamProtocol
|
from kasa.protocols.smartcamprotocol import SmartCamProtocol
|
||||||
|
from kasa.smartcam.smartcamchild import CHILD_INFO_FROM_PARENT
|
||||||
from kasa.transports.basetransport import BaseTransport
|
from kasa.transports.basetransport import BaseTransport
|
||||||
|
|
||||||
from .fakeprotocol_smart import FakeSmartTransport
|
from .fakeprotocol_smart import FakeSmartTransport
|
||||||
@ -125,10 +126,26 @@ class FakeSmartCamTransport(BaseTransport):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_param_set_value(info: dict, set_keys: list[str], value):
|
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]:
|
for key in set_keys[:-1]:
|
||||||
info = info[key]
|
info = info[key]
|
||||||
info[set_keys[-1]] = value
|
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 = {
|
FIXTURE_MISSING_MAP = {
|
||||||
"getMatterSetupInfo": (
|
"getMatterSetupInfo": (
|
||||||
"matter",
|
"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_DEVICES = (
|
||||||
SUPPORTED_IOT_DEVICES
|
SUPPORTED_IOT_DEVICES
|
||||||
+ SUPPORTED_SMART_DEVICES
|
+ SUPPORTED_SMART_DEVICES
|
||||||
+ SUPPORTED_SMART_CHILD_DEVICES
|
+ SUPPORTED_SMART_CHILD_DEVICES
|
||||||
+ SUPPORTED_SMARTCAM_DEVICES
|
+ SUPPORTED_SMARTCAM_DEVICES
|
||||||
|
+ SUPPORTED_SMARTCAM_CHILD_DEVICES
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -82,14 +90,8 @@ def get_fixture_infos() -> list[FixtureInfo]:
|
|||||||
fixture_data = []
|
fixture_data = []
|
||||||
for file, protocol in SUPPORTED_DEVICES:
|
for file, protocol in SUPPORTED_DEVICES:
|
||||||
p = Path(file)
|
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)
|
data = json.load(f)
|
||||||
|
|
||||||
fixture_name = p.name
|
fixture_name = p.name
|
||||||
@ -188,7 +190,7 @@ def filter_fixtures(
|
|||||||
IotDevice._get_device_type_from_sys_info(fixture_data.data)
|
IotDevice._get_device_type_from_sys_info(fixture_data.data)
|
||||||
in device_type
|
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"]
|
info = fixture_data.data["getDeviceInfo"]["device_info"]["basic_info"]
|
||||||
return SmartCamDevice._get_device_type_from_sysinfo(info) in device_type
|
return SmartCamDevice._get_device_type_from_sysinfo(info) in device_type
|
||||||
return False
|
return False
|
||||||
|
@ -10,7 +10,13 @@ import pytest
|
|||||||
|
|
||||||
from kasa import Credentials, Device, DeviceType, Module, StreamResolution
|
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
|
@device_smartcam
|
||||||
@ -24,7 +30,7 @@ async def test_state(dev: Device):
|
|||||||
assert dev.is_on is not state
|
assert dev.is_on is not state
|
||||||
|
|
||||||
|
|
||||||
@camera_smartcam
|
@not_child_camera_smartcam
|
||||||
async def test_stream_rtsp_url(dev: Device):
|
async def test_stream_rtsp_url(dev: Device):
|
||||||
camera_module = dev.modules.get(Module.Camera)
|
camera_module = dev.modules.get(Module.Camera)
|
||||||
assert camera_module
|
assert camera_module
|
||||||
@ -84,7 +90,7 @@ async def test_stream_rtsp_url(dev: Device):
|
|||||||
assert url is None
|
assert url is None
|
||||||
|
|
||||||
|
|
||||||
@camera_smartcam
|
@not_child_camera_smartcam
|
||||||
async def test_onvif_url(dev: Device):
|
async def test_onvif_url(dev: Device):
|
||||||
"""Test the onvif url."""
|
"""Test the onvif url."""
|
||||||
camera_module = dev.modules.get(Module.Camera)
|
camera_module = dev.modules.get(Module.Camera)
|
||||||
|
@ -52,12 +52,12 @@ async def test_alias(dev):
|
|||||||
async def test_hub(dev):
|
async def test_hub(dev):
|
||||||
assert dev.children
|
assert dev.children
|
||||||
for child in dev.children:
|
for child in dev.children:
|
||||||
assert "Cloud" in child.modules
|
assert child.modules
|
||||||
assert child.modules["Cloud"].data
|
assert child.device_info
|
||||||
|
|
||||||
assert child.alias
|
assert child.alias
|
||||||
await child.update()
|
await child.update()
|
||||||
assert "Time" not in child.modules
|
assert child.device_id
|
||||||
assert child.time
|
|
||||||
|
|
||||||
|
|
||||||
@device_smartcam
|
@device_smartcam
|
||||||
|
@ -31,7 +31,7 @@ from kasa.iot.iottimezone import (
|
|||||||
)
|
)
|
||||||
from kasa.iot.modules import IotLightPreset
|
from kasa.iot.modules import IotLightPreset
|
||||||
from kasa.smart import SmartChildDevice, SmartDevice
|
from kasa.smart import SmartChildDevice, SmartDevice
|
||||||
from kasa.smartcam import SmartCamDevice
|
from kasa.smartcam import SmartCamChild, SmartCamDevice
|
||||||
|
|
||||||
|
|
||||||
def _get_subclasses(of_class):
|
def _get_subclasses(of_class):
|
||||||
@ -84,13 +84,24 @@ async def test_device_class_ctors(device_class_name_obj):
|
|||||||
credentials = Credentials("foo", "bar")
|
credentials = Credentials("foo", "bar")
|
||||||
config = DeviceConfig(host, port_override=port, credentials=credentials)
|
config = DeviceConfig(host, port_override=port, credentials=credentials)
|
||||||
klass = device_class_name_obj[1]
|
klass = device_class_name_obj[1]
|
||||||
if issubclass(klass, SmartChildDevice):
|
if issubclass(klass, SmartChildDevice | SmartCamChild):
|
||||||
parent = SmartDevice(host, config=config)
|
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(
|
dev = klass(
|
||||||
parent,
|
parent,
|
||||||
{"dummy": "info", "device_id": "dummy"},
|
{"dummy": "info", "device_id": "dummy", **smartcam_required},
|
||||||
{
|
{
|
||||||
"component_list": [{"id": "device", "ver_code": 1}],
|
"component_list": [{"id": "device", "ver_code": 1}],
|
||||||
|
"app_component_list": [{"name": "device", "version": 1}],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -108,13 +119,24 @@ async def test_device_class_repr(device_class_name_obj):
|
|||||||
credentials = Credentials("foo", "bar")
|
credentials = Credentials("foo", "bar")
|
||||||
config = DeviceConfig(host, port_override=port, credentials=credentials)
|
config = DeviceConfig(host, port_override=port, credentials=credentials)
|
||||||
klass = device_class_name_obj[1]
|
klass = device_class_name_obj[1]
|
||||||
if issubclass(klass, SmartChildDevice):
|
if issubclass(klass, SmartChildDevice | SmartCamChild):
|
||||||
parent = SmartDevice(host, config=config)
|
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(
|
dev = klass(
|
||||||
parent,
|
parent,
|
||||||
{"dummy": "info", "device_id": "dummy"},
|
{"dummy": "info", "device_id": "dummy", **smartcam_required},
|
||||||
{
|
{
|
||||||
"component_list": [{"id": "device", "ver_code": 1}],
|
"component_list": [{"id": "device", "ver_code": 1}],
|
||||||
|
"app_component_list": [{"name": "device", "version": 1}],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -132,11 +154,14 @@ async def test_device_class_repr(device_class_name_obj):
|
|||||||
SmartChildDevice: DeviceType.Unknown,
|
SmartChildDevice: DeviceType.Unknown,
|
||||||
SmartDevice: DeviceType.Unknown,
|
SmartDevice: DeviceType.Unknown,
|
||||||
SmartCamDevice: DeviceType.Camera,
|
SmartCamDevice: DeviceType.Camera,
|
||||||
|
SmartCamChild: DeviceType.Camera,
|
||||||
}
|
}
|
||||||
type_ = CLASS_TO_DEFAULT_TYPE[klass]
|
type_ = CLASS_TO_DEFAULT_TYPE[klass]
|
||||||
child_repr = "<DeviceType.Unknown(child) of <DeviceType.Unknown at 127.0.0.2 - update() needed>>"
|
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>"
|
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
|
assert repr(dev) == expected_repr
|
||||||
|
|
||||||
|
|
||||||
|
@ -4,11 +4,18 @@ import copy
|
|||||||
|
|
||||||
import pytest
|
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.iot import IotDevice
|
||||||
from kasa.protocols import IotProtocol
|
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.smart import SmartDevice
|
||||||
from kasa.smartcam import SmartCamDevice
|
from kasa.smartcam import SmartCamDevice
|
||||||
|
from kasa.smartcam.smartcamchild import CHILD_INFO_FROM_PARENT
|
||||||
|
|
||||||
from .conftest import (
|
from .conftest import (
|
||||||
FixtureInfo,
|
FixtureInfo,
|
||||||
@ -113,6 +120,18 @@ async def test_smartcam_fixtures(fixture_info: FixtureInfo):
|
|||||||
saved_fixture_data = {
|
saved_fixture_data = {
|
||||||
key: val for key, val in saved_fixture_data.items() if val != -1001
|
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
|
assert saved_fixture_data == created_child_fixture.data
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user