Update smartcamera to support single get/set/do requests (#1187)

Not supported by H200 hub
This commit is contained in:
Steven B. 2024-10-24 13:11:28 +01:00 committed by GitHub
parent c839aaa1dd
commit 8ee8c17bdc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 505 additions and 123 deletions

View File

@ -12,6 +12,7 @@ from __future__ import annotations
import base64 import base64
import collections.abc import collections.abc
import dataclasses
import json import json
import logging import logging
import re import re
@ -23,6 +24,7 @@ from pprint import pprint
import asyncclick as click import asyncclick as click
from devtools.helpers.smartcamerarequests import SMARTCAMERA_REQUESTS
from devtools.helpers.smartrequests import SmartRequest, get_component_requests from devtools.helpers.smartrequests import SmartRequest, get_component_requests
from kasa import ( from kasa import (
AuthenticationError, AuthenticationError,
@ -46,10 +48,10 @@ from kasa.smart import SmartChildDevice
from kasa.smartprotocol import SmartProtocol, _ChildProtocolWrapper from kasa.smartprotocol import SmartProtocol, _ChildProtocolWrapper
Call = namedtuple("Call", "module method") Call = namedtuple("Call", "module method")
SmartCall = namedtuple("SmartCall", "module request should_succeed child_device_id")
FixtureResult = namedtuple("FixtureResult", "filename, folder, data") FixtureResult = namedtuple("FixtureResult", "filename, folder, data")
SMART_FOLDER = "kasa/tests/fixtures/smart/" SMART_FOLDER = "kasa/tests/fixtures/smart/"
SMARTCAMERA_FOLDER = "kasa/tests/fixtures/smartcamera/"
SMART_CHILD_FOLDER = "kasa/tests/fixtures/smart/child/" SMART_CHILD_FOLDER = "kasa/tests/fixtures/smart/child/"
IOT_FOLDER = "kasa/tests/fixtures/" IOT_FOLDER = "kasa/tests/fixtures/"
@ -58,6 +60,17 @@ ENCRYPT_TYPES = [encrypt_type.value for encrypt_type in DeviceEncryptionType]
_LOGGER = logging.getLogger(__name__) _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): def scrub(res):
"""Remove identifiers from the given dict.""" """Remove identifiers from the given dict."""
keys_to_scrub = [ keys_to_scrub = [
@ -136,7 +149,7 @@ def scrub(res):
v = base64.b64encode(b"#MASKED_SSID#").decode() v = base64.b64encode(b"#MASKED_SSID#").decode()
elif k in ["nickname"]: elif k in ["nickname"]:
v = base64.b64encode(b"#MASKED_NAME#").decode() v = base64.b64encode(b"#MASKED_NAME#").decode()
elif k in ["alias", "device_alias"]: elif k in ["alias", "device_alias", "device_name"]:
v = "#MASKED_NAME#" v = "#MASKED_NAME#"
elif isinstance(res[k], int): elif isinstance(res[k], int):
v = 0 v = 0
@ -477,6 +490,44 @@ def format_exception(e):
return exception_str 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( async def _make_requests_or_exit(
protocol: SmartProtocol, protocol: SmartProtocol,
requests: dict, requests: dict,
@ -534,69 +585,28 @@ async def get_smart_camera_test_calls(protocol: SmartProtocol):
test_calls: list[SmartCall] = [] test_calls: list[SmartCall] = []
successes: 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 = [] 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( test_calls.append(
SmartCall( SmartCall(
module=method, module=module,
request={method: params}, request=request,
should_succeed=True, should_succeed=True,
child_device_id="", child_device_id="",
supports_multiple=(method != "get"),
) )
) )
# Now get the child device requests # Now get the child device requests
child_request = {
"getChildDeviceList": {"childControl": {"start_index": 0}},
}
try: try:
child_request = {"getChildDeviceList": {"childControl": {"start_index": 0}}}
child_response = await protocol.query(child_request) child_response = await protocol.query(child_request)
except Exception: except Exception:
_LOGGER.debug("Device does not have any children.") _LOGGER.debug("Device does not have any children.")
@ -607,6 +617,7 @@ async def get_smart_camera_test_calls(protocol: SmartProtocol):
request=child_request, request=child_request,
should_succeed=True, should_succeed=True,
child_device_id="", child_device_id="",
supports_multiple=True,
) )
) )
child_list = child_response["getChildDeviceList"]["child_device_list"] 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(f"Skipping {component_id}..", nl=False)
click.echo(click.style("UNSUPPORTED", fg="yellow")) click.echo(click.style("UNSUPPORTED", fg="yellow"))
else: # Not a smart protocol device so assume camera protocol 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( test_calls.append(
SmartCall( SmartCall(
module=method, module=method,
request={method: params}, request=request,
should_succeed=True, should_succeed=True,
child_device_id=child_id, child_device_id=child_id,
) )
@ -804,7 +818,9 @@ async def get_smart_test_calls(protocol: SmartProtocol):
click.echo(click.style("UNSUPPORTED", fg="yellow")) click.echo(click.style("UNSUPPORTED", fg="yellow"))
# Add the extra calls for each child # Add the extra calls for each child
for extra_call in extra_test_calls: 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) test_calls.append(extra_child_call)
return test_calls, successes return test_calls, successes
@ -879,10 +895,10 @@ async def get_smart_fixtures(
finally: finally:
await protocol.close() await protocol.close()
device_requests: dict[str, dict] = {} device_requests: dict[str, list[SmartCall]] = {}
for success in successes: for success in successes:
device_request = device_requests.setdefault(success.child_device_id, {}) device_request = device_requests.setdefault(success.child_device_id, [])
device_request.update(success.request) device_request.append(success)
scrubbed_device_ids = { scrubbed_device_ids = {
device_id: f"SCRUBBED_CHILD_DEVICE_ID_{index}" device_id: f"SCRUBBED_CHILD_DEVICE_ID_{index}"
@ -890,24 +906,21 @@ async def get_smart_fixtures(
if device_id != "" if device_id != ""
} }
final = await _make_requests_or_exit( final = await _make_final_calls(
protocol, protocol, device_requests[""], "All successes", batch_size, child_device_id=""
device_requests[""],
"all successes at once",
batch_size,
child_device_id="",
) )
fixture_results = [] fixture_results = []
for child_device_id, requests in device_requests.items(): for child_device_id, requests in device_requests.items():
if child_device_id == "": if child_device_id == "":
continue continue
response = await _make_requests_or_exit( response = await _make_final_calls(
protocol, protocol,
requests, requests,
"all child successes at once", "All child successes",
batch_size, batch_size,
child_device_id=child_device_id, child_device_id=child_device_id,
) )
scrubbed = scrubbed_device_ids[child_device_id] scrubbed = scrubbed_device_ids[child_device_id]
if "get_device_info" in response and "device_id" in response["get_device_info"]: if "get_device_info" in response and "device_id" in response["get_device_info"]:
response["get_device_info"]["device_id"] = scrubbed 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)) click.echo(click.style("## device info file ##", bold=True))
if "get_device_info" in final: if "get_device_info" in final:
# smart protocol
hw_version = final["get_device_info"]["hw_ver"] hw_version = final["get_device_info"]["hw_ver"]
sw_version = final["get_device_info"]["fw_ver"] sw_version = final["get_device_info"]["fw_ver"]
if discovery_info: if discovery_info:
@ -970,16 +984,19 @@ async def get_smart_fixtures(
else: else:
model = final["get_device_info"]["model"] + "(XX)" model = final["get_device_info"]["model"] + "(XX)"
sw_version = sw_version.split(" ", maxsplit=1)[0] sw_version = sw_version.split(" ", maxsplit=1)[0]
copy_folder = SMART_FOLDER
else: else:
# smart camera protocol
hw_version = final["getDeviceInfo"]["device_info"]["basic_info"]["hw_version"] hw_version = final["getDeviceInfo"]["device_info"]["basic_info"]["hw_version"]
sw_version = final["getDeviceInfo"]["device_info"]["basic_info"]["sw_version"] sw_version = final["getDeviceInfo"]["device_info"]["basic_info"]["sw_version"]
model = final["getDeviceInfo"]["device_info"]["basic_info"]["device_model"] model = final["getDeviceInfo"]["device_info"]["basic_info"]["device_model"]
region = final["getDeviceInfo"]["device_info"]["basic_info"]["region"] region = final["getDeviceInfo"]["device_info"]["basic_info"]["region"]
sw_version = sw_version.split(" ", maxsplit=1)[0] sw_version = sw_version.split(" ", maxsplit=1)[0]
model = f"{model}({region})" model = f"{model}({region})"
copy_folder = SMARTCAMERA_FOLDER
save_filename = f"{model}_{hw_version}_{sw_version}.json" save_filename = f"{model}_{hw_version}_{sw_version}.json"
copy_folder = SMART_FOLDER
fixture_results.insert( fixture_results.insert(
0, FixtureResult(filename=save_filename, folder=copy_folder, data=final) 0, FixtureResult(filename=save_filename, folder=copy_folder, data=final)
) )

View File

@ -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"]}}},
]

View File

@ -3,6 +3,7 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from dataclasses import dataclass
from pprint import pformat as pf from pprint import pformat as pf
from typing import Any from typing import Any
@ -22,6 +23,28 @@ from .sslaestransport import (
_LOGGER = logging.getLogger(__name__) _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 SmartCameraProtocol(SmartProtocol):
"""Class for SmartCamera Protocol.""" """Class for SmartCamera Protocol."""
@ -63,37 +86,70 @@ class SmartCameraProtocol(SmartProtocol):
"""Close the underlying transport.""" """Close the underlying transport."""
await self._transport.close() 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( async def _execute_query(
self, request: str | dict, *, retry_count: int, iterate_list_pages: bool = True self, request: str | dict, *, retry_count: int, iterate_list_pages: bool = True
) -> dict: ) -> dict:
debug_enabled = _LOGGER.isEnabledFor(logging.DEBUG) debug_enabled = _LOGGER.isEnabledFor(logging.DEBUG)
if isinstance(request, dict): if isinstance(request, dict):
if len(request) == 1: method = next(iter(request))
method = next(iter(request)) if len(request) == 1 and method in {"get", "set", "do", "multipleRequest"}:
if method == "multipleRequest": single_request = self._get_smart_camera_single_request(request)
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)
else: else:
return await self._execute_multiple_query(request, retry_count) return await self._execute_multiple_query(request, retry_count)
else: else:
# If method like getSomeThing then module will be some_thing single_request = self._make_smart_camera_single_request(request)
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: {}}
smart_request = json_dumps(req) smart_request = json_dumps(single_request.request)
if debug_enabled: if debug_enabled:
_LOGGER.debug( _LOGGER.debug(
"%s >> %s", "%s >> %s",
@ -111,15 +167,29 @@ class SmartCameraProtocol(SmartProtocol):
if "error_code" in response_data: if "error_code" in response_data:
# H200 does not return an error code # 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 # 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 {} return {}
if method == "multipleRequest": if single_request.method_type == "multi":
return {method: response_data["result"]} return {single_request.method_name: response_data["result"]}
return {method: {params: response_data[params]}} return {
single_request.method_name: {
single_request.param_name: response_data[single_request.param_name]
}
}
class _ChildCameraProtocolWrapper(SmartProtocol): class _ChildCameraProtocolWrapper(SmartProtocol):

View File

@ -163,6 +163,10 @@ class SmartProtocol(BaseProtocol):
] ]
end = len(multi_requests) 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 # Break the requests down as there can be a size limit
step = self._multi_request_batch_size step = self._multi_request_batch_size
@ -172,14 +176,12 @@ class SmartProtocol(BaseProtocol):
method = request["method"] method = request["method"]
req = self.get_smart_request(method, request.get("params")) req = self.get_smart_request(method, request.get("params"))
resp = await self._transport.send(req) 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"] multi_result[method] = resp["result"]
return multi_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)): for batch_num, i in enumerate(range(0, end, step)):
requests_step = multi_requests[i : i + step] requests_step = multi_requests[i : i + step]

