mirror of
https://github.com/python-kasa/python-kasa.git
synced 2025-08-09 20:24:02 +00:00
Update smartcamera to support single get/set/do requests (#1187)
Not supported by H200 hub
This commit is contained in:
@@ -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):
|
||||
|
@@ -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]
|
||||
|
||||
|
@@ -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():
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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": {
|
||||
|
Reference in New Issue
Block a user