Update dump_devinfo for smart camera protocol (#1169)

Introduces the child camera protocol wrapper, required to get the child device info with the new protocol.
This commit is contained in:
Steven B. 2024-10-18 12:06:22 +01:00 committed by GitHub
parent 486984fff8
commit acd0202cab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 1076 additions and 97 deletions

View File

@ -28,14 +28,22 @@ from kasa import (
AuthenticationError, AuthenticationError,
Credentials, Credentials,
Device, Device,
DeviceConfig,
DeviceConnectionParameters,
Discover, Discover,
KasaException, KasaException,
TimeoutError, TimeoutError,
) )
from kasa.device_factory import get_protocol
from kasa.deviceconfig import DeviceEncryptionType, DeviceFamily
from kasa.discover import DiscoveryResult from kasa.discover import DiscoveryResult
from kasa.exceptions import SmartErrorCode from kasa.exceptions import SmartErrorCode
from kasa.smart import SmartDevice from kasa.experimental.smartcameraprotocol import (
from kasa.smartprotocol import _ChildProtocolWrapper SmartCameraProtocol,
_ChildCameraProtocolWrapper,
)
from kasa.smart import SmartChildDevice
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") SmartCall = namedtuple("SmartCall", "module request should_succeed child_device_id")
@ -45,6 +53,8 @@ SMART_FOLDER = "kasa/tests/fixtures/smart/"
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/"
ENCRYPT_TYPES = [encrypt_type.value for encrypt_type in DeviceEncryptionType]
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -82,10 +92,22 @@ def scrub(res):
"mfi_setup_id", "mfi_setup_id",
"mfi_token_token", "mfi_token_token",
"mfi_token_uuid", "mfi_token_uuid",
"dev_id",
"device_name",
"device_alias",
"connect_ssid",
"encrypt_info",
"local_ip",
] ]
for k, v in res.items(): for k, v in res.items():
if isinstance(v, collections.abc.Mapping): if isinstance(v, collections.abc.Mapping):
if k == "encrypt_info":
if "data" in v:
v["data"] = ""
if "key" in v:
v["key"] = ""
else:
res[k] = scrub(res.get(k)) res[k] = scrub(res.get(k))
elif ( elif (
isinstance(v, list) isinstance(v, list)
@ -107,20 +129,20 @@ def scrub(res):
v = f"{v[:8]}{delim}{rest}" v = f"{v[:8]}{delim}{rest}"
elif k in ["latitude", "latitude_i", "longitude", "longitude_i"]: elif k in ["latitude", "latitude_i", "longitude", "longitude_i"]:
v = 0 v = 0
elif k in ["ip"]: elif k in ["ip", "local_ip"]:
v = "127.0.0.123" v = "127.0.0.123"
elif k in ["ssid"]: elif k in ["ssid"]:
# Need a valid base64 value here # Need a valid base64 value here
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"]: elif k in ["alias", "device_alias"]:
v = "#MASKED_NAME#" v = "#MASKED_NAME#"
elif isinstance(res[k], int): elif isinstance(res[k], int):
v = 0 v = 0
elif k == "device_id" and "SCRUBBED" in v: elif k in ["device_id", "dev_id"] and "SCRUBBED" in v:
pass # already scrubbed pass # already scrubbed
elif k == "device_id" and len(v) > 40: elif k == ["device_id", "dev_id"] and len(v) > 40:
# retain the last two chars when scrubbing child ids # retain the last two chars when scrubbing child ids
end = v[-2:] end = v[-2:]
v = re.sub(r"\w", "0", v) v = re.sub(r"\w", "0", v)
@ -142,14 +164,18 @@ def default_to_regular(d):
return d return d
async def handle_device(basedir, autosave, device: Device, batch_size: int): async def handle_device(
basedir, autosave, protocol, *, discovery_info=None, batch_size: int
):
"""Create a fixture for a single device instance.""" """Create a fixture for a single device instance."""
if isinstance(device, SmartDevice): if isinstance(protocol, SmartProtocol):
fixture_results: list[FixtureResult] = await get_smart_fixtures( fixture_results: list[FixtureResult] = await get_smart_fixtures(
device, batch_size protocol, discovery_info=discovery_info, batch_size=batch_size
) )
else: else:
fixture_results = [await get_legacy_fixture(device)] fixture_results = [
await get_legacy_fixture(protocol, discovery_info=discovery_info)
]
for fixture_result in fixture_results: for fixture_result in fixture_results:
save_filename = Path(basedir) / fixture_result.folder / fixture_result.filename save_filename = Path(basedir) / fixture_result.folder / fixture_result.filename
@ -207,6 +233,44 @@ async def handle_device(basedir, autosave, device: Device, batch_size: int):
+ " Do not use this flag unless you are sure you know what it means." + " Do not use this flag unless you are sure you know what it means."
), ),
) )
@click.option(
"--discovery-timeout",
envvar="KASA_DISCOVERY_TIMEOUT",
default=10,
required=False,
show_default=True,
help="Timeout for discovery.",
)
@click.option(
"-e",
"--encrypt-type",
envvar="KASA_ENCRYPT_TYPE",
default=None,
type=click.Choice(ENCRYPT_TYPES, case_sensitive=False),
)
@click.option(
"-df",
"--device-family",
envvar="KASA_DEVICE_FAMILY",
default="SMART.TAPOPLUG",
help="Device family type, e.g. `SMART.KASASWITCH`.",
)
@click.option(
"-lv",
"--login-version",
envvar="KASA_LOGIN_VERSION",
default=2,
type=int,
help="The login version for device authentication. Defaults to 2",
)
@click.option(
"--https/--no-https",
envvar="KASA_HTTPS",
default=False,
is_flag=True,
type=bool,
help="Set flag if the device encryption uses https.",
)
@click.option("--port", help="Port override", type=int) @click.option("--port", help="Port override", type=int)
async def cli( async def cli(
host, host,
@ -215,9 +279,14 @@ async def cli(
autosave, autosave,
debug, debug,
username, username,
discovery_timeout,
password, password,
batch_size, batch_size,
discovery_info, discovery_info,
encrypt_type,
https,
device_family,
login_version,
port, port,
): ):
"""Generate devinfo files for devices. """Generate devinfo files for devices.
@ -227,11 +296,14 @@ async def cli(
if debug: if debug:
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
from kasa.experimental.enabled import Enabled
Enabled.set(True)
credentials = Credentials(username=username, password=password) credentials = Credentials(username=username, password=password)
if host is not None: if host is not None:
if discovery_info: if discovery_info:
click.echo("Host and discovery info given, trying connect on %s." % host) click.echo("Host and discovery info given, trying connect on %s." % host)
from kasa import DeviceConfig, DeviceConnectionParameters
di = json.loads(discovery_info) di = json.loads(discovery_info)
dr = DiscoveryResult(**di) dr = DiscoveryResult(**di)
@ -247,25 +319,68 @@ async def cli(
credentials=credentials, credentials=credentials,
) )
device = await Device.connect(config=dc) device = await Device.connect(config=dc)
device.update_from_discover_info(dr.get_dict()) await handle_device(
basedir,
autosave,
device.protocol,
discovery_info=dr.get_dict(),
batch_size=batch_size,
)
elif device_family and encrypt_type:
ctype = DeviceConnectionParameters(
DeviceFamily(device_family),
DeviceEncryptionType(encrypt_type),
login_version,
https,
)
config = DeviceConfig(
host=host,
port_override=port,
credentials=credentials,
connection_type=ctype,
)
if protocol := get_protocol(config):
await handle_device(basedir, autosave, protocol, batch_size=batch_size)
else:
raise KasaException(
"Could not find a protocol for the given parameters. "
+ "Maybe you need to enable --experimental."
)
else: else:
click.echo("Host given, performing discovery on %s." % host) click.echo("Host given, performing discovery on %s." % host)
device = await Discover.discover_single( device = await Discover.discover_single(
host, credentials=credentials, port=port host,
credentials=credentials,
port=port,
discovery_timeout=discovery_timeout,
)
await handle_device(
basedir,
autosave,
device.protocol,
discovery_info=device._discovery_info,
batch_size=batch_size,
) )
await handle_device(basedir, autosave, device, batch_size)
else: else:
click.echo( click.echo(
"No --host given, performing discovery on %s. Use --target to override." "No --host given, performing discovery on %s. Use --target to override."
% target % target
) )
devices = await Discover.discover(target=target, credentials=credentials) devices = await Discover.discover(
target=target, credentials=credentials, discovery_timeout=discovery_timeout
)
click.echo("Detected %s devices" % len(devices)) click.echo("Detected %s devices" % len(devices))
for dev in devices.values(): for dev in devices.values():
await handle_device(basedir, autosave, dev, batch_size) await handle_device(
basedir,
autosave,
dev.protocol,
discovery_info=dev._discovery_info,
batch_size=batch_size,
)
async def get_legacy_fixture(device): async def get_legacy_fixture(protocol, *, discovery_info):
"""Get fixture for legacy IOT style protocol.""" """Get fixture for legacy IOT style protocol."""
items = [ items = [
Call(module="system", method="get_sysinfo"), Call(module="system", method="get_sysinfo"),
@ -284,9 +399,7 @@ async def get_legacy_fixture(device):
for test_call in items: for test_call in items:
try: try:
click.echo(f"Testing {test_call}..", nl=False) click.echo(f"Testing {test_call}..", nl=False)
info = await device.protocol.query( info = await protocol.query({test_call.module: {test_call.method: {}}})
{test_call.module: {test_call.method: {}}}
)
resp = info[test_call.module] resp = info[test_call.module]
except Exception as ex: except Exception as ex:
click.echo(click.style(f"FAIL {ex}", fg="red")) click.echo(click.style(f"FAIL {ex}", fg="red"))
@ -297,7 +410,7 @@ async def get_legacy_fixture(device):
click.echo(click.style("OK", fg="green")) click.echo(click.style("OK", fg="green"))
successes.append((test_call, info)) successes.append((test_call, info))
finally: finally:
await device.protocol.close() await protocol.close()
final_query = defaultdict(defaultdict) final_query = defaultdict(defaultdict)
final = defaultdict(defaultdict) final = defaultdict(defaultdict)
@ -308,15 +421,15 @@ async def get_legacy_fixture(device):
final = default_to_regular(final) final = default_to_regular(final)
try: try:
final = await device.protocol.query(final_query) final = await protocol.query(final_query)
except Exception as ex: except Exception as ex:
_echo_error(f"Unable to query all successes at once: {ex}", bold=True, fg="red") _echo_error(f"Unable to query all successes at once: {ex}", bold=True, fg="red")
finally: finally:
await device.protocol.close() await protocol.close()
if device._discovery_info and not device._discovery_info.get("system"): if discovery_info and not discovery_info.get("system"):
# Need to recreate a DiscoverResult here because we don't want the aliases # Need to recreate a DiscoverResult here because we don't want the aliases
# in the fixture, we want the actual field names as returned by the device. # in the fixture, we want the actual field names as returned by the device.
dr = DiscoveryResult(**device._discovery_info) dr = DiscoveryResult(**protocol._discovery_info)
final["discovery_result"] = dr.dict( final["discovery_result"] = dr.dict(
by_alias=False, exclude_unset=True, exclude_none=True, exclude_defaults=True by_alias=False, exclude_unset=True, exclude_none=True, exclude_defaults=True
) )
@ -365,29 +478,29 @@ def format_exception(e):
async def _make_requests_or_exit( async def _make_requests_or_exit(
device: SmartDevice, protocol: SmartProtocol,
requests: list[SmartRequest], requests: dict,
name: str, name: str,
batch_size: int, batch_size: int,
*, *,
child_device_id: str, child_device_id: str,
) -> dict[str, dict]: ) -> dict[str, dict]:
final = {} final = {}
protocol = ( # Calling close on child protocol wrappers is a noop
device.protocol protocol_to_close = protocol
if child_device_id == "" if child_device_id:
else _ChildProtocolWrapper(child_device_id, device.protocol) if isinstance(protocol, SmartCameraProtocol):
) protocol = _ChildCameraProtocolWrapper(child_device_id, protocol)
else:
protocol = _ChildProtocolWrapper(child_device_id, protocol)
try: try:
end = len(requests) end = len(requests)
step = batch_size # Break the requests down as there seems to be a size limit step = batch_size # Break the requests down as there seems to be a size limit
keys = [key for key in requests]
for i in range(0, end, step): for i in range(0, end, step):
x = i x = i
requests_step = requests[x : x + step] requests_step = {key: requests[key] for key in keys[x : x + step]}
request: list[SmartRequest] | SmartRequest = ( responses = await protocol.query(requests_step)
requests_step[0] if len(requests_step) == 1 else requests_step
)
responses = await protocol.query(SmartRequest._create_request_dict(request))
for method, result in responses.items(): for method, result in responses.items():
final[method] = result final[method] = result
return final return final
@ -413,10 +526,155 @@ async def _make_requests_or_exit(
_echo_error(format_exception(ex)) _echo_error(format_exception(ex))
exit(1) exit(1)
finally: finally:
await device.protocol.close() await protocol_to_close.close()
async def get_smart_test_calls(device: SmartDevice): async def get_smart_camera_test_calls(protocol: SmartProtocol):
"""Get the list of test calls to make."""
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():
test_calls.append(
SmartCall(
module=method,
request={method: params},
should_succeed=True,
child_device_id="",
)
)
# Now get the child device requests
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:
successes.append(
SmartCall(
module="getChildDeviceList",
request=child_request,
should_succeed=True,
child_device_id="",
)
)
child_list = child_response["getChildDeviceList"]["child_device_list"]
for child in child_list:
child_id = child.get("device_id") or child.get("dev_id")
if not child_id:
_LOGGER.error("Could not find child device id in %s", child)
# If category is in the child device map the protocol is smart.
if (
category := child.get("category")
) and category in SmartChildDevice.CHILD_DEVICE_TYPE_MAP:
child_protocol = _ChildCameraProtocolWrapper(child_id, protocol)
try:
nego_response = await child_protocol.query({"component_nego": None})
except Exception as ex:
_LOGGER.error("Error calling component_nego: %s", ex)
continue
if "component_nego" not in nego_response:
_LOGGER.error(
"Could not find component_nego in device response: %s",
nego_response,
)
continue
successes.append(
SmartCall(
module="component_nego",
request={"component_nego": None},
should_succeed=True,
child_device_id=child_id,
)
)
child_components = {
item["id"]: item["ver_code"]
for item in nego_response["component_nego"]["component_list"]
}
for component_id, ver_code in child_components.items():
if (
requests := get_component_requests(component_id, ver_code)
) is not None:
component_test_calls = [
SmartCall(
module=component_id,
request={key: val},
should_succeed=True,
child_device_id=child_id,
)
for key, val in requests.items()
]
test_calls.extend(component_test_calls)
else:
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():
test_calls.append(
SmartCall(
module=method,
request={method: params},
should_succeed=True,
child_device_id=child_id,
)
)
finally:
await protocol.close()
return test_calls, successes
async def get_smart_test_calls(protocol: SmartProtocol):
"""Get the list of test calls to make.""" """Get the list of test calls to make."""
test_calls = [] test_calls = []
successes = [] successes = []
@ -425,7 +683,7 @@ async def get_smart_test_calls(device: SmartDevice):
extra_test_calls = [ extra_test_calls = [
SmartCall( SmartCall(
module="temp_humidity_records", module="temp_humidity_records",
request=SmartRequest.get_raw_request("get_temp_humidity_records"), request=SmartRequest.get_raw_request("get_temp_humidity_records").to_dict(),
should_succeed=False, should_succeed=False,
child_device_id="", child_device_id="",
), ),
@ -433,7 +691,7 @@ async def get_smart_test_calls(device: SmartDevice):
module="trigger_logs", module="trigger_logs",
request=SmartRequest.get_raw_request( request=SmartRequest.get_raw_request(
"get_trigger_logs", SmartRequest.GetTriggerLogsParams() "get_trigger_logs", SmartRequest.GetTriggerLogsParams()
), ).to_dict(),
should_succeed=False, should_succeed=False,
child_device_id="", child_device_id="",
), ),
@ -441,8 +699,8 @@ async def get_smart_test_calls(device: SmartDevice):
click.echo("Testing component_nego call ..", nl=False) click.echo("Testing component_nego call ..", nl=False)
responses = await _make_requests_or_exit( responses = await _make_requests_or_exit(
device, protocol,
[SmartRequest.component_nego()], SmartRequest.component_nego().to_dict(),
"component_nego call", "component_nego call",
batch_size=1, batch_size=1,
child_device_id="", child_device_id="",
@ -452,7 +710,7 @@ async def get_smart_test_calls(device: SmartDevice):
successes.append( successes.append(
SmartCall( SmartCall(
module="component_nego", module="component_nego",
request=SmartRequest("component_nego"), request=SmartRequest("component_nego").to_dict(),
should_succeed=True, should_succeed=True,
child_device_id="", child_device_id="",
) )
@ -464,8 +722,8 @@ async def get_smart_test_calls(device: SmartDevice):
if "child_device" in components: if "child_device" in components:
child_components = await _make_requests_or_exit( child_components = await _make_requests_or_exit(
device, protocol,
[SmartRequest.get_child_device_component_list()], SmartRequest.get_child_device_component_list().to_dict(),
"child device component list", "child device component list",
batch_size=1, batch_size=1,
child_device_id="", child_device_id="",
@ -473,7 +731,7 @@ async def get_smart_test_calls(device: SmartDevice):
successes.append( successes.append(
SmartCall( SmartCall(
module="child_component_list", module="child_component_list",
request=SmartRequest.get_child_device_component_list(), request=SmartRequest.get_child_device_component_list().to_dict(),
should_succeed=True, should_succeed=True,
child_device_id="", child_device_id="",
) )
@ -481,7 +739,7 @@ async def get_smart_test_calls(device: SmartDevice):
test_calls.append( test_calls.append(
SmartCall( SmartCall(
module="child_device_list", module="child_device_list",
request=SmartRequest.get_child_device_list(), request=SmartRequest.get_child_device_list().to_dict(),
should_succeed=True, should_succeed=True,
child_device_id="", child_device_id="",
) )
@ -506,11 +764,11 @@ async def get_smart_test_calls(device: SmartDevice):
component_test_calls = [ component_test_calls = [
SmartCall( SmartCall(
module=component_id, module=component_id,
request=request, request={key: val},
should_succeed=True, should_succeed=True,
child_device_id="", child_device_id="",
) )
for request in requests for key, val in requests.items()
] ]
test_calls.extend(component_test_calls) test_calls.extend(component_test_calls)
else: else:
@ -524,7 +782,7 @@ async def get_smart_test_calls(device: SmartDevice):
test_calls.append( test_calls.append(
SmartCall( SmartCall(
module="component_nego", module="component_nego",
request=SmartRequest("component_nego"), request=SmartRequest("component_nego").to_dict(),
should_succeed=True, should_succeed=True,
child_device_id=child_device_id, child_device_id=child_device_id,
) )
@ -534,11 +792,11 @@ async def get_smart_test_calls(device: SmartDevice):
component_test_calls = [ component_test_calls = [
SmartCall( SmartCall(
module=component_id, module=component_id,
request=request, request={key: val},
should_succeed=True, should_succeed=True,
child_device_id=child_device_id, child_device_id=child_device_id,
) )
for request in requests for key, val in requests.items()
] ]
test_calls.extend(component_test_calls) test_calls.extend(component_test_calls)
else: else:
@ -568,23 +826,28 @@ def get_smart_child_fixture(response):
) )
async def get_smart_fixtures(device: SmartDevice, batch_size: int): async def get_smart_fixtures(
protocol: SmartProtocol, *, discovery_info=None, batch_size: int
):
"""Get fixture for new TAPO style protocol.""" """Get fixture for new TAPO style protocol."""
test_calls, successes = await get_smart_test_calls(device) if isinstance(protocol, SmartCameraProtocol):
test_calls, successes = await get_smart_camera_test_calls(protocol)
child_wrapper: type[_ChildProtocolWrapper | _ChildCameraProtocolWrapper] = (
_ChildCameraProtocolWrapper
)
else:
test_calls, successes = await get_smart_test_calls(protocol)
child_wrapper = _ChildProtocolWrapper
for test_call in test_calls: for test_call in test_calls:
click.echo(f"Testing {test_call.module}..", nl=False) click.echo(f"Testing {test_call.module}..", nl=False)
try: try:
click.echo(f"Testing {test_call}..", nl=False) click.echo(f"Testing {test_call}..", nl=False)
if test_call.child_device_id == "": if test_call.child_device_id == "":
response = await device.protocol.query( response = await protocol.query(test_call.request)
SmartRequest._create_request_dict(test_call.request)
)
else: else:
cp = _ChildProtocolWrapper(test_call.child_device_id, device.protocol) cp = child_wrapper(test_call.child_device_id, protocol)
response = await cp.query( response = await cp.query(test_call.request)
SmartRequest._create_request_dict(test_call.request)
)
except AuthenticationError as ex: except AuthenticationError as ex:
_echo_error( _echo_error(
f"Unable to query the device due to an authentication error: {ex}", f"Unable to query the device due to an authentication error: {ex}",
@ -614,12 +877,12 @@ async def get_smart_fixtures(device: SmartDevice, batch_size: int):
click.echo(click.style("OK", fg="green")) click.echo(click.style("OK", fg="green"))
successes.append(test_call) successes.append(test_call)
finally: finally:
await device.protocol.close() await protocol.close()
device_requests: dict[str, list[SmartRequest]] = {} device_requests: dict[str, dict] = {}
for success in successes: for success in successes:
device_request = device_requests.setdefault(success.child_device_id, []) device_request = device_requests.setdefault(success.child_device_id, {})
device_request.append(success.request) device_request.update(success.request)
scrubbed_device_ids = { scrubbed_device_ids = {
device_id: f"SCRUBBED_CHILD_DEVICE_ID_{index}" device_id: f"SCRUBBED_CHILD_DEVICE_ID_{index}"
@ -628,7 +891,7 @@ async def get_smart_fixtures(device: SmartDevice, batch_size: int):
} }
final = await _make_requests_or_exit( final = await _make_requests_or_exit(
device, protocol,
device_requests[""], device_requests[""],
"all successes at once", "all successes at once",
batch_size, batch_size,
@ -639,7 +902,7 @@ async def get_smart_fixtures(device: SmartDevice, batch_size: int):
if child_device_id == "": if child_device_id == "":
continue continue
response = await _make_requests_or_exit( response = await _make_requests_or_exit(
device, protocol,
requests, requests,
"all child successes at once", "all child successes at once",
batch_size, batch_size,
@ -649,18 +912,26 @@ async def get_smart_fixtures(device: SmartDevice, batch_size: int):
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
# If the child is a different model to the parent create a seperate fixture # If the child is a different model to the parent create a seperate fixture
if "get_device_info" in final:
parent_model = final["get_device_info"]["model"]
elif "getDeviceInfo" in final:
parent_model = final["getDeviceInfo"]["device_info"]["basic_info"][
"device_model"
]
else:
raise KasaException("Cannot determine parent device model.")
if ( if (
"component_nego" in response "component_nego" in response
and "get_device_info" in response and "get_device_info" in response
and (child_model := response["get_device_info"].get("model")) and (child_model := response["get_device_info"].get("model"))
and child_model != final["get_device_info"]["model"] and child_model != parent_model
): ):
fixture_results.append(get_smart_child_fixture(response)) fixture_results.append(get_smart_child_fixture(response))
else: else:
cd = final.setdefault("child_devices", {}) cd = final.setdefault("child_devices", {})
cd[scrubbed] = response cd[scrubbed] = response
# Scrub the device ids in the parent # Scrub the device ids in the parent for smart protocol
if gc := final.get("get_child_device_component_list"): if gc := final.get("get_child_device_component_list"):
for child in gc["child_component_list"]: for child in gc["child_component_list"]:
device_id = child["device_id"] device_id = child["device_id"]
@ -669,9 +940,21 @@ async def get_smart_fixtures(device: SmartDevice, batch_size: int):
device_id = child["device_id"] device_id = child["device_id"]
child["device_id"] = scrubbed_device_ids[device_id] child["device_id"] = scrubbed_device_ids[device_id]
# Scrub the device ids in the parent for the smart camera protocol
if gc := final.get("getChildDeviceList"):
for child in gc["child_device_list"]:
if device_id := child.get("device_id"):
child["device_id"] = scrubbed_device_ids[device_id]
continue
if device_id := child.get("dev_id"):
child["dev_id"] = scrubbed_device_ids[device_id]
continue
_LOGGER.error("Could not find a device for the child device: %s", child)
# Need to recreate a DiscoverResult here because we don't want the aliases # Need to recreate a DiscoverResult here because we don't want the aliases
# in the fixture, we want the actual field names as returned by the device. # in the fixture, we want the actual field names as returned by the device.
dr = DiscoveryResult(**device._discovery_info) # type: ignore if discovery_info:
dr = DiscoveryResult(**discovery_info) # type: ignore
final["discovery_result"] = dr.dict( final["discovery_result"] = dr.dict(
by_alias=False, exclude_unset=True, exclude_none=True, exclude_defaults=True by_alias=False, exclude_unset=True, exclude_none=True, exclude_defaults=True
) )
@ -679,10 +962,21 @@ async def get_smart_fixtures(device: SmartDevice, batch_size: int):
click.echo("Got %s successes" % len(successes)) click.echo("Got %s successes" % len(successes))
click.echo(click.style("## device info file ##", bold=True)) click.echo(click.style("## device info file ##", bold=True))
if "get_device_info" in final:
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"]
model = final["discovery_result"]["device_model"] if discovery_info:
model = discovery_info["device_model"]
else:
model = final["get_device_info"]["model"] + "(XX)"
sw_version = sw_version.split(" ", maxsplit=1)[0] sw_version = sw_version.split(" ", maxsplit=1)[0]
else:
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})"
save_filename = f"{model}_{hw_version}_{sw_version}.json" save_filename = f"{model}_{hw_version}_{sw_version}.json"
copy_folder = SMART_FOLDER copy_folder = SMART_FOLDER

View File

@ -356,8 +356,8 @@ def get_component_requests(component_id, ver_code):
if (cr := COMPONENT_REQUESTS.get(component_id)) is None: if (cr := COMPONENT_REQUESTS.get(component_id)) is None:
return None return None
if callable(cr): if callable(cr):
return cr(ver_code) return SmartRequest._create_request_dict(cr(ver_code))
return cr return SmartRequest._create_request_dict(cr)
COMPONENT_REQUESTS = { COMPONENT_REQUESTS = {

View File

@ -6,7 +6,12 @@ import logging
from pprint import pformat as pf from pprint import pformat as pf
from typing import Any from typing import Any
from ..exceptions import AuthenticationError, DeviceError, _RetryableError from ..exceptions import (
AuthenticationError,
DeviceError,
KasaException,
_RetryableError,
)
from ..json import dumps as json_dumps from ..json import dumps as json_dumps
from ..smartprotocol import SmartProtocol from ..smartprotocol import SmartProtocol
from .sslaestransport import ( from .sslaestransport import (
@ -65,22 +70,28 @@ class SmartCameraProtocol(SmartProtocol):
if isinstance(request, dict): if isinstance(request, dict):
if len(request) == 1: if len(request) == 1:
multi_method = next(iter(request)) method = next(iter(request))
module = next(iter(request[multi_method])) if method == "multipleRequest":
params = request["multipleRequest"]
req = {"method": "multipleRequest", "params": params}
elif method[:3] == "set":
params = next(iter(request[method]))
req = { req = {
"method": multi_method[:3], "method": method[:3],
module: request[multi_method][module], params: request[method][params],
} }
else: else:
return await self._execute_multiple_query(request, retry_count) return await self._execute_multiple_query(request, retry_count)
else:
return await self._execute_multiple_query(request, retry_count)
else: else:
# If method like getSomeThing then module will be some_thing # If method like getSomeThing then module will be some_thing
multi_method = request method = request
snake_name = "".join( snake_name = "".join(
["_" + i.lower() if i.isupper() else i for i in multi_method] ["_" + i.lower() if i.isupper() else i for i in method]
).lstrip("_") ).lstrip("_")
module = snake_name[4:] params = snake_name[4:]
req = {"method": snake_name[:3], module: {}} req = {"method": snake_name[:3], params: {}}
smart_request = json_dumps(req) smart_request = json_dumps(req)
if debug_enabled: if debug_enabled:
@ -100,10 +111,71 @@ 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, multi_method) self._handle_response_error_code(response_data, method)
# TODO need to update handle response lists # TODO need to update handle response lists
if multi_method[:3] == "set": if method[:3] == "set":
return {} return {}
return {multi_method: {module: response_data[module]}} if method == "multipleRequest":
return {method: response_data["result"]}
return {method: {params: response_data[params]}}
class _ChildCameraProtocolWrapper(SmartProtocol):
"""Protocol wrapper for controlling child devices.
This is an internal class used to communicate with child devices,
and should not be used directly.
This class overrides query() method of the protocol to modify all
outgoing queries to use ``controlChild`` command, and unwraps the
device responses before returning to the caller.
"""
def __init__(self, device_id: str, base_protocol: SmartProtocol):
self._device_id = device_id
self._protocol = base_protocol
self._transport = base_protocol._transport
async def query(self, request: str | dict, retry_count: int = 3) -> dict:
"""Wrap request inside controlChild envelope."""
return await self._query(request, retry_count)
async def _query(self, request: str | dict, retry_count: int = 3) -> dict:
"""Wrap request inside controlChild envelope."""
if not isinstance(request, dict):
raise KasaException("Child requests must be dictionaries.")
requests = []
methods = []
for key, val in request.items():
request = {
"method": "controlChild",
"params": {
"childControl": {
"device_id": self._device_id,
"request_data": {"method": key, "params": val},
}
},
}
methods.append(key)
requests.append(request)
multipleRequest = {"multipleRequest": {"requests": requests}}
response = await self._protocol.query(multipleRequest, retry_count)
responses = response["multipleRequest"]["responses"]
response_dict = {}
for index_id, response in enumerate(responses):
response_data = response["result"]["response_data"]
method = methods[index_id]
self._handle_response_error_code(
response_data, method, raise_on_error=False
)
response_dict[method] = response_data.get("result")
return response_dict
async def close(self) -> None:
"""Do nothing as the parent owns the protocol."""

View File

@ -507,5 +507,5 @@ SMART_RETRYABLE_ERRORS = [
] ]
SMART_AUTHENTICATION_ERRORS = [ SMART_AUTHENTICATION_ERRORS = [
SmartErrorCode.INVALID_ARGUMENTS, SmartErrorCode.HOMEKIT_LOGIN_FAIL,
] ]

View File

@ -163,6 +163,7 @@ class SmartProtocol(BaseProtocol):
] ]
end = len(multi_requests) end = len(multi_requests)
# 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
if step == 1: if step == 1:
@ -175,6 +176,10 @@ class SmartProtocol(BaseProtocol):
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]
@ -222,7 +227,9 @@ class SmartProtocol(BaseProtocol):
responses = response_step["result"]["responses"] responses = response_step["result"]["responses"]
for response in responses: for response in responses:
method = response["method"] 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) result = response.get("result", None)
await self._handle_response_lists( await self._handle_response_lists(
result, method, retry_count=retry_count result, method, retry_count=retry_count

View File

@ -0,0 +1,606 @@
{
"discovery_result": {
"decrypted_data": {
"connect_ssid": "0000000000",
"connect_type": "wireless",
"device_id": "0000000000000000000000000000000000000000",
"http_port": 443,
"last_alarm_time": "0",
"last_alarm_type": "",
"owner": "00000000000000000000000000000000",
"sd_status": "offline"
},
"device_id": "00000000000000000000000000000000",
"device_model": "C210",
"device_name": "00000 000",
"device_type": "SMART.IPCAMERA",
"encrypt_info": {
"data": "",
"key": "",
"sym_schm": "AES"
},
"encrypt_type": [
"3"
],
"factory_default": false,
"firmware_version": "1.4.2 Build 240829 Rel.54953n",
"hardware_version": "2.0",
"ip": "127.0.0.123",
"is_support_iot_cloud": true,
"mac": "40-AE-30-00-00-00",
"mgt_encrypt_schm": {
"is_support_https": true
}
},
"getAlertConfig": {
"msg_alarm": {
"capability": {
"alarm_duration_support": "1",
"alarm_volume_support": "1",
"alert_event_type_support": "1",
"usr_def_audio_alarm_max_num": "15",
"usr_def_audio_alarm_support": "1",
"usr_def_audio_max_duration": "15",
"usr_def_audio_type": "0",
"usr_def_start_file_id": "8195"
},
"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"
},
"usr_def_audio": []
}
},
"getAlertTypeList": {
"msg_alarm": {
"alert_type": {
"alert_type_list": [
"Siren",
"Tone"
]
}
}
},
"getAudioConfig": {
"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"
}
}
},
"getBCDConfig": {
"sound_detection": {
"bcd": {
"digital_sensitivity": "50",
"enabled": "off",
"sensitivity": "medium"
}
}
},
"getCircularRecordingConfig": {
"harddisk_manage": {
"harddisk": {
"loop": "on"
}
}
},
"getConnectionType": {
"link_type": "wifi",
"rssi": "2",
"rssiValue": -64,
"ssid": "I01BU0tFRF9TU0lEIw=="
},
"getDetectionConfig": {
"motion_detection": {
"motion_det": {
"digital_sensitivity": "50",
"enabled": "on",
"non_vehicle_enabled": "off",
"people_enabled": "off",
"sensitivity": "medium",
"vehicle_enabled": "off"
}
}
},
"getDeviceInfo": {
"device_info": {
"basic_info": {
"avatar": "Home",
"barcode": "",
"dev_id": "0000000000000000000000000000000000000000",
"device_alias": "#MASKED_NAME#",
"device_info": "C210 2.0 IPC",
"device_model": "C210",
"device_name": "0000 0.0",
"device_type": "SMART.IPCAMERA",
"features": 3,
"ffs": false,
"has_set_location_info": 1,
"hw_desc": "00000000000000000000000000000000",
"hw_id": "00000000000000000000000000000000",
"hw_version": "2.0",
"is_cal": true,
"latitude": 0,
"longitude": 0,
"mac": "40-AE-30-00-00-00",
"manufacturer_name": "TP-LINK",
"mobile_access": "0",
"oem_id": "00000000000000000000000000000000",
"region": "EU",
"sw_version": "1.4.2 Build 240829 Rel.54953n"
}
}
},
"getFirmwareAutoUpgradeConfig": {
"auto_upgrade": {
"common": {
"enabled": "on",
"random_range": "120",
"time": "03:00"
}
}
},
"getFirmwareUpdateStatus": {
"cloud_config": {
"upgrade_status": {
"lastUpgradingSuccess": true,
"state": "normal"
}
}
},
"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"
}
}
},
"getLdc": {
"image": {
"common": {
"area_compensation": "default",
"auto_exp_antiflicker": "off",
"auto_exp_gain_max": "0",
"backlight": "off",
"chroma": "50",
"contrast": "50",
"dehaze": "off",
"eis": "off",
"exp_gain": "0",
"exp_level": "0",
"exp_type": "auto",
"focus_limited": "10",
"focus_type": "manual",
"high_light_compensation": "off",
"inf_delay": "5",
"inf_end_time": "21600",
"inf_sensitivity": "1",
"inf_sensitivity_day2night": "1400",
"inf_sensitivity_night2day": "9100",
"inf_start_time": "64800",
"inf_type": "auto",
"iris_level": "160",
"light_freq_mode": "auto",
"lock_blue_colton": "0",
"lock_blue_gain": "0",
"lock_gb_gain": "0",
"lock_gr_gain": "0",
"lock_green_colton": "0",
"lock_red_colton": "0",
"lock_red_gain": "0",
"lock_source": "local",
"luma": "50",
"saturation": "50",
"sharpness": "50",
"shutter": "1/25",
"smartir": "off",
"smartir_level": "100",
"smartwtl": "auto_wtl",
"smartwtl_digital_level": "100",
"smartwtl_level": "5",
"style": "standard",
"wb_B_gain": "50",
"wb_G_gain": "50",
"wb_R_gain": "50",
"wb_type": "auto",
"wd_gain": "50",
"wide_dynamic": "off",
"wtl_delay": "5",
"wtl_end_time": "21600",
"wtl_sensitivity": "4",
"wtl_sensitivity_day2night": "1400",
"wtl_sensitivity_night2day": "9100",
"wtl_start_time": "64800",
"wtl_type": "auto"
},
"switch": {
"best_view_distance": "0",
"clear_licence_plate_mode": "off",
"flip_type": "off",
"full_color_min_keep_time": "5",
"full_color_people_enhance": "off",
"image_scene_mode": "normal",
"image_scene_mode_autoday": "normal",
"image_scene_mode_autonight": "normal",
"image_scene_mode_common": "normal",
"image_scene_mode_shedday": "normal",
"image_scene_mode_shednight": "normal",
"ldc": "off",
"night_vision_mode": "inf_night_vision",
"overexposure_people_suppression": "off",
"rotate_type": "off",
"schedule_end_time": "64800",
"schedule_start_time": "21600",
"switch_mode": "common",
"wtl_force_time": "300",
"wtl_intensity_level": "5",
"wtl_manual_start_flag": "off"
}
}
},
"getLedStatus": {
"led": {
"config": {
"enabled": "on"
}
}
},
"getLensMaskConfig": {
"lens_mask": {
"lens_mask_info": {
"enabled": "off"
}
}
},
"getLightFrequencyInfo": {
"image": {
"common": {
"area_compensation": "default",
"auto_exp_antiflicker": "off",
"auto_exp_gain_max": "0",
"backlight": "off",
"chroma": "50",
"contrast": "50",
"dehaze": "off",
"eis": "off",
"exp_gain": "0",
"exp_level": "0",
"exp_type": "auto",
"focus_limited": "10",
"focus_type": "manual",
"high_light_compensation": "off",
"inf_delay": "5",
"inf_end_time": "21600",
"inf_sensitivity": "1",
"inf_sensitivity_day2night": "1400",
"inf_sensitivity_night2day": "9100",
"inf_start_time": "64800",
"inf_type": "auto",
"iris_level": "160",
"light_freq_mode": "auto",
"lock_blue_colton": "0",
"lock_blue_gain": "0",
"lock_gb_gain": "0",
"lock_gr_gain": "0",
"lock_green_colton": "0",
"lock_red_colton": "0",
"lock_red_gain": "0",
"lock_source": "local",
"luma": "50",
"saturation": "50",
"sharpness": "50",
"shutter": "1/25",
"smartir": "off",
"smartir_level": "100",
"smartwtl": "auto_wtl",
"smartwtl_digital_level": "100",
"smartwtl_level": "5",
"style": "standard",
"wb_B_gain": "50",
"wb_G_gain": "50",
"wb_R_gain": "50",
"wb_type": "auto",
"wd_gain": "50",
"wide_dynamic": "off",
"wtl_delay": "5",
"wtl_end_time": "21600",
"wtl_sensitivity": "4",
"wtl_sensitivity_day2night": "1400",
"wtl_sensitivity_night2day": "9100",
"wtl_start_time": "64800",
"wtl_type": "auto"
}
}
},
"getMediaEncrypt": {
"cet": {
"media_encrypt": {
"enabled": "on"
}
}
},
"getMsgPushConfig": {
"msg_push": {
"chn1_msg_push_info": {
"notification_enabled": "on",
"rich_notification_enabled": "off"
}
}
},
"getNightVisionCapability": {
"image_capability": {
"supplement_lamp": {
"night_vision_mode_range": [
"inf_night_vision"
],
"supplement_lamp_type": [
"infrared_lamp"
]
}
}
},
"getNightVisionModeConfig": {
"image": {
"switch": {
"best_view_distance": "0",
"clear_licence_plate_mode": "off",
"flip_type": "off",
"full_color_min_keep_time": "5",
"full_color_people_enhance": "off",
"image_scene_mode": "normal",
"image_scene_mode_autoday": "normal",
"image_scene_mode_autonight": "normal",
"image_scene_mode_common": "normal",
"image_scene_mode_shedday": "normal",
"image_scene_mode_shednight": "normal",
"ldc": "off",
"night_vision_mode": "inf_night_vision",
"overexposure_people_suppression": "off",
"rotate_type": "off",
"schedule_end_time": "64800",
"schedule_start_time": "21600",
"switch_mode": "common",
"wtl_force_time": "300",
"wtl_intensity_level": "5",
"wtl_manual_start_flag": "off"
}
}
},
"getPersonDetectionConfig": {
"people_detection": {
"detection": {
"enabled": "on",
"sensitivity": "50"
}
}
},
"getPresetConfig": {
"preset": {
"preset": {
"id": [],
"name": [],
"position_pan": [],
"position_tilt": [],
"position_zoom": [],
"read_only": []
}
}
},
"getRecordPlan": {
"record_plan": {
"chn1_channel": {
"enabled": "on",
"friday": "[\"0000-2400:2\"]",
"monday": "[\"0000-2400:2\"]",
"saturday": "[\"0000-2400:2\"]",
"sunday": "[\"0000-2400:2\"]",
"thursday": "[\"0000-2400:2\"]",
"tuesday": "[\"0000-2400:2\"]",
"wednesday": "[\"0000-2400:2\"]"
}
}
},
"getRotationStatus": {
"image": {
"switch": {
"best_view_distance": "0",
"clear_licence_plate_mode": "off",
"flip_type": "off",
"full_color_min_keep_time": "5",
"full_color_people_enhance": "off",
"image_scene_mode": "normal",
"image_scene_mode_autoday": "normal",
"image_scene_mode_autonight": "normal",
"image_scene_mode_common": "normal",
"image_scene_mode_shedday": "normal",
"image_scene_mode_shednight": "normal",
"ldc": "off",
"night_vision_mode": "inf_night_vision",
"overexposure_people_suppression": "off",
"rotate_type": "off",
"schedule_end_time": "64800",
"schedule_start_time": "21600",
"switch_mode": "common",
"wtl_force_time": "300",
"wtl_intensity_level": "5",
"wtl_manual_start_flag": "off"
}
}
},
"getSdCardStatus": {
"harddisk_manage": {
"hd_info": [
{
"hd_info_1": {
"crossline_free_space": "0B",
"crossline_free_space_accurate": "0B",
"crossline_total_space": "0B",
"crossline_total_space_accurate": "0B",
"detect_status": "offline",
"disk_name": "1",
"free_space": "0B",
"free_space_accurate": "0B",
"loop_record_status": "0",
"msg_push_free_space": "0B",
"msg_push_free_space_accurate": "0B",
"msg_push_total_space": "0B",
"msg_push_total_space_accurate": "0B",
"percent": "0",
"picture_free_space": "0B",
"picture_free_space_accurate": "0B",
"picture_total_space": "0B",
"picture_total_space_accurate": "0B",
"record_duration": "0",
"record_free_duration": "0",
"record_start_time": "0",
"rw_attr": "r",
"status": "offline",
"total_space": "0B",
"total_space_accurate": "0B",
"type": "local",
"video_free_space": "0B",
"video_free_space_accurate": "0B",
"video_total_space": "0B",
"video_total_space_accurate": "0B",
"write_protect": "0"
}
}
]
}
},
"getTamperDetectionConfig": {
"tamper_detection": {
"tamper_det": {
"digital_sensitivity": "50",
"enabled": "off",
"sensitivity": "medium"
}
}
},
"getTargetTrackConfig": {
"target_track": {
"target_track_info": {
"back_time": "30",
"enabled": "off",
"track_mode": "pantilt",
"track_time": "0"
}
}
},
"getVideoCapability": {
"video_capability": {
"main": {
"bitrate_types": [
"cbr",
"vbr"
],
"bitrates": [
"256",
"512",
"1024",
"1382",
"2048"
],
"change_fps_support": "1",
"encode_types": [
"H264",
"H265"
],
"frame_rates": [
"65551",
"65556",
"65561"
],
"minor_stream_support": "0",
"qualitys": [
"1",
"3",
"5"
],
"resolutions": [
"2304*1296",
"1920*1080",
"1280*720"
]
}
}
},
"getVideoQualities": {
"video": {
"main": {
"bitrate": "1382",
"bitrate_type": "vbr",
"default_bitrate": "1382",
"encode_type": "H264",
"frame_rate": "65551",
"name": "VideoEncoder_1",
"quality": "3",
"resolution": "1920*1080",
"smart_codec": "off"
}
}
},
"getWhitelampConfig": {
"image": {
"switch": {
"best_view_distance": "0",
"clear_licence_plate_mode": "off",
"flip_type": "off",
"full_color_min_keep_time": "5",
"full_color_people_enhance": "off",
"image_scene_mode": "normal",
"image_scene_mode_autoday": "normal",
"image_scene_mode_autonight": "normal",
"image_scene_mode_common": "normal",
"image_scene_mode_shedday": "normal",
"image_scene_mode_shednight": "normal",
"ldc": "off",
"night_vision_mode": "inf_night_vision",
"overexposure_people_suppression": "off",
"rotate_type": "off",
"schedule_end_time": "64800",
"schedule_start_time": "21600",
"switch_mode": "common",
"wtl_force_time": "300",
"wtl_intensity_level": "5",
"wtl_manual_start_flag": "off"
}
}
},
"getWhitelampStatus": {
"rest_time": 0,
"status": 0
}
}