diff --git a/devtools/dump_devinfo.py b/devtools/dump_devinfo.py index 12e4c3cb..da46f10a 100644 --- a/devtools/dump_devinfo.py +++ b/devtools/dump_devinfo.py @@ -12,6 +12,7 @@ from __future__ import annotations import base64 import collections.abc +import dataclasses import json import logging import re @@ -23,6 +24,7 @@ from pprint import pprint import asyncclick as click +from devtools.helpers.smartcamerarequests import SMARTCAMERA_REQUESTS from devtools.helpers.smartrequests import SmartRequest, get_component_requests from kasa import ( AuthenticationError, @@ -46,10 +48,10 @@ from kasa.smart import SmartChildDevice 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/" +SMARTCAMERA_FOLDER = "kasa/tests/fixtures/smartcamera/" SMART_CHILD_FOLDER = "kasa/tests/fixtures/smart/child/" IOT_FOLDER = "kasa/tests/fixtures/" @@ -58,6 +60,17 @@ ENCRYPT_TYPES = [encrypt_type.value for encrypt_type in DeviceEncryptionType] _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 = [ @@ -136,7 +149,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 @@ -477,6 +490,44 @@ 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]: + """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 + 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, @@ -534,69 +585,28 @@ 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"], - } - }, - "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"}}, - } test_calls = [] - for method, params in requests.items(): + for request in SMARTCAMERA_REQUESTS: + method = next(iter(request)) + if method == "get": + module = method + "_" + next(iter(request[method])) + else: + module = method test_calls.append( SmartCall( - module=method, - request={method: params}, + module=module, + request=request, should_succeed=True, child_device_id="", + supports_multiple=(method != "get"), ) ) # Now get the child device requests + child_request = { + "getChildDeviceList": {"childControl": {"start_index": 0}}, + } 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.") @@ -607,6 +617,7 @@ async def get_smart_camera_test_calls(protocol: SmartProtocol): request=child_request, should_succeed=True, child_device_id="", + supports_multiple=True, ) ) child_list = child_response["getChildDeviceList"]["child_device_list"] @@ -660,11 +671,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 SMARTCAMERA_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, ) @@ -804,7 +818,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 @@ -879,10 +895,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}" @@ -890,24 +906,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 @@ -963,6 +976,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 +984,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/devtools/helpers/smartcamerarequests.py b/devtools/helpers/smartcamerarequests.py new file mode 100644 index 00000000..3f5596f7 --- /dev/null +++ b/devtools/helpers/smartcamerarequests.py @@ -0,0 +1,61 @@ +"""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"}}}, + {"getTimezone": {"system": {"name": "basic"}}}, + {"getClockStatus": {"system": {"name": "clock_status"}}}, + # 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 78579616..b298fbd2 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 @@ -22,6 +23,28 @@ from .sslaestransport import ( _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", +} + + +@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.""" @@ -63,37 +86,70 @@ class SmartCameraProtocol(SmartProtocol): """Close the underlying transport.""" await self._transport.close() + @staticmethod + def _get_smart_camera_single_request( + request: dict[str, dict[str, Any]], + ) -> SingleRequest: + method = next(iter(request)) + if method == "multipleRequest": + method_type = "multi" + params = request["multipleRequest"] + req = {"method": "multipleRequest", "params": params} + return SingleRequest("multi", "multipleRequest", "", req) + + param = next(iter(request[method])) + method_type = method + req = { + "method": method, + param: request[method][param], + } + 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, + ) -> 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 = SmartCameraProtocol._make_snake_name(request) + param = snake_name[4:] + 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: + 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 ) -> dict: debug_enabled = _LOGGER.isEnabledFor(logging.DEBUG) - 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[:3] == "set": - params = next(iter(request[method])) - req = { - "method": method[:3], - params: request[method][params], - } - else: - return await self._execute_multiple_query(request, retry_count) + method = next(iter(request)) + if len(request) == 1 and method in {"get", "set", "do", "multipleRequest"}: + 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 - snake_name = "".join( - ["_" + i.lower() if i.isupper() else i for i in method] - ).lstrip("_") - params = snake_name[4:] - req = {"method": snake_name[:3], params: {}} + single_request = self._make_smart_camera_single_request(request) - smart_request = json_dumps(req) + smart_request = json_dumps(single_request.request) if debug_enabled: _LOGGER.debug( "%s >> %s", @@ -111,15 +167,29 @@ class SmartCameraProtocol(SmartProtocol): 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 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.method_name}" + ) # TODO need to update handle response lists - if method[:3] == "set": + if single_request.method_type == "do": + return {single_request.method_name: response_data} + if single_request.method_type == "set": return {} - if method == "multipleRequest": - return {method: response_data["result"]} - return {method: {params: response_data[params]}} + 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/smartprotocol.py b/kasa/smartprotocol.py index 0c2a2bba..71be7dee 100644 --- a/kasa/smartprotocol.py +++ b/kasa/smartprotocol.py @@ -163,6 +163,10 @@ class SmartProtocol(BaseProtocol): ] 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 @@ -172,14 +176,12 @@ class SmartProtocol(BaseProtocol): 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 - # 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] diff --git a/kasa/tests/fakeprotocol_smartcamera.py b/kasa/tests/fakeprotocol_smartcamera.py index e2a849db..50d34e93 100644 --- a/kasa/tests/fakeprotocol_smartcamera.py +++ b/kasa/tests/fakeprotocol_smartcamera.py @@ -173,10 +173,16 @@ class FakeSmartCameraTransport(BaseTransport): 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(): 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 304a1e12..a4c529a5 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 @@ -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": "", @@ -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": { @@ -106,10 +114,18 @@ } } }, + "getClockStatus": { + "system": { + "clock_status": { + "local_time": "2024-10-24 12:49:09", + "seconds_from_1970": 1729770549 + } + } + }, "getConnectionType": { "link_type": "wifi", - "rssi": "2", - "rssiValue": -64, + "rssi": "3", + "rssiValue": -62, "ssid": "I01BU0tFRF9TU0lEIw==" }, "getDetectionConfig": { @@ -133,7 +149,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, @@ -171,19 +187,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" } } }, @@ -519,6 +526,15 @@ } } }, + "getTimezone": { + "system": { + "basic": { + "timezone": "UTC-00:00", + "timing_mode": "ntp", + "zone_id": "Europe/Berlin" + } + } + }, "getVideoCapability": { "video_capability": { "main": { @@ -602,5 +618,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 c7666296..544ab267 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": {