Add smartcam child device support for smartcam hubs (#1413)

This commit is contained in:
Steven B.
2025-01-14 08:38:04 +00:00
committed by GitHub
parent a211cc0af5
commit 589d15091a
13 changed files with 431 additions and 92 deletions

View File

@@ -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(

View File

@@ -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