View File

@ -173,10 +173,16 @@ class FakeSmartCameraTransport(BaseTransport):
request_dict["params"]["childControl"] request_dict["params"]["childControl"]
) )
if method == "set": if method[:3] == "set":
for key, val in request_dict.items(): for key, val in request_dict.items():
if key != "method": 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)) section = next(iter(val))
skey_val = val[section] skey_val = val[section]
for skey, sval in skey_val.items(): for skey, sval in skey_val.items():

View File

@ -5,14 +5,14 @@
"connect_type": "wireless", "connect_type": "wireless",
"device_id": "0000000000000000000000000000000000000000", "device_id": "0000000000000000000000000000000000000000",
"http_port": 443, "http_port": 443,
"last_alarm_time": "0", "last_alarm_time": "1729264456",
"last_alarm_type": "", "last_alarm_type": "motion",
"owner": "00000000000000000000000000000000", "owner": "00000000000000000000000000000000",
"sd_status": "offline" "sd_status": "offline"
}, },
"device_id": "00000000000000000000000000000000", "device_id": "00000000000000000000000000000000",
"device_model": "C210", "device_model": "C210",
"device_name": "00000 000", "device_name": "#MASKED_NAME#",
"device_type": "SMART.IPCAMERA", "device_type": "SMART.IPCAMERA",
"encrypt_info": { "encrypt_info": {
"data": "", "data": "",
@ -60,6 +60,14 @@
"usr_def_audio": [] "usr_def_audio": []
} }
}, },
"getAlertPlan": {
"msg_alarm_plan": {
"chn1_msg_alarm_plan": {
"alarm_plan_1": "0000-0000,127",
"enabled": "off"
}
}
},
"getAlertTypeList": { "getAlertTypeList": {
"msg_alarm": { "msg_alarm": {
"alert_type": { "alert_type": {
@ -106,10 +114,18 @@
} }
} }
}, },
"getClockStatus": {
"system": {
"clock_status": {
"local_time": "2024-10-24 12:49:09",
"seconds_from_1970": 1729770549
}
}
},
"getConnectionType": { "getConnectionType": {
"link_type": "wifi", "link_type": "wifi",
"rssi": "2", "rssi": "3",
"rssiValue": -64, "rssiValue": -62,
"ssid": "I01BU0tFRF9TU0lEIw==" "ssid": "I01BU0tFRF9TU0lEIw=="
}, },
"getDetectionConfig": { "getDetectionConfig": {
@ -133,7 +149,7 @@
"device_alias": "#MASKED_NAME#", "device_alias": "#MASKED_NAME#",
"device_info": "C210 2.0 IPC", "device_info": "C210 2.0 IPC",
"device_model": "C210", "device_model": "C210",
"device_name": "0000 0.0", "device_name": "#MASKED_NAME#",
"device_type": "SMART.IPCAMERA", "device_type": "SMART.IPCAMERA",
"features": 3, "features": 3,
"ffs": false, "ffs": false,
@ -171,19 +187,10 @@
} }
}, },
"getLastAlarmInfo": { "getLastAlarmInfo": {
"msg_alarm": { "system": {
"chn1_msg_alarm_info": { "last_alarm_info": {
"alarm_duration": "0", "last_alarm_time": "1729264456",
"alarm_mode": [ "last_alarm_type": "motion"
"sound",
"light"
],
"alarm_type": "0",
"alarm_volume": "high",
"enabled": "off",
"light_alarm_enabled": "on",
"light_type": "1",
"sound_alarm_enabled": "on"
} }
} }
}, },
@ -519,6 +526,15 @@
} }
} }
}, },
"getTimezone": {
"system": {
"basic": {
"timezone": "UTC-00:00",
"timing_mode": "ntp",
"zone_id": "Europe/Berlin"
}
}
},
"getVideoCapability": { "getVideoCapability": {
"video_capability": { "video_capability": {
"main": { "main": {
@ -602,5 +618,199 @@
"getWhitelampStatus": { "getWhitelampStatus": {
"rest_time": 0, "rest_time": 0,
"status": 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"
}
}
}
} }
} }

View File

@ -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": { "getFirmwareAutoUpgradeConfig": {
"auto_upgrade": { "auto_upgrade": {
"common": { "common": {