From e9f9eb95f933a4c9f85488d52d2c11f811a9fe59 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Wed, 23 Oct 2024 12:12:32 +0100 Subject: [PATCH 01/17] Enable plain get requests --- devtools/dump_devinfo.py | 125 ++++++++++-------- kasa/experimental/smartcameraprotocol.py | 2 +- .../C210(EU)_2.0_1.4.2.json | 12 +- .../H200(US)_1.0_1.3.6.json | 0 4 files changed, 79 insertions(+), 60 deletions(-) rename kasa/tests/fixtures/{experimental => smartcamera}/C210(EU)_2.0_1.4.2.json (98%) rename kasa/tests/fixtures/{experimental => smartcamera}/H200(US)_1.0_1.3.6.json (100%) diff --git a/devtools/dump_devinfo.py b/devtools/dump_devinfo.py index 12e4c3cb8..44581793e 100644 --- a/devtools/dump_devinfo.py +++ b/devtools/dump_devinfo.py @@ -50,6 +50,7 @@ FixtureResult = namedtuple("FixtureResult", "filename, folder, data") SMART_FOLDER = "kasa/tests/fixtures/smart/" +SMARTCAMERA_FOLDER = "kasa/tests/fixtures/smartcamera/" SMART_CHILD_FOLDER = "kasa/tests/fixtures/smart/child/" IOT_FOLDER = "kasa/tests/fixtures/" @@ -136,7 +137,7 @@ def scrub(res): v = base64.b64encode(b"#MASKED_SSID#").decode() elif k in ["nickname"]: v = base64.b64encode(b"#MASKED_NAME#").decode() - elif k in ["alias", "device_alias"]: + elif k in ["alias", "device_alias", "device_name"]: v = "#MASKED_NAME#" elif isinstance(res[k], int): v = 0 @@ -534,61 +535,72 @@ async def get_smart_camera_test_calls(protocol: SmartProtocol): test_calls: list[SmartCall] = [] successes: list[SmartCall] = [] - requests = { - "getAlertTypeList": {"msg_alarm": {"name": "alert_type"}}, - "getNightVisionCapability": {"image_capability": {"name": ["supplement_lamp"]}}, - "getDeviceInfo": {"device_info": {"name": ["basic_info"]}}, - "getDetectionConfig": {"motion_detection": {"name": ["motion_det"]}}, - "getPersonDetectionConfig": {"people_detection": {"name": ["detection"]}}, - "getVehicleDetectionConfig": {"vehicle_detection": {"name": ["detection"]}}, - "getBCDConfig": {"sound_detection": {"name": ["bcd"]}}, - "getPetDetectionConfig": {"pet_detection": {"name": ["detection"]}}, - "getBarkDetectionConfig": {"bark_detection": {"name": ["detection"]}}, - "getMeowDetectionConfig": {"meow_detection": {"name": ["detection"]}}, - "getGlassDetectionConfig": {"glass_detection": {"name": ["detection"]}}, - "getTamperDetectionConfig": {"tamper_detection": {"name": "tamper_det"}}, - "getLensMaskConfig": {"lens_mask": {"name": ["lens_mask_info"]}}, - "getLdc": {"image": {"name": ["switch", "common"]}}, - "getLastAlarmInfo": {"msg_alarm": {"name": ["chn1_msg_alarm_info"]}}, - "getLedStatus": {"led": {"name": ["config"]}}, - "getTargetTrackConfig": {"target_track": {"name": ["target_track_info"]}}, - "getPresetConfig": {"preset": {"name": ["preset"]}}, - "getFirmwareUpdateStatus": {"cloud_config": {"name": "upgrade_status"}}, - "getMediaEncrypt": {"cet": {"name": ["media_encrypt"]}}, - "getConnectionType": {"network": {"get_connection_type": []}}, - "getAlarmConfig": {"msg_alarm": {}}, - "getAlarmPlan": {"msg_alarm_plan": {}}, - "getSirenTypeList": {"siren": {}}, - "getSirenConfig": {"siren": {}}, - "getAlertConfig": { - "msg_alarm": { - "name": ["chn1_msg_alarm_info", "capability"], - "table": ["usr_def_audio"], + requests: list[dict] = [ + {"getAlertTypeList": {"msg_alarm": {"name": "alert_type"}}}, + { + "getNightVisionCapability": { + "image_capability": {"name": ["supplement_lamp"]} } }, - "getLightTypeList": {"msg_alarm": {}}, - "getSirenStatus": {"siren": {}}, - "getLightFrequencyInfo": {"image": {"name": "common"}}, - "getLightFrequencyCapability": {"image": {"name": "common"}}, - "getRotationStatus": {"image": {"name": ["switch"]}}, - "getNightVisionModeConfig": {"image": {"name": "switch"}}, - "getWhitelampStatus": {"image": {"get_wtl_status": ["null"]}}, - "getWhitelampConfig": {"image": {"name": "switch"}}, - "getMsgPushConfig": {"msg_push": {"name": ["chn1_msg_push_info"]}}, - "getSdCardStatus": {"harddisk_manage": {"table": ["hd_info"]}}, - "getCircularRecordingConfig": {"harddisk_manage": {"name": "harddisk"}}, - "getRecordPlan": {"record_plan": {"name": ["chn1_channel"]}}, - "getAudioConfig": {"audio_config": {"name": ["speaker", "microphone"]}}, - "getFirmwareAutoUpgradeConfig": {"auto_upgrade": {"name": ["common"]}}, - "getVideoQualities": {"video": {"name": ["main"]}}, - "getVideoCapability": {"video_capability": {"name": "main"}}, - } + {"getDeviceInfo": {"device_info": {"name": ["basic_info"]}}}, + {"getDetectionConfig": {"motion_detection": {"name": ["motion_det"]}}}, + {"getPersonDetectionConfig": {"people_detection": {"name": ["detection"]}}}, + {"getVehicleDetectionConfig": {"vehicle_detection": {"name": ["detection"]}}}, + {"getBCDConfig": {"sound_detection": {"name": ["bcd"]}}}, + {"getPetDetectionConfig": {"pet_detection": {"name": ["detection"]}}}, + {"getBarkDetectionConfig": {"bark_detection": {"name": ["detection"]}}}, + {"getMeowDetectionConfig": {"meow_detection": {"name": ["detection"]}}}, + {"getGlassDetectionConfig": {"glass_detection": {"name": ["detection"]}}}, + {"getTamperDetectionConfig": {"tamper_detection": {"name": "tamper_det"}}}, + {"getLensMaskConfig": {"lens_mask": {"name": ["lens_mask_info"]}}}, + {"getLdc": {"image": {"name": ["switch", "common"]}}}, + {"getLastAlarmInfo": {"msg_alarm": {"name": ["chn1_msg_alarm_info"]}}}, + {"getLedStatus": {"led": {"name": ["config"]}}}, + {"getTargetTrackConfig": {"target_track": {"name": ["target_track_info"]}}}, + {"getPresetConfig": {"preset": {"name": ["preset"]}}}, + {"getFirmwareUpdateStatus": {"cloud_config": {"name": "upgrade_status"}}}, + {"getMediaEncrypt": {"cet": {"name": ["media_encrypt"]}}}, + {"getConnectionType": {"network": {"get_connection_type": []}}}, + {"getAlarmConfig": {"msg_alarm": {}}}, + {"getAlarmPlan": {"msg_alarm_plan": {}}}, + {"getSirenTypeList": {"siren": {}}}, + {"getSirenConfig": {"siren": {}}}, + { + "getAlertConfig": { + "msg_alarm": { + "name": ["chn1_msg_alarm_info", "capability"], + "table": ["usr_def_audio"], + } + } + }, + {"getLightTypeList": {"msg_alarm": {}}}, + {"getSirenStatus": {"siren": {}}}, + {"getLightFrequencyInfo": {"image": {"name": "common"}}}, + {"getLightFrequencyCapability": {"image": {"name": "common"}}}, + {"getRotationStatus": {"image": {"name": ["switch"]}}}, + {"getNightVisionModeConfig": {"image": {"name": "switch"}}}, + {"getWhitelampStatus": {"image": {"get_wtl_status": ["null"]}}}, + {"getWhitelampConfig": {"image": {"name": "switch"}}}, + {"getMsgPushConfig": {"msg_push": {"name": ["chn1_msg_push_info"]}}}, + {"getSdCardStatus": {"harddisk_manage": {"table": ["hd_info"]}}}, + {"getCircularRecordingConfig": {"harddisk_manage": {"name": "harddisk"}}}, + {"getRecordPlan": {"record_plan": {"name": ["chn1_channel"]}}}, + {"getAudioConfig": {"audio_config": {"name": ["speaker", "microphone"]}}}, + {"getFirmwareAutoUpgradeConfig": {"auto_upgrade": {"name": ["common"]}}}, + {"getVideoQualities": {"video": {"name": ["main"]}}}, + {"getVideoCapability": {"video_capability": {"name": "main"}}}, + # get only methods + # {"get": {"function": {"name": ["module_spec"]}}}, + ] test_calls = [] - for method, params in requests.items(): + for request in requests: + method = next(iter(request)) + if method == "get": + method = method + "_" + next(iter(request[method])) test_calls.append( SmartCall( module=method, - request={method: params}, + request=request, should_succeed=True, child_device_id="", ) @@ -660,11 +672,14 @@ async def get_smart_camera_test_calls(protocol: SmartProtocol): click.echo(f"Skipping {component_id}..", nl=False) click.echo(click.style("UNSUPPORTED", fg="yellow")) else: # Not a smart protocol device so assume camera protocol - for method, params in requests.items(): + for request in requests: + method = next(iter(request)) + if method == "get": + method = method + "_" + next(iter(request[method])) test_calls.append( SmartCall( module=method, - request={method: params}, + request=request, should_succeed=True, child_device_id=child_id, ) @@ -963,6 +978,7 @@ async def get_smart_fixtures( click.echo(click.style("## device info file ##", bold=True)) if "get_device_info" in final: + # smart protocol hw_version = final["get_device_info"]["hw_ver"] sw_version = final["get_device_info"]["fw_ver"] if discovery_info: @@ -970,16 +986,19 @@ async def get_smart_fixtures( else: model = final["get_device_info"]["model"] + "(XX)" sw_version = sw_version.split(" ", maxsplit=1)[0] + copy_folder = SMART_FOLDER else: + # smart camera protocol hw_version = final["getDeviceInfo"]["device_info"]["basic_info"]["hw_version"] sw_version = final["getDeviceInfo"]["device_info"]["basic_info"]["sw_version"] model = final["getDeviceInfo"]["device_info"]["basic_info"]["device_model"] region = final["getDeviceInfo"]["device_info"]["basic_info"]["region"] sw_version = sw_version.split(" ", maxsplit=1)[0] model = f"{model}({region})" + copy_folder = SMARTCAMERA_FOLDER save_filename = f"{model}_{hw_version}_{sw_version}.json" - copy_folder = SMART_FOLDER + fixture_results.insert( 0, FixtureResult(filename=save_filename, folder=copy_folder, data=final) ) diff --git a/kasa/experimental/smartcameraprotocol.py b/kasa/experimental/smartcameraprotocol.py index 785796160..46ffab9fc 100644 --- a/kasa/experimental/smartcameraprotocol.py +++ b/kasa/experimental/smartcameraprotocol.py @@ -74,7 +74,7 @@ async def _execute_query( if method == "multipleRequest": params = request["multipleRequest"] req = {"method": "multipleRequest", "params": params} - elif method[:3] == "set": + elif method.startswith("set") or method == "get": params = next(iter(request[method])) req = { "method": method[:3], diff --git a/kasa/tests/fixtures/experimental/C210(EU)_2.0_1.4.2.json b/kasa/tests/fixtures/smartcamera/C210(EU)_2.0_1.4.2.json similarity index 98% rename from kasa/tests/fixtures/experimental/C210(EU)_2.0_1.4.2.json rename to kasa/tests/fixtures/smartcamera/C210(EU)_2.0_1.4.2.json index 304a1e126..17a8cd650 100644 --- a/kasa/tests/fixtures/experimental/C210(EU)_2.0_1.4.2.json +++ b/kasa/tests/fixtures/smartcamera/C210(EU)_2.0_1.4.2.json @@ -5,14 +5,14 @@ "connect_type": "wireless", "device_id": "0000000000000000000000000000000000000000", "http_port": 443, - "last_alarm_time": "0", - "last_alarm_type": "", + "last_alarm_time": "1729264456", + "last_alarm_type": "motion", "owner": "00000000000000000000000000000000", "sd_status": "offline" }, "device_id": "00000000000000000000000000000000", "device_model": "C210", - "device_name": "00000 000", + "device_name": "#MASKED_NAME#", "device_type": "SMART.IPCAMERA", "encrypt_info": { "data": "", @@ -108,8 +108,8 @@ }, "getConnectionType": { "link_type": "wifi", - "rssi": "2", - "rssiValue": -64, + "rssi": "3", + "rssiValue": -62, "ssid": "I01BU0tFRF9TU0lEIw==" }, "getDetectionConfig": { @@ -133,7 +133,7 @@ "device_alias": "#MASKED_NAME#", "device_info": "C210 2.0 IPC", "device_model": "C210", - "device_name": "0000 0.0", + "device_name": "#MASKED_NAME#", "device_type": "SMART.IPCAMERA", "features": 3, "ffs": false, diff --git a/kasa/tests/fixtures/experimental/H200(US)_1.0_1.3.6.json b/kasa/tests/fixtures/smartcamera/H200(US)_1.0_1.3.6.json similarity index 100% rename from kasa/tests/fixtures/experimental/H200(US)_1.0_1.3.6.json rename to kasa/tests/fixtures/smartcamera/H200(US)_1.0_1.3.6.json From 2e2c13499af917c1ac439a6e9dc6534de48191ab Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Wed, 23 Oct 2024 14:52:26 +0100 Subject: [PATCH 02/17] Update dump_devinfo to handle single smartcamera requests --- devtools/dump_devinfo.py | 97 ++++++-- kasa/experimental/smartcameraprotocol.py | 38 ++- kasa/smartprotocol.py | 8 +- .../smartcamera/C210(EU)_2.0_1.4.2.json | 223 ++++++++++++++++-- .../smartcamera/H200(US)_1.0_1.3.6.json | 16 ++ 5 files changed, 332 insertions(+), 50 deletions(-) diff --git a/devtools/dump_devinfo.py b/devtools/dump_devinfo.py index 44581793e..f597610bd 100644 --- a/devtools/dump_devinfo.py +++ b/devtools/dump_devinfo.py @@ -12,6 +12,7 @@ import base64 import collections.abc +import dataclasses import json import logging import re @@ -46,7 +47,6 @@ from kasa.smartprotocol import SmartProtocol, _ChildProtocolWrapper Call = namedtuple("Call", "module method") -SmartCall = namedtuple("SmartCall", "module request should_succeed child_device_id") FixtureResult = namedtuple("FixtureResult", "filename, folder, data") SMART_FOLDER = "kasa/tests/fixtures/smart/" @@ -59,6 +59,17 @@ _LOGGER = logging.getLogger(__name__) +@dataclasses.dataclass +class SmartCall: + """Class for smart and smartcamera calls.""" + + module: str + request: dict + should_succeed: bool + child_device_id: str + supports_multiple: bool = True + + def scrub(res): """Remove identifiers from the given dict.""" keys_to_scrub = [ @@ -478,6 +489,38 @@ def format_exception(e): return exception_str +async def _make_final_calls( + protocol: SmartProtocol, + calls: list[SmartCall], + name: str, + batch_size: int, + *, + child_device_id: str, +) -> dict[str, dict]: + multiple_requests = { + key: smartcall.request[key] + for smartcall in calls + if smartcall.supports_multiple and (key := next(iter(smartcall.request))) + } + final = await _make_requests_or_exit( + protocol, + multiple_requests, + name + " - multiple", + batch_size, + child_device_id=child_device_id, + ) + single_calls = [smartcall for smartcall in calls if not smartcall.supports_multiple] + for smartcall in single_calls: + final[smartcall.module] = await _make_requests_or_exit( + protocol, + smartcall.request, + f"{name} + {smartcall.module}", + batch_size, + child_device_id=child_device_id, + ) + return final + + async def _make_requests_or_exit( protocol: SmartProtocol, requests: dict, @@ -554,17 +597,13 @@ async def get_smart_camera_test_calls(protocol: SmartProtocol): {"getTamperDetectionConfig": {"tamper_detection": {"name": "tamper_det"}}}, {"getLensMaskConfig": {"lens_mask": {"name": ["lens_mask_info"]}}}, {"getLdc": {"image": {"name": ["switch", "common"]}}}, - {"getLastAlarmInfo": {"msg_alarm": {"name": ["chn1_msg_alarm_info"]}}}, + {"getLastAlarmInfo": {"system": {"name": ["last_alarm_info"]}}}, {"getLedStatus": {"led": {"name": ["config"]}}}, {"getTargetTrackConfig": {"target_track": {"name": ["target_track_info"]}}}, {"getPresetConfig": {"preset": {"name": ["preset"]}}}, {"getFirmwareUpdateStatus": {"cloud_config": {"name": "upgrade_status"}}}, {"getMediaEncrypt": {"cet": {"name": ["media_encrypt"]}}}, {"getConnectionType": {"network": {"get_connection_type": []}}}, - {"getAlarmConfig": {"msg_alarm": {}}}, - {"getAlarmPlan": {"msg_alarm_plan": {}}}, - {"getSirenTypeList": {"siren": {}}}, - {"getSirenConfig": {"siren": {}}}, { "getAlertConfig": { "msg_alarm": { @@ -573,10 +612,12 @@ async def get_smart_camera_test_calls(protocol: SmartProtocol): } } }, + {"getAlertPlan": {"msg_alarm_plan": {"name": "chn1_msg_alarm_plan"}}}, + {"getSirenTypeList": {"siren": {}}}, + {"getSirenConfig": {"siren": {}}}, {"getLightTypeList": {"msg_alarm": {}}}, {"getSirenStatus": {"siren": {}}}, {"getLightFrequencyInfo": {"image": {"name": "common"}}}, - {"getLightFrequencyCapability": {"image": {"name": "common"}}}, {"getRotationStatus": {"image": {"name": ["switch"]}}}, {"getNightVisionModeConfig": {"image": {"name": "switch"}}}, {"getWhitelampStatus": {"image": {"get_wtl_status": ["null"]}}}, @@ -589,20 +630,31 @@ async def get_smart_camera_test_calls(protocol: SmartProtocol): {"getFirmwareAutoUpgradeConfig": {"auto_upgrade": {"name": ["common"]}}}, {"getVideoQualities": {"video": {"name": ["main"]}}}, {"getVideoCapability": {"video_capability": {"name": "main"}}}, - # get only methods - # {"get": {"function": {"name": ["module_spec"]}}}, + # single request only methods + {"get": {"function": {"name": ["module_spec"]}}}, + {"get": {"cet": {"name": ["vhttpd"]}}}, + {"get": {"motor": {"name": ["capability"]}}}, + { + "get": { + "audio_capability": {"name": ["device_speaker", "device_microphone"]} + } + }, + {"get": {"audio_config": {"name": ["speaker", "microphone"]}}}, ] test_calls = [] for request in requests: method = next(iter(request)) if method == "get": - method = method + "_" + next(iter(request[method])) + module = method + "_" + next(iter(request[method])) + else: + module = method test_calls.append( SmartCall( - module=method, + module=module, request=request, should_succeed=True, child_device_id="", + supports_multiple=(method != "get"), ) ) @@ -819,7 +871,9 @@ async def get_smart_test_calls(protocol: SmartProtocol): click.echo(click.style("UNSUPPORTED", fg="yellow")) # Add the extra calls for each child for extra_call in extra_test_calls: - extra_child_call = extra_call._replace(child_device_id=child_device_id) + extra_child_call = dataclasses.replace( + extra_call, child_device_id=child_device_id + ) test_calls.append(extra_child_call) return test_calls, successes @@ -894,10 +948,10 @@ async def get_smart_fixtures( finally: await protocol.close() - device_requests: dict[str, dict] = {} + device_requests: dict[str, list[SmartCall]] = {} for success in successes: - device_request = device_requests.setdefault(success.child_device_id, {}) - device_request.update(success.request) + device_request = device_requests.setdefault(success.child_device_id, []) + device_request.append(success) scrubbed_device_ids = { device_id: f"SCRUBBED_CHILD_DEVICE_ID_{index}" @@ -905,24 +959,21 @@ async def get_smart_fixtures( if device_id != "" } - final = await _make_requests_or_exit( - protocol, - device_requests[""], - "all successes at once", - batch_size, - child_device_id="", + final = await _make_final_calls( + protocol, device_requests[""], "All successes", batch_size, child_device_id="" ) fixture_results = [] for child_device_id, requests in device_requests.items(): if child_device_id == "": continue - response = await _make_requests_or_exit( + response = await _make_final_calls( protocol, requests, - "all child successes at once", + "All child successes", batch_size, child_device_id=child_device_id, ) + 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 diff --git a/kasa/experimental/smartcameraprotocol.py b/kasa/experimental/smartcameraprotocol.py index 46ffab9fc..9e584997a 100644 --- a/kasa/experimental/smartcameraprotocol.py +++ b/kasa/experimental/smartcameraprotocol.py @@ -22,6 +22,18 @@ _LOGGER = logging.getLogger(__name__) +# List of getMethodNames that should be sent as {"method":"do"} +# https://md.depau.eu/s/r1Ys_oWoP#Modules +GET_METHODS_AS_DO = { + "getSdCardFormatStatus", + "getConnectionType", + "getUserID", + "getP2PSharePassword", + "getAESEncryptKey", + "getFirmwareAFResult", + "getWhitelampStatus", +} + class SmartCameraProtocol(SmartProtocol): """Class for SmartCamera Protocol.""" @@ -67,21 +79,29 @@ async def _execute_query( self, request: str | dict, *, retry_count: int, iterate_list_pages: bool = True ) -> dict: debug_enabled = _LOGGER.isEnabledFor(logging.DEBUG) - + is_do_method = False if isinstance(request, dict): if len(request) == 1: method = next(iter(request)) if method == "multipleRequest": params = request["multipleRequest"] req = {"method": "multipleRequest", "params": params} - elif method.startswith("set") or method == "get": + elif (short_method := method[:3]) and short_method in {"get", "set"}: params = next(iter(request[method])) + if method in GET_METHODS_AS_DO: + is_do_method = True + short_method = "do" req = { - "method": method[:3], + "method": short_method, params: request[method][params], } else: - return await self._execute_multiple_query(request, retry_count) + is_do_method = True + params = next(iter(request[method])) + req = { + "method": "do", + params: request[method][params], + } else: return await self._execute_multiple_query(request, retry_count) else: @@ -112,10 +132,18 @@ async def _execute_query( if "error_code" in response_data: # H200 does not return an error code self._handle_response_error_code(response_data, method) + # Requests that are invalid and raise PROTOCOL_FORMAT_ERROR when sent + # as a multipleRequest will return {} when sent as a single request. + if method.startswith("get") and ( + not (section := next(iter(response_data))) or response_data[section] == {} + ): + raise DeviceError(f"No results for get request {method}") # TODO need to update handle response lists - if method[:3] == "set": + if is_do_method: + return {method: response_data} + if method.startswith("set"): return {} if method == "multipleRequest": return {method: response_data["result"]} diff --git a/kasa/smartprotocol.py b/kasa/smartprotocol.py index 0c2a2bba5..33841b3a8 100644 --- a/kasa/smartprotocol.py +++ b/kasa/smartprotocol.py @@ -176,10 +176,6 @@ async def _execute_multiple_query(self, requests: dict, retry_count: int) -> dic multi_result[method] = resp["result"] return multi_result - # The SmartCameraProtocol sends requests with a length 1 as a - # multipleRequest. The SmartProtocol doesn't so will never - # raise_on_error - raise_on_error = end == 1 for batch_num, i in enumerate(range(0, end, step)): requests_step = multi_requests[i : i + step] @@ -227,9 +223,7 @@ async def _execute_multiple_query(self, requests: dict, retry_count: int) -> dic responses = response_step["result"]["responses"] for response in responses: method = response["method"] - self._handle_response_error_code( - response, method, raise_on_error=raise_on_error - ) + self._handle_response_error_code(response, method, raise_on_error=False) result = response.get("result", None) await self._handle_response_lists( result, method, retry_count=retry_count diff --git a/kasa/tests/fixtures/smartcamera/C210(EU)_2.0_1.4.2.json b/kasa/tests/fixtures/smartcamera/C210(EU)_2.0_1.4.2.json index 17a8cd650..e3d8c42be 100644 --- a/kasa/tests/fixtures/smartcamera/C210(EU)_2.0_1.4.2.json +++ b/kasa/tests/fixtures/smartcamera/C210(EU)_2.0_1.4.2.json @@ -60,6 +60,14 @@ "usr_def_audio": [] } }, + "getAlertPlan": { + "msg_alarm_plan": { + "chn1_msg_alarm_plan": { + "alarm_plan_1": "0000-0000,127", + "enabled": "off" + } + } + }, "getAlertTypeList": { "msg_alarm": { "alert_type": { @@ -108,8 +116,8 @@ }, "getConnectionType": { "link_type": "wifi", - "rssi": "3", - "rssiValue": -62, + "rssi": "2", + "rssiValue": -63, "ssid": "I01BU0tFRF9TU0lEIw==" }, "getDetectionConfig": { @@ -171,19 +179,10 @@ } }, "getLastAlarmInfo": { - "msg_alarm": { - "chn1_msg_alarm_info": { - "alarm_duration": "0", - "alarm_mode": [ - "sound", - "light" - ], - "alarm_type": "0", - "alarm_volume": "high", - "enabled": "off", - "light_alarm_enabled": "on", - "light_type": "1", - "sound_alarm_enabled": "on" + "system": { + "last_alarm_info": { + "last_alarm_time": "1729264456", + "last_alarm_type": "motion" } } }, @@ -602,5 +601,199 @@ "getWhitelampStatus": { "rest_time": 0, "status": 0 + }, + "get_audio_capability": { + "get": { + "audio_capability": { + "device_microphone": { + "aec": "1", + "channels": "1", + "echo_cancelling": "0", + "encode_type": [ + "G711alaw" + ], + "half_duplex": "1", + "mute": "1", + "noise_cancelling": "1", + "sampling_rate": [ + "8", + "16" + ], + "volume": "1" + }, + "device_speaker": { + "channels": "1", + "decode_type": [ + "G711alaw", + "G711ulaw" + ], + "mute": "0", + "output_device_type": "0", + "sampling_rate": [ + "8", + "16" + ], + "system_volume": "100", + "volume": "1" + } + } + } + }, + "get_audio_config": { + "get": { + "audio_config": { + "microphone": { + "bitrate": "64", + "channels": "1", + "echo_cancelling": "off", + "encode_type": "G711alaw", + "input_device_type": "MicIn", + "mute": "off", + "noise_cancelling": "on", + "sampling_rate": "8", + "volume": "100" + }, + "speaker": { + "mute": "off", + "output_device_type": "SpeakerOut", + "volume": "100" + } + } + } + }, + "get_cet": { + "get": { + "cet": { + "vhttpd": { + "port": "8800" + } + } + } + }, + "get_function": { + "get": { + "function": { + "module_spec": { + "ae_weighting_table_resolution": "5*5", + "ai_enhance_capability": "1", + "ai_enhance_range": [ + "traditional_enhance" + ], + "ai_firmware_upgrade": "0", + "alarm_out_num": "0", + "app_version": "1.0.0", + "audio": [ + "speaker", + "microphone" + ], + "auth_encrypt": "1", + "auto_ip_configurable": "1", + "backlight_coexistence": "1", + "change_password": "1", + "client_info": "1", + "cloud_storage_version": "1.0", + "config_recovery": [ + "audio_config", + "OSD", + "image", + "video" + ], + "custom_area_compensation": "1", + "custom_auto_mode_exposure_level": "1", + "daynight_subdivision": "1", + "device_share": [ + "preview", + "playback", + "voice", + "cloud_storage", + "motor" + ], + "download": [ + "video" + ], + "events": [ + "motion", + "tamper" + ], + "force_iframe_support": "1", + "greeter": "1.0", + "http_system_state_audio_support": "1", + "image_capability": "1", + "image_list": [ + "supplement_lamp", + "expose" + ], + "ir_led_pwm_control": "1", + "led": "1", + "lens_mask": "1", + "linkage_capability": "1", + "local_storage": "1", + "media_encrypt": "1", + "motor": "0", + "msg_alarm": "1", + "msg_alarm_list": [ + "sound", + "light" + ], + "msg_push": "1", + "multi_user": "0", + "multicast": "0", + "network": [ + "wifi" + ], + "osd_capability": "1", + "ota_upgrade": "1", + "p2p_support_versions": [ + "1.1" + ], + "personalized_audio_alarm": "0", + "playback": [ + "local", + "p2p", + "relay" + ], + "playback_scale": "1", + "preview": [ + "local", + "p2p", + "relay" + ], + "privacy_mask_api_version": "1.0", + "ptz": "1", + "record_max_slot_cnt": "10", + "record_type": [ + "timing", + "motion" + ], + "relay_support_versions": [ + "1.3" + ], + "remote_upgrade": "1", + "reonboarding": "1", + "smart_codec": "0", + "smart_detection": "1", + "smart_msg_push_capability": "1", + "ssl_cer_version": "1.0", + "storage_api_version": "2.2", + "storage_capability": "1", + "stream_max_sessions": "10", + "streaming_support_versions": [ + "1.0" + ], + "tapo_care_version": "1.0.0", + "target_track": "1", + "timing_reboot": "1", + "verification_change_password": "1", + "video_codec": [ + "h264" + ], + "video_detection_digital_sensitivity": "1", + "wide_range_inf_sensitivity": "1", + "wifi_cascade_connection": "1", + "wifi_connection_info": "1", + "wireless_hotspot": "1" + } + } + } } } diff --git a/kasa/tests/fixtures/smartcamera/H200(US)_1.0_1.3.6.json b/kasa/tests/fixtures/smartcamera/H200(US)_1.0_1.3.6.json index c76662960..544ab267f 100644 --- a/kasa/tests/fixtures/smartcamera/H200(US)_1.0_1.3.6.json +++ b/kasa/tests/fixtures/smartcamera/H200(US)_1.0_1.3.6.json @@ -210,6 +210,22 @@ } } }, + "getTimezone": { + "system": { + "basic": { + "zone_id": "Australia/Canberra", + "timezone": "UTC+10:00" + } + } + }, + "getClockStatus": { + "system": { + "clock_status": { + "seconds_from_1970": 1729509322, + "local_time": "2024-10-21 22:15:22" + } + } + }, "getFirmwareAutoUpgradeConfig": { "auto_upgrade": { "common": { From 72618bfe8b5f5f6a4ec30e65d072bd7b4d101cdd Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Wed, 23 Oct 2024 18:07:19 +0100 Subject: [PATCH 03/17] Update post review --- devtools/dump_devinfo.py | 68 +--------------- devtools/helpers/smartcamerarequests.py | 59 ++++++++++++++ kasa/experimental/smartcameraprotocol.py | 80 ++++++++++++------- .../smartcamera/C210(EU)_2.0_1.4.2.json | 4 +- 4 files changed, 113 insertions(+), 98 deletions(-) create mode 100644 devtools/helpers/smartcamerarequests.py diff --git a/devtools/dump_devinfo.py b/devtools/dump_devinfo.py index f597610bd..c37a6a389 100644 --- a/devtools/dump_devinfo.py +++ b/devtools/dump_devinfo.py @@ -24,6 +24,7 @@ import asyncclick as click +from devtools.helpers.smartcamerarequests import SMARTCAMERA_REQUESTS from devtools.helpers.smartrequests import SmartRequest, get_component_requests from kasa import ( AuthenticationError, @@ -578,71 +579,8 @@ async def get_smart_camera_test_calls(protocol: SmartProtocol): test_calls: list[SmartCall] = [] successes: list[SmartCall] = [] - requests: list[dict] = [ - {"getAlertTypeList": {"msg_alarm": {"name": "alert_type"}}}, - { - "getNightVisionCapability": { - "image_capability": {"name": ["supplement_lamp"]} - } - }, - {"getDeviceInfo": {"device_info": {"name": ["basic_info"]}}}, - {"getDetectionConfig": {"motion_detection": {"name": ["motion_det"]}}}, - {"getPersonDetectionConfig": {"people_detection": {"name": ["detection"]}}}, - {"getVehicleDetectionConfig": {"vehicle_detection": {"name": ["detection"]}}}, - {"getBCDConfig": {"sound_detection": {"name": ["bcd"]}}}, - {"getPetDetectionConfig": {"pet_detection": {"name": ["detection"]}}}, - {"getBarkDetectionConfig": {"bark_detection": {"name": ["detection"]}}}, - {"getMeowDetectionConfig": {"meow_detection": {"name": ["detection"]}}}, - {"getGlassDetectionConfig": {"glass_detection": {"name": ["detection"]}}}, - {"getTamperDetectionConfig": {"tamper_detection": {"name": "tamper_det"}}}, - {"getLensMaskConfig": {"lens_mask": {"name": ["lens_mask_info"]}}}, - {"getLdc": {"image": {"name": ["switch", "common"]}}}, - {"getLastAlarmInfo": {"system": {"name": ["last_alarm_info"]}}}, - {"getLedStatus": {"led": {"name": ["config"]}}}, - {"getTargetTrackConfig": {"target_track": {"name": ["target_track_info"]}}}, - {"getPresetConfig": {"preset": {"name": ["preset"]}}}, - {"getFirmwareUpdateStatus": {"cloud_config": {"name": "upgrade_status"}}}, - {"getMediaEncrypt": {"cet": {"name": ["media_encrypt"]}}}, - {"getConnectionType": {"network": {"get_connection_type": []}}}, - { - "getAlertConfig": { - "msg_alarm": { - "name": ["chn1_msg_alarm_info", "capability"], - "table": ["usr_def_audio"], - } - } - }, - {"getAlertPlan": {"msg_alarm_plan": {"name": "chn1_msg_alarm_plan"}}}, - {"getSirenTypeList": {"siren": {}}}, - {"getSirenConfig": {"siren": {}}}, - {"getLightTypeList": {"msg_alarm": {}}}, - {"getSirenStatus": {"siren": {}}}, - {"getLightFrequencyInfo": {"image": {"name": "common"}}}, - {"getRotationStatus": {"image": {"name": ["switch"]}}}, - {"getNightVisionModeConfig": {"image": {"name": "switch"}}}, - {"getWhitelampStatus": {"image": {"get_wtl_status": ["null"]}}}, - {"getWhitelampConfig": {"image": {"name": "switch"}}}, - {"getMsgPushConfig": {"msg_push": {"name": ["chn1_msg_push_info"]}}}, - {"getSdCardStatus": {"harddisk_manage": {"table": ["hd_info"]}}}, - {"getCircularRecordingConfig": {"harddisk_manage": {"name": "harddisk"}}}, - {"getRecordPlan": {"record_plan": {"name": ["chn1_channel"]}}}, - {"getAudioConfig": {"audio_config": {"name": ["speaker", "microphone"]}}}, - {"getFirmwareAutoUpgradeConfig": {"auto_upgrade": {"name": ["common"]}}}, - {"getVideoQualities": {"video": {"name": ["main"]}}}, - {"getVideoCapability": {"video_capability": {"name": "main"}}}, - # single request only methods - {"get": {"function": {"name": ["module_spec"]}}}, - {"get": {"cet": {"name": ["vhttpd"]}}}, - {"get": {"motor": {"name": ["capability"]}}}, - { - "get": { - "audio_capability": {"name": ["device_speaker", "device_microphone"]} - } - }, - {"get": {"audio_config": {"name": ["speaker", "microphone"]}}}, - ] test_calls = [] - for request in requests: + for request in SMARTCAMERA_REQUESTS: method = next(iter(request)) if method == "get": module = method + "_" + next(iter(request[method])) @@ -724,7 +662,7 @@ async def get_smart_camera_test_calls(protocol: SmartProtocol): click.echo(f"Skipping {component_id}..", nl=False) click.echo(click.style("UNSUPPORTED", fg="yellow")) else: # Not a smart protocol device so assume camera protocol - for request in requests: + for request in SMARTCAMERA_REQUESTS: method = next(iter(request)) if method == "get": method = method + "_" + next(iter(request[method])) diff --git a/devtools/helpers/smartcamerarequests.py b/devtools/helpers/smartcamerarequests.py new file mode 100644 index 000000000..8c623e838 --- /dev/null +++ b/devtools/helpers/smartcamerarequests.py @@ -0,0 +1,59 @@ +"""Module for smart camera requests.""" + +from __future__ import annotations + +SMARTCAMERA_REQUESTS: list[dict] = [ + {"getAlertTypeList": {"msg_alarm": {"name": "alert_type"}}}, + {"getNightVisionCapability": {"image_capability": {"name": ["supplement_lamp"]}}}, + {"getDeviceInfo": {"device_info": {"name": ["basic_info"]}}}, + {"getDetectionConfig": {"motion_detection": {"name": ["motion_det"]}}}, + {"getPersonDetectionConfig": {"people_detection": {"name": ["detection"]}}}, + {"getVehicleDetectionConfig": {"vehicle_detection": {"name": ["detection"]}}}, + {"getBCDConfig": {"sound_detection": {"name": ["bcd"]}}}, + {"getPetDetectionConfig": {"pet_detection": {"name": ["detection"]}}}, + {"getBarkDetectionConfig": {"bark_detection": {"name": ["detection"]}}}, + {"getMeowDetectionConfig": {"meow_detection": {"name": ["detection"]}}}, + {"getGlassDetectionConfig": {"glass_detection": {"name": ["detection"]}}}, + {"getTamperDetectionConfig": {"tamper_detection": {"name": "tamper_det"}}}, + {"getLensMaskConfig": {"lens_mask": {"name": ["lens_mask_info"]}}}, + {"getLdc": {"image": {"name": ["switch", "common"]}}}, + {"getLastAlarmInfo": {"system": {"name": ["last_alarm_info"]}}}, + {"getLedStatus": {"led": {"name": ["config"]}}}, + {"getTargetTrackConfig": {"target_track": {"name": ["target_track_info"]}}}, + {"getPresetConfig": {"preset": {"name": ["preset"]}}}, + {"getFirmwareUpdateStatus": {"cloud_config": {"name": "upgrade_status"}}}, + {"getMediaEncrypt": {"cet": {"name": ["media_encrypt"]}}}, + {"getConnectionType": {"network": {"get_connection_type": []}}}, + { + "getAlertConfig": { + "msg_alarm": { + "name": ["chn1_msg_alarm_info", "capability"], + "table": ["usr_def_audio"], + } + } + }, + {"getAlertPlan": {"msg_alarm_plan": {"name": "chn1_msg_alarm_plan"}}}, + {"getSirenTypeList": {"siren": {}}}, + {"getSirenConfig": {"siren": {}}}, + {"getLightTypeList": {"msg_alarm": {}}}, + {"getSirenStatus": {"siren": {}}}, + {"getLightFrequencyInfo": {"image": {"name": "common"}}}, + {"getRotationStatus": {"image": {"name": ["switch"]}}}, + {"getNightVisionModeConfig": {"image": {"name": "switch"}}}, + {"getWhitelampStatus": {"image": {"get_wtl_status": ["null"]}}}, + {"getWhitelampConfig": {"image": {"name": "switch"}}}, + {"getMsgPushConfig": {"msg_push": {"name": ["chn1_msg_push_info"]}}}, + {"getSdCardStatus": {"harddisk_manage": {"table": ["hd_info"]}}}, + {"getCircularRecordingConfig": {"harddisk_manage": {"name": "harddisk"}}}, + {"getRecordPlan": {"record_plan": {"name": ["chn1_channel"]}}}, + {"getAudioConfig": {"audio_config": {"name": ["speaker", "microphone"]}}}, + {"getFirmwareAutoUpgradeConfig": {"auto_upgrade": {"name": ["common"]}}}, + {"getVideoQualities": {"video": {"name": ["main"]}}}, + {"getVideoCapability": {"video_capability": {"name": "main"}}}, + # single request only methods + {"get": {"function": {"name": ["module_spec"]}}}, + {"get": {"cet": {"name": ["vhttpd"]}}}, + {"get": {"motor": {"name": ["capability"]}}}, + {"get": {"audio_capability": {"name": ["device_speaker", "device_microphone"]}}}, + {"get": {"audio_config": {"name": ["speaker", "microphone"]}}}, +] diff --git a/kasa/experimental/smartcameraprotocol.py b/kasa/experimental/smartcameraprotocol.py index 9e584997a..4cdfb4cef 100644 --- a/kasa/experimental/smartcameraprotocol.py +++ b/kasa/experimental/smartcameraprotocol.py @@ -75,45 +75,63 @@ async def close(self) -> None: """Close the underlying transport.""" await self._transport.close() + @staticmethod + def _get_smart_camera_single_request( + request: dict[str, dict[str, Any]], + ) -> tuple[str, str, str, dict]: + method = next(iter(request)) + if method == "multipleRequest": + method_type = "multi" + params = request["multipleRequest"] + req = {"method": "multipleRequest", "params": params} + return "multi", "multipleRequest", "", req + + if (short_method := method[:3]) and short_method in {"get", "set"}: + method_type = short_method + param = next(iter(request[method])) + if method in GET_METHODS_AS_DO: + method_type = "do" + req = { + "method": method_type, + param: request[method][param], + } + else: + method_type = "do" + param = next(iter(request[method])) + req = { + "method": method_type, + param: request[method][param], + } + return method_type, method, param, req + async def _execute_query( self, request: str | dict, *, retry_count: int, iterate_list_pages: bool = True ) -> dict: debug_enabled = _LOGGER.isEnabledFor(logging.DEBUG) - is_do_method = False if isinstance(request, dict): if len(request) == 1: - method = next(iter(request)) - if method == "multipleRequest": - params = request["multipleRequest"] - req = {"method": "multipleRequest", "params": params} - elif (short_method := method[:3]) and short_method in {"get", "set"}: - params = next(iter(request[method])) - if method in GET_METHODS_AS_DO: - is_do_method = True - short_method = "do" - req = { - "method": short_method, - params: request[method][params], - } - else: - is_do_method = True - params = next(iter(request[method])) - req = { - "method": "do", - params: request[method][params], - } + method_type, method, param, single_request = ( + self._get_smart_camera_single_request(request) + ) else: return await self._execute_multiple_query(request, retry_count) else: # If method like getSomeThing then module will be some_thing method = request + method_type = request[:3] snake_name = "".join( - ["_" + i.lower() if i.isupper() else i for i in method] + ["_" + i.lower() if i.isupper() else i for i in request] ).lstrip("_") - params = snake_name[4:] - req = {"method": snake_name[:3], params: {}} + param = snake_name[4:] + if (short_method := method[:3]) and short_method in {"get", "set"}: + method_type = short_method + param = snake_name[4:] + else: + method_type = "do" + param = snake_name + single_request = {"method": method_type, param: {}} - smart_request = json_dumps(req) + smart_request = json_dumps(single_request) if debug_enabled: _LOGGER.debug( "%s >> %s", @@ -134,20 +152,20 @@ async def _execute_query( self._handle_response_error_code(response_data, method) # Requests that are invalid and raise PROTOCOL_FORMAT_ERROR when sent # as a multipleRequest will return {} when sent as a single request. - if method.startswith("get") and ( + if method_type == "get" and ( not (section := next(iter(response_data))) or response_data[section] == {} ): - raise DeviceError(f"No results for get request {method}") + raise DeviceError(f"No results for get request {single_request}") # TODO need to update handle response lists - if is_do_method: + if method_type == "do": return {method: response_data} - if method.startswith("set"): + if method_type == "set": return {} - if method == "multipleRequest": + if method_type == "multi": return {method: response_data["result"]} - return {method: {params: response_data[params]}} + return {method: {param: response_data[param]}} class _ChildCameraProtocolWrapper(SmartProtocol): diff --git a/kasa/tests/fixtures/smartcamera/C210(EU)_2.0_1.4.2.json b/kasa/tests/fixtures/smartcamera/C210(EU)_2.0_1.4.2.json index e3d8c42be..ed20a0264 100644 --- a/kasa/tests/fixtures/smartcamera/C210(EU)_2.0_1.4.2.json +++ b/kasa/tests/fixtures/smartcamera/C210(EU)_2.0_1.4.2.json @@ -116,8 +116,8 @@ }, "getConnectionType": { "link_type": "wifi", - "rssi": "2", - "rssiValue": -63, + "rssi": "3", + "rssiValue": -61, "ssid": "I01BU0tFRF9TU0lEIw==" }, "getDetectionConfig": { From f5f772318aa999183419fde5f0f877b541ce3b30 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Thu, 24 Oct 2024 08:32:30 +0100 Subject: [PATCH 04/17] Add dataclass for single request --- devtools/dump_devinfo.py | 6 ++ kasa/experimental/smartcameraprotocol.py | 85 ++++++++++++------- .../smartcamera/C210(EU)_2.0_1.4.2.json | 2 +- 3 files changed, 62 insertions(+), 31 deletions(-) diff --git a/devtools/dump_devinfo.py b/devtools/dump_devinfo.py index c37a6a389..804501831 100644 --- a/devtools/dump_devinfo.py +++ b/devtools/dump_devinfo.py @@ -498,6 +498,12 @@ async def _make_final_calls( *, child_device_id: str, ) -> dict[str, dict]: + """Call all successes again. + + After trying each call individually make the calls again either as a + multiple request or as single requests for those that don't support + multiple queries. + """ multiple_requests = { key: smartcall.request[key] for smartcall in calls diff --git a/kasa/experimental/smartcameraprotocol.py b/kasa/experimental/smartcameraprotocol.py index 4cdfb4cef..f8165a2e5 100644 --- a/kasa/experimental/smartcameraprotocol.py +++ b/kasa/experimental/smartcameraprotocol.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from dataclasses import dataclass from pprint import pformat as pf from typing import Any @@ -35,6 +36,16 @@ } +@dataclass +class SingleRequest: + """Class for returning single request details from helper functions.""" + + method_type: str + method_name: str + param_name: str + request: dict[str, Any] + + class SmartCameraProtocol(SmartProtocol): """Class for SmartCamera Protocol.""" @@ -78,13 +89,13 @@ async def close(self) -> None: @staticmethod def _get_smart_camera_single_request( request: dict[str, dict[str, Any]], - ) -> tuple[str, str, str, dict]: + ) -> SingleRequest: method = next(iter(request)) if method == "multipleRequest": method_type = "multi" params = request["multipleRequest"] req = {"method": "multipleRequest", "params": params} - return "multi", "multipleRequest", "", req + SingleRequest("multi", "multipleRequest", "", req) if (short_method := method[:3]) and short_method in {"get", "set"}: method_type = short_method @@ -102,7 +113,30 @@ def _get_smart_camera_single_request( "method": method_type, param: request[method][param], } - return method_type, method, param, req + return SingleRequest(method_type, method, param, req) + + @staticmethod + def _make_smart_camera_single_request( + request: str, + ) -> SingleRequest: + """Make a single request given a method name and no params. + + If method like getSomeThing then module will be some_thing. + """ + method = request + method_type = request[:3] + snake_name = "".join( + ["_" + i.lower() if i.isupper() else i for i in request] + ).lstrip("_") + param = snake_name[4:] + if (short_method := method[:3]) and short_method in {"get", "set"}: + method_type = short_method + param = snake_name[4:] + else: + method_type = "do" + param = snake_name + req = {"method": method_type, param: {}} + return SingleRequest(method_type, method, param, req) async def _execute_query( self, request: str | dict, *, retry_count: int, iterate_list_pages: bool = True @@ -110,28 +144,13 @@ async def _execute_query( debug_enabled = _LOGGER.isEnabledFor(logging.DEBUG) if isinstance(request, dict): if len(request) == 1: - method_type, method, param, single_request = ( - self._get_smart_camera_single_request(request) - ) + single_request = self._get_smart_camera_single_request(request) else: return await self._execute_multiple_query(request, retry_count) else: - # If method like getSomeThing then module will be some_thing - method = request - method_type = request[:3] - snake_name = "".join( - ["_" + i.lower() if i.isupper() else i for i in request] - ).lstrip("_") - param = snake_name[4:] - if (short_method := method[:3]) and short_method in {"get", "set"}: - method_type = short_method - param = snake_name[4:] - else: - method_type = "do" - param = snake_name - single_request = {"method": method_type, param: {}} + single_request = self._make_smart_camera_single_request(request) - smart_request = json_dumps(single_request) + smart_request = json_dumps(single_request.request) if debug_enabled: _LOGGER.debug( "%s >> %s", @@ -149,23 +168,29 @@ async def _execute_query( if "error_code" in response_data: # H200 does not return an error code - self._handle_response_error_code(response_data, method) + self._handle_response_error_code(response_data, single_request.method_name) # Requests that are invalid and raise PROTOCOL_FORMAT_ERROR when sent # as a multipleRequest will return {} when sent as a single request. - if method_type == "get" and ( + if single_request.method_type == "get" and ( not (section := next(iter(response_data))) or response_data[section] == {} ): - raise DeviceError(f"No results for get request {single_request}") + raise DeviceError( + f"No results for get request {single_request.method_name}" + ) # TODO need to update handle response lists - if method_type == "do": - return {method: response_data} - if method_type == "set": + if single_request.method_type == "do": + return {single_request.method_name: response_data} + if single_request.method_type == "set": return {} - if method_type == "multi": - return {method: response_data["result"]} - return {method: {param: response_data[param]}} + if single_request.method_type == "multi": + return {single_request.method_name: response_data["result"]} + return { + single_request.method_name: { + single_request.param_name: response_data[single_request.param_name] + } + } class _ChildCameraProtocolWrapper(SmartProtocol): diff --git a/kasa/tests/fixtures/smartcamera/C210(EU)_2.0_1.4.2.json b/kasa/tests/fixtures/smartcamera/C210(EU)_2.0_1.4.2.json index ed20a0264..40cba4b62 100644 --- a/kasa/tests/fixtures/smartcamera/C210(EU)_2.0_1.4.2.json +++ b/kasa/tests/fixtures/smartcamera/C210(EU)_2.0_1.4.2.json @@ -117,7 +117,7 @@ "getConnectionType": { "link_type": "wifi", "rssi": "3", - "rssiValue": -61, + "rssiValue": -62, "ssid": "I01BU0tFRF9TU0lEIw==" }, "getDetectionConfig": { From f255cdc6d576f2573520865b3aff2e3dd03841d6 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Thu, 24 Oct 2024 08:35:58 +0100 Subject: [PATCH 05/17] Add snake name function --- kasa/experimental/smartcameraprotocol.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/kasa/experimental/smartcameraprotocol.py b/kasa/experimental/smartcameraprotocol.py index f8165a2e5..fc7773b6f 100644 --- a/kasa/experimental/smartcameraprotocol.py +++ b/kasa/experimental/smartcameraprotocol.py @@ -115,6 +115,12 @@ def _get_smart_camera_single_request( } return SingleRequest(method_type, method, param, req) + @staticmethod + def _make_snake_name(name: str) -> str: + """Convert camel or pascal case to snake name.""" + sn = "".join(["_" + i.lower() if i.isupper() else i for i in name]).lstrip("_") + return sn + @staticmethod def _make_smart_camera_single_request( request: str, @@ -125,9 +131,7 @@ def _make_smart_camera_single_request( """ method = request method_type = request[:3] - snake_name = "".join( - ["_" + i.lower() if i.isupper() else i for i in request] - ).lstrip("_") + snake_name = SmartCameraProtocol._make_snake_name(request) param = snake_name[4:] if (short_method := method[:3]) and short_method in {"get", "set"}: method_type = short_method From 4312dfc2763a72e8b27adc2b3849d8e154b368a3 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Thu, 24 Oct 2024 09:14:01 +0100 Subject: [PATCH 06/17] Disable single calls for hubs --- devtools/dump_devinfo.py | 12 +++++++++++- kasa/experimental/smartcameraprotocol.py | 2 ++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/devtools/dump_devinfo.py b/devtools/dump_devinfo.py index 804501831..e6a4aa54e 100644 --- a/devtools/dump_devinfo.py +++ b/devtools/dump_devinfo.py @@ -69,6 +69,7 @@ class SmartCall: should_succeed: bool child_device_id: str supports_multiple: bool = True + supports_single: bool = True def scrub(res): @@ -603,18 +604,25 @@ async def get_smart_camera_test_calls(protocol: SmartProtocol): ) # Now get the child device requests + child_request = { + "getChildDeviceList": {"childControl": {"start_index": 0}}, + "multi": None, + } try: - child_request = {"getChildDeviceList": {"childControl": {"start_index": 0}}} child_response = await protocol.query(child_request) except Exception: _LOGGER.debug("Device does not have any children.") else: + # H200 hubs do not support a lot of single requests + for testcall in test_calls: + testcall.supports_single = False successes.append( SmartCall( module="getChildDeviceList", request=child_request, should_succeed=True, child_device_id="", + supports_multiple=True, ) ) child_list = child_response["getChildDeviceList"]["child_device_list"] @@ -857,6 +865,8 @@ async def get_smart_fixtures( try: click.echo(f"Testing {test_call}..", nl=False) if test_call.child_device_id == "": + if not test_call.supports_single: + test_call.request["multi"] = None response = await protocol.query(test_call.request) else: cp = child_wrapper(test_call.child_device_id, protocol) diff --git a/kasa/experimental/smartcameraprotocol.py b/kasa/experimental/smartcameraprotocol.py index fc7773b6f..11fbaf663 100644 --- a/kasa/experimental/smartcameraprotocol.py +++ b/kasa/experimental/smartcameraprotocol.py @@ -150,6 +150,8 @@ async def _execute_query( if len(request) == 1: single_request = self._get_smart_camera_single_request(request) else: + # H200 hubs do not handle single requests very well + request.pop("multi", None) return await self._execute_multiple_query(request, retry_count) else: single_request = self._make_smart_camera_single_request(request) From d91aab07b853d1af669dd9c9af79adc78e469e42 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Thu, 24 Oct 2024 09:23:55 +0100 Subject: [PATCH 07/17] Fix disable single calls for hubs --- kasa/experimental/smartcameraprotocol.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/kasa/experimental/smartcameraprotocol.py b/kasa/experimental/smartcameraprotocol.py index 11fbaf663..c0c03337a 100644 --- a/kasa/experimental/smartcameraprotocol.py +++ b/kasa/experimental/smartcameraprotocol.py @@ -34,6 +34,9 @@ "getFirmwareAFResult", "getWhitelampStatus", } +# If this key is in a single request it will be removed and a multi request will be +# forced +FORCE_MULTI_KEY = "multi" @dataclass @@ -150,9 +153,12 @@ async def _execute_query( if len(request) == 1: single_request = self._get_smart_camera_single_request(request) else: - # H200 hubs do not handle single requests very well - request.pop("multi", None) - return await self._execute_multiple_query(request, retry_count) + # H200 hubs do not handle single requests very well so an extra + # key is provided to force multi + multi_request = { + key: val for key, val in request.items() if key != FORCE_MULTI_KEY + } + return await self._execute_multiple_query(multi_request, retry_count) else: single_request = self._make_smart_camera_single_request(request) From ee4718052acb168458a1ce278d7f53da8bae17d8 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Thu, 24 Oct 2024 09:48:25 +0100 Subject: [PATCH 08/17] Remove single test calls for hubs --- devtools/dump_devinfo.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/devtools/dump_devinfo.py b/devtools/dump_devinfo.py index e6a4aa54e..180e9a11c 100644 --- a/devtools/dump_devinfo.py +++ b/devtools/dump_devinfo.py @@ -614,8 +614,11 @@ async def get_smart_camera_test_calls(protocol: SmartProtocol): _LOGGER.debug("Device does not have any children.") else: # H200 hubs do not support a lot of single requests - for testcall in test_calls: - testcall.supports_single = False + for test_call in test_calls: + test_call.supports_single = False + test_calls = [ + testcall for testcall in test_calls if test_call.supports_multiple + ] successes.append( SmartCall( module="getChildDeviceList", From d3884be2be1c3b3e99f1540c0e412522cafacc89 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Thu, 24 Oct 2024 10:07:30 +0100 Subject: [PATCH 09/17] Replace raise error on single multiple --- kasa/smartprotocol.py | 10 +++++++--- .../tests/fixtures/smartcamera/C210(EU)_2.0_1.4.2.json | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/kasa/smartprotocol.py b/kasa/smartprotocol.py index 33841b3a8..429ff9773 100644 --- a/kasa/smartprotocol.py +++ b/kasa/smartprotocol.py @@ -163,7 +163,7 @@ async def _execute_multiple_query(self, requests: dict, retry_count: int) -> dic ] end = len(multi_requests) - + raise_on_error = end == 1 # Break the requests down as there can be a size limit step = self._multi_request_batch_size if step == 1: @@ -172,7 +172,9 @@ async def _execute_multiple_query(self, requests: dict, retry_count: int) -> dic method = request["method"] req = self.get_smart_request(method, request.get("params")) resp = await self._transport.send(req) - self._handle_response_error_code(resp, method, raise_on_error=False) + self._handle_response_error_code( + resp, method, raise_on_error=raise_on_error + ) multi_result[method] = resp["result"] return multi_result @@ -223,7 +225,9 @@ async def _execute_multiple_query(self, requests: dict, retry_count: int) -> dic responses = response_step["result"]["responses"] for response in responses: method = response["method"] - self._handle_response_error_code(response, method, raise_on_error=False) + self._handle_response_error_code( + response, method, raise_on_error=raise_on_error + ) result = response.get("result", None) await self._handle_response_lists( result, method, retry_count=retry_count diff --git a/kasa/tests/fixtures/smartcamera/C210(EU)_2.0_1.4.2.json b/kasa/tests/fixtures/smartcamera/C210(EU)_2.0_1.4.2.json index 40cba4b62..ed20a0264 100644 --- a/kasa/tests/fixtures/smartcamera/C210(EU)_2.0_1.4.2.json +++ b/kasa/tests/fixtures/smartcamera/C210(EU)_2.0_1.4.2.json @@ -117,7 +117,7 @@ "getConnectionType": { "link_type": "wifi", "rssi": "3", - "rssiValue": -62, + "rssiValue": -61, "ssid": "I01BU0tFRF9TU0lEIw==" }, "getDetectionConfig": { From 74522d0fe49a99e05fa78a2feeb887e0335c91a5 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Thu, 24 Oct 2024 10:16:20 +0100 Subject: [PATCH 10/17] Fix broken child request --- kasa/experimental/smartcameraprotocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kasa/experimental/smartcameraprotocol.py b/kasa/experimental/smartcameraprotocol.py index c0c03337a..827292c93 100644 --- a/kasa/experimental/smartcameraprotocol.py +++ b/kasa/experimental/smartcameraprotocol.py @@ -98,7 +98,7 @@ def _get_smart_camera_single_request( method_type = "multi" params = request["multipleRequest"] req = {"method": "multipleRequest", "params": params} - SingleRequest("multi", "multipleRequest", "", req) + return SingleRequest("multi", "multipleRequest", "", req) if (short_method := method[:3]) and short_method in {"get", "set"}: method_type = short_method From 27847c5e3b44d719680259fa08088173936afcd9 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Thu, 24 Oct 2024 10:34:55 +0100 Subject: [PATCH 11/17] Fix force multi --- devtools/dump_devinfo.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/devtools/dump_devinfo.py b/devtools/dump_devinfo.py index 180e9a11c..ab2e05c2e 100644 --- a/devtools/dump_devinfo.py +++ b/devtools/dump_devinfo.py @@ -606,10 +606,9 @@ async def get_smart_camera_test_calls(protocol: SmartProtocol): # Now get the child device requests child_request = { "getChildDeviceList": {"childControl": {"start_index": 0}}, - "multi": None, } try: - child_response = await protocol.query(child_request) + child_response = await protocol.query({**child_request, "multi": None}) except Exception: _LOGGER.debug("Device does not have any children.") else: @@ -868,9 +867,10 @@ async def get_smart_fixtures( try: click.echo(f"Testing {test_call}..", nl=False) if test_call.child_device_id == "": + req = {**test_call.request} if not test_call.supports_single: - test_call.request["multi"] = None - response = await protocol.query(test_call.request) + req["multi"] = None + response = await protocol.query(req) else: cp = child_wrapper(test_call.child_device_id, protocol) response = await cp.query(test_call.request) From ade8e3884480459f6fb6756447d97e4728368d57 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Thu, 24 Oct 2024 10:57:05 +0100 Subject: [PATCH 12/17] Disable single requests --- kasa/experimental/smartcameraprotocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kasa/experimental/smartcameraprotocol.py b/kasa/experimental/smartcameraprotocol.py index 827292c93..5fd2a2bde 100644 --- a/kasa/experimental/smartcameraprotocol.py +++ b/kasa/experimental/smartcameraprotocol.py @@ -150,7 +150,7 @@ async def _execute_query( ) -> dict: debug_enabled = _LOGGER.isEnabledFor(logging.DEBUG) if isinstance(request, dict): - if len(request) == 1: + if len(request) == 0: single_request = self._get_smart_camera_single_request(request) else: # H200 hubs do not handle single requests very well so an extra From 8e5aae16dfa9f8b417a973e867bfbea165984c46 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Thu, 24 Oct 2024 11:18:59 +0100 Subject: [PATCH 13/17] Keep single for multipleRequest --- kasa/experimental/smartcameraprotocol.py | 3 +- .../smartcamera/C210(EU)_2.0_1.4.2.json | 196 +----------------- 2 files changed, 3 insertions(+), 196 deletions(-) diff --git a/kasa/experimental/smartcameraprotocol.py b/kasa/experimental/smartcameraprotocol.py index 5fd2a2bde..7845d2667 100644 --- a/kasa/experimental/smartcameraprotocol.py +++ b/kasa/experimental/smartcameraprotocol.py @@ -150,7 +150,8 @@ async def _execute_query( ) -> dict: debug_enabled = _LOGGER.isEnabledFor(logging.DEBUG) if isinstance(request, dict): - if len(request) == 0: + method = next(iter(request)) + if len(request) == 0 or method == "multipleRequest": single_request = self._get_smart_camera_single_request(request) else: # H200 hubs do not handle single requests very well so an extra diff --git a/kasa/tests/fixtures/smartcamera/C210(EU)_2.0_1.4.2.json b/kasa/tests/fixtures/smartcamera/C210(EU)_2.0_1.4.2.json index ed20a0264..412760513 100644 --- a/kasa/tests/fixtures/smartcamera/C210(EU)_2.0_1.4.2.json +++ b/kasa/tests/fixtures/smartcamera/C210(EU)_2.0_1.4.2.json @@ -117,7 +117,7 @@ "getConnectionType": { "link_type": "wifi", "rssi": "3", - "rssiValue": -61, + "rssiValue": -62, "ssid": "I01BU0tFRF9TU0lEIw==" }, "getDetectionConfig": { @@ -601,199 +601,5 @@ "getWhitelampStatus": { "rest_time": 0, "status": 0 - }, - "get_audio_capability": { - "get": { - "audio_capability": { - "device_microphone": { - "aec": "1", - "channels": "1", - "echo_cancelling": "0", - "encode_type": [ - "G711alaw" - ], - "half_duplex": "1", - "mute": "1", - "noise_cancelling": "1", - "sampling_rate": [ - "8", - "16" - ], - "volume": "1" - }, - "device_speaker": { - "channels": "1", - "decode_type": [ - "G711alaw", - "G711ulaw" - ], - "mute": "0", - "output_device_type": "0", - "sampling_rate": [ - "8", - "16" - ], - "system_volume": "100", - "volume": "1" - } - } - } - }, - "get_audio_config": { - "get": { - "audio_config": { - "microphone": { - "bitrate": "64", - "channels": "1", - "echo_cancelling": "off", - "encode_type": "G711alaw", - "input_device_type": "MicIn", - "mute": "off", - "noise_cancelling": "on", - "sampling_rate": "8", - "volume": "100" - }, - "speaker": { - "mute": "off", - "output_device_type": "SpeakerOut", - "volume": "100" - } - } - } - }, - "get_cet": { - "get": { - "cet": { - "vhttpd": { - "port": "8800" - } - } - } - }, - "get_function": { - "get": { - "function": { - "module_spec": { - "ae_weighting_table_resolution": "5*5", - "ai_enhance_capability": "1", - "ai_enhance_range": [ - "traditional_enhance" - ], - "ai_firmware_upgrade": "0", - "alarm_out_num": "0", - "app_version": "1.0.0", - "audio": [ - "speaker", - "microphone" - ], - "auth_encrypt": "1", - "auto_ip_configurable": "1", - "backlight_coexistence": "1", - "change_password": "1", - "client_info": "1", - "cloud_storage_version": "1.0", - "config_recovery": [ - "audio_config", - "OSD", - "image", - "video" - ], - "custom_area_compensation": "1", - "custom_auto_mode_exposure_level": "1", - "daynight_subdivision": "1", - "device_share": [ - "preview", - "playback", - "voice", - "cloud_storage", - "motor" - ], - "download": [ - "video" - ], - "events": [ - "motion", - "tamper" - ], - "force_iframe_support": "1", - "greeter": "1.0", - "http_system_state_audio_support": "1", - "image_capability": "1", - "image_list": [ - "supplement_lamp", - "expose" - ], - "ir_led_pwm_control": "1", - "led": "1", - "lens_mask": "1", - "linkage_capability": "1", - "local_storage": "1", - "media_encrypt": "1", - "motor": "0", - "msg_alarm": "1", - "msg_alarm_list": [ - "sound", - "light" - ], - "msg_push": "1", - "multi_user": "0", - "multicast": "0", - "network": [ - "wifi" - ], - "osd_capability": "1", - "ota_upgrade": "1", - "p2p_support_versions": [ - "1.1" - ], - "personalized_audio_alarm": "0", - "playback": [ - "local", - "p2p", - "relay" - ], - "playback_scale": "1", - "preview": [ - "local", - "p2p", - "relay" - ], - "privacy_mask_api_version": "1.0", - "ptz": "1", - "record_max_slot_cnt": "10", - "record_type": [ - "timing", - "motion" - ], - "relay_support_versions": [ - "1.3" - ], - "remote_upgrade": "1", - "reonboarding": "1", - "smart_codec": "0", - "smart_detection": "1", - "smart_msg_push_capability": "1", - "ssl_cer_version": "1.0", - "storage_api_version": "2.2", - "storage_capability": "1", - "stream_max_sessions": "10", - "streaming_support_versions": [ - "1.0" - ], - "tapo_care_version": "1.0.0", - "target_track": "1", - "timing_reboot": "1", - "verification_change_password": "1", - "video_codec": [ - "h264" - ], - "video_detection_digital_sensitivity": "1", - "wide_range_inf_sensitivity": "1", - "wifi_cascade_connection": "1", - "wifi_connection_info": "1", - "wireless_hotspot": "1" - } - } - } } } From 48dc81f20dd8b17cf1bcc42ba731225011303e23 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Thu, 24 Oct 2024 11:47:53 +0100 Subject: [PATCH 14/17] Only send multiRequest, get, set and do as single --- devtools/dump_devinfo.py | 14 +- kasa/experimental/smartcameraprotocol.py | 40 ++-- .../smartcamera/C210(EU)_2.0_1.4.2.json | 194 ++++++++++++++++++ 3 files changed, 209 insertions(+), 39 deletions(-) diff --git a/devtools/dump_devinfo.py b/devtools/dump_devinfo.py index ab2e05c2e..da46f10a3 100644 --- a/devtools/dump_devinfo.py +++ b/devtools/dump_devinfo.py @@ -69,7 +69,6 @@ class SmartCall: should_succeed: bool child_device_id: str supports_multiple: bool = True - supports_single: bool = True def scrub(res): @@ -608,16 +607,10 @@ async def get_smart_camera_test_calls(protocol: SmartProtocol): "getChildDeviceList": {"childControl": {"start_index": 0}}, } try: - child_response = await protocol.query({**child_request, "multi": None}) + child_response = await protocol.query(child_request) except Exception: _LOGGER.debug("Device does not have any children.") else: - # H200 hubs do not support a lot of single requests - for test_call in test_calls: - test_call.supports_single = False - test_calls = [ - testcall for testcall in test_calls if test_call.supports_multiple - ] successes.append( SmartCall( module="getChildDeviceList", @@ -867,10 +860,7 @@ async def get_smart_fixtures( try: click.echo(f"Testing {test_call}..", nl=False) if test_call.child_device_id == "": - req = {**test_call.request} - if not test_call.supports_single: - req["multi"] = None - response = await protocol.query(req) + response = await protocol.query(test_call.request) else: cp = child_wrapper(test_call.child_device_id, protocol) response = await cp.query(test_call.request) diff --git a/kasa/experimental/smartcameraprotocol.py b/kasa/experimental/smartcameraprotocol.py index 7845d2667..b298fbd2e 100644 --- a/kasa/experimental/smartcameraprotocol.py +++ b/kasa/experimental/smartcameraprotocol.py @@ -34,9 +34,6 @@ "getFirmwareAFResult", "getWhitelampStatus", } -# If this key is in a single request it will be removed and a multi request will be -# forced -FORCE_MULTI_KEY = "multi" @dataclass @@ -100,22 +97,12 @@ def _get_smart_camera_single_request( req = {"method": "multipleRequest", "params": params} return SingleRequest("multi", "multipleRequest", "", req) - if (short_method := method[:3]) and short_method in {"get", "set"}: - method_type = short_method - param = next(iter(request[method])) - if method in GET_METHODS_AS_DO: - method_type = "do" - req = { - "method": method_type, - param: request[method][param], - } - else: - method_type = "do" - param = next(iter(request[method])) - req = { - "method": method_type, - param: request[method][param], - } + param = next(iter(request[method])) + method_type = method + req = { + "method": method, + param: request[method][param], + } return SingleRequest(method_type, method, param, req) @staticmethod @@ -136,7 +123,11 @@ def _make_smart_camera_single_request( method_type = request[:3] snake_name = SmartCameraProtocol._make_snake_name(request) param = snake_name[4:] - if (short_method := method[:3]) and short_method in {"get", "set"}: + if ( + (short_method := method[:3]) + and short_method in {"get", "set"} + and method not in GET_METHODS_AS_DO + ): method_type = short_method param = snake_name[4:] else: @@ -151,15 +142,10 @@ async def _execute_query( debug_enabled = _LOGGER.isEnabledFor(logging.DEBUG) if isinstance(request, dict): method = next(iter(request)) - if len(request) == 0 or method == "multipleRequest": + if len(request) == 1 and method in {"get", "set", "do", "multipleRequest"}: single_request = self._get_smart_camera_single_request(request) else: - # H200 hubs do not handle single requests very well so an extra - # key is provided to force multi - multi_request = { - key: val for key, val in request.items() if key != FORCE_MULTI_KEY - } - return await self._execute_multiple_query(multi_request, retry_count) + return await self._execute_multiple_query(request, retry_count) else: single_request = self._make_smart_camera_single_request(request) diff --git a/kasa/tests/fixtures/smartcamera/C210(EU)_2.0_1.4.2.json b/kasa/tests/fixtures/smartcamera/C210(EU)_2.0_1.4.2.json index 412760513..40cba4b62 100644 --- a/kasa/tests/fixtures/smartcamera/C210(EU)_2.0_1.4.2.json +++ b/kasa/tests/fixtures/smartcamera/C210(EU)_2.0_1.4.2.json @@ -601,5 +601,199 @@ "getWhitelampStatus": { "rest_time": 0, "status": 0 + }, + "get_audio_capability": { + "get": { + "audio_capability": { + "device_microphone": { + "aec": "1", + "channels": "1", + "echo_cancelling": "0", + "encode_type": [ + "G711alaw" + ], + "half_duplex": "1", + "mute": "1", + "noise_cancelling": "1", + "sampling_rate": [ + "8", + "16" + ], + "volume": "1" + }, + "device_speaker": { + "channels": "1", + "decode_type": [ + "G711alaw", + "G711ulaw" + ], + "mute": "0", + "output_device_type": "0", + "sampling_rate": [ + "8", + "16" + ], + "system_volume": "100", + "volume": "1" + } + } + } + }, + "get_audio_config": { + "get": { + "audio_config": { + "microphone": { + "bitrate": "64", + "channels": "1", + "echo_cancelling": "off", + "encode_type": "G711alaw", + "input_device_type": "MicIn", + "mute": "off", + "noise_cancelling": "on", + "sampling_rate": "8", + "volume": "100" + }, + "speaker": { + "mute": "off", + "output_device_type": "SpeakerOut", + "volume": "100" + } + } + } + }, + "get_cet": { + "get": { + "cet": { + "vhttpd": { + "port": "8800" + } + } + } + }, + "get_function": { + "get": { + "function": { + "module_spec": { + "ae_weighting_table_resolution": "5*5", + "ai_enhance_capability": "1", + "ai_enhance_range": [ + "traditional_enhance" + ], + "ai_firmware_upgrade": "0", + "alarm_out_num": "0", + "app_version": "1.0.0", + "audio": [ + "speaker", + "microphone" + ], + "auth_encrypt": "1", + "auto_ip_configurable": "1", + "backlight_coexistence": "1", + "change_password": "1", + "client_info": "1", + "cloud_storage_version": "1.0", + "config_recovery": [ + "audio_config", + "OSD", + "image", + "video" + ], + "custom_area_compensation": "1", + "custom_auto_mode_exposure_level": "1", + "daynight_subdivision": "1", + "device_share": [ + "preview", + "playback", + "voice", + "cloud_storage", + "motor" + ], + "download": [ + "video" + ], + "events": [ + "motion", + "tamper" + ], + "force_iframe_support": "1", + "greeter": "1.0", + "http_system_state_audio_support": "1", + "image_capability": "1", + "image_list": [ + "supplement_lamp", + "expose" + ], + "ir_led_pwm_control": "1", + "led": "1", + "lens_mask": "1", + "linkage_capability": "1", + "local_storage": "1", + "media_encrypt": "1", + "motor": "0", + "msg_alarm": "1", + "msg_alarm_list": [ + "sound", + "light" + ], + "msg_push": "1", + "multi_user": "0", + "multicast": "0", + "network": [ + "wifi" + ], + "osd_capability": "1", + "ota_upgrade": "1", + "p2p_support_versions": [ + "1.1" + ], + "personalized_audio_alarm": "0", + "playback": [ + "local", + "p2p", + "relay" + ], + "playback_scale": "1", + "preview": [ + "local", + "p2p", + "relay" + ], + "privacy_mask_api_version": "1.0", + "ptz": "1", + "record_max_slot_cnt": "10", + "record_type": [ + "timing", + "motion" + ], + "relay_support_versions": [ + "1.3" + ], + "remote_upgrade": "1", + "reonboarding": "1", + "smart_codec": "0", + "smart_detection": "1", + "smart_msg_push_capability": "1", + "ssl_cer_version": "1.0", + "storage_api_version": "2.2", + "storage_capability": "1", + "stream_max_sessions": "10", + "streaming_support_versions": [ + "1.0" + ], + "tapo_care_version": "1.0.0", + "target_track": "1", + "timing_reboot": "1", + "verification_change_password": "1", + "video_codec": [ + "h264" + ], + "video_detection_digital_sensitivity": "1", + "wide_range_inf_sensitivity": "1", + "wifi_cascade_connection": "1", + "wifi_connection_info": "1", + "wireless_hotspot": "1" + } + } + } } } From d82f79b1227df82b4aa927faa7805fe07224d26c Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Thu, 24 Oct 2024 12:00:31 +0100 Subject: [PATCH 15/17] Add comment to raise on error --- kasa/smartprotocol.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/kasa/smartprotocol.py b/kasa/smartprotocol.py index 429ff9773..71be7dee1 100644 --- a/kasa/smartprotocol.py +++ b/kasa/smartprotocol.py @@ -163,7 +163,11 @@ async def _execute_multiple_query(self, requests: dict, retry_count: int) -> dic ] end = len(multi_requests) + # The SmartCameraProtocol sends requests with a length 1 as a + # multipleRequest. The SmartProtocol doesn't so will never + # raise_on_error raise_on_error = end == 1 + # Break the requests down as there can be a size limit step = self._multi_request_batch_size if step == 1: From bdffd746bfed847932f664c9dbefdb9ed0e1aa29 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Thu, 24 Oct 2024 12:50:02 +0100 Subject: [PATCH 16/17] Add clock status and timezone queries --- devtools/helpers/smartcamerarequests.py | 2 ++ .../smartcamera/C210(EU)_2.0_1.4.2.json | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/devtools/helpers/smartcamerarequests.py b/devtools/helpers/smartcamerarequests.py index 8c623e838..3f5596f76 100644 --- a/devtools/helpers/smartcamerarequests.py +++ b/devtools/helpers/smartcamerarequests.py @@ -50,6 +50,8 @@ {"getFirmwareAutoUpgradeConfig": {"auto_upgrade": {"name": ["common"]}}}, {"getVideoQualities": {"video": {"name": ["main"]}}}, {"getVideoCapability": {"video_capability": {"name": "main"}}}, + {"getTimezone": {"system": {"name": "basic"}}}, + {"getClockStatus": {"system": {"name": "clock_status"}}}, # single request only methods {"get": {"function": {"name": ["module_spec"]}}}, {"get": {"cet": {"name": ["vhttpd"]}}}, diff --git a/kasa/tests/fixtures/smartcamera/C210(EU)_2.0_1.4.2.json b/kasa/tests/fixtures/smartcamera/C210(EU)_2.0_1.4.2.json index 40cba4b62..a4c529a53 100644 --- a/kasa/tests/fixtures/smartcamera/C210(EU)_2.0_1.4.2.json +++ b/kasa/tests/fixtures/smartcamera/C210(EU)_2.0_1.4.2.json @@ -114,6 +114,14 @@ } } }, + "getClockStatus": { + "system": { + "clock_status": { + "local_time": "2024-10-24 12:49:09", + "seconds_from_1970": 1729770549 + } + } + }, "getConnectionType": { "link_type": "wifi", "rssi": "3", @@ -518,6 +526,15 @@ } } }, + "getTimezone": { + "system": { + "basic": { + "timezone": "UTC-00:00", + "timing_mode": "ntp", + "zone_id": "Europe/Berlin" + } + } + }, "getVideoCapability": { "video_capability": { "main": { From 4921b5c536010572b1f243c050dedb60d179eefe Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Thu, 24 Oct 2024 13:07:00 +0100 Subject: [PATCH 17/17] Fix test --- kasa/tests/fakeprotocol_smartcamera.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/kasa/tests/fakeprotocol_smartcamera.py b/kasa/tests/fakeprotocol_smartcamera.py index e2a849dba..50d34e938 100644 --- a/kasa/tests/fakeprotocol_smartcamera.py +++ b/kasa/tests/fakeprotocol_smartcamera.py @@ -173,10 +173,16 @@ async def _send_request(self, request_dict: dict): request_dict["params"]["childControl"] ) - if method == "set": + if method[:3] == "set": for key, val in request_dict.items(): if key != "method": - module = key + # key is params for multi request and the actual params + # for single requests + if key == "params": + module = next(iter(val)) + val = val[module] + else: + module = key section = next(iter(val)) skey_val = val[section] for skey, sval in skey_val.items():