pyezvizapi 1.0.2.9__py3-none-any.whl → 1.0.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pyezvizapi might be problematic. Click here for more details.
- pyezvizapi/api_endpoints.py +49 -0
- pyezvizapi/client.py +2059 -27
- pyezvizapi/constants.py +6 -0
- {pyezvizapi-1.0.2.9.dist-info → pyezvizapi-1.0.3.1.dist-info}/METADATA +1 -1
- {pyezvizapi-1.0.2.9.dist-info → pyezvizapi-1.0.3.1.dist-info}/RECORD +10 -10
- {pyezvizapi-1.0.2.9.dist-info → pyezvizapi-1.0.3.1.dist-info}/WHEEL +0 -0
- {pyezvizapi-1.0.2.9.dist-info → pyezvizapi-1.0.3.1.dist-info}/entry_points.txt +0 -0
- {pyezvizapi-1.0.2.9.dist-info → pyezvizapi-1.0.3.1.dist-info}/licenses/LICENSE +0 -0
- {pyezvizapi-1.0.2.9.dist-info → pyezvizapi-1.0.3.1.dist-info}/licenses/LICENSE.md +0 -0
- {pyezvizapi-1.0.2.9.dist-info → pyezvizapi-1.0.3.1.dist-info}/top_level.txt +0 -0
pyezvizapi/client.py
CHANGED
|
@@ -8,14 +8,22 @@ import hashlib
|
|
|
8
8
|
import json
|
|
9
9
|
import logging
|
|
10
10
|
from typing import Any, ClassVar, TypedDict, cast
|
|
11
|
+
from urllib.parse import urlencode
|
|
11
12
|
from uuid import uuid4
|
|
12
13
|
|
|
13
14
|
import requests
|
|
14
15
|
|
|
15
16
|
from .api_endpoints import (
|
|
16
17
|
API_ENDPOINT_2FA_VALIDATE_POST_AUTH,
|
|
18
|
+
API_ENDPOINT_ALARM_DEVICE_CHIME,
|
|
19
|
+
API_ENDPOINT_ALARM_GET_WHISTLE_STATUS_BY_CHANNEL,
|
|
20
|
+
API_ENDPOINT_ALARM_GET_WHISTLE_STATUS_BY_DEVICE,
|
|
21
|
+
API_ENDPOINT_ALARM_SET_CHANNEL_WHISTLE,
|
|
22
|
+
API_ENDPOINT_ALARM_SET_DEVICE_WHISTLE,
|
|
17
23
|
API_ENDPOINT_ALARM_SOUND,
|
|
24
|
+
API_ENDPOINT_ALARM_STOP_WHISTLE,
|
|
18
25
|
API_ENDPOINT_ALARMINFO_GET,
|
|
26
|
+
API_ENDPOINT_AUTOUPGRADE_SWITCH,
|
|
19
27
|
API_ENDPOINT_CALLING_NOTIFY,
|
|
20
28
|
API_ENDPOINT_CAM_AUTH_CODE,
|
|
21
29
|
API_ENDPOINT_CAM_ENCRYPTKEY,
|
|
@@ -24,39 +32,77 @@ from .api_endpoints import (
|
|
|
24
32
|
API_ENDPOINT_CREATE_PANORAMIC,
|
|
25
33
|
API_ENDPOINT_DETECTION_SENSIBILITY,
|
|
26
34
|
API_ENDPOINT_DETECTION_SENSIBILITY_GET,
|
|
35
|
+
API_ENDPOINT_DEVCONFIG_BASE,
|
|
27
36
|
API_ENDPOINT_DEVCONFIG_BY_KEY,
|
|
37
|
+
API_ENDPOINT_DEVCONFIG_MOTOR,
|
|
38
|
+
API_ENDPOINT_DEVCONFIG_OP,
|
|
39
|
+
API_ENDPOINT_DEVCONFIG_SECURITY_ACTIVATE,
|
|
40
|
+
API_ENDPOINT_DEVCONFIG_SECURITY_CHALLENGE,
|
|
41
|
+
API_ENDPOINT_DEVICE_ACCESSORY_LINK,
|
|
28
42
|
API_ENDPOINT_DEVICE_BASICS,
|
|
43
|
+
API_ENDPOINT_DEVICE_EMAIL_ALERT,
|
|
29
44
|
API_ENDPOINT_DEVICE_STORAGE_STATUS,
|
|
30
45
|
API_ENDPOINT_DEVICE_SWITCH_STATUS_LEGACY,
|
|
31
46
|
API_ENDPOINT_DEVICE_SYS_OPERATION,
|
|
47
|
+
API_ENDPOINT_DEVICE_UPDATE_NAME,
|
|
32
48
|
API_ENDPOINT_DEVICES,
|
|
49
|
+
API_ENDPOINT_DEVICES_ASSOCIATION_LINKED_IPC,
|
|
50
|
+
API_ENDPOINT_DEVICES_AUTHENTICATE,
|
|
51
|
+
API_ENDPOINT_DEVICES_ENCRYPTKEY_BATCH,
|
|
52
|
+
API_ENDPOINT_DEVICES_LOC,
|
|
53
|
+
API_ENDPOINT_DEVICES_P2P_INFO,
|
|
54
|
+
API_ENDPOINT_DEVICES_SET_SWITCH_ENABLE,
|
|
33
55
|
API_ENDPOINT_DO_NOT_DISTURB,
|
|
56
|
+
API_ENDPOINT_DOORLOCK_USERS,
|
|
57
|
+
API_ENDPOINT_FEEDBACK,
|
|
34
58
|
API_ENDPOINT_GROUP_DEFENCE_MODE,
|
|
35
59
|
API_ENDPOINT_INTELLIGENT_APP,
|
|
36
60
|
API_ENDPOINT_IOT_ACTION,
|
|
37
61
|
API_ENDPOINT_IOT_FEATURE,
|
|
62
|
+
API_ENDPOINT_IOT_FEATURE_PRODUCT_VOICE_CONFIG,
|
|
63
|
+
API_ENDPOINT_IOT_VIRTUAL_BIND,
|
|
38
64
|
API_ENDPOINT_LOGIN,
|
|
39
65
|
API_ENDPOINT_LOGOUT,
|
|
66
|
+
API_ENDPOINT_MANAGED_DEVICE_BASE,
|
|
40
67
|
API_ENDPOINT_OFFLINE_NOTIFY,
|
|
41
68
|
API_ENDPOINT_OSD,
|
|
42
69
|
API_ENDPOINT_PAGELIST,
|
|
43
70
|
API_ENDPOINT_PANORAMIC_DEVICES_OPERATION,
|
|
44
71
|
API_ENDPOINT_PTZCONTROL,
|
|
45
72
|
API_ENDPOINT_REFRESH_SESSION_ID,
|
|
73
|
+
API_ENDPOINT_REMOTE_UNBIND_PROGRESS,
|
|
46
74
|
API_ENDPOINT_REMOTE_UNLOCK,
|
|
47
75
|
API_ENDPOINT_RETURN_PANORAMIC,
|
|
76
|
+
API_ENDPOINT_SCD_APP_DEVICE_ADD,
|
|
77
|
+
API_ENDPOINT_SDCARD_BLACK_LEVEL,
|
|
48
78
|
API_ENDPOINT_SEND_CODE,
|
|
49
79
|
API_ENDPOINT_SENSITIVITY,
|
|
50
80
|
API_ENDPOINT_SERVER_INFO,
|
|
51
81
|
API_ENDPOINT_SET_DEFENCE_SCHEDULE,
|
|
52
82
|
API_ENDPOINT_SET_LUMINANCE,
|
|
83
|
+
API_ENDPOINT_SHARE_ACCEPT,
|
|
84
|
+
API_ENDPOINT_SHARE_QUIT,
|
|
85
|
+
API_ENDPOINT_SMARTHOME_OUTLET_LOG,
|
|
86
|
+
API_ENDPOINT_SPECIAL_BIZS_A1S,
|
|
87
|
+
API_ENDPOINT_SPECIAL_BIZS_V1_BATTERY,
|
|
88
|
+
API_ENDPOINT_SPECIAL_BIZS_VOICES,
|
|
89
|
+
API_ENDPOINT_STREAMING_RECORDS,
|
|
53
90
|
API_ENDPOINT_SWITCH_DEFENCE_MODE,
|
|
54
91
|
API_ENDPOINT_SWITCH_OTHER,
|
|
55
92
|
API_ENDPOINT_SWITCH_SOUND_ALARM,
|
|
56
93
|
API_ENDPOINT_SWITCH_STATUS,
|
|
94
|
+
API_ENDPOINT_TIME_PLAN_INFOS,
|
|
57
95
|
API_ENDPOINT_UNIFIEDMSG_LIST_GET,
|
|
58
96
|
API_ENDPOINT_UPGRADE_DEVICE,
|
|
97
|
+
API_ENDPOINT_UPGRADE_RULE,
|
|
59
98
|
API_ENDPOINT_USER_ID,
|
|
99
|
+
API_ENDPOINT_USERDEVICES_KMS,
|
|
100
|
+
API_ENDPOINT_USERDEVICES_P2P_INFO,
|
|
101
|
+
API_ENDPOINT_USERDEVICES_SEARCH,
|
|
102
|
+
API_ENDPOINT_USERDEVICES_STATUS,
|
|
103
|
+
API_ENDPOINT_USERDEVICES_TOKEN,
|
|
104
|
+
API_ENDPOINT_USERDEVICES_V2,
|
|
105
|
+
API_ENDPOINT_USERS_LBS_SUB_DOMAIN,
|
|
60
106
|
API_ENDPOINT_V3_ALARMS,
|
|
61
107
|
API_ENDPOINT_VIDEO_ENCRYPT,
|
|
62
108
|
)
|
|
@@ -356,6 +402,26 @@ class EzvizClient:
|
|
|
356
402
|
+ str(resp.text)
|
|
357
403
|
) from err
|
|
358
404
|
|
|
405
|
+
@staticmethod
|
|
406
|
+
def _normalize_json_payload(payload: Any) -> Any:
|
|
407
|
+
"""Return a payload suitable for json= usage, decoding strings when needed."""
|
|
408
|
+
|
|
409
|
+
if isinstance(payload, (Mapping, list)):
|
|
410
|
+
return payload
|
|
411
|
+
if isinstance(payload, tuple):
|
|
412
|
+
return list(payload)
|
|
413
|
+
if isinstance(payload, (bytes, bytearray)):
|
|
414
|
+
try:
|
|
415
|
+
return json.loads(payload.decode())
|
|
416
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as err:
|
|
417
|
+
raise PyEzvizError("Invalid JSON payload provided") from err
|
|
418
|
+
if isinstance(payload, str):
|
|
419
|
+
try:
|
|
420
|
+
return json.loads(payload)
|
|
421
|
+
except json.JSONDecodeError as err:
|
|
422
|
+
raise PyEzvizError("Invalid JSON payload provided") from err
|
|
423
|
+
raise PyEzvizError("Unsupported payload type for JSON body")
|
|
424
|
+
|
|
359
425
|
@staticmethod
|
|
360
426
|
def _is_ok(payload: dict) -> bool:
|
|
361
427
|
"""Return True if payload indicates success for both API styles."""
|
|
@@ -530,6 +596,18 @@ class EzvizClient:
|
|
|
530
596
|
service_urls["sysConf"] = str(service_urls.get("sysConf", "")).split("|")
|
|
531
597
|
return service_urls
|
|
532
598
|
|
|
599
|
+
def lbs_domain(self, max_retries: int = 0) -> dict:
|
|
600
|
+
"""Retrieve the LBS sub-domain information."""
|
|
601
|
+
|
|
602
|
+
json_output = self._request_json(
|
|
603
|
+
"GET",
|
|
604
|
+
API_ENDPOINT_USERS_LBS_SUB_DOMAIN,
|
|
605
|
+
retry_401=True,
|
|
606
|
+
max_retries=max_retries,
|
|
607
|
+
)
|
|
608
|
+
self._ensure_ok(json_output, "Could not get LBS domain")
|
|
609
|
+
return json_output
|
|
610
|
+
|
|
533
611
|
def _api_get_pagelist(
|
|
534
612
|
self,
|
|
535
613
|
page_filter: str,
|
|
@@ -648,6 +726,222 @@ class EzvizClient:
|
|
|
648
726
|
self._ensure_ok(json_output, "Could not get unified message list")
|
|
649
727
|
return json_output
|
|
650
728
|
|
|
729
|
+
def add_device(
|
|
730
|
+
self,
|
|
731
|
+
serial: str,
|
|
732
|
+
validate_code: str,
|
|
733
|
+
*,
|
|
734
|
+
add_type: str | None = None,
|
|
735
|
+
max_retries: int = 0,
|
|
736
|
+
) -> dict:
|
|
737
|
+
"""Add a new device to the current account."""
|
|
738
|
+
|
|
739
|
+
data = {
|
|
740
|
+
"deviceSerial": serial,
|
|
741
|
+
"validateCode": validate_code,
|
|
742
|
+
}
|
|
743
|
+
if add_type is not None:
|
|
744
|
+
data["addType"] = add_type
|
|
745
|
+
json_output = self._request_json(
|
|
746
|
+
"POST",
|
|
747
|
+
API_ENDPOINT_USERDEVICES_V2,
|
|
748
|
+
data=data,
|
|
749
|
+
retry_401=True,
|
|
750
|
+
max_retries=max_retries,
|
|
751
|
+
)
|
|
752
|
+
self._ensure_ok(json_output, "Could not add device")
|
|
753
|
+
return json_output
|
|
754
|
+
|
|
755
|
+
def add_hik_activate(
|
|
756
|
+
self,
|
|
757
|
+
serial: str,
|
|
758
|
+
payload: Any,
|
|
759
|
+
*,
|
|
760
|
+
max_retries: int = 0,
|
|
761
|
+
) -> dict:
|
|
762
|
+
"""Activate a Hikvision device using the security endpoint."""
|
|
763
|
+
|
|
764
|
+
body = self._normalize_json_payload(payload)
|
|
765
|
+
json_output = self._request_json(
|
|
766
|
+
"POST",
|
|
767
|
+
f"{API_ENDPOINT_DEVCONFIG_SECURITY_ACTIVATE}{serial}",
|
|
768
|
+
json_body=body,
|
|
769
|
+
retry_401=True,
|
|
770
|
+
max_retries=max_retries,
|
|
771
|
+
)
|
|
772
|
+
self._ensure_ok(json_output, "Could not activate Hik device")
|
|
773
|
+
return json_output
|
|
774
|
+
|
|
775
|
+
def add_hik_challenge(
|
|
776
|
+
self,
|
|
777
|
+
serial: str,
|
|
778
|
+
payload: Any,
|
|
779
|
+
*,
|
|
780
|
+
max_retries: int = 0,
|
|
781
|
+
) -> dict:
|
|
782
|
+
"""Request a Hikvision security challenge."""
|
|
783
|
+
|
|
784
|
+
body = self._normalize_json_payload(payload)
|
|
785
|
+
json_output = self._request_json(
|
|
786
|
+
"POST",
|
|
787
|
+
f"{API_ENDPOINT_DEVCONFIG_SECURITY_CHALLENGE}{serial}",
|
|
788
|
+
json_body=body,
|
|
789
|
+
retry_401=True,
|
|
790
|
+
max_retries=max_retries,
|
|
791
|
+
)
|
|
792
|
+
self._ensure_ok(json_output, "Could not request Hik challenge")
|
|
793
|
+
return json_output
|
|
794
|
+
|
|
795
|
+
def add_local_device(
|
|
796
|
+
self,
|
|
797
|
+
payload: Any,
|
|
798
|
+
*,
|
|
799
|
+
max_retries: int = 0,
|
|
800
|
+
) -> dict:
|
|
801
|
+
"""Add a device discovered on the local network."""
|
|
802
|
+
|
|
803
|
+
body = self._normalize_json_payload(payload)
|
|
804
|
+
json_output = self._request_json(
|
|
805
|
+
"POST",
|
|
806
|
+
API_ENDPOINT_DEVICES_LOC,
|
|
807
|
+
json_body=body,
|
|
808
|
+
retry_401=True,
|
|
809
|
+
max_retries=max_retries,
|
|
810
|
+
)
|
|
811
|
+
self._ensure_ok(json_output, "Could not add local device")
|
|
812
|
+
return json_output
|
|
813
|
+
|
|
814
|
+
def save_hik_dev_code(
|
|
815
|
+
self,
|
|
816
|
+
payload: Any,
|
|
817
|
+
*,
|
|
818
|
+
max_retries: int = 0,
|
|
819
|
+
) -> dict:
|
|
820
|
+
"""Submit a Hikvision device code via the SCD endpoint."""
|
|
821
|
+
|
|
822
|
+
body = self._normalize_json_payload(payload)
|
|
823
|
+
json_output = self._request_json(
|
|
824
|
+
"POST",
|
|
825
|
+
API_ENDPOINT_SCD_APP_DEVICE_ADD,
|
|
826
|
+
json_body=body,
|
|
827
|
+
retry_401=True,
|
|
828
|
+
max_retries=max_retries,
|
|
829
|
+
)
|
|
830
|
+
self._ensure_ok(json_output, "Could not save Hik device code")
|
|
831
|
+
return json_output
|
|
832
|
+
|
|
833
|
+
def bind_virtual_device(
|
|
834
|
+
self,
|
|
835
|
+
product_id: str,
|
|
836
|
+
version: str,
|
|
837
|
+
*,
|
|
838
|
+
max_retries: int = 0,
|
|
839
|
+
) -> dict:
|
|
840
|
+
"""Bind a virtual IoT device using product identifier and version."""
|
|
841
|
+
|
|
842
|
+
params = {"productId": product_id, "version": version}
|
|
843
|
+
json_output = self._request_json(
|
|
844
|
+
"PUT",
|
|
845
|
+
API_ENDPOINT_IOT_VIRTUAL_BIND,
|
|
846
|
+
params=params,
|
|
847
|
+
retry_401=True,
|
|
848
|
+
max_retries=max_retries,
|
|
849
|
+
)
|
|
850
|
+
self._ensure_ok(json_output, "Could not bind virtual device")
|
|
851
|
+
return json_output
|
|
852
|
+
|
|
853
|
+
def dev_config_search(
|
|
854
|
+
self,
|
|
855
|
+
serial: str,
|
|
856
|
+
channel: int,
|
|
857
|
+
*,
|
|
858
|
+
max_retries: int = 0,
|
|
859
|
+
) -> dict:
|
|
860
|
+
"""Trigger a network search on the device."""
|
|
861
|
+
|
|
862
|
+
path = f"{API_ENDPOINT_DEVCONFIG_BASE}/{serial}/{channel}/netWork"
|
|
863
|
+
json_output = self._request_json(
|
|
864
|
+
"POST",
|
|
865
|
+
path,
|
|
866
|
+
retry_401=True,
|
|
867
|
+
max_retries=max_retries,
|
|
868
|
+
)
|
|
869
|
+
self._ensure_ok(json_output, "Could not start network search")
|
|
870
|
+
return json_output
|
|
871
|
+
|
|
872
|
+
def dev_config_send_config_command(
|
|
873
|
+
self,
|
|
874
|
+
serial: str,
|
|
875
|
+
channel: int,
|
|
876
|
+
target_serial: str,
|
|
877
|
+
*,
|
|
878
|
+
max_retries: int = 0,
|
|
879
|
+
) -> dict:
|
|
880
|
+
"""Send a network configuration command to a target device."""
|
|
881
|
+
|
|
882
|
+
path = f"{API_ENDPOINT_DEVCONFIG_BASE}/{serial}/{channel}/netWork/command"
|
|
883
|
+
json_output = self._request_json(
|
|
884
|
+
"POST",
|
|
885
|
+
path,
|
|
886
|
+
params={"targetDeviceSerial": target_serial},
|
|
887
|
+
retry_401=True,
|
|
888
|
+
max_retries=max_retries,
|
|
889
|
+
)
|
|
890
|
+
self._ensure_ok(json_output, "Could not send network command")
|
|
891
|
+
return json_output
|
|
892
|
+
|
|
893
|
+
def dev_config_wifi_list(
|
|
894
|
+
self,
|
|
895
|
+
serial: str,
|
|
896
|
+
channel: int,
|
|
897
|
+
*,
|
|
898
|
+
max_retries: int = 0,
|
|
899
|
+
) -> dict:
|
|
900
|
+
"""Retrieve Wi-Fi network list detected by the device."""
|
|
901
|
+
|
|
902
|
+
path = f"{API_ENDPOINT_DEVCONFIG_BASE}/{serial}/{channel}/netWork"
|
|
903
|
+
json_output = self._request_json(
|
|
904
|
+
"GET",
|
|
905
|
+
path,
|
|
906
|
+
retry_401=True,
|
|
907
|
+
max_retries=max_retries,
|
|
908
|
+
)
|
|
909
|
+
self._ensure_ok(json_output, "Could not get Wi-Fi list")
|
|
910
|
+
return json_output
|
|
911
|
+
|
|
912
|
+
def device_between_error(
|
|
913
|
+
self,
|
|
914
|
+
serial: str,
|
|
915
|
+
channel: int,
|
|
916
|
+
target_serial: str,
|
|
917
|
+
*,
|
|
918
|
+
max_retries: int = 0,
|
|
919
|
+
) -> dict:
|
|
920
|
+
"""Retrieve error details for a network configuration attempt."""
|
|
921
|
+
|
|
922
|
+
path = f"{API_ENDPOINT_DEVCONFIG_BASE}/{serial}/{channel}/netWork/result"
|
|
923
|
+
json_output = self._request_json(
|
|
924
|
+
"GET",
|
|
925
|
+
path,
|
|
926
|
+
params={"targetDeviceSerial": target_serial},
|
|
927
|
+
retry_401=True,
|
|
928
|
+
max_retries=max_retries,
|
|
929
|
+
)
|
|
930
|
+
self._ensure_ok(json_output, "Could not get network error info")
|
|
931
|
+
return json_output
|
|
932
|
+
|
|
933
|
+
def dev_token(self, max_retries: int = 0) -> dict:
|
|
934
|
+
"""Request a device token for provisioning flows."""
|
|
935
|
+
|
|
936
|
+
json_output = self._request_json(
|
|
937
|
+
"GET",
|
|
938
|
+
API_ENDPOINT_USERDEVICES_TOKEN,
|
|
939
|
+
retry_401=True,
|
|
940
|
+
max_retries=max_retries,
|
|
941
|
+
)
|
|
942
|
+
self._ensure_ok(json_output, "Could not get device token")
|
|
943
|
+
return json_output
|
|
944
|
+
|
|
651
945
|
def set_switch_v3(
|
|
652
946
|
self,
|
|
653
947
|
serial: str,
|
|
@@ -747,6 +1041,32 @@ class EzvizClient:
|
|
|
747
1041
|
self._cameras[serial]["switches"][status_type] = target_state
|
|
748
1042
|
return True
|
|
749
1043
|
|
|
1044
|
+
def device_switch(
|
|
1045
|
+
self,
|
|
1046
|
+
serial: str,
|
|
1047
|
+
channel: int,
|
|
1048
|
+
enable: int,
|
|
1049
|
+
switch_type: int,
|
|
1050
|
+
*,
|
|
1051
|
+
max_retries: int = 0,
|
|
1052
|
+
) -> dict:
|
|
1053
|
+
"""Direct wrapper for /v3/devices/{serial}/switch endpoint."""
|
|
1054
|
+
|
|
1055
|
+
params = {
|
|
1056
|
+
"channelNo": channel,
|
|
1057
|
+
"enable": enable,
|
|
1058
|
+
"switchType": switch_type,
|
|
1059
|
+
}
|
|
1060
|
+
json_output = self._request_json(
|
|
1061
|
+
"PUT",
|
|
1062
|
+
f"{API_ENDPOINT_DEVICES}{serial}{API_ENDPOINT_SWITCH_OTHER}",
|
|
1063
|
+
params=params,
|
|
1064
|
+
retry_401=True,
|
|
1065
|
+
max_retries=max_retries,
|
|
1066
|
+
)
|
|
1067
|
+
self._ensure_ok(json_output, "Could not toggle device switch")
|
|
1068
|
+
return json_output
|
|
1069
|
+
|
|
750
1070
|
def switch_status_other(
|
|
751
1071
|
self,
|
|
752
1072
|
serial: str,
|
|
@@ -909,6 +1229,31 @@ class EzvizClient:
|
|
|
909
1229
|
self._ensure_ok(payload, "Could not set devconfig key")
|
|
910
1230
|
return payload
|
|
911
1231
|
|
|
1232
|
+
def set_common_key_value(
|
|
1233
|
+
self,
|
|
1234
|
+
serial: str,
|
|
1235
|
+
channel: int,
|
|
1236
|
+
key: str,
|
|
1237
|
+
value: str,
|
|
1238
|
+
*,
|
|
1239
|
+
max_retries: int = 0,
|
|
1240
|
+
) -> dict:
|
|
1241
|
+
"""Update a devconfig key/value pair using query parameters."""
|
|
1242
|
+
|
|
1243
|
+
params = {
|
|
1244
|
+
"key": key,
|
|
1245
|
+
"value": value if isinstance(value, str) else str(value),
|
|
1246
|
+
}
|
|
1247
|
+
payload = self._request_json(
|
|
1248
|
+
"PUT",
|
|
1249
|
+
f"{API_ENDPOINT_DEVCONFIG_BY_KEY}{serial}/{channel}/op",
|
|
1250
|
+
params=params,
|
|
1251
|
+
retry_401=True,
|
|
1252
|
+
max_retries=max_retries,
|
|
1253
|
+
)
|
|
1254
|
+
self._ensure_ok(payload, "Could not set common key value")
|
|
1255
|
+
return payload
|
|
1256
|
+
|
|
912
1257
|
def set_device_config_by_key(
|
|
913
1258
|
self,
|
|
914
1259
|
serial: str,
|
|
@@ -927,6 +1272,89 @@ class EzvizClient:
|
|
|
927
1272
|
)
|
|
928
1273
|
return True
|
|
929
1274
|
|
|
1275
|
+
def set_device_key_value(
|
|
1276
|
+
self,
|
|
1277
|
+
serial: str,
|
|
1278
|
+
channel: int,
|
|
1279
|
+
key: str,
|
|
1280
|
+
value: str,
|
|
1281
|
+
*,
|
|
1282
|
+
max_retries: int = 0,
|
|
1283
|
+
) -> dict:
|
|
1284
|
+
"""Alias for the query-based key/value setter."""
|
|
1285
|
+
|
|
1286
|
+
return self.set_common_key_value(
|
|
1287
|
+
serial,
|
|
1288
|
+
channel,
|
|
1289
|
+
key,
|
|
1290
|
+
value,
|
|
1291
|
+
max_retries=max_retries,
|
|
1292
|
+
)
|
|
1293
|
+
|
|
1294
|
+
def audition_request(
|
|
1295
|
+
self,
|
|
1296
|
+
serial: str,
|
|
1297
|
+
channel: int,
|
|
1298
|
+
request: str,
|
|
1299
|
+
payload: str,
|
|
1300
|
+
*,
|
|
1301
|
+
max_retries: int = 0,
|
|
1302
|
+
) -> dict:
|
|
1303
|
+
"""Send an audition request via /v3/devconfig/op."""
|
|
1304
|
+
|
|
1305
|
+
data = {
|
|
1306
|
+
"deviceSerial": serial,
|
|
1307
|
+
"channelNo": channel,
|
|
1308
|
+
"request": request,
|
|
1309
|
+
"data": payload,
|
|
1310
|
+
}
|
|
1311
|
+
json_output = self._request_json(
|
|
1312
|
+
"POST",
|
|
1313
|
+
API_ENDPOINT_DEVCONFIG_OP,
|
|
1314
|
+
data=data,
|
|
1315
|
+
retry_401=True,
|
|
1316
|
+
max_retries=max_retries,
|
|
1317
|
+
)
|
|
1318
|
+
self._ensure_ok(json_output, "Could not send audition request")
|
|
1319
|
+
return json_output
|
|
1320
|
+
|
|
1321
|
+
def baby_control(
|
|
1322
|
+
self,
|
|
1323
|
+
serial: str,
|
|
1324
|
+
channel: int,
|
|
1325
|
+
local_index: int,
|
|
1326
|
+
command: str,
|
|
1327
|
+
action: str,
|
|
1328
|
+
speed: int,
|
|
1329
|
+
uuid: str,
|
|
1330
|
+
control: str,
|
|
1331
|
+
hardware_code: str,
|
|
1332
|
+
*,
|
|
1333
|
+
max_retries: int = 0,
|
|
1334
|
+
) -> dict:
|
|
1335
|
+
"""Send the baby monitor motor control request."""
|
|
1336
|
+
|
|
1337
|
+
data = {
|
|
1338
|
+
"deviceSerial": serial,
|
|
1339
|
+
"channelNo": channel,
|
|
1340
|
+
"localIndex": local_index,
|
|
1341
|
+
"command": command,
|
|
1342
|
+
"action": action,
|
|
1343
|
+
"speed": speed,
|
|
1344
|
+
"uuid": uuid,
|
|
1345
|
+
"control": control,
|
|
1346
|
+
"hardwareCode": hardware_code,
|
|
1347
|
+
}
|
|
1348
|
+
json_output = self._request_json(
|
|
1349
|
+
"POST",
|
|
1350
|
+
API_ENDPOINT_DEVCONFIG_MOTOR,
|
|
1351
|
+
data=data,
|
|
1352
|
+
retry_401=True,
|
|
1353
|
+
max_retries=max_retries,
|
|
1354
|
+
)
|
|
1355
|
+
self._ensure_ok(json_output, "Could not control baby motor")
|
|
1356
|
+
return json_output
|
|
1357
|
+
|
|
930
1358
|
def set_device_feature_by_key(
|
|
931
1359
|
self,
|
|
932
1360
|
serial: str,
|
|
@@ -947,8 +1375,10 @@ class EzvizClient:
|
|
|
947
1375
|
|
|
948
1376
|
full_url = f"https://{self._token['api_url']}{API_ENDPOINT_IOT_FEATURE}{serial.upper()}/0"
|
|
949
1377
|
|
|
950
|
-
headers =
|
|
951
|
-
|
|
1378
|
+
headers = {
|
|
1379
|
+
**self._session.headers,
|
|
1380
|
+
"Content-Type": "application/json",
|
|
1381
|
+
}
|
|
952
1382
|
|
|
953
1383
|
req_prep = requests.Request(
|
|
954
1384
|
method="PUT", url=full_url, headers=headers, data=payload
|
|
@@ -963,15 +1393,344 @@ class EzvizClient:
|
|
|
963
1393
|
|
|
964
1394
|
return True
|
|
965
1395
|
|
|
966
|
-
def
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1396
|
+
def _iot_request(
|
|
1397
|
+
self,
|
|
1398
|
+
method: str,
|
|
1399
|
+
endpoint: str,
|
|
1400
|
+
serial: str,
|
|
1401
|
+
resource_identifier: str,
|
|
1402
|
+
local_index: str,
|
|
1403
|
+
domain_id: str,
|
|
1404
|
+
action_id: str,
|
|
1405
|
+
*,
|
|
1406
|
+
payload: Any = None,
|
|
1407
|
+
max_retries: int = 0,
|
|
1408
|
+
error_message: str,
|
|
1409
|
+
) -> dict:
|
|
1410
|
+
"""Helper to perform IoT feature/action requests with JSON payload support."""
|
|
1411
|
+
|
|
1412
|
+
path = (
|
|
1413
|
+
f"{endpoint}{serial.upper()}/{resource_identifier}/"
|
|
1414
|
+
f"{local_index}/{domain_id}/{action_id}"
|
|
1415
|
+
)
|
|
1416
|
+
|
|
1417
|
+
headers = dict(self._session.headers)
|
|
1418
|
+
data: str | bytes | bytearray | None = None
|
|
1419
|
+
if payload is not None:
|
|
1420
|
+
headers["Content-Type"] = "application/json"
|
|
1421
|
+
if isinstance(payload, (bytes, bytearray, str)):
|
|
1422
|
+
data = payload
|
|
1423
|
+
else:
|
|
1424
|
+
data = json.dumps(payload, separators=(",", ":"))
|
|
1425
|
+
|
|
1426
|
+
req = requests.Request(
|
|
1427
|
+
method=method,
|
|
1428
|
+
url=self._url(path),
|
|
1429
|
+
headers=headers,
|
|
1430
|
+
data=data,
|
|
1431
|
+
).prepare()
|
|
1432
|
+
|
|
1433
|
+
resp = self._send_prepared(
|
|
1434
|
+
req,
|
|
1435
|
+
retry_401=True,
|
|
1436
|
+
max_retries=max_retries,
|
|
1437
|
+
)
|
|
1438
|
+
json_output = self._parse_json(resp)
|
|
1439
|
+
if not self._meta_ok(json_output):
|
|
1440
|
+
raise PyEzvizError(f"{error_message}: Got {json_output})")
|
|
1441
|
+
return json_output
|
|
1442
|
+
|
|
1443
|
+
def get_low_battery_keep_alive(
|
|
1444
|
+
self,
|
|
1445
|
+
serial: str,
|
|
1446
|
+
resource_identifier: str,
|
|
1447
|
+
local_index: str,
|
|
1448
|
+
domain_id: str,
|
|
1449
|
+
action_id: str,
|
|
1450
|
+
*,
|
|
1451
|
+
max_retries: int = 0,
|
|
1452
|
+
) -> dict:
|
|
1453
|
+
"""Fetch low-battery keep-alive status exposed under the IoT feature API."""
|
|
1454
|
+
|
|
1455
|
+
return self._iot_request(
|
|
1456
|
+
"GET",
|
|
1457
|
+
API_ENDPOINT_IOT_FEATURE,
|
|
1458
|
+
serial,
|
|
1459
|
+
resource_identifier,
|
|
1460
|
+
local_index,
|
|
1461
|
+
domain_id,
|
|
1462
|
+
action_id,
|
|
1463
|
+
max_retries=max_retries,
|
|
1464
|
+
error_message="Could not fetch low battery keep-alive status",
|
|
1465
|
+
)
|
|
1466
|
+
|
|
1467
|
+
def get_object_removal_status(
|
|
1468
|
+
self,
|
|
1469
|
+
serial: str,
|
|
1470
|
+
resource_identifier: str,
|
|
1471
|
+
local_index: str,
|
|
1472
|
+
domain_id: str,
|
|
1473
|
+
action_id: str,
|
|
1474
|
+
*,
|
|
1475
|
+
payload: Any | None = None,
|
|
1476
|
+
max_retries: int = 0,
|
|
1477
|
+
) -> dict:
|
|
1478
|
+
"""Fetch object-removal (left-behind) status for supported devices."""
|
|
1479
|
+
|
|
1480
|
+
return self._iot_request(
|
|
1481
|
+
"GET",
|
|
1482
|
+
API_ENDPOINT_IOT_FEATURE,
|
|
1483
|
+
serial,
|
|
1484
|
+
resource_identifier,
|
|
1485
|
+
local_index,
|
|
1486
|
+
domain_id,
|
|
1487
|
+
action_id,
|
|
1488
|
+
payload=payload,
|
|
1489
|
+
max_retries=max_retries,
|
|
1490
|
+
error_message="Could not fetch object removal status",
|
|
1491
|
+
)
|
|
1492
|
+
|
|
1493
|
+
def get_remote_control_path_list(
|
|
1494
|
+
self,
|
|
1495
|
+
serial: str,
|
|
1496
|
+
resource_identifier: str,
|
|
1497
|
+
local_index: str,
|
|
1498
|
+
domain_id: str,
|
|
1499
|
+
action_id: str,
|
|
1500
|
+
*,
|
|
1501
|
+
max_retries: int = 0,
|
|
1502
|
+
) -> dict:
|
|
1503
|
+
"""Return the remote control patrol path list for auto-tracking models."""
|
|
1504
|
+
|
|
1505
|
+
return self._iot_request(
|
|
1506
|
+
"GET",
|
|
1507
|
+
API_ENDPOINT_IOT_FEATURE,
|
|
1508
|
+
serial,
|
|
1509
|
+
resource_identifier,
|
|
1510
|
+
local_index,
|
|
1511
|
+
domain_id,
|
|
1512
|
+
action_id,
|
|
1513
|
+
max_retries=max_retries,
|
|
1514
|
+
error_message="Could not fetch remote control path list",
|
|
1515
|
+
)
|
|
1516
|
+
|
|
1517
|
+
def get_tracking_status(
|
|
1518
|
+
self,
|
|
1519
|
+
serial: str,
|
|
1520
|
+
resource_identifier: str,
|
|
1521
|
+
local_index: str,
|
|
1522
|
+
domain_id: str,
|
|
1523
|
+
action_id: str,
|
|
1524
|
+
*,
|
|
1525
|
+
max_retries: int = 0,
|
|
1526
|
+
) -> dict:
|
|
1527
|
+
"""Obtain the current subject-tracking status from the IoT feature API."""
|
|
1528
|
+
|
|
1529
|
+
return self._iot_request(
|
|
1530
|
+
"GET",
|
|
1531
|
+
API_ENDPOINT_IOT_FEATURE,
|
|
1532
|
+
serial,
|
|
1533
|
+
resource_identifier,
|
|
1534
|
+
local_index,
|
|
1535
|
+
domain_id,
|
|
1536
|
+
action_id,
|
|
1537
|
+
max_retries=max_retries,
|
|
1538
|
+
error_message="Could not fetch tracking status",
|
|
1539
|
+
)
|
|
1540
|
+
|
|
1541
|
+
def get_port_security(
|
|
1542
|
+
self,
|
|
1543
|
+
serial: str,
|
|
1544
|
+
*,
|
|
1545
|
+
resource_identifier: str = "Video",
|
|
1546
|
+
local_index: str = "1",
|
|
1547
|
+
domain_id: str = "NetworkSecurityProtection",
|
|
1548
|
+
action_id: str = "PortSecurity",
|
|
1549
|
+
max_retries: int = 0,
|
|
1550
|
+
) -> dict:
|
|
1551
|
+
"""Fetch port security configuration via the IoT feature API."""
|
|
1552
|
+
|
|
1553
|
+
return self._iot_request(
|
|
1554
|
+
"GET",
|
|
1555
|
+
API_ENDPOINT_IOT_FEATURE,
|
|
1556
|
+
serial,
|
|
1557
|
+
resource_identifier,
|
|
1558
|
+
local_index,
|
|
1559
|
+
domain_id,
|
|
1560
|
+
action_id,
|
|
1561
|
+
max_retries=max_retries,
|
|
1562
|
+
error_message="Could not fetch port security status",
|
|
1563
|
+
)
|
|
1564
|
+
|
|
1565
|
+
def set_port_security(
|
|
1566
|
+
self,
|
|
1567
|
+
serial: str,
|
|
1568
|
+
value: Mapping[str, Any] | dict[str, Any],
|
|
1569
|
+
*,
|
|
1570
|
+
resource_identifier: str = "Video",
|
|
1571
|
+
local_index: str = "1",
|
|
1572
|
+
domain_id: str = "NetworkSecurityProtection",
|
|
1573
|
+
action_id: str = "PortSecurity",
|
|
1574
|
+
max_retries: int = 0,
|
|
1575
|
+
) -> dict:
|
|
1576
|
+
"""Update port security configuration via the IoT feature API."""
|
|
1577
|
+
|
|
1578
|
+
payload = {"value": value}
|
|
1579
|
+
return self._iot_request(
|
|
1580
|
+
"PUT",
|
|
1581
|
+
API_ENDPOINT_IOT_FEATURE,
|
|
1582
|
+
serial,
|
|
1583
|
+
resource_identifier,
|
|
1584
|
+
local_index,
|
|
1585
|
+
domain_id,
|
|
1586
|
+
action_id,
|
|
1587
|
+
payload=payload,
|
|
1588
|
+
max_retries=max_retries,
|
|
1589
|
+
error_message="Could not set port security status",
|
|
1590
|
+
)
|
|
1591
|
+
|
|
1592
|
+
def get_device_feature_value(
|
|
1593
|
+
self,
|
|
1594
|
+
serial: str,
|
|
1595
|
+
resource_identifier: str,
|
|
1596
|
+
domain_identifier: str,
|
|
1597
|
+
prop_identifier: str,
|
|
1598
|
+
*,
|
|
1599
|
+
local_index: str | int = "1",
|
|
1600
|
+
max_retries: int = 0,
|
|
1601
|
+
) -> dict:
|
|
1602
|
+
"""Retrieve a device feature value via the IoT feature API."""
|
|
1603
|
+
|
|
1604
|
+
local_idx = str(local_index)
|
|
1605
|
+
return self._iot_request(
|
|
1606
|
+
"GET",
|
|
1607
|
+
API_ENDPOINT_IOT_FEATURE,
|
|
1608
|
+
serial,
|
|
1609
|
+
resource_identifier,
|
|
1610
|
+
local_idx,
|
|
1611
|
+
domain_identifier,
|
|
1612
|
+
prop_identifier,
|
|
1613
|
+
max_retries=max_retries,
|
|
1614
|
+
error_message="Could not fetch device feature value",
|
|
1615
|
+
)
|
|
1616
|
+
|
|
1617
|
+
def set_image_flip_iot(
|
|
1618
|
+
self,
|
|
1619
|
+
serial: str,
|
|
1620
|
+
*,
|
|
1621
|
+
enabled: bool | None = None,
|
|
1622
|
+
payload: Any | None = None,
|
|
1623
|
+
local_index: str = "1",
|
|
1624
|
+
max_retries: int = 0,
|
|
1625
|
+
) -> dict:
|
|
1626
|
+
"""Set image flip configuration using the IoT feature endpoint."""
|
|
1627
|
+
|
|
1628
|
+
if payload is None:
|
|
1629
|
+
if enabled is None:
|
|
1630
|
+
raise PyEzvizError("Either 'enabled' or 'payload' must be provided")
|
|
1631
|
+
payload = {"value": {"enabled": bool(enabled)}}
|
|
1632
|
+
body = self._normalize_json_payload(payload)
|
|
1633
|
+
return self._iot_request(
|
|
1634
|
+
"PUT",
|
|
1635
|
+
API_ENDPOINT_IOT_FEATURE,
|
|
1636
|
+
serial,
|
|
1637
|
+
"Video",
|
|
1638
|
+
local_index,
|
|
1639
|
+
"VideoAdjustment",
|
|
1640
|
+
"ImageFlip",
|
|
1641
|
+
payload=body,
|
|
1642
|
+
max_retries=max_retries,
|
|
1643
|
+
error_message="Could not set image flip",
|
|
1644
|
+
)
|
|
1645
|
+
|
|
1646
|
+
def set_iot_action(
|
|
1647
|
+
self,
|
|
1648
|
+
serial: str,
|
|
1649
|
+
resource_identifier: str,
|
|
1650
|
+
local_index: str,
|
|
1651
|
+
domain_id: str,
|
|
1652
|
+
action_id: str,
|
|
1653
|
+
value: Any,
|
|
1654
|
+
*,
|
|
1655
|
+
max_retries: int = 0,
|
|
1656
|
+
) -> dict:
|
|
1657
|
+
"""Trigger an IoT action (setAction/putAction in the mobile API)."""
|
|
1658
|
+
|
|
1659
|
+
return self._iot_request(
|
|
1660
|
+
"PUT",
|
|
1661
|
+
API_ENDPOINT_IOT_ACTION,
|
|
1662
|
+
serial,
|
|
1663
|
+
resource_identifier,
|
|
1664
|
+
local_index,
|
|
1665
|
+
domain_id,
|
|
1666
|
+
action_id,
|
|
1667
|
+
payload=value,
|
|
1668
|
+
max_retries=max_retries,
|
|
1669
|
+
error_message="Could not execute IoT action",
|
|
1670
|
+
)
|
|
1671
|
+
|
|
1672
|
+
def set_iot_feature(
|
|
1673
|
+
self,
|
|
1674
|
+
serial: str,
|
|
1675
|
+
resource_identifier: str,
|
|
1676
|
+
local_index: str,
|
|
1677
|
+
domain_id: str,
|
|
1678
|
+
action_id: str,
|
|
1679
|
+
value: Any,
|
|
1680
|
+
*,
|
|
1681
|
+
max_retries: int = 0,
|
|
1682
|
+
) -> dict:
|
|
1683
|
+
"""Update an IoT feature value via the feature endpoint."""
|
|
1684
|
+
|
|
1685
|
+
return self._iot_request(
|
|
1686
|
+
"PUT",
|
|
1687
|
+
API_ENDPOINT_IOT_FEATURE,
|
|
1688
|
+
serial,
|
|
1689
|
+
resource_identifier,
|
|
1690
|
+
local_index,
|
|
1691
|
+
domain_id,
|
|
1692
|
+
action_id,
|
|
1693
|
+
payload=value,
|
|
1694
|
+
max_retries=max_retries,
|
|
1695
|
+
error_message="Could not set IoT feature value",
|
|
1696
|
+
)
|
|
1697
|
+
|
|
1698
|
+
def update_device_name(
|
|
1699
|
+
self,
|
|
1700
|
+
serial: str,
|
|
1701
|
+
name: str,
|
|
1702
|
+
*,
|
|
1703
|
+
max_retries: int = 0,
|
|
1704
|
+
) -> dict:
|
|
1705
|
+
"""Rename a device via the legacy updateName endpoint."""
|
|
1706
|
+
|
|
1707
|
+
if not name:
|
|
1708
|
+
raise PyEzvizError("Device name must not be empty")
|
|
1709
|
+
|
|
1710
|
+
data = {
|
|
1711
|
+
"deviceSerialNo": serial,
|
|
1712
|
+
"deviceName": name,
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
json_output = self._request_json(
|
|
1716
|
+
"POST",
|
|
1717
|
+
API_ENDPOINT_DEVICE_UPDATE_NAME,
|
|
1718
|
+
data=data,
|
|
1719
|
+
retry_401=True,
|
|
1720
|
+
max_retries=max_retries,
|
|
1721
|
+
)
|
|
1722
|
+
self._ensure_ok(json_output, "Could not update device name")
|
|
1723
|
+
return json_output
|
|
1724
|
+
|
|
1725
|
+
def upgrade_device(self, serial: str, max_retries: int = 0) -> bool:
|
|
1726
|
+
"""Upgrade device firmware."""
|
|
1727
|
+
json_output = self._request_json(
|
|
1728
|
+
"PUT",
|
|
1729
|
+
f"{API_ENDPOINT_UPGRADE_DEVICE}{serial}/0/upgrade",
|
|
1730
|
+
retry_401=True,
|
|
1731
|
+
max_retries=max_retries,
|
|
1732
|
+
)
|
|
1733
|
+
self._ensure_ok(json_output, "Could not initiate firmware upgrade")
|
|
975
1734
|
return True
|
|
976
1735
|
|
|
977
1736
|
def get_storage_status(self, serial: str, max_retries: int = 0) -> Any:
|
|
@@ -1056,6 +1815,32 @@ class EzvizClient:
|
|
|
1056
1815
|
|
|
1057
1816
|
return True
|
|
1058
1817
|
|
|
1818
|
+
def device_authenticate(
|
|
1819
|
+
self,
|
|
1820
|
+
serial: str,
|
|
1821
|
+
*,
|
|
1822
|
+
need_check_code: bool,
|
|
1823
|
+
check_code: str | None,
|
|
1824
|
+
sender_type: int,
|
|
1825
|
+
max_retries: int = 0,
|
|
1826
|
+
) -> dict:
|
|
1827
|
+
"""Authenticate a device, optionally requiring check code."""
|
|
1828
|
+
|
|
1829
|
+
data = {
|
|
1830
|
+
"needCheckCode": str(bool(need_check_code)).lower(),
|
|
1831
|
+
"checkCode": check_code or "",
|
|
1832
|
+
"senderType": sender_type,
|
|
1833
|
+
}
|
|
1834
|
+
json_output = self._request_json(
|
|
1835
|
+
"PUT",
|
|
1836
|
+
f"{API_ENDPOINT_DEVICES_AUTHENTICATE}{serial}",
|
|
1837
|
+
data=data,
|
|
1838
|
+
retry_401=True,
|
|
1839
|
+
max_retries=max_retries,
|
|
1840
|
+
)
|
|
1841
|
+
self._ensure_ok(json_output, "Could not authenticate device")
|
|
1842
|
+
return json_output
|
|
1843
|
+
|
|
1059
1844
|
def reboot_camera(
|
|
1060
1845
|
self,
|
|
1061
1846
|
serial: str,
|
|
@@ -1118,6 +1903,57 @@ class EzvizClient:
|
|
|
1118
1903
|
raise PyEzvizError(f"Could not set offline notification {json_output})")
|
|
1119
1904
|
raise PyEzvizError("Could not set offline notification: exceeded retries")
|
|
1120
1905
|
|
|
1906
|
+
def device_email_alert_state(
|
|
1907
|
+
self,
|
|
1908
|
+
serials: list[str] | str,
|
|
1909
|
+
*,
|
|
1910
|
+
max_retries: int = 0,
|
|
1911
|
+
) -> dict:
|
|
1912
|
+
"""Get email alert state for one or more devices."""
|
|
1913
|
+
|
|
1914
|
+
if isinstance(serials, (list, tuple, set)):
|
|
1915
|
+
serial_param = ",".join(sorted({str(s) for s in serials}))
|
|
1916
|
+
else:
|
|
1917
|
+
serial_param = str(serials)
|
|
1918
|
+
|
|
1919
|
+
json_output = self._request_json(
|
|
1920
|
+
"GET",
|
|
1921
|
+
API_ENDPOINT_DEVICE_EMAIL_ALERT,
|
|
1922
|
+
params={"devices": serial_param},
|
|
1923
|
+
retry_401=True,
|
|
1924
|
+
max_retries=max_retries,
|
|
1925
|
+
)
|
|
1926
|
+
self._ensure_ok(json_output, "Could not get device email alert state")
|
|
1927
|
+
return json_output
|
|
1928
|
+
|
|
1929
|
+
def save_device_email_alert_state(
|
|
1930
|
+
self,
|
|
1931
|
+
enable: bool,
|
|
1932
|
+
serials: list[str] | str,
|
|
1933
|
+
*,
|
|
1934
|
+
max_retries: int = 0,
|
|
1935
|
+
) -> dict:
|
|
1936
|
+
"""Update email alert state for the provided devices."""
|
|
1937
|
+
|
|
1938
|
+
if isinstance(serials, (list, tuple, set)):
|
|
1939
|
+
serial_param = ",".join(sorted({str(s) for s in serials}))
|
|
1940
|
+
else:
|
|
1941
|
+
serial_param = str(serials)
|
|
1942
|
+
|
|
1943
|
+
data = {
|
|
1944
|
+
"enable": str(bool(enable)).lower(),
|
|
1945
|
+
"devices": serial_param,
|
|
1946
|
+
}
|
|
1947
|
+
json_output = self._request_json(
|
|
1948
|
+
"POST",
|
|
1949
|
+
API_ENDPOINT_DEVICE_EMAIL_ALERT,
|
|
1950
|
+
data=data,
|
|
1951
|
+
retry_401=True,
|
|
1952
|
+
max_retries=max_retries,
|
|
1953
|
+
)
|
|
1954
|
+
self._ensure_ok(json_output, "Could not save device email alert state")
|
|
1955
|
+
return json_output
|
|
1956
|
+
|
|
1121
1957
|
def get_group_defence_mode(self, max_retries: int = 0) -> Any:
|
|
1122
1958
|
"""Get group arm status. The alarm arm/disarm concept on 1st page of app."""
|
|
1123
1959
|
if max_retries > MAX_RETRIES:
|
|
@@ -1298,6 +2134,48 @@ class EzvizClient:
|
|
|
1298
2134
|
return records
|
|
1299
2135
|
return records.get(serial) or devices.get(serial, {})
|
|
1300
2136
|
|
|
2137
|
+
def get_accessory(
|
|
2138
|
+
self,
|
|
2139
|
+
serial: str,
|
|
2140
|
+
local_index: str,
|
|
2141
|
+
*,
|
|
2142
|
+
max_retries: int = 0,
|
|
2143
|
+
) -> dict:
|
|
2144
|
+
"""Retrieve accessory information linked to a device."""
|
|
2145
|
+
|
|
2146
|
+
path = (
|
|
2147
|
+
f"{API_ENDPOINT_DEVICE_ACCESSORY_LINK}{serial}/{local_index}/1/linked/info"
|
|
2148
|
+
)
|
|
2149
|
+
json_output = self._request_json(
|
|
2150
|
+
"GET",
|
|
2151
|
+
path,
|
|
2152
|
+
retry_401=True,
|
|
2153
|
+
max_retries=max_retries,
|
|
2154
|
+
)
|
|
2155
|
+
self._ensure_ok(json_output, "Could not get accessory info")
|
|
2156
|
+
return json_output
|
|
2157
|
+
|
|
2158
|
+
def get_dev_config(
|
|
2159
|
+
self,
|
|
2160
|
+
serial: str,
|
|
2161
|
+
channel: int,
|
|
2162
|
+
key: str,
|
|
2163
|
+
*,
|
|
2164
|
+
max_retries: int = 0,
|
|
2165
|
+
) -> dict:
|
|
2166
|
+
"""Retrieve a devconfig value by key."""
|
|
2167
|
+
|
|
2168
|
+
params = {"key": key}
|
|
2169
|
+
json_output = self._request_json(
|
|
2170
|
+
"GET",
|
|
2171
|
+
f"{API_ENDPOINT_DEVCONFIG_BY_KEY}{serial}/{channel}/op",
|
|
2172
|
+
params=params,
|
|
2173
|
+
retry_401=True,
|
|
2174
|
+
max_retries=max_retries,
|
|
2175
|
+
)
|
|
2176
|
+
self._ensure_ok(json_output, "Could not get devconfig value")
|
|
2177
|
+
return json_output
|
|
2178
|
+
|
|
1301
2179
|
def ptz_control(
|
|
1302
2180
|
self, command: str, serial: str, action: str, speed: int = 5
|
|
1303
2181
|
) -> Any:
|
|
@@ -1330,6 +2208,25 @@ class EzvizClient:
|
|
|
1330
2208
|
|
|
1331
2209
|
return True
|
|
1332
2210
|
|
|
2211
|
+
def capture_picture(
|
|
2212
|
+
self,
|
|
2213
|
+
serial: str,
|
|
2214
|
+
channel: int,
|
|
2215
|
+
*,
|
|
2216
|
+
max_retries: int = 0,
|
|
2217
|
+
) -> dict:
|
|
2218
|
+
"""Trigger a snapshot capture on the device."""
|
|
2219
|
+
|
|
2220
|
+
path = f"/v3/devconfig/v1/{serial}/{channel}/capture"
|
|
2221
|
+
json_output = self._request_json(
|
|
2222
|
+
"PUT",
|
|
2223
|
+
path,
|
|
2224
|
+
retry_401=True,
|
|
2225
|
+
max_retries=max_retries,
|
|
2226
|
+
)
|
|
2227
|
+
self._ensure_ok(json_output, "Could not capture picture")
|
|
2228
|
+
return json_output
|
|
2229
|
+
|
|
1333
2230
|
def get_cam_key(
|
|
1334
2231
|
self, serial: str, smscode: int | None = None, max_retries: int = 0
|
|
1335
2232
|
) -> Any:
|
|
@@ -1590,9 +2487,26 @@ class EzvizClient:
|
|
|
1590
2487
|
|
|
1591
2488
|
return True
|
|
1592
2489
|
|
|
1593
|
-
def
|
|
1594
|
-
|
|
1595
|
-
|
|
2490
|
+
def get_door_lock_users(
|
|
2491
|
+
self,
|
|
2492
|
+
serial: str,
|
|
2493
|
+
*,
|
|
2494
|
+
max_retries: int = 0,
|
|
2495
|
+
) -> dict:
|
|
2496
|
+
"""Retrieve users associated with a door lock device."""
|
|
2497
|
+
|
|
2498
|
+
json_output = self._request_json(
|
|
2499
|
+
"GET",
|
|
2500
|
+
f"{API_ENDPOINT_DOORLOCK_USERS}{serial}/users",
|
|
2501
|
+
retry_401=True,
|
|
2502
|
+
max_retries=max_retries,
|
|
2503
|
+
)
|
|
2504
|
+
self._ensure_ok(json_output, "Could not get door lock users")
|
|
2505
|
+
return json_output
|
|
2506
|
+
|
|
2507
|
+
def remote_unlock(self, serial: str, user_id: str, lock_no: int) -> bool:
|
|
2508
|
+
"""Sends a remote command to unlock a specific lock.
|
|
2509
|
+
|
|
1596
2510
|
Args:
|
|
1597
2511
|
serial (str): The camera serial.
|
|
1598
2512
|
user_id (str): The user id.
|
|
@@ -1629,6 +2543,23 @@ class EzvizClient:
|
|
|
1629
2543
|
)
|
|
1630
2544
|
return True
|
|
1631
2545
|
|
|
2546
|
+
def get_remote_unbind_progress(
|
|
2547
|
+
self,
|
|
2548
|
+
serial: str,
|
|
2549
|
+
*,
|
|
2550
|
+
max_retries: int = 0,
|
|
2551
|
+
) -> dict:
|
|
2552
|
+
"""Check progress of a remote unbind request."""
|
|
2553
|
+
|
|
2554
|
+
json_output = self._request_json(
|
|
2555
|
+
"GET",
|
|
2556
|
+
f"{API_ENDPOINT_REMOTE_UNBIND_PROGRESS}{serial}/progress",
|
|
2557
|
+
retry_401=True,
|
|
2558
|
+
max_retries=max_retries,
|
|
2559
|
+
)
|
|
2560
|
+
self._ensure_ok(json_output, "Could not get unbind progress")
|
|
2561
|
+
return json_output
|
|
2562
|
+
|
|
1632
2563
|
def login(self, sms_code: int | None = None) -> dict[Any, Any]:
|
|
1633
2564
|
"""Get or refresh ezviz login token."""
|
|
1634
2565
|
if self._token["session_id"] and self._token["rf_session_id"]:
|
|
@@ -1767,18 +2698,64 @@ class EzvizClient:
|
|
|
1767
2698
|
raise PyEzvizError(f"Could not set the schedule: Got {json_output})")
|
|
1768
2699
|
return True
|
|
1769
2700
|
|
|
1770
|
-
def api_set_defence_mode(
|
|
2701
|
+
def api_set_defence_mode(
|
|
2702
|
+
self,
|
|
2703
|
+
mode: DefenseModeType | int,
|
|
2704
|
+
*,
|
|
2705
|
+
visual_alarm: int | None = None,
|
|
2706
|
+
sound_mode: int | None = None,
|
|
2707
|
+
max_retries: int = 0,
|
|
2708
|
+
) -> bool:
|
|
1771
2709
|
"""Set defence mode for all devices. The alarm panel from main page is used."""
|
|
2710
|
+
data: dict[str, Any] = {
|
|
2711
|
+
"groupId": -1,
|
|
2712
|
+
"mode": int(mode.value if isinstance(mode, DefenseModeType) else mode),
|
|
2713
|
+
}
|
|
2714
|
+
if visual_alarm is not None:
|
|
2715
|
+
data["visualAlarm"] = visual_alarm
|
|
2716
|
+
if sound_mode is not None:
|
|
2717
|
+
data["soundMode"] = sound_mode
|
|
2718
|
+
|
|
1772
2719
|
json_output = self._request_json(
|
|
1773
2720
|
"POST",
|
|
1774
2721
|
API_ENDPOINT_SWITCH_DEFENCE_MODE,
|
|
1775
|
-
data=
|
|
2722
|
+
data=data,
|
|
1776
2723
|
retry_401=True,
|
|
1777
2724
|
max_retries=max_retries,
|
|
1778
2725
|
)
|
|
1779
2726
|
self._ensure_ok(json_output, "Could not set defence mode")
|
|
1780
2727
|
return True
|
|
1781
2728
|
|
|
2729
|
+
def switch_defence_mode(
|
|
2730
|
+
self,
|
|
2731
|
+
group_id: int,
|
|
2732
|
+
mode: int,
|
|
2733
|
+
*,
|
|
2734
|
+
visual_alarm: int | None = None,
|
|
2735
|
+
sound_mode: int | None = None,
|
|
2736
|
+
max_retries: int = 0,
|
|
2737
|
+
) -> dict:
|
|
2738
|
+
"""Set defence mode for a specific group with optional sound/visual flags."""
|
|
2739
|
+
|
|
2740
|
+
data: dict[str, Any] = {
|
|
2741
|
+
"groupId": group_id,
|
|
2742
|
+
"mode": mode,
|
|
2743
|
+
}
|
|
2744
|
+
if visual_alarm is not None:
|
|
2745
|
+
data["visualAlarm"] = visual_alarm
|
|
2746
|
+
if sound_mode is not None:
|
|
2747
|
+
data["soundMode"] = sound_mode
|
|
2748
|
+
|
|
2749
|
+
json_output = self._request_json(
|
|
2750
|
+
"POST",
|
|
2751
|
+
API_ENDPOINT_SWITCH_DEFENCE_MODE,
|
|
2752
|
+
data=data,
|
|
2753
|
+
retry_401=True,
|
|
2754
|
+
max_retries=max_retries,
|
|
2755
|
+
)
|
|
2756
|
+
self._ensure_ok(json_output, "Could not switch defence mode")
|
|
2757
|
+
return json_output
|
|
2758
|
+
|
|
1782
2759
|
def do_not_disturb(
|
|
1783
2760
|
self,
|
|
1784
2761
|
serial: str,
|
|
@@ -1908,6 +2885,26 @@ class EzvizClient:
|
|
|
1908
2885
|
max_retries=max_retries,
|
|
1909
2886
|
)
|
|
1910
2887
|
|
|
2888
|
+
def device_mirror(
|
|
2889
|
+
self,
|
|
2890
|
+
serial: str,
|
|
2891
|
+
channel: int,
|
|
2892
|
+
command: str,
|
|
2893
|
+
*,
|
|
2894
|
+
max_retries: int = 0,
|
|
2895
|
+
) -> dict:
|
|
2896
|
+
"""Send a mirror command using the basics API."""
|
|
2897
|
+
|
|
2898
|
+
path = f"{API_ENDPOINT_DEVICE_BASICS}{serial}/{channel}/{command}/mirror"
|
|
2899
|
+
json_output = self._request_json(
|
|
2900
|
+
"PUT",
|
|
2901
|
+
path,
|
|
2902
|
+
retry_401=True,
|
|
2903
|
+
max_retries=max_retries,
|
|
2904
|
+
)
|
|
2905
|
+
self._ensure_ok(json_output, "Could not set mirror state")
|
|
2906
|
+
return json_output
|
|
2907
|
+
|
|
1911
2908
|
def flip_image(
|
|
1912
2909
|
self,
|
|
1913
2910
|
serial: str,
|
|
@@ -2100,6 +3097,42 @@ class EzvizClient:
|
|
|
2100
3097
|
|
|
2101
3098
|
return True
|
|
2102
3099
|
|
|
3100
|
+
def get_motion_detect_sensitivity(
|
|
3101
|
+
self,
|
|
3102
|
+
serial: str,
|
|
3103
|
+
channel: int,
|
|
3104
|
+
*,
|
|
3105
|
+
max_retries: int = 0,
|
|
3106
|
+
) -> dict:
|
|
3107
|
+
"""Get motion detection sensitivity via v1 devconfig endpoint."""
|
|
3108
|
+
|
|
3109
|
+
json_output = self._request_json(
|
|
3110
|
+
"GET",
|
|
3111
|
+
f"{API_ENDPOINT_SENSITIVITY}{serial}/{channel}",
|
|
3112
|
+
retry_401=True,
|
|
3113
|
+
max_retries=max_retries,
|
|
3114
|
+
)
|
|
3115
|
+
self._ensure_ok(json_output, "Could not get motion detect sensitivity")
|
|
3116
|
+
return json_output
|
|
3117
|
+
|
|
3118
|
+
def get_motion_detect_sensitivity_dp1s(
|
|
3119
|
+
self,
|
|
3120
|
+
serial: str,
|
|
3121
|
+
channel: int,
|
|
3122
|
+
*,
|
|
3123
|
+
max_retries: int = 0,
|
|
3124
|
+
) -> dict:
|
|
3125
|
+
"""Get motion detection sensitivity for DP1S devices."""
|
|
3126
|
+
|
|
3127
|
+
json_output = self._request_json(
|
|
3128
|
+
"GET",
|
|
3129
|
+
f"{API_ENDPOINT_DEVICES}{serial}/{channel}/sensitivity",
|
|
3130
|
+
retry_401=True,
|
|
3131
|
+
max_retries=max_retries,
|
|
3132
|
+
)
|
|
3133
|
+
self._ensure_ok(json_output, "Could not get DP1S motion sensitivity")
|
|
3134
|
+
return json_output
|
|
3135
|
+
|
|
2103
3136
|
def set_detection_sensitivity(
|
|
2104
3137
|
self,
|
|
2105
3138
|
serial: str,
|
|
@@ -2159,18 +3192,1017 @@ class EzvizClient:
|
|
|
2159
3192
|
|
|
2160
3193
|
return None
|
|
2161
3194
|
|
|
3195
|
+
def get_detector_setting_info(
|
|
3196
|
+
self,
|
|
3197
|
+
device_serial: str,
|
|
3198
|
+
detector_serial: str,
|
|
3199
|
+
key: str,
|
|
3200
|
+
*,
|
|
3201
|
+
max_retries: int = 0,
|
|
3202
|
+
) -> dict:
|
|
3203
|
+
"""Fetch a specific configuration key for an A1S detector."""
|
|
3204
|
+
|
|
3205
|
+
path = (
|
|
3206
|
+
f"{API_ENDPOINT_SPECIAL_BIZS_A1S}{device_serial}/detector/"
|
|
3207
|
+
f"{detector_serial}/{key}"
|
|
3208
|
+
)
|
|
3209
|
+
json_output = self._request_json(
|
|
3210
|
+
"GET",
|
|
3211
|
+
path,
|
|
3212
|
+
retry_401=True,
|
|
3213
|
+
max_retries=max_retries,
|
|
3214
|
+
)
|
|
3215
|
+
self._ensure_ok(json_output, "Could not get detector setting info")
|
|
3216
|
+
return json_output
|
|
3217
|
+
|
|
3218
|
+
def set_detector_setting_info(
|
|
3219
|
+
self,
|
|
3220
|
+
device_serial: str,
|
|
3221
|
+
detector_serial: str,
|
|
3222
|
+
key: str,
|
|
3223
|
+
value: int,
|
|
3224
|
+
*,
|
|
3225
|
+
max_retries: int = 0,
|
|
3226
|
+
) -> dict:
|
|
3227
|
+
"""Update a configuration key for an A1S detector."""
|
|
3228
|
+
|
|
3229
|
+
path = (
|
|
3230
|
+
f"{API_ENDPOINT_SPECIAL_BIZS_A1S}{device_serial}/detector/{detector_serial}"
|
|
3231
|
+
)
|
|
3232
|
+
json_output = self._request_json(
|
|
3233
|
+
"POST",
|
|
3234
|
+
path,
|
|
3235
|
+
params={"key": key},
|
|
3236
|
+
data={"value": value},
|
|
3237
|
+
retry_401=True,
|
|
3238
|
+
max_retries=max_retries,
|
|
3239
|
+
)
|
|
3240
|
+
self._ensure_ok(json_output, "Could not set detector setting info")
|
|
3241
|
+
return json_output
|
|
3242
|
+
|
|
3243
|
+
def get_detector_info(
|
|
3244
|
+
self,
|
|
3245
|
+
detector_serial: str,
|
|
3246
|
+
*,
|
|
3247
|
+
max_retries: int = 0,
|
|
3248
|
+
) -> dict:
|
|
3249
|
+
"""Retrieve status/details for an A1S detector."""
|
|
3250
|
+
|
|
3251
|
+
path = f"{API_ENDPOINT_SPECIAL_BIZS_A1S}detector/{detector_serial}"
|
|
3252
|
+
json_output = self._request_json(
|
|
3253
|
+
"GET",
|
|
3254
|
+
path,
|
|
3255
|
+
retry_401=True,
|
|
3256
|
+
max_retries=max_retries,
|
|
3257
|
+
)
|
|
3258
|
+
self._ensure_ok(json_output, "Could not get detector info")
|
|
3259
|
+
return json_output
|
|
3260
|
+
|
|
3261
|
+
def get_radio_signals(
|
|
3262
|
+
self,
|
|
3263
|
+
device_serial: str,
|
|
3264
|
+
child_device_serial: str,
|
|
3265
|
+
*,
|
|
3266
|
+
max_retries: int = 0,
|
|
3267
|
+
) -> dict:
|
|
3268
|
+
"""Return radio signal metrics for a detector connected to a device."""
|
|
3269
|
+
|
|
3270
|
+
path = f"{API_ENDPOINT_SPECIAL_BIZS_A1S}{device_serial}/radioSignal"
|
|
3271
|
+
json_output = self._request_json(
|
|
3272
|
+
"GET",
|
|
3273
|
+
path,
|
|
3274
|
+
params={"childDevSerial": child_device_serial},
|
|
3275
|
+
retry_401=True,
|
|
3276
|
+
max_retries=max_retries,
|
|
3277
|
+
)
|
|
3278
|
+
self._ensure_ok(json_output, "Could not get radio signals")
|
|
3279
|
+
return json_output
|
|
3280
|
+
|
|
3281
|
+
def get_voice_config(
|
|
3282
|
+
self,
|
|
3283
|
+
product_id: str,
|
|
3284
|
+
version: str,
|
|
3285
|
+
*,
|
|
3286
|
+
max_retries: int = 0,
|
|
3287
|
+
) -> dict:
|
|
3288
|
+
"""Fetch voice configuration metadata for a product."""
|
|
3289
|
+
|
|
3290
|
+
params = {"productId": product_id, "version": version}
|
|
3291
|
+
json_output = self._request_json(
|
|
3292
|
+
"GET",
|
|
3293
|
+
API_ENDPOINT_IOT_FEATURE_PRODUCT_VOICE_CONFIG,
|
|
3294
|
+
params=params,
|
|
3295
|
+
retry_401=True,
|
|
3296
|
+
max_retries=max_retries,
|
|
3297
|
+
)
|
|
3298
|
+
self._ensure_ok(json_output, "Could not get voice config")
|
|
3299
|
+
return json_output
|
|
3300
|
+
|
|
2162
3301
|
# soundtype: 0 = normal, 1 = intensive, 2 = disabled ... don't ask me why...
|
|
2163
|
-
def
|
|
2164
|
-
self,
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
3302
|
+
def get_voice_info(
|
|
3303
|
+
self,
|
|
3304
|
+
serial: str,
|
|
3305
|
+
*,
|
|
3306
|
+
local_index: str | None = None,
|
|
3307
|
+
max_retries: int = 0,
|
|
3308
|
+
) -> dict:
|
|
3309
|
+
"""Retrieve uploaded custom voice prompts for a device."""
|
|
2169
3310
|
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
3311
|
+
params: dict[str, Any] = {"deviceSerial": serial}
|
|
3312
|
+
if local_index is not None:
|
|
3313
|
+
params["localIndex"] = local_index
|
|
3314
|
+
|
|
3315
|
+
json_output = self._request_json(
|
|
3316
|
+
"GET",
|
|
3317
|
+
API_ENDPOINT_SPECIAL_BIZS_VOICES,
|
|
3318
|
+
params=params,
|
|
3319
|
+
retry_401=True,
|
|
3320
|
+
max_retries=max_retries,
|
|
3321
|
+
)
|
|
3322
|
+
self._ensure_ok(json_output, "Could not get voice list")
|
|
3323
|
+
return json_output
|
|
3324
|
+
|
|
3325
|
+
def add_voice_info(
|
|
3326
|
+
self,
|
|
3327
|
+
serial: str,
|
|
3328
|
+
voice_name: str,
|
|
3329
|
+
voice_url: str,
|
|
3330
|
+
*,
|
|
3331
|
+
local_index: str | None = None,
|
|
3332
|
+
max_retries: int = 0,
|
|
3333
|
+
) -> dict:
|
|
3334
|
+
"""Upload metadata for a new custom voice prompt."""
|
|
3335
|
+
|
|
3336
|
+
data: dict[str, Any] = {
|
|
3337
|
+
"deviceSerial": serial,
|
|
3338
|
+
"voiceName": voice_name,
|
|
3339
|
+
"voiceUrl": voice_url,
|
|
3340
|
+
}
|
|
3341
|
+
if local_index is not None:
|
|
3342
|
+
data["localIndex"] = local_index
|
|
3343
|
+
|
|
3344
|
+
json_output = self._request_json(
|
|
3345
|
+
"POST",
|
|
3346
|
+
API_ENDPOINT_SPECIAL_BIZS_VOICES,
|
|
3347
|
+
data=data,
|
|
3348
|
+
retry_401=True,
|
|
3349
|
+
max_retries=max_retries,
|
|
3350
|
+
)
|
|
3351
|
+
self._ensure_ok(json_output, "Could not add voice info")
|
|
3352
|
+
return json_output
|
|
3353
|
+
|
|
3354
|
+
def add_shared_voice_info(
|
|
3355
|
+
self,
|
|
3356
|
+
serial: str,
|
|
3357
|
+
voice_name: str,
|
|
3358
|
+
voice_url: str,
|
|
3359
|
+
local_index: str,
|
|
3360
|
+
*,
|
|
3361
|
+
max_retries: int = 0,
|
|
3362
|
+
) -> dict:
|
|
3363
|
+
"""Upload a shared voice with explicit local index, mirroring the mobile API."""
|
|
3364
|
+
|
|
3365
|
+
return self.add_voice_info(
|
|
3366
|
+
serial,
|
|
3367
|
+
voice_name,
|
|
3368
|
+
voice_url,
|
|
3369
|
+
local_index=local_index,
|
|
3370
|
+
max_retries=max_retries,
|
|
3371
|
+
)
|
|
3372
|
+
|
|
3373
|
+
def set_voice_info(
|
|
3374
|
+
self,
|
|
3375
|
+
serial: str,
|
|
3376
|
+
voice_id: int,
|
|
3377
|
+
voice_name: str,
|
|
3378
|
+
*,
|
|
3379
|
+
local_index: str | None = None,
|
|
3380
|
+
max_retries: int = 0,
|
|
3381
|
+
) -> dict:
|
|
3382
|
+
"""Update metadata for an existing voice prompt."""
|
|
3383
|
+
|
|
3384
|
+
data: dict[str, Any] = {
|
|
3385
|
+
"deviceSerial": serial,
|
|
3386
|
+
"voiceId": voice_id,
|
|
3387
|
+
"voiceName": voice_name,
|
|
3388
|
+
}
|
|
3389
|
+
if local_index is not None:
|
|
3390
|
+
data["localIndex"] = local_index
|
|
3391
|
+
|
|
3392
|
+
json_output = self._request_json(
|
|
3393
|
+
"PUT",
|
|
3394
|
+
API_ENDPOINT_SPECIAL_BIZS_VOICES,
|
|
3395
|
+
data=data,
|
|
3396
|
+
retry_401=True,
|
|
3397
|
+
max_retries=max_retries,
|
|
3398
|
+
)
|
|
3399
|
+
self._ensure_ok(json_output, "Could not update voice info")
|
|
3400
|
+
return json_output
|
|
3401
|
+
|
|
3402
|
+
def set_shared_voice_info(
|
|
3403
|
+
self,
|
|
3404
|
+
serial: str,
|
|
3405
|
+
voice_id: int,
|
|
3406
|
+
voice_name: str,
|
|
3407
|
+
local_index: str,
|
|
3408
|
+
*,
|
|
3409
|
+
max_retries: int = 0,
|
|
3410
|
+
) -> dict:
|
|
3411
|
+
"""Alias for updating shared voices that ensures local index is supplied."""
|
|
3412
|
+
|
|
3413
|
+
return self.set_voice_info(
|
|
3414
|
+
serial,
|
|
3415
|
+
voice_id,
|
|
3416
|
+
voice_name,
|
|
3417
|
+
local_index=local_index,
|
|
3418
|
+
max_retries=max_retries,
|
|
3419
|
+
)
|
|
3420
|
+
|
|
3421
|
+
def delete_voice_info(
|
|
3422
|
+
self,
|
|
3423
|
+
serial: str,
|
|
3424
|
+
voice_id: int,
|
|
3425
|
+
*,
|
|
3426
|
+
voice_url: str | None = None,
|
|
3427
|
+
local_index: str | None = None,
|
|
3428
|
+
max_retries: int = 0,
|
|
3429
|
+
) -> dict:
|
|
3430
|
+
"""Remove a voice prompt from a device."""
|
|
3431
|
+
|
|
3432
|
+
params: dict[str, Any] = {
|
|
3433
|
+
"deviceSerial": serial,
|
|
3434
|
+
"voiceId": voice_id,
|
|
3435
|
+
}
|
|
3436
|
+
if voice_url is not None:
|
|
3437
|
+
params["voiceUrl"] = voice_url
|
|
3438
|
+
if local_index is not None:
|
|
3439
|
+
params["localIndex"] = local_index
|
|
3440
|
+
|
|
3441
|
+
json_output = self._request_json(
|
|
3442
|
+
"DELETE",
|
|
3443
|
+
API_ENDPOINT_SPECIAL_BIZS_VOICES,
|
|
3444
|
+
params=params,
|
|
3445
|
+
retry_401=True,
|
|
3446
|
+
max_retries=max_retries,
|
|
3447
|
+
)
|
|
3448
|
+
self._ensure_ok(json_output, "Could not delete voice info")
|
|
3449
|
+
return json_output
|
|
3450
|
+
|
|
3451
|
+
def delete_shared_voice_info(
|
|
3452
|
+
self,
|
|
3453
|
+
serial: str,
|
|
3454
|
+
voice_id: int,
|
|
3455
|
+
voice_url: str,
|
|
3456
|
+
local_index: str,
|
|
3457
|
+
*,
|
|
3458
|
+
max_retries: int = 0,
|
|
3459
|
+
) -> dict:
|
|
3460
|
+
"""Alias for deleting shared voices with required parameters."""
|
|
3461
|
+
|
|
3462
|
+
return self.delete_voice_info(
|
|
3463
|
+
serial,
|
|
3464
|
+
voice_id,
|
|
3465
|
+
voice_url=voice_url,
|
|
3466
|
+
local_index=local_index,
|
|
3467
|
+
max_retries=max_retries,
|
|
3468
|
+
)
|
|
3469
|
+
|
|
3470
|
+
def get_whistle_status_by_channel(
|
|
3471
|
+
self,
|
|
3472
|
+
serial: str,
|
|
3473
|
+
*,
|
|
3474
|
+
max_retries: int = 0,
|
|
3475
|
+
) -> dict:
|
|
3476
|
+
"""Return whistle configuration per channel for a device."""
|
|
3477
|
+
|
|
3478
|
+
json_output = self._request_json(
|
|
3479
|
+
"GET",
|
|
3480
|
+
f"{API_ENDPOINT_DEVICES}{serial}{API_ENDPOINT_ALARM_GET_WHISTLE_STATUS_BY_CHANNEL}",
|
|
3481
|
+
retry_401=True,
|
|
3482
|
+
max_retries=max_retries,
|
|
3483
|
+
)
|
|
3484
|
+
self._ensure_ok(json_output, "Could not get whistle status by channel")
|
|
3485
|
+
return json_output
|
|
3486
|
+
|
|
3487
|
+
def get_whistle_status_by_device(
|
|
3488
|
+
self,
|
|
3489
|
+
serial: str,
|
|
3490
|
+
*,
|
|
3491
|
+
max_retries: int = 0,
|
|
3492
|
+
) -> dict:
|
|
3493
|
+
"""Return whistle configuration at the device level."""
|
|
3494
|
+
|
|
3495
|
+
json_output = self._request_json(
|
|
3496
|
+
"GET",
|
|
3497
|
+
f"{API_ENDPOINT_DEVICES}{serial}{API_ENDPOINT_ALARM_GET_WHISTLE_STATUS_BY_DEVICE}",
|
|
3498
|
+
retry_401=True,
|
|
3499
|
+
max_retries=max_retries,
|
|
3500
|
+
)
|
|
3501
|
+
self._ensure_ok(json_output, "Could not get whistle status by device")
|
|
3502
|
+
return json_output
|
|
3503
|
+
|
|
3504
|
+
def set_channel_whistle(
|
|
3505
|
+
self,
|
|
3506
|
+
serial: str,
|
|
3507
|
+
channel_whistles: list[Mapping[str, Any]] | list[dict[str, Any]],
|
|
3508
|
+
*,
|
|
3509
|
+
max_retries: int = 0,
|
|
3510
|
+
) -> dict:
|
|
3511
|
+
"""Configure whistle behaviour for individual channels."""
|
|
3512
|
+
|
|
3513
|
+
if not channel_whistles:
|
|
3514
|
+
raise PyEzvizError("channel_whistles must contain at least one entry")
|
|
3515
|
+
|
|
3516
|
+
entries: list[dict[str, Any]] = []
|
|
3517
|
+
required_fields = {"channel", "status", "duration", "volume"}
|
|
3518
|
+
for item in channel_whistles:
|
|
3519
|
+
entry = dict(item)
|
|
3520
|
+
entry.setdefault("deviceSerial", serial)
|
|
3521
|
+
missing = [field for field in required_fields if field not in entry]
|
|
3522
|
+
if missing:
|
|
3523
|
+
raise PyEzvizError(
|
|
3524
|
+
"channel_whistles entries must include " + ", ".join(missing)
|
|
3525
|
+
)
|
|
3526
|
+
entries.append(entry)
|
|
3527
|
+
|
|
3528
|
+
payload = {"channelWhistleList": entries}
|
|
3529
|
+
|
|
3530
|
+
json_output = self._request_json(
|
|
3531
|
+
"POST",
|
|
3532
|
+
f"{API_ENDPOINT_DEVICES}{serial}{API_ENDPOINT_ALARM_SET_CHANNEL_WHISTLE}",
|
|
3533
|
+
json_body=payload,
|
|
3534
|
+
retry_401=True,
|
|
3535
|
+
max_retries=max_retries,
|
|
3536
|
+
)
|
|
3537
|
+
self._ensure_ok(json_output, "Could not set channel whistle")
|
|
3538
|
+
return json_output
|
|
3539
|
+
|
|
3540
|
+
def set_device_whistle(
|
|
3541
|
+
self,
|
|
3542
|
+
serial: str,
|
|
3543
|
+
*,
|
|
3544
|
+
status: int,
|
|
3545
|
+
duration: int,
|
|
3546
|
+
volume: int,
|
|
3547
|
+
max_retries: int = 0,
|
|
3548
|
+
) -> dict:
|
|
3549
|
+
"""Configure whistle behaviour at the device level."""
|
|
3550
|
+
|
|
3551
|
+
params = {
|
|
3552
|
+
"status": status,
|
|
3553
|
+
"duration": duration,
|
|
3554
|
+
"volume": volume,
|
|
3555
|
+
}
|
|
3556
|
+
|
|
3557
|
+
json_output = self._request_json(
|
|
3558
|
+
"PUT",
|
|
3559
|
+
f"{API_ENDPOINT_DEVICES}{serial}{API_ENDPOINT_ALARM_SET_DEVICE_WHISTLE}",
|
|
3560
|
+
params=params,
|
|
3561
|
+
retry_401=True,
|
|
3562
|
+
max_retries=max_retries,
|
|
3563
|
+
)
|
|
3564
|
+
self._ensure_ok(json_output, "Could not set device whistle")
|
|
3565
|
+
return json_output
|
|
3566
|
+
|
|
3567
|
+
def stop_whistle(
|
|
3568
|
+
self,
|
|
3569
|
+
serial: str,
|
|
3570
|
+
*,
|
|
3571
|
+
max_retries: int = 0,
|
|
3572
|
+
) -> dict:
|
|
3573
|
+
"""Stop any ongoing whistle sound."""
|
|
3574
|
+
|
|
3575
|
+
json_output = self._request_json(
|
|
3576
|
+
"PUT",
|
|
3577
|
+
f"{API_ENDPOINT_DEVICES}{serial}{API_ENDPOINT_ALARM_STOP_WHISTLE}",
|
|
3578
|
+
retry_401=True,
|
|
3579
|
+
max_retries=max_retries,
|
|
3580
|
+
)
|
|
3581
|
+
self._ensure_ok(json_output, "Could not stop whistle")
|
|
3582
|
+
return json_output
|
|
3583
|
+
|
|
3584
|
+
def delay_battery_device_sleep(
|
|
3585
|
+
self,
|
|
3586
|
+
serial: str,
|
|
3587
|
+
channel: int,
|
|
3588
|
+
sleep_type: int,
|
|
3589
|
+
*,
|
|
3590
|
+
max_retries: int = 0,
|
|
3591
|
+
) -> dict:
|
|
3592
|
+
"""Request additional awake time for a battery-powered device."""
|
|
3593
|
+
|
|
3594
|
+
path = f"{API_ENDPOINT_SPECIAL_BIZS_V1_BATTERY}{serial}/{channel}/{sleep_type}/sleep"
|
|
3595
|
+
json_output = self._request_json(
|
|
3596
|
+
"PUT",
|
|
3597
|
+
path,
|
|
3598
|
+
retry_401=True,
|
|
3599
|
+
max_retries=max_retries,
|
|
3600
|
+
)
|
|
3601
|
+
self._ensure_ok(json_output, "Could not delay battery device sleep")
|
|
3602
|
+
return json_output
|
|
3603
|
+
|
|
3604
|
+
def get_device_chime_info(
|
|
3605
|
+
self,
|
|
3606
|
+
serial: str,
|
|
3607
|
+
channel: int,
|
|
3608
|
+
*,
|
|
3609
|
+
max_retries: int = 0,
|
|
3610
|
+
) -> dict:
|
|
3611
|
+
"""Fetch chime configuration for a specific channel."""
|
|
3612
|
+
|
|
3613
|
+
json_output = self._request_json(
|
|
3614
|
+
"GET",
|
|
3615
|
+
f"{API_ENDPOINT_ALARM_DEVICE_CHIME}{serial}/{channel}",
|
|
3616
|
+
retry_401=True,
|
|
3617
|
+
max_retries=max_retries,
|
|
3618
|
+
)
|
|
3619
|
+
self._ensure_ok(json_output, "Could not get chime info")
|
|
3620
|
+
return json_output
|
|
3621
|
+
|
|
3622
|
+
def set_device_chime_info(
|
|
3623
|
+
self,
|
|
3624
|
+
serial: str,
|
|
3625
|
+
channel: int,
|
|
3626
|
+
*,
|
|
3627
|
+
sound_type: int,
|
|
3628
|
+
duration: int,
|
|
3629
|
+
max_retries: int = 0,
|
|
3630
|
+
) -> dict:
|
|
3631
|
+
"""Update chime type and duration for a channel."""
|
|
3632
|
+
|
|
3633
|
+
data = {
|
|
3634
|
+
"type": sound_type,
|
|
3635
|
+
"duration": duration,
|
|
3636
|
+
}
|
|
3637
|
+
|
|
3638
|
+
json_output = self._request_json(
|
|
3639
|
+
"POST",
|
|
3640
|
+
f"{API_ENDPOINT_ALARM_DEVICE_CHIME}{serial}/{channel}",
|
|
3641
|
+
data=data,
|
|
3642
|
+
retry_401=True,
|
|
3643
|
+
max_retries=max_retries,
|
|
3644
|
+
)
|
|
3645
|
+
self._ensure_ok(json_output, "Could not set chime info")
|
|
3646
|
+
return json_output
|
|
3647
|
+
|
|
3648
|
+
def set_switch_enable_req(
|
|
3649
|
+
self,
|
|
3650
|
+
serial: str,
|
|
3651
|
+
channel: int,
|
|
3652
|
+
enable: int,
|
|
3653
|
+
switch_type: int,
|
|
3654
|
+
*,
|
|
3655
|
+
max_retries: int = 0,
|
|
3656
|
+
) -> dict:
|
|
3657
|
+
"""Call the legacy setSwitchEnableReq endpoint."""
|
|
3658
|
+
|
|
3659
|
+
params = {
|
|
3660
|
+
"enable": enable,
|
|
3661
|
+
"type": switch_type,
|
|
3662
|
+
}
|
|
3663
|
+
json_output = self._request_json(
|
|
3664
|
+
"PUT",
|
|
3665
|
+
f"{API_ENDPOINT_DEVICES}{serial}/{channel}{API_ENDPOINT_DEVICES_SET_SWITCH_ENABLE}",
|
|
3666
|
+
params=params,
|
|
3667
|
+
retry_401=True,
|
|
3668
|
+
max_retries=max_retries,
|
|
3669
|
+
)
|
|
3670
|
+
self._ensure_ok(json_output, "Could not set switch enable request")
|
|
3671
|
+
return json_output
|
|
3672
|
+
|
|
3673
|
+
def get_managed_device_info(
|
|
3674
|
+
self,
|
|
3675
|
+
serial: str,
|
|
3676
|
+
*,
|
|
3677
|
+
max_retries: int = 0,
|
|
3678
|
+
) -> dict:
|
|
3679
|
+
"""Return metadata for a managed device (e.g. base station)."""
|
|
3680
|
+
|
|
3681
|
+
path = f"{API_ENDPOINT_MANAGED_DEVICE_BASE}{serial}/base"
|
|
3682
|
+
json_output = self._request_json(
|
|
3683
|
+
"GET",
|
|
3684
|
+
path,
|
|
3685
|
+
retry_401=True,
|
|
3686
|
+
max_retries=max_retries,
|
|
3687
|
+
)
|
|
3688
|
+
self._ensure_ok(json_output, "Could not get managed device info")
|
|
3689
|
+
return json_output
|
|
3690
|
+
|
|
3691
|
+
def get_managed_device_ipcs(
|
|
3692
|
+
self,
|
|
3693
|
+
serial: str,
|
|
3694
|
+
*,
|
|
3695
|
+
max_retries: int = 0,
|
|
3696
|
+
) -> dict:
|
|
3697
|
+
"""List IPC sub-devices that belong to a managed device."""
|
|
3698
|
+
|
|
3699
|
+
path = f"{API_ENDPOINT_MANAGED_DEVICE_BASE}{serial}/ipcs"
|
|
3700
|
+
json_output = self._request_json(
|
|
3701
|
+
"GET",
|
|
3702
|
+
path,
|
|
3703
|
+
retry_401=True,
|
|
3704
|
+
max_retries=max_retries,
|
|
3705
|
+
)
|
|
3706
|
+
self._ensure_ok(json_output, "Could not get managed IPC list")
|
|
3707
|
+
return json_output
|
|
3708
|
+
|
|
3709
|
+
def get_devices_status(
|
|
3710
|
+
self,
|
|
3711
|
+
serials: list[str] | str,
|
|
3712
|
+
*,
|
|
3713
|
+
max_retries: int = 0,
|
|
3714
|
+
) -> dict:
|
|
3715
|
+
"""Fetch online/offline status for one or more devices."""
|
|
3716
|
+
|
|
3717
|
+
if isinstance(serials, (list, tuple, set)):
|
|
3718
|
+
serial_param = ",".join(sorted({str(s) for s in serials}))
|
|
3719
|
+
else:
|
|
3720
|
+
serial_param = str(serials)
|
|
3721
|
+
|
|
3722
|
+
json_output = self._request_json(
|
|
3723
|
+
"GET",
|
|
3724
|
+
API_ENDPOINT_USERDEVICES_STATUS,
|
|
3725
|
+
params={"deviceSerials": serial_param},
|
|
3726
|
+
retry_401=True,
|
|
3727
|
+
max_retries=max_retries,
|
|
3728
|
+
)
|
|
3729
|
+
self._ensure_ok(json_output, "Could not get device status")
|
|
3730
|
+
return json_output
|
|
3731
|
+
|
|
3732
|
+
def get_device_secret_key_info(
|
|
3733
|
+
self,
|
|
3734
|
+
serials: list[str] | str,
|
|
3735
|
+
*,
|
|
3736
|
+
max_retries: int = 0,
|
|
3737
|
+
) -> dict:
|
|
3738
|
+
"""Retrieve KMS secret key metadata for devices."""
|
|
3739
|
+
|
|
3740
|
+
if isinstance(serials, (list, tuple, set)):
|
|
3741
|
+
serial_param = ",".join(sorted({str(s) for s in serials}))
|
|
3742
|
+
else:
|
|
3743
|
+
serial_param = str(serials)
|
|
3744
|
+
|
|
3745
|
+
json_output = self._request_json(
|
|
3746
|
+
"GET",
|
|
3747
|
+
API_ENDPOINT_USERDEVICES_KMS,
|
|
3748
|
+
params={"deviceSerials": serial_param},
|
|
3749
|
+
retry_401=True,
|
|
3750
|
+
max_retries=max_retries,
|
|
3751
|
+
)
|
|
3752
|
+
self._ensure_ok(json_output, "Could not get device secret key info")
|
|
3753
|
+
return json_output
|
|
3754
|
+
|
|
3755
|
+
def get_device_list_encrypt_key(
|
|
3756
|
+
self,
|
|
3757
|
+
area_id: int,
|
|
3758
|
+
form_data: Mapping[str, Any] | bytes | bytearray | str,
|
|
3759
|
+
*,
|
|
3760
|
+
max_retries: int = 0,
|
|
3761
|
+
) -> dict:
|
|
3762
|
+
"""Batch query encrypt keys for devices, matching the mobile client's risk API."""
|
|
3763
|
+
|
|
3764
|
+
headers = {
|
|
3765
|
+
**self._session.headers,
|
|
3766
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
3767
|
+
"areaId": str(area_id),
|
|
3768
|
+
}
|
|
3769
|
+
if isinstance(form_data, (bytes, bytearray, str)):
|
|
3770
|
+
body = form_data
|
|
3771
|
+
else:
|
|
3772
|
+
body = urlencode(form_data, doseq=True)
|
|
3773
|
+
req = requests.Request(
|
|
3774
|
+
method="POST",
|
|
3775
|
+
url=self._url(API_ENDPOINT_DEVICES_ENCRYPTKEY_BATCH),
|
|
3776
|
+
headers=headers,
|
|
3777
|
+
data=body,
|
|
3778
|
+
).prepare()
|
|
3779
|
+
|
|
3780
|
+
resp = self._send_prepared(
|
|
3781
|
+
req,
|
|
3782
|
+
retry_401=True,
|
|
3783
|
+
max_retries=max_retries,
|
|
3784
|
+
)
|
|
3785
|
+
json_output = self._parse_json(resp)
|
|
3786
|
+
if not self._meta_ok(json_output):
|
|
3787
|
+
raise PyEzvizError(
|
|
3788
|
+
f"Could not get device encrypt key list: Got {json_output})"
|
|
3789
|
+
)
|
|
3790
|
+
return json_output
|
|
3791
|
+
|
|
3792
|
+
def get_p2p_info(
|
|
3793
|
+
self,
|
|
3794
|
+
serials: list[str] | str,
|
|
3795
|
+
*,
|
|
3796
|
+
max_retries: int = 0,
|
|
3797
|
+
) -> dict:
|
|
3798
|
+
"""Retrieve P2P info via the device-scoped endpoint."""
|
|
3799
|
+
|
|
3800
|
+
if isinstance(serials, (list, tuple, set)):
|
|
3801
|
+
serial_param = ",".join(sorted({str(s) for s in serials}))
|
|
3802
|
+
else:
|
|
3803
|
+
serial_param = str(serials)
|
|
3804
|
+
|
|
3805
|
+
json_output = self._request_json(
|
|
3806
|
+
"GET",
|
|
3807
|
+
API_ENDPOINT_DEVICES_P2P_INFO,
|
|
3808
|
+
params={"deviceSerials": serial_param},
|
|
3809
|
+
retry_401=True,
|
|
3810
|
+
max_retries=max_retries,
|
|
3811
|
+
)
|
|
3812
|
+
self._ensure_ok(json_output, "Could not get P2P info")
|
|
3813
|
+
return json_output
|
|
3814
|
+
|
|
3815
|
+
def get_p2p_server_info(
|
|
3816
|
+
self,
|
|
3817
|
+
serials: list[str] | str,
|
|
3818
|
+
*,
|
|
3819
|
+
max_retries: int = 0,
|
|
3820
|
+
) -> dict:
|
|
3821
|
+
"""Retrieve P2P server info via the userdevices endpoint."""
|
|
3822
|
+
|
|
3823
|
+
if isinstance(serials, (list, tuple, set)):
|
|
3824
|
+
serial_param = ",".join(sorted({str(s) for s in serials}))
|
|
3825
|
+
else:
|
|
3826
|
+
serial_param = str(serials)
|
|
3827
|
+
|
|
3828
|
+
json_output = self._request_json(
|
|
3829
|
+
"GET",
|
|
3830
|
+
API_ENDPOINT_USERDEVICES_P2P_INFO,
|
|
3831
|
+
params={"deviceSerials": serial_param},
|
|
3832
|
+
retry_401=True,
|
|
3833
|
+
max_retries=max_retries,
|
|
3834
|
+
)
|
|
3835
|
+
self._ensure_ok(json_output, "Could not get P2P server info")
|
|
3836
|
+
return json_output
|
|
3837
|
+
|
|
3838
|
+
def check_device_upgrade_rule(
|
|
3839
|
+
self,
|
|
3840
|
+
*,
|
|
3841
|
+
max_retries: int = 0,
|
|
3842
|
+
) -> dict:
|
|
3843
|
+
"""Check firmware upgrade eligibility rules."""
|
|
3844
|
+
|
|
3845
|
+
json_output = self._request_json(
|
|
3846
|
+
"GET",
|
|
3847
|
+
API_ENDPOINT_UPGRADE_RULE,
|
|
3848
|
+
retry_401=True,
|
|
3849
|
+
max_retries=max_retries,
|
|
3850
|
+
)
|
|
3851
|
+
self._ensure_ok(json_output, "Could not get upgrade rules")
|
|
3852
|
+
return json_output
|
|
3853
|
+
|
|
3854
|
+
def get_autoupgrade_switch(
|
|
3855
|
+
self,
|
|
3856
|
+
*,
|
|
3857
|
+
max_retries: int = 0,
|
|
3858
|
+
) -> dict:
|
|
3859
|
+
"""Return the current auto-upgrade switch settings."""
|
|
3860
|
+
|
|
3861
|
+
json_output = self._request_json(
|
|
3862
|
+
"GET",
|
|
3863
|
+
API_ENDPOINT_AUTOUPGRADE_SWITCH,
|
|
3864
|
+
retry_401=True,
|
|
3865
|
+
max_retries=max_retries,
|
|
3866
|
+
)
|
|
3867
|
+
self._ensure_ok(json_output, "Could not get auto-upgrade switch")
|
|
3868
|
+
return json_output
|
|
3869
|
+
|
|
3870
|
+
def set_autoupgrade_switch(
|
|
3871
|
+
self,
|
|
3872
|
+
auto_upgrade: int,
|
|
3873
|
+
time_type: int,
|
|
3874
|
+
*,
|
|
3875
|
+
max_retries: int = 0,
|
|
3876
|
+
) -> dict:
|
|
3877
|
+
"""Update the auto-upgrade switch configuration."""
|
|
3878
|
+
|
|
3879
|
+
data = {
|
|
3880
|
+
"autoUpgrade": auto_upgrade,
|
|
3881
|
+
"timeType": time_type,
|
|
3882
|
+
}
|
|
3883
|
+
|
|
3884
|
+
json_output = self._request_json(
|
|
3885
|
+
"PUT",
|
|
3886
|
+
API_ENDPOINT_AUTOUPGRADE_SWITCH,
|
|
3887
|
+
data=data,
|
|
3888
|
+
retry_401=True,
|
|
3889
|
+
max_retries=max_retries,
|
|
3890
|
+
)
|
|
3891
|
+
self._ensure_ok(json_output, "Could not set auto-upgrade switch")
|
|
3892
|
+
return json_output
|
|
3893
|
+
|
|
3894
|
+
def get_black_level_list(
|
|
3895
|
+
self,
|
|
3896
|
+
serial: str,
|
|
3897
|
+
*,
|
|
3898
|
+
max_retries: int = 0,
|
|
3899
|
+
) -> dict:
|
|
3900
|
+
"""Retrieve SD-card black level data for a device."""
|
|
3901
|
+
|
|
3902
|
+
json_output = self._request_json(
|
|
3903
|
+
"GET",
|
|
3904
|
+
f"{API_ENDPOINT_SDCARD_BLACK_LEVEL}{serial}",
|
|
3905
|
+
retry_401=True,
|
|
3906
|
+
max_retries=max_retries,
|
|
3907
|
+
)
|
|
3908
|
+
self._ensure_ok(json_output, "Could not get black level list")
|
|
3909
|
+
return json_output
|
|
3910
|
+
|
|
3911
|
+
def get_time_plan_infos(
|
|
3912
|
+
self,
|
|
3913
|
+
serial: str,
|
|
3914
|
+
channel: int,
|
|
3915
|
+
timing_plan_type: int,
|
|
3916
|
+
*,
|
|
3917
|
+
max_retries: int = 0,
|
|
3918
|
+
) -> dict:
|
|
3919
|
+
"""Fetch timing plan information for a device/channel."""
|
|
3920
|
+
|
|
3921
|
+
params = {
|
|
3922
|
+
"deviceSerial": serial,
|
|
3923
|
+
"channelNo": channel,
|
|
3924
|
+
"timingPlanType": timing_plan_type,
|
|
3925
|
+
}
|
|
3926
|
+
json_output = self._request_json(
|
|
3927
|
+
"GET",
|
|
3928
|
+
API_ENDPOINT_TIME_PLAN_INFOS,
|
|
3929
|
+
params=params,
|
|
3930
|
+
retry_401=True,
|
|
3931
|
+
max_retries=max_retries,
|
|
3932
|
+
)
|
|
3933
|
+
self._ensure_ok(json_output, "Could not get time plan infos")
|
|
3934
|
+
return json_output
|
|
3935
|
+
|
|
3936
|
+
def set_time_plan_infos(
|
|
3937
|
+
self,
|
|
3938
|
+
serial: str,
|
|
3939
|
+
channel: int,
|
|
3940
|
+
timing_plan_type: int,
|
|
3941
|
+
enable: int,
|
|
3942
|
+
timer_defence_qos: Any,
|
|
3943
|
+
*,
|
|
3944
|
+
max_retries: int = 0,
|
|
3945
|
+
) -> dict:
|
|
3946
|
+
"""Update timing plan configuration."""
|
|
3947
|
+
|
|
3948
|
+
params: dict[str, Any] = {
|
|
3949
|
+
"deviceSerial": serial,
|
|
3950
|
+
"channelNo": channel,
|
|
3951
|
+
"timingPlanType": timing_plan_type,
|
|
3952
|
+
"enable": enable,
|
|
3953
|
+
}
|
|
3954
|
+
if not isinstance(timer_defence_qos, str):
|
|
3955
|
+
params["timerDefenceQos"] = json.dumps(timer_defence_qos)
|
|
3956
|
+
else:
|
|
3957
|
+
params["timerDefenceQos"] = timer_defence_qos
|
|
3958
|
+
|
|
3959
|
+
json_output = self._request_json(
|
|
3960
|
+
"PUT",
|
|
3961
|
+
API_ENDPOINT_TIME_PLAN_INFOS,
|
|
3962
|
+
params=params,
|
|
3963
|
+
retry_401=True,
|
|
3964
|
+
max_retries=max_retries,
|
|
3965
|
+
)
|
|
3966
|
+
self._ensure_ok(json_output, "Could not set time plan infos")
|
|
3967
|
+
return json_output
|
|
3968
|
+
|
|
3969
|
+
def search_records(
|
|
3970
|
+
self,
|
|
3971
|
+
serial: str,
|
|
3972
|
+
channel: int,
|
|
3973
|
+
channel_serial: str,
|
|
3974
|
+
start_time: str,
|
|
3975
|
+
stop_time: str,
|
|
3976
|
+
*,
|
|
3977
|
+
size: int = 20,
|
|
3978
|
+
max_retries: int = 0,
|
|
3979
|
+
) -> dict:
|
|
3980
|
+
"""Search recorded video clips for a device."""
|
|
3981
|
+
|
|
3982
|
+
params = {
|
|
3983
|
+
"deviceSerial": serial,
|
|
3984
|
+
"channelNo": channel,
|
|
3985
|
+
"channelSerial": channel_serial,
|
|
3986
|
+
"startTime": start_time,
|
|
3987
|
+
"stopTime": stop_time,
|
|
3988
|
+
"size": size,
|
|
3989
|
+
}
|
|
3990
|
+
json_output = self._request_json(
|
|
3991
|
+
"GET",
|
|
3992
|
+
API_ENDPOINT_STREAMING_RECORDS,
|
|
3993
|
+
params=params,
|
|
3994
|
+
retry_401=True,
|
|
3995
|
+
max_retries=max_retries,
|
|
3996
|
+
)
|
|
3997
|
+
self._ensure_ok(json_output, "Could not search records")
|
|
3998
|
+
return json_output
|
|
3999
|
+
|
|
4000
|
+
def search_device(
|
|
4001
|
+
self,
|
|
4002
|
+
serial: str,
|
|
4003
|
+
*,
|
|
4004
|
+
user_ssid: str | None = None,
|
|
4005
|
+
max_retries: int = 0,
|
|
4006
|
+
) -> dict:
|
|
4007
|
+
"""Find device information by serial."""
|
|
4008
|
+
|
|
4009
|
+
headers = dict(self._session.headers)
|
|
4010
|
+
if user_ssid is not None:
|
|
4011
|
+
headers["userSsid"] = user_ssid
|
|
4012
|
+
|
|
4013
|
+
params = {"deviceSerial": serial}
|
|
4014
|
+
req = requests.Request(
|
|
4015
|
+
method="GET",
|
|
4016
|
+
url=self._url(API_ENDPOINT_USERDEVICES_SEARCH),
|
|
4017
|
+
headers=headers,
|
|
4018
|
+
params=params,
|
|
4019
|
+
).prepare()
|
|
4020
|
+
|
|
4021
|
+
resp = self._send_prepared(
|
|
4022
|
+
req,
|
|
4023
|
+
retry_401=True,
|
|
4024
|
+
max_retries=max_retries,
|
|
4025
|
+
)
|
|
4026
|
+
json_output = self._parse_json(resp)
|
|
4027
|
+
if not self._meta_ok(json_output):
|
|
4028
|
+
raise PyEzvizError(f"Could not search device: Got {json_output})")
|
|
4029
|
+
return json_output
|
|
4030
|
+
|
|
4031
|
+
def get_socket_log_info(
|
|
4032
|
+
self,
|
|
4033
|
+
serial: str,
|
|
4034
|
+
start: str,
|
|
4035
|
+
end: str,
|
|
4036
|
+
*,
|
|
4037
|
+
max_retries: int = 0,
|
|
4038
|
+
) -> dict:
|
|
4039
|
+
"""Fetch smart outlet switch logs within a time range."""
|
|
4040
|
+
|
|
4041
|
+
path = API_ENDPOINT_SMARTHOME_OUTLET_LOG.format(**{"from": start, "to": end})
|
|
4042
|
+
json_output = self._request_json(
|
|
4043
|
+
"GET",
|
|
4044
|
+
path,
|
|
4045
|
+
params={"deviceSerial": serial},
|
|
4046
|
+
retry_401=True,
|
|
4047
|
+
max_retries=max_retries,
|
|
4048
|
+
)
|
|
4049
|
+
self._ensure_ok(json_output, "Could not get socket log info")
|
|
4050
|
+
return json_output
|
|
4051
|
+
|
|
4052
|
+
def linked_cameras(
|
|
4053
|
+
self,
|
|
4054
|
+
serial: str,
|
|
4055
|
+
detector_serial: str,
|
|
4056
|
+
*,
|
|
4057
|
+
max_retries: int = 0,
|
|
4058
|
+
) -> dict:
|
|
4059
|
+
"""List cameras linked to a detector device."""
|
|
4060
|
+
|
|
4061
|
+
params = {
|
|
4062
|
+
"deviceSerial": serial,
|
|
4063
|
+
"detectorDeviceSerial": detector_serial,
|
|
4064
|
+
}
|
|
4065
|
+
json_output = self._request_json(
|
|
4066
|
+
"GET",
|
|
4067
|
+
API_ENDPOINT_DEVICES_ASSOCIATION_LINKED_IPC,
|
|
4068
|
+
params=params,
|
|
4069
|
+
retry_401=True,
|
|
4070
|
+
max_retries=max_retries,
|
|
4071
|
+
)
|
|
4072
|
+
self._ensure_ok(json_output, "Could not get linked cameras")
|
|
4073
|
+
return json_output
|
|
4074
|
+
|
|
4075
|
+
def set_microscope(
|
|
4076
|
+
self,
|
|
4077
|
+
serial: str,
|
|
4078
|
+
multiple: float,
|
|
4079
|
+
x: int,
|
|
4080
|
+
y: int,
|
|
4081
|
+
index: int,
|
|
4082
|
+
*,
|
|
4083
|
+
max_retries: int = 0,
|
|
4084
|
+
) -> dict:
|
|
4085
|
+
"""Configure microscope lens parameters."""
|
|
4086
|
+
|
|
4087
|
+
data = {
|
|
4088
|
+
"multiple": multiple,
|
|
4089
|
+
"x": x,
|
|
4090
|
+
"y": y,
|
|
4091
|
+
"index": index,
|
|
4092
|
+
}
|
|
4093
|
+
json_output = self._request_json(
|
|
4094
|
+
"PUT",
|
|
4095
|
+
f"{API_ENDPOINT_DEVICES}{serial}/microscope",
|
|
4096
|
+
data=data,
|
|
4097
|
+
retry_401=True,
|
|
4098
|
+
max_retries=max_retries,
|
|
4099
|
+
)
|
|
4100
|
+
self._ensure_ok(json_output, "Could not set microscope")
|
|
4101
|
+
return json_output
|
|
4102
|
+
|
|
4103
|
+
def share_accept(
|
|
4104
|
+
self,
|
|
4105
|
+
serial: str,
|
|
4106
|
+
*,
|
|
4107
|
+
max_retries: int = 0,
|
|
4108
|
+
) -> dict:
|
|
4109
|
+
"""Accept a device share invitation."""
|
|
4110
|
+
|
|
4111
|
+
json_output = self._request_json(
|
|
4112
|
+
"POST",
|
|
4113
|
+
API_ENDPOINT_SHARE_ACCEPT,
|
|
4114
|
+
data={"deviceSerial": serial},
|
|
4115
|
+
retry_401=True,
|
|
4116
|
+
max_retries=max_retries,
|
|
4117
|
+
)
|
|
4118
|
+
self._ensure_ok(json_output, "Could not accept share")
|
|
4119
|
+
return json_output
|
|
4120
|
+
|
|
4121
|
+
def share_quit(
|
|
4122
|
+
self,
|
|
4123
|
+
serial: str,
|
|
4124
|
+
*,
|
|
4125
|
+
max_retries: int = 0,
|
|
4126
|
+
) -> dict:
|
|
4127
|
+
"""Leave a shared device."""
|
|
4128
|
+
|
|
4129
|
+
json_output = self._request_json(
|
|
4130
|
+
"DELETE",
|
|
4131
|
+
API_ENDPOINT_SHARE_QUIT,
|
|
4132
|
+
params={"deviceSerial": serial},
|
|
4133
|
+
retry_401=True,
|
|
4134
|
+
max_retries=max_retries,
|
|
4135
|
+
)
|
|
4136
|
+
self._ensure_ok(json_output, "Could not quit share")
|
|
4137
|
+
return json_output
|
|
4138
|
+
|
|
4139
|
+
def send_feedback(
|
|
4140
|
+
self,
|
|
4141
|
+
*,
|
|
4142
|
+
email: str,
|
|
4143
|
+
account: str,
|
|
4144
|
+
score: int,
|
|
4145
|
+
feedback: str,
|
|
4146
|
+
pic_url: str | None = None,
|
|
4147
|
+
max_retries: int = 0,
|
|
4148
|
+
) -> dict:
|
|
4149
|
+
"""Submit feedback to Ezviz support."""
|
|
4150
|
+
|
|
4151
|
+
params: dict[str, Any] = {
|
|
4152
|
+
"email": email,
|
|
4153
|
+
"account": account,
|
|
4154
|
+
"score": score,
|
|
4155
|
+
"feedback": feedback,
|
|
4156
|
+
}
|
|
4157
|
+
if pic_url is not None:
|
|
4158
|
+
params["picUrl"] = pic_url
|
|
4159
|
+
|
|
4160
|
+
json_output = self._request_json(
|
|
4161
|
+
"POST",
|
|
4162
|
+
API_ENDPOINT_FEEDBACK,
|
|
4163
|
+
params=params,
|
|
4164
|
+
retry_401=True,
|
|
4165
|
+
max_retries=max_retries,
|
|
4166
|
+
)
|
|
4167
|
+
self._ensure_ok(json_output, "Could not send feedback")
|
|
4168
|
+
return json_output
|
|
4169
|
+
|
|
4170
|
+
def upload_device_log(
|
|
4171
|
+
self,
|
|
4172
|
+
serial: str,
|
|
4173
|
+
*,
|
|
4174
|
+
max_retries: int = 0,
|
|
4175
|
+
) -> dict:
|
|
4176
|
+
"""Trigger device log upload to Ezviz cloud."""
|
|
4177
|
+
|
|
4178
|
+
json_output = self._request_json(
|
|
4179
|
+
"POST",
|
|
4180
|
+
"/v3/devconfig/dump/app/trigger",
|
|
4181
|
+
data={"deviceSerial": serial},
|
|
4182
|
+
retry_401=True,
|
|
4183
|
+
max_retries=max_retries,
|
|
4184
|
+
)
|
|
4185
|
+
self._ensure_ok(json_output, "Could not upload device log")
|
|
4186
|
+
return json_output
|
|
4187
|
+
|
|
4188
|
+
def alarm_sound(
|
|
4189
|
+
self,
|
|
4190
|
+
serial: str,
|
|
4191
|
+
sound_type: int,
|
|
4192
|
+
enable: int = 1,
|
|
4193
|
+
voice_id: int | None = None,
|
|
4194
|
+
max_retries: int = 0,
|
|
4195
|
+
) -> bool:
|
|
4196
|
+
"""Enable alarm sound by API."""
|
|
4197
|
+
if max_retries > MAX_RETRIES:
|
|
4198
|
+
raise PyEzvizError("Can't gather proper data. Max retries exceeded.")
|
|
4199
|
+
|
|
4200
|
+
if sound_type not in [0, 1, 2]:
|
|
4201
|
+
raise PyEzvizError(
|
|
4202
|
+
"Invalid sound_type, should be 0,1,2: " + str(sound_type)
|
|
4203
|
+
)
|
|
4204
|
+
|
|
4205
|
+
voice_id_value = 0 if voice_id is None else voice_id
|
|
2174
4206
|
|
|
2175
4207
|
response_json = self._request_json(
|
|
2176
4208
|
"PUT",
|
|
@@ -2178,7 +4210,7 @@ class EzvizClient:
|
|
|
2178
4210
|
data={
|
|
2179
4211
|
"enable": enable,
|
|
2180
4212
|
"soundType": sound_type,
|
|
2181
|
-
"voiceId":
|
|
4213
|
+
"voiceId": voice_id_value,
|
|
2182
4214
|
"deviceSerial": serial,
|
|
2183
4215
|
},
|
|
2184
4216
|
retry_401=True,
|