pyezvizapi 1.0.3.0__py3-none-any.whl → 1.0.3.2__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/__init__.py +30 -0
- pyezvizapi/api_endpoints.py +49 -0
- pyezvizapi/client.py +2187 -37
- pyezvizapi/feature.py +251 -0
- {pyezvizapi-1.0.3.0.dist-info → pyezvizapi-1.0.3.2.dist-info}/METADATA +1 -1
- {pyezvizapi-1.0.3.0.dist-info → pyezvizapi-1.0.3.2.dist-info}/RECORD +11 -10
- {pyezvizapi-1.0.3.0.dist-info → pyezvizapi-1.0.3.2.dist-info}/WHEEL +0 -0
- {pyezvizapi-1.0.3.0.dist-info → pyezvizapi-1.0.3.2.dist-info}/entry_points.txt +0 -0
- {pyezvizapi-1.0.3.0.dist-info → pyezvizapi-1.0.3.2.dist-info}/licenses/LICENSE +0 -0
- {pyezvizapi-1.0.3.0.dist-info → pyezvizapi-1.0.3.2.dist-info}/licenses/LICENSE.md +0 -0
- {pyezvizapi-1.0.3.0.dist-info → pyezvizapi-1.0.3.2.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
|
)
|
|
@@ -80,6 +126,7 @@ from .exceptions import (
|
|
|
80
126
|
InvalidURL,
|
|
81
127
|
PyEzvizError,
|
|
82
128
|
)
|
|
129
|
+
from .feature import optionals_mapping
|
|
83
130
|
from .light_bulb import EzvizLightBulb
|
|
84
131
|
from .models import EzvizDeviceRecord, build_device_records_map
|
|
85
132
|
from .mqtt import MQTTClient
|
|
@@ -356,6 +403,26 @@ class EzvizClient:
|
|
|
356
403
|
+ str(resp.text)
|
|
357
404
|
) from err
|
|
358
405
|
|
|
406
|
+
@staticmethod
|
|
407
|
+
def _normalize_json_payload(payload: Any) -> Any:
|
|
408
|
+
"""Return a payload suitable for json= usage, decoding strings when needed."""
|
|
409
|
+
|
|
410
|
+
if isinstance(payload, (Mapping, list)):
|
|
411
|
+
return payload
|
|
412
|
+
if isinstance(payload, tuple):
|
|
413
|
+
return list(payload)
|
|
414
|
+
if isinstance(payload, (bytes, bytearray)):
|
|
415
|
+
try:
|
|
416
|
+
return json.loads(payload.decode())
|
|
417
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as err:
|
|
418
|
+
raise PyEzvizError("Invalid JSON payload provided") from err
|
|
419
|
+
if isinstance(payload, str):
|
|
420
|
+
try:
|
|
421
|
+
return json.loads(payload)
|
|
422
|
+
except json.JSONDecodeError as err:
|
|
423
|
+
raise PyEzvizError("Invalid JSON payload provided") from err
|
|
424
|
+
raise PyEzvizError("Unsupported payload type for JSON body")
|
|
425
|
+
|
|
359
426
|
@staticmethod
|
|
360
427
|
def _is_ok(payload: dict) -> bool:
|
|
361
428
|
"""Return True if payload indicates success for both API styles."""
|
|
@@ -530,6 +597,18 @@ class EzvizClient:
|
|
|
530
597
|
service_urls["sysConf"] = str(service_urls.get("sysConf", "")).split("|")
|
|
531
598
|
return service_urls
|
|
532
599
|
|
|
600
|
+
def lbs_domain(self, max_retries: int = 0) -> dict:
|
|
601
|
+
"""Retrieve the LBS sub-domain information."""
|
|
602
|
+
|
|
603
|
+
json_output = self._request_json(
|
|
604
|
+
"GET",
|
|
605
|
+
API_ENDPOINT_USERS_LBS_SUB_DOMAIN,
|
|
606
|
+
retry_401=True,
|
|
607
|
+
max_retries=max_retries,
|
|
608
|
+
)
|
|
609
|
+
self._ensure_ok(json_output, "Could not get LBS domain")
|
|
610
|
+
return json_output
|
|
611
|
+
|
|
533
612
|
def _api_get_pagelist(
|
|
534
613
|
self,
|
|
535
614
|
page_filter: str,
|
|
@@ -648,6 +727,222 @@ class EzvizClient:
|
|
|
648
727
|
self._ensure_ok(json_output, "Could not get unified message list")
|
|
649
728
|
return json_output
|
|
650
729
|
|
|
730
|
+
def add_device(
|
|
731
|
+
self,
|
|
732
|
+
serial: str,
|
|
733
|
+
validate_code: str,
|
|
734
|
+
*,
|
|
735
|
+
add_type: str | None = None,
|
|
736
|
+
max_retries: int = 0,
|
|
737
|
+
) -> dict:
|
|
738
|
+
"""Add a new device to the current account."""
|
|
739
|
+
|
|
740
|
+
data = {
|
|
741
|
+
"deviceSerial": serial,
|
|
742
|
+
"validateCode": validate_code,
|
|
743
|
+
}
|
|
744
|
+
if add_type is not None:
|
|
745
|
+
data["addType"] = add_type
|
|
746
|
+
json_output = self._request_json(
|
|
747
|
+
"POST",
|
|
748
|
+
API_ENDPOINT_USERDEVICES_V2,
|
|
749
|
+
data=data,
|
|
750
|
+
retry_401=True,
|
|
751
|
+
max_retries=max_retries,
|
|
752
|
+
)
|
|
753
|
+
self._ensure_ok(json_output, "Could not add device")
|
|
754
|
+
return json_output
|
|
755
|
+
|
|
756
|
+
def add_hik_activate(
|
|
757
|
+
self,
|
|
758
|
+
serial: str,
|
|
759
|
+
payload: Any,
|
|
760
|
+
*,
|
|
761
|
+
max_retries: int = 0,
|
|
762
|
+
) -> dict:
|
|
763
|
+
"""Activate a Hikvision device using the security endpoint."""
|
|
764
|
+
|
|
765
|
+
body = self._normalize_json_payload(payload)
|
|
766
|
+
json_output = self._request_json(
|
|
767
|
+
"POST",
|
|
768
|
+
f"{API_ENDPOINT_DEVCONFIG_SECURITY_ACTIVATE}{serial}",
|
|
769
|
+
json_body=body,
|
|
770
|
+
retry_401=True,
|
|
771
|
+
max_retries=max_retries,
|
|
772
|
+
)
|
|
773
|
+
self._ensure_ok(json_output, "Could not activate Hik device")
|
|
774
|
+
return json_output
|
|
775
|
+
|
|
776
|
+
def add_hik_challenge(
|
|
777
|
+
self,
|
|
778
|
+
serial: str,
|
|
779
|
+
payload: Any,
|
|
780
|
+
*,
|
|
781
|
+
max_retries: int = 0,
|
|
782
|
+
) -> dict:
|
|
783
|
+
"""Request a Hikvision security challenge."""
|
|
784
|
+
|
|
785
|
+
body = self._normalize_json_payload(payload)
|
|
786
|
+
json_output = self._request_json(
|
|
787
|
+
"POST",
|
|
788
|
+
f"{API_ENDPOINT_DEVCONFIG_SECURITY_CHALLENGE}{serial}",
|
|
789
|
+
json_body=body,
|
|
790
|
+
retry_401=True,
|
|
791
|
+
max_retries=max_retries,
|
|
792
|
+
)
|
|
793
|
+
self._ensure_ok(json_output, "Could not request Hik challenge")
|
|
794
|
+
return json_output
|
|
795
|
+
|
|
796
|
+
def add_local_device(
|
|
797
|
+
self,
|
|
798
|
+
payload: Any,
|
|
799
|
+
*,
|
|
800
|
+
max_retries: int = 0,
|
|
801
|
+
) -> dict:
|
|
802
|
+
"""Add a device discovered on the local network."""
|
|
803
|
+
|
|
804
|
+
body = self._normalize_json_payload(payload)
|
|
805
|
+
json_output = self._request_json(
|
|
806
|
+
"POST",
|
|
807
|
+
API_ENDPOINT_DEVICES_LOC,
|
|
808
|
+
json_body=body,
|
|
809
|
+
retry_401=True,
|
|
810
|
+
max_retries=max_retries,
|
|
811
|
+
)
|
|
812
|
+
self._ensure_ok(json_output, "Could not add local device")
|
|
813
|
+
return json_output
|
|
814
|
+
|
|
815
|
+
def save_hik_dev_code(
|
|
816
|
+
self,
|
|
817
|
+
payload: Any,
|
|
818
|
+
*,
|
|
819
|
+
max_retries: int = 0,
|
|
820
|
+
) -> dict:
|
|
821
|
+
"""Submit a Hikvision device code via the SCD endpoint."""
|
|
822
|
+
|
|
823
|
+
body = self._normalize_json_payload(payload)
|
|
824
|
+
json_output = self._request_json(
|
|
825
|
+
"POST",
|
|
826
|
+
API_ENDPOINT_SCD_APP_DEVICE_ADD,
|
|
827
|
+
json_body=body,
|
|
828
|
+
retry_401=True,
|
|
829
|
+
max_retries=max_retries,
|
|
830
|
+
)
|
|
831
|
+
self._ensure_ok(json_output, "Could not save Hik device code")
|
|
832
|
+
return json_output
|
|
833
|
+
|
|
834
|
+
def bind_virtual_device(
|
|
835
|
+
self,
|
|
836
|
+
product_id: str,
|
|
837
|
+
version: str,
|
|
838
|
+
*,
|
|
839
|
+
max_retries: int = 0,
|
|
840
|
+
) -> dict:
|
|
841
|
+
"""Bind a virtual IoT device using product identifier and version."""
|
|
842
|
+
|
|
843
|
+
params = {"productId": product_id, "version": version}
|
|
844
|
+
json_output = self._request_json(
|
|
845
|
+
"PUT",
|
|
846
|
+
API_ENDPOINT_IOT_VIRTUAL_BIND,
|
|
847
|
+
params=params,
|
|
848
|
+
retry_401=True,
|
|
849
|
+
max_retries=max_retries,
|
|
850
|
+
)
|
|
851
|
+
self._ensure_ok(json_output, "Could not bind virtual device")
|
|
852
|
+
return json_output
|
|
853
|
+
|
|
854
|
+
def dev_config_search(
|
|
855
|
+
self,
|
|
856
|
+
serial: str,
|
|
857
|
+
channel: int,
|
|
858
|
+
*,
|
|
859
|
+
max_retries: int = 0,
|
|
860
|
+
) -> dict:
|
|
861
|
+
"""Trigger a network search on the device."""
|
|
862
|
+
|
|
863
|
+
path = f"{API_ENDPOINT_DEVCONFIG_BASE}/{serial}/{channel}/netWork"
|
|
864
|
+
json_output = self._request_json(
|
|
865
|
+
"POST",
|
|
866
|
+
path,
|
|
867
|
+
retry_401=True,
|
|
868
|
+
max_retries=max_retries,
|
|
869
|
+
)
|
|
870
|
+
self._ensure_ok(json_output, "Could not start network search")
|
|
871
|
+
return json_output
|
|
872
|
+
|
|
873
|
+
def dev_config_send_config_command(
|
|
874
|
+
self,
|
|
875
|
+
serial: str,
|
|
876
|
+
channel: int,
|
|
877
|
+
target_serial: str,
|
|
878
|
+
*,
|
|
879
|
+
max_retries: int = 0,
|
|
880
|
+
) -> dict:
|
|
881
|
+
"""Send a network configuration command to a target device."""
|
|
882
|
+
|
|
883
|
+
path = f"{API_ENDPOINT_DEVCONFIG_BASE}/{serial}/{channel}/netWork/command"
|
|
884
|
+
json_output = self._request_json(
|
|
885
|
+
"POST",
|
|
886
|
+
path,
|
|
887
|
+
params={"targetDeviceSerial": target_serial},
|
|
888
|
+
retry_401=True,
|
|
889
|
+
max_retries=max_retries,
|
|
890
|
+
)
|
|
891
|
+
self._ensure_ok(json_output, "Could not send network command")
|
|
892
|
+
return json_output
|
|
893
|
+
|
|
894
|
+
def dev_config_wifi_list(
|
|
895
|
+
self,
|
|
896
|
+
serial: str,
|
|
897
|
+
channel: int,
|
|
898
|
+
*,
|
|
899
|
+
max_retries: int = 0,
|
|
900
|
+
) -> dict:
|
|
901
|
+
"""Retrieve Wi-Fi network list detected by the device."""
|
|
902
|
+
|
|
903
|
+
path = f"{API_ENDPOINT_DEVCONFIG_BASE}/{serial}/{channel}/netWork"
|
|
904
|
+
json_output = self._request_json(
|
|
905
|
+
"GET",
|
|
906
|
+
path,
|
|
907
|
+
retry_401=True,
|
|
908
|
+
max_retries=max_retries,
|
|
909
|
+
)
|
|
910
|
+
self._ensure_ok(json_output, "Could not get Wi-Fi list")
|
|
911
|
+
return json_output
|
|
912
|
+
|
|
913
|
+
def device_between_error(
|
|
914
|
+
self,
|
|
915
|
+
serial: str,
|
|
916
|
+
channel: int,
|
|
917
|
+
target_serial: str,
|
|
918
|
+
*,
|
|
919
|
+
max_retries: int = 0,
|
|
920
|
+
) -> dict:
|
|
921
|
+
"""Retrieve error details for a network configuration attempt."""
|
|
922
|
+
|
|
923
|
+
path = f"{API_ENDPOINT_DEVCONFIG_BASE}/{serial}/{channel}/netWork/result"
|
|
924
|
+
json_output = self._request_json(
|
|
925
|
+
"GET",
|
|
926
|
+
path,
|
|
927
|
+
params={"targetDeviceSerial": target_serial},
|
|
928
|
+
retry_401=True,
|
|
929
|
+
max_retries=max_retries,
|
|
930
|
+
)
|
|
931
|
+
self._ensure_ok(json_output, "Could not get network error info")
|
|
932
|
+
return json_output
|
|
933
|
+
|
|
934
|
+
def dev_token(self, max_retries: int = 0) -> dict:
|
|
935
|
+
"""Request a device token for provisioning flows."""
|
|
936
|
+
|
|
937
|
+
json_output = self._request_json(
|
|
938
|
+
"GET",
|
|
939
|
+
API_ENDPOINT_USERDEVICES_TOKEN,
|
|
940
|
+
retry_401=True,
|
|
941
|
+
max_retries=max_retries,
|
|
942
|
+
)
|
|
943
|
+
self._ensure_ok(json_output, "Could not get device token")
|
|
944
|
+
return json_output
|
|
945
|
+
|
|
651
946
|
def set_switch_v3(
|
|
652
947
|
self,
|
|
653
948
|
serial: str,
|
|
@@ -747,6 +1042,32 @@ class EzvizClient:
|
|
|
747
1042
|
self._cameras[serial]["switches"][status_type] = target_state
|
|
748
1043
|
return True
|
|
749
1044
|
|
|
1045
|
+
def device_switch(
|
|
1046
|
+
self,
|
|
1047
|
+
serial: str,
|
|
1048
|
+
channel: int,
|
|
1049
|
+
enable: int,
|
|
1050
|
+
switch_type: int,
|
|
1051
|
+
*,
|
|
1052
|
+
max_retries: int = 0,
|
|
1053
|
+
) -> dict:
|
|
1054
|
+
"""Direct wrapper for /v3/devices/{serial}/switch endpoint."""
|
|
1055
|
+
|
|
1056
|
+
params = {
|
|
1057
|
+
"channelNo": channel,
|
|
1058
|
+
"enable": enable,
|
|
1059
|
+
"switchType": switch_type,
|
|
1060
|
+
}
|
|
1061
|
+
json_output = self._request_json(
|
|
1062
|
+
"PUT",
|
|
1063
|
+
f"{API_ENDPOINT_DEVICES}{serial}{API_ENDPOINT_SWITCH_OTHER}",
|
|
1064
|
+
params=params,
|
|
1065
|
+
retry_401=True,
|
|
1066
|
+
max_retries=max_retries,
|
|
1067
|
+
)
|
|
1068
|
+
self._ensure_ok(json_output, "Could not toggle device switch")
|
|
1069
|
+
return json_output
|
|
1070
|
+
|
|
750
1071
|
def switch_status_other(
|
|
751
1072
|
self,
|
|
752
1073
|
serial: str,
|
|
@@ -909,6 +1230,31 @@ class EzvizClient:
|
|
|
909
1230
|
self._ensure_ok(payload, "Could not set devconfig key")
|
|
910
1231
|
return payload
|
|
911
1232
|
|
|
1233
|
+
def set_common_key_value(
|
|
1234
|
+
self,
|
|
1235
|
+
serial: str,
|
|
1236
|
+
channel: int,
|
|
1237
|
+
key: str,
|
|
1238
|
+
value: str,
|
|
1239
|
+
*,
|
|
1240
|
+
max_retries: int = 0,
|
|
1241
|
+
) -> dict:
|
|
1242
|
+
"""Update a devconfig key/value pair using query parameters."""
|
|
1243
|
+
|
|
1244
|
+
params = {
|
|
1245
|
+
"key": key,
|
|
1246
|
+
"value": value if isinstance(value, str) else str(value),
|
|
1247
|
+
}
|
|
1248
|
+
payload = self._request_json(
|
|
1249
|
+
"PUT",
|
|
1250
|
+
f"{API_ENDPOINT_DEVCONFIG_BY_KEY}{serial}/{channel}/op",
|
|
1251
|
+
params=params,
|
|
1252
|
+
retry_401=True,
|
|
1253
|
+
max_retries=max_retries,
|
|
1254
|
+
)
|
|
1255
|
+
self._ensure_ok(payload, "Could not set common key value")
|
|
1256
|
+
return payload
|
|
1257
|
+
|
|
912
1258
|
def set_device_config_by_key(
|
|
913
1259
|
self,
|
|
914
1260
|
serial: str,
|
|
@@ -927,6 +1273,89 @@ class EzvizClient:
|
|
|
927
1273
|
)
|
|
928
1274
|
return True
|
|
929
1275
|
|
|
1276
|
+
def set_device_key_value(
|
|
1277
|
+
self,
|
|
1278
|
+
serial: str,
|
|
1279
|
+
channel: int,
|
|
1280
|
+
key: str,
|
|
1281
|
+
value: str,
|
|
1282
|
+
*,
|
|
1283
|
+
max_retries: int = 0,
|
|
1284
|
+
) -> dict:
|
|
1285
|
+
"""Alias for the query-based key/value setter."""
|
|
1286
|
+
|
|
1287
|
+
return self.set_common_key_value(
|
|
1288
|
+
serial,
|
|
1289
|
+
channel,
|
|
1290
|
+
key,
|
|
1291
|
+
value,
|
|
1292
|
+
max_retries=max_retries,
|
|
1293
|
+
)
|
|
1294
|
+
|
|
1295
|
+
def audition_request(
|
|
1296
|
+
self,
|
|
1297
|
+
serial: str,
|
|
1298
|
+
channel: int,
|
|
1299
|
+
request: str,
|
|
1300
|
+
payload: str,
|
|
1301
|
+
*,
|
|
1302
|
+
max_retries: int = 0,
|
|
1303
|
+
) -> dict:
|
|
1304
|
+
"""Send an audition request via /v3/devconfig/op."""
|
|
1305
|
+
|
|
1306
|
+
data = {
|
|
1307
|
+
"deviceSerial": serial,
|
|
1308
|
+
"channelNo": channel,
|
|
1309
|
+
"request": request,
|
|
1310
|
+
"data": payload,
|
|
1311
|
+
}
|
|
1312
|
+
json_output = self._request_json(
|
|
1313
|
+
"POST",
|
|
1314
|
+
API_ENDPOINT_DEVCONFIG_OP,
|
|
1315
|
+
data=data,
|
|
1316
|
+
retry_401=True,
|
|
1317
|
+
max_retries=max_retries,
|
|
1318
|
+
)
|
|
1319
|
+
self._ensure_ok(json_output, "Could not send audition request")
|
|
1320
|
+
return json_output
|
|
1321
|
+
|
|
1322
|
+
def baby_control(
|
|
1323
|
+
self,
|
|
1324
|
+
serial: str,
|
|
1325
|
+
channel: int,
|
|
1326
|
+
local_index: int,
|
|
1327
|
+
command: str,
|
|
1328
|
+
action: str,
|
|
1329
|
+
speed: int,
|
|
1330
|
+
uuid: str,
|
|
1331
|
+
control: str,
|
|
1332
|
+
hardware_code: str,
|
|
1333
|
+
*,
|
|
1334
|
+
max_retries: int = 0,
|
|
1335
|
+
) -> dict:
|
|
1336
|
+
"""Send the baby monitor motor control request."""
|
|
1337
|
+
|
|
1338
|
+
data = {
|
|
1339
|
+
"deviceSerial": serial,
|
|
1340
|
+
"channelNo": channel,
|
|
1341
|
+
"localIndex": local_index,
|
|
1342
|
+
"command": command,
|
|
1343
|
+
"action": action,
|
|
1344
|
+
"speed": speed,
|
|
1345
|
+
"uuid": uuid,
|
|
1346
|
+
"control": control,
|
|
1347
|
+
"hardwareCode": hardware_code,
|
|
1348
|
+
}
|
|
1349
|
+
json_output = self._request_json(
|
|
1350
|
+
"POST",
|
|
1351
|
+
API_ENDPOINT_DEVCONFIG_MOTOR,
|
|
1352
|
+
data=data,
|
|
1353
|
+
retry_401=True,
|
|
1354
|
+
max_retries=max_retries,
|
|
1355
|
+
)
|
|
1356
|
+
self._ensure_ok(json_output, "Could not control baby motor")
|
|
1357
|
+
return json_output
|
|
1358
|
+
|
|
930
1359
|
def set_device_feature_by_key(
|
|
931
1360
|
self,
|
|
932
1361
|
serial: str,
|
|
@@ -947,8 +1376,10 @@ class EzvizClient:
|
|
|
947
1376
|
|
|
948
1377
|
full_url = f"https://{self._token['api_url']}{API_ENDPOINT_IOT_FEATURE}{serial.upper()}/0"
|
|
949
1378
|
|
|
950
|
-
headers =
|
|
951
|
-
|
|
1379
|
+
headers = {
|
|
1380
|
+
**self._session.headers,
|
|
1381
|
+
"Content-Type": "application/json",
|
|
1382
|
+
}
|
|
952
1383
|
|
|
953
1384
|
req_prep = requests.Request(
|
|
954
1385
|
method="PUT", url=full_url, headers=headers, data=payload
|
|
@@ -963,15 +1394,385 @@ class EzvizClient:
|
|
|
963
1394
|
|
|
964
1395
|
return True
|
|
965
1396
|
|
|
966
|
-
def
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1397
|
+
def _iot_request(
|
|
1398
|
+
self,
|
|
1399
|
+
method: str,
|
|
1400
|
+
endpoint: str,
|
|
1401
|
+
serial: str,
|
|
1402
|
+
resource_identifier: str,
|
|
1403
|
+
local_index: str,
|
|
1404
|
+
domain_id: str,
|
|
1405
|
+
action_id: str,
|
|
1406
|
+
*,
|
|
1407
|
+
payload: Any = None,
|
|
1408
|
+
max_retries: int = 0,
|
|
1409
|
+
error_message: str,
|
|
1410
|
+
) -> dict:
|
|
1411
|
+
"""Helper to perform IoT feature/action requests with JSON payload support."""
|
|
1412
|
+
|
|
1413
|
+
path = (
|
|
1414
|
+
f"{endpoint}{serial.upper()}/{resource_identifier}/"
|
|
1415
|
+
f"{local_index}/{domain_id}/{action_id}"
|
|
1416
|
+
)
|
|
1417
|
+
|
|
1418
|
+
headers = dict(self._session.headers)
|
|
1419
|
+
data: str | bytes | bytearray | None = None
|
|
1420
|
+
if payload is not None:
|
|
1421
|
+
headers["Content-Type"] = "application/json"
|
|
1422
|
+
if isinstance(payload, (bytes, bytearray, str)):
|
|
1423
|
+
data = payload
|
|
1424
|
+
else:
|
|
1425
|
+
data = json.dumps(payload, separators=(",", ":"))
|
|
1426
|
+
|
|
1427
|
+
req = requests.Request(
|
|
1428
|
+
method=method,
|
|
1429
|
+
url=self._url(path),
|
|
1430
|
+
headers=headers,
|
|
1431
|
+
data=data,
|
|
1432
|
+
).prepare()
|
|
1433
|
+
|
|
1434
|
+
resp = self._send_prepared(
|
|
1435
|
+
req,
|
|
1436
|
+
retry_401=True,
|
|
1437
|
+
max_retries=max_retries,
|
|
1438
|
+
)
|
|
1439
|
+
json_output = self._parse_json(resp)
|
|
1440
|
+
if not self._meta_ok(json_output):
|
|
1441
|
+
raise PyEzvizError(f"{error_message}: Got {json_output})")
|
|
1442
|
+
return json_output
|
|
1443
|
+
|
|
1444
|
+
def get_low_battery_keep_alive(
|
|
1445
|
+
self,
|
|
1446
|
+
serial: str,
|
|
1447
|
+
resource_identifier: str,
|
|
1448
|
+
local_index: str,
|
|
1449
|
+
domain_id: str,
|
|
1450
|
+
action_id: str,
|
|
1451
|
+
*,
|
|
1452
|
+
max_retries: int = 0,
|
|
1453
|
+
) -> dict:
|
|
1454
|
+
"""Fetch low-battery keep-alive status exposed under the IoT feature API."""
|
|
1455
|
+
|
|
1456
|
+
return self._iot_request(
|
|
1457
|
+
"GET",
|
|
1458
|
+
API_ENDPOINT_IOT_FEATURE,
|
|
1459
|
+
serial,
|
|
1460
|
+
resource_identifier,
|
|
1461
|
+
local_index,
|
|
1462
|
+
domain_id,
|
|
1463
|
+
action_id,
|
|
1464
|
+
max_retries=max_retries,
|
|
1465
|
+
error_message="Could not fetch low battery keep-alive status",
|
|
1466
|
+
)
|
|
1467
|
+
|
|
1468
|
+
def get_object_removal_status(
|
|
1469
|
+
self,
|
|
1470
|
+
serial: str,
|
|
1471
|
+
resource_identifier: str,
|
|
1472
|
+
local_index: str,
|
|
1473
|
+
domain_id: str,
|
|
1474
|
+
action_id: str,
|
|
1475
|
+
*,
|
|
1476
|
+
payload: Any | None = None,
|
|
1477
|
+
max_retries: int = 0,
|
|
1478
|
+
) -> dict:
|
|
1479
|
+
"""Fetch object-removal (left-behind) status for supported devices."""
|
|
1480
|
+
|
|
1481
|
+
return self._iot_request(
|
|
1482
|
+
"GET",
|
|
1483
|
+
API_ENDPOINT_IOT_FEATURE,
|
|
1484
|
+
serial,
|
|
1485
|
+
resource_identifier,
|
|
1486
|
+
local_index,
|
|
1487
|
+
domain_id,
|
|
1488
|
+
action_id,
|
|
1489
|
+
payload=payload,
|
|
1490
|
+
max_retries=max_retries,
|
|
1491
|
+
error_message="Could not fetch object removal status",
|
|
1492
|
+
)
|
|
1493
|
+
|
|
1494
|
+
def get_remote_control_path_list(
|
|
1495
|
+
self,
|
|
1496
|
+
serial: str,
|
|
1497
|
+
resource_identifier: str,
|
|
1498
|
+
local_index: str,
|
|
1499
|
+
domain_id: str,
|
|
1500
|
+
action_id: str,
|
|
1501
|
+
*,
|
|
1502
|
+
max_retries: int = 0,
|
|
1503
|
+
) -> dict:
|
|
1504
|
+
"""Return the remote control patrol path list for auto-tracking models."""
|
|
1505
|
+
|
|
1506
|
+
return self._iot_request(
|
|
1507
|
+
"GET",
|
|
1508
|
+
API_ENDPOINT_IOT_FEATURE,
|
|
1509
|
+
serial,
|
|
1510
|
+
resource_identifier,
|
|
1511
|
+
local_index,
|
|
1512
|
+
domain_id,
|
|
1513
|
+
action_id,
|
|
1514
|
+
max_retries=max_retries,
|
|
1515
|
+
error_message="Could not fetch remote control path list",
|
|
1516
|
+
)
|
|
1517
|
+
|
|
1518
|
+
def get_tracking_status(
|
|
1519
|
+
self,
|
|
1520
|
+
serial: str,
|
|
1521
|
+
resource_identifier: str,
|
|
1522
|
+
local_index: str,
|
|
1523
|
+
domain_id: str,
|
|
1524
|
+
action_id: str,
|
|
1525
|
+
*,
|
|
1526
|
+
max_retries: int = 0,
|
|
1527
|
+
) -> dict:
|
|
1528
|
+
"""Obtain the current subject-tracking status from the IoT feature API."""
|
|
1529
|
+
|
|
1530
|
+
return self._iot_request(
|
|
1531
|
+
"GET",
|
|
1532
|
+
API_ENDPOINT_IOT_FEATURE,
|
|
1533
|
+
serial,
|
|
1534
|
+
resource_identifier,
|
|
1535
|
+
local_index,
|
|
1536
|
+
domain_id,
|
|
1537
|
+
action_id,
|
|
1538
|
+
max_retries=max_retries,
|
|
1539
|
+
error_message="Could not fetch tracking status",
|
|
1540
|
+
)
|
|
1541
|
+
|
|
1542
|
+
def get_port_security(
|
|
1543
|
+
self,
|
|
1544
|
+
serial: str,
|
|
1545
|
+
*,
|
|
1546
|
+
resource_identifier: str = "Video",
|
|
1547
|
+
local_index: str = "1",
|
|
1548
|
+
domain_id: str = "NetworkSecurityProtection",
|
|
1549
|
+
action_id: str = "PortSecurity",
|
|
1550
|
+
max_retries: int = 0,
|
|
1551
|
+
) -> dict:
|
|
1552
|
+
"""Fetch port security configuration via the IoT feature API."""
|
|
1553
|
+
|
|
1554
|
+
return self._iot_request(
|
|
1555
|
+
"GET",
|
|
1556
|
+
API_ENDPOINT_IOT_FEATURE,
|
|
1557
|
+
serial,
|
|
1558
|
+
resource_identifier,
|
|
1559
|
+
local_index,
|
|
1560
|
+
domain_id,
|
|
1561
|
+
action_id,
|
|
1562
|
+
max_retries=max_retries,
|
|
1563
|
+
error_message="Could not fetch port security status",
|
|
1564
|
+
)
|
|
1565
|
+
|
|
1566
|
+
def set_port_security(
|
|
1567
|
+
self,
|
|
1568
|
+
serial: str,
|
|
1569
|
+
value: Mapping[str, Any] | dict[str, Any],
|
|
1570
|
+
*,
|
|
1571
|
+
resource_identifier: str = "Video",
|
|
1572
|
+
local_index: str = "1",
|
|
1573
|
+
domain_id: str = "NetworkSecurityProtection",
|
|
1574
|
+
action_id: str = "PortSecurity",
|
|
1575
|
+
max_retries: int = 0,
|
|
1576
|
+
) -> dict:
|
|
1577
|
+
"""Update port security configuration via the IoT feature API."""
|
|
1578
|
+
|
|
1579
|
+
payload = {"value": value}
|
|
1580
|
+
return self._iot_request(
|
|
1581
|
+
"PUT",
|
|
1582
|
+
API_ENDPOINT_IOT_FEATURE,
|
|
1583
|
+
serial,
|
|
1584
|
+
resource_identifier,
|
|
1585
|
+
local_index,
|
|
1586
|
+
domain_id,
|
|
1587
|
+
action_id,
|
|
1588
|
+
payload=payload,
|
|
1589
|
+
max_retries=max_retries,
|
|
1590
|
+
error_message="Could not set port security status",
|
|
1591
|
+
)
|
|
1592
|
+
|
|
1593
|
+
def get_device_feature_value(
|
|
1594
|
+
self,
|
|
1595
|
+
serial: str,
|
|
1596
|
+
resource_identifier: str,
|
|
1597
|
+
domain_identifier: str,
|
|
1598
|
+
prop_identifier: str,
|
|
1599
|
+
*,
|
|
1600
|
+
local_index: str | int = "1",
|
|
1601
|
+
max_retries: int = 0,
|
|
1602
|
+
) -> dict:
|
|
1603
|
+
"""Retrieve a device feature value via the IoT feature API."""
|
|
1604
|
+
|
|
1605
|
+
local_idx = str(local_index)
|
|
1606
|
+
return self._iot_request(
|
|
1607
|
+
"GET",
|
|
1608
|
+
API_ENDPOINT_IOT_FEATURE,
|
|
1609
|
+
serial,
|
|
1610
|
+
resource_identifier,
|
|
1611
|
+
local_idx,
|
|
1612
|
+
domain_identifier,
|
|
1613
|
+
prop_identifier,
|
|
1614
|
+
max_retries=max_retries,
|
|
1615
|
+
error_message="Could not fetch device feature value",
|
|
1616
|
+
)
|
|
1617
|
+
|
|
1618
|
+
def set_image_flip_iot(
|
|
1619
|
+
self,
|
|
1620
|
+
serial: str,
|
|
1621
|
+
*,
|
|
1622
|
+
enabled: bool | None = None,
|
|
1623
|
+
payload: Any | None = None,
|
|
1624
|
+
local_index: str = "1",
|
|
1625
|
+
max_retries: int = 0,
|
|
1626
|
+
) -> dict:
|
|
1627
|
+
"""Set image flip configuration using the IoT feature endpoint."""
|
|
1628
|
+
|
|
1629
|
+
if payload is None:
|
|
1630
|
+
if enabled is None:
|
|
1631
|
+
raise PyEzvizError("Either 'enabled' or 'payload' must be provided")
|
|
1632
|
+
payload = {"value": {"enabled": bool(enabled)}}
|
|
1633
|
+
body = self._normalize_json_payload(payload)
|
|
1634
|
+
return self._iot_request(
|
|
1635
|
+
"PUT",
|
|
1636
|
+
API_ENDPOINT_IOT_FEATURE,
|
|
1637
|
+
serial,
|
|
1638
|
+
"Video",
|
|
1639
|
+
local_index,
|
|
1640
|
+
"VideoAdjustment",
|
|
1641
|
+
"ImageFlip",
|
|
1642
|
+
payload=body,
|
|
1643
|
+
max_retries=max_retries,
|
|
1644
|
+
error_message="Could not set image flip",
|
|
1645
|
+
)
|
|
1646
|
+
|
|
1647
|
+
def set_iot_action(
|
|
1648
|
+
self,
|
|
1649
|
+
serial: str,
|
|
1650
|
+
resource_identifier: str,
|
|
1651
|
+
local_index: str,
|
|
1652
|
+
domain_id: str,
|
|
1653
|
+
action_id: str,
|
|
1654
|
+
value: Any,
|
|
1655
|
+
*,
|
|
1656
|
+
max_retries: int = 0,
|
|
1657
|
+
) -> dict:
|
|
1658
|
+
"""Trigger an IoT action (setAction/putAction in the mobile API)."""
|
|
1659
|
+
|
|
1660
|
+
return self._iot_request(
|
|
1661
|
+
"PUT",
|
|
1662
|
+
API_ENDPOINT_IOT_ACTION,
|
|
1663
|
+
serial,
|
|
1664
|
+
resource_identifier,
|
|
1665
|
+
local_index,
|
|
1666
|
+
domain_id,
|
|
1667
|
+
action_id,
|
|
1668
|
+
payload=value,
|
|
1669
|
+
max_retries=max_retries,
|
|
1670
|
+
error_message="Could not execute IoT action",
|
|
1671
|
+
)
|
|
1672
|
+
|
|
1673
|
+
def set_iot_feature(
|
|
1674
|
+
self,
|
|
1675
|
+
serial: str,
|
|
1676
|
+
resource_identifier: str,
|
|
1677
|
+
local_index: str,
|
|
1678
|
+
domain_id: str,
|
|
1679
|
+
action_id: str,
|
|
1680
|
+
value: Any,
|
|
1681
|
+
*,
|
|
1682
|
+
max_retries: int = 0,
|
|
1683
|
+
) -> dict:
|
|
1684
|
+
"""Update an IoT feature value via the feature endpoint."""
|
|
1685
|
+
|
|
1686
|
+
return self._iot_request(
|
|
1687
|
+
"PUT",
|
|
1688
|
+
API_ENDPOINT_IOT_FEATURE,
|
|
1689
|
+
serial,
|
|
1690
|
+
resource_identifier,
|
|
1691
|
+
local_index,
|
|
1692
|
+
domain_id,
|
|
1693
|
+
action_id,
|
|
1694
|
+
payload=value,
|
|
1695
|
+
max_retries=max_retries,
|
|
1696
|
+
error_message="Could not set IoT feature value",
|
|
1697
|
+
)
|
|
1698
|
+
|
|
1699
|
+
def set_lens_defog_mode(
|
|
1700
|
+
self,
|
|
1701
|
+
serial: str,
|
|
1702
|
+
value: int,
|
|
1703
|
+
*,
|
|
1704
|
+
local_index: str = "1",
|
|
1705
|
+
max_retries: int = 0,
|
|
1706
|
+
) -> tuple[bool, str]:
|
|
1707
|
+
"""Update the lens defog configuration using canonical option index.
|
|
1708
|
+
|
|
1709
|
+
Args:
|
|
1710
|
+
serial: Device serial number.
|
|
1711
|
+
value: Select option index (0=auto, 1=on, 2=off).
|
|
1712
|
+
local_index: Channel index for multi-channel devices.
|
|
1713
|
+
max_retries: Number of retries for transient failures.
|
|
1714
|
+
|
|
1715
|
+
Returns:
|
|
1716
|
+
A tuple of (enabled flag, defog mode string) reflecting the
|
|
1717
|
+
configuration that was sent to the device.
|
|
1718
|
+
"""
|
|
1719
|
+
|
|
1720
|
+
if value == 1:
|
|
1721
|
+
enabled, mode = True, "open"
|
|
1722
|
+
elif value == 2:
|
|
1723
|
+
enabled, mode = False, "auto"
|
|
1724
|
+
else:
|
|
1725
|
+
enabled, mode = True, "auto"
|
|
1726
|
+
|
|
1727
|
+
payload = {"value": {"enabled": enabled, "defogMode": mode}}
|
|
1728
|
+
self.set_iot_feature(
|
|
1729
|
+
serial,
|
|
1730
|
+
resource_identifier="Video",
|
|
1731
|
+
local_index=local_index,
|
|
1732
|
+
domain_id="LensCleaning",
|
|
1733
|
+
action_id="DefogCfg",
|
|
1734
|
+
value=payload,
|
|
1735
|
+
max_retries=max_retries,
|
|
1736
|
+
)
|
|
1737
|
+
|
|
1738
|
+
return enabled, mode
|
|
1739
|
+
|
|
1740
|
+
def update_device_name(
|
|
1741
|
+
self,
|
|
1742
|
+
serial: str,
|
|
1743
|
+
name: str,
|
|
1744
|
+
*,
|
|
1745
|
+
max_retries: int = 0,
|
|
1746
|
+
) -> dict:
|
|
1747
|
+
"""Rename a device via the legacy updateName endpoint."""
|
|
1748
|
+
|
|
1749
|
+
if not name:
|
|
1750
|
+
raise PyEzvizError("Device name must not be empty")
|
|
1751
|
+
|
|
1752
|
+
data = {
|
|
1753
|
+
"deviceSerialNo": serial,
|
|
1754
|
+
"deviceName": name,
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
json_output = self._request_json(
|
|
1758
|
+
"POST",
|
|
1759
|
+
API_ENDPOINT_DEVICE_UPDATE_NAME,
|
|
1760
|
+
data=data,
|
|
1761
|
+
retry_401=True,
|
|
1762
|
+
max_retries=max_retries,
|
|
1763
|
+
)
|
|
1764
|
+
self._ensure_ok(json_output, "Could not update device name")
|
|
1765
|
+
return json_output
|
|
1766
|
+
|
|
1767
|
+
def upgrade_device(self, serial: str, max_retries: int = 0) -> bool:
|
|
1768
|
+
"""Upgrade device firmware."""
|
|
1769
|
+
json_output = self._request_json(
|
|
1770
|
+
"PUT",
|
|
1771
|
+
f"{API_ENDPOINT_UPGRADE_DEVICE}{serial}/0/upgrade",
|
|
1772
|
+
retry_401=True,
|
|
1773
|
+
max_retries=max_retries,
|
|
1774
|
+
)
|
|
1775
|
+
self._ensure_ok(json_output, "Could not initiate firmware upgrade")
|
|
975
1776
|
return True
|
|
976
1777
|
|
|
977
1778
|
def get_storage_status(self, serial: str, max_retries: int = 0) -> Any:
|
|
@@ -1056,6 +1857,32 @@ class EzvizClient:
|
|
|
1056
1857
|
|
|
1057
1858
|
return True
|
|
1058
1859
|
|
|
1860
|
+
def device_authenticate(
|
|
1861
|
+
self,
|
|
1862
|
+
serial: str,
|
|
1863
|
+
*,
|
|
1864
|
+
need_check_code: bool,
|
|
1865
|
+
check_code: str | None,
|
|
1866
|
+
sender_type: int,
|
|
1867
|
+
max_retries: int = 0,
|
|
1868
|
+
) -> dict:
|
|
1869
|
+
"""Authenticate a device, optionally requiring check code."""
|
|
1870
|
+
|
|
1871
|
+
data = {
|
|
1872
|
+
"needCheckCode": str(bool(need_check_code)).lower(),
|
|
1873
|
+
"checkCode": check_code or "",
|
|
1874
|
+
"senderType": sender_type,
|
|
1875
|
+
}
|
|
1876
|
+
json_output = self._request_json(
|
|
1877
|
+
"PUT",
|
|
1878
|
+
f"{API_ENDPOINT_DEVICES_AUTHENTICATE}{serial}",
|
|
1879
|
+
data=data,
|
|
1880
|
+
retry_401=True,
|
|
1881
|
+
max_retries=max_retries,
|
|
1882
|
+
)
|
|
1883
|
+
self._ensure_ok(json_output, "Could not authenticate device")
|
|
1884
|
+
return json_output
|
|
1885
|
+
|
|
1059
1886
|
def reboot_camera(
|
|
1060
1887
|
self,
|
|
1061
1888
|
serial: str,
|
|
@@ -1118,6 +1945,57 @@ class EzvizClient:
|
|
|
1118
1945
|
raise PyEzvizError(f"Could not set offline notification {json_output})")
|
|
1119
1946
|
raise PyEzvizError("Could not set offline notification: exceeded retries")
|
|
1120
1947
|
|
|
1948
|
+
def device_email_alert_state(
|
|
1949
|
+
self,
|
|
1950
|
+
serials: list[str] | str,
|
|
1951
|
+
*,
|
|
1952
|
+
max_retries: int = 0,
|
|
1953
|
+
) -> dict:
|
|
1954
|
+
"""Get email alert state for one or more devices."""
|
|
1955
|
+
|
|
1956
|
+
if isinstance(serials, (list, tuple, set)):
|
|
1957
|
+
serial_param = ",".join(sorted({str(s) for s in serials}))
|
|
1958
|
+
else:
|
|
1959
|
+
serial_param = str(serials)
|
|
1960
|
+
|
|
1961
|
+
json_output = self._request_json(
|
|
1962
|
+
"GET",
|
|
1963
|
+
API_ENDPOINT_DEVICE_EMAIL_ALERT,
|
|
1964
|
+
params={"devices": serial_param},
|
|
1965
|
+
retry_401=True,
|
|
1966
|
+
max_retries=max_retries,
|
|
1967
|
+
)
|
|
1968
|
+
self._ensure_ok(json_output, "Could not get device email alert state")
|
|
1969
|
+
return json_output
|
|
1970
|
+
|
|
1971
|
+
def save_device_email_alert_state(
|
|
1972
|
+
self,
|
|
1973
|
+
enable: bool,
|
|
1974
|
+
serials: list[str] | str,
|
|
1975
|
+
*,
|
|
1976
|
+
max_retries: int = 0,
|
|
1977
|
+
) -> dict:
|
|
1978
|
+
"""Update email alert state for the provided devices."""
|
|
1979
|
+
|
|
1980
|
+
if isinstance(serials, (list, tuple, set)):
|
|
1981
|
+
serial_param = ",".join(sorted({str(s) for s in serials}))
|
|
1982
|
+
else:
|
|
1983
|
+
serial_param = str(serials)
|
|
1984
|
+
|
|
1985
|
+
data = {
|
|
1986
|
+
"enable": str(bool(enable)).lower(),
|
|
1987
|
+
"devices": serial_param,
|
|
1988
|
+
}
|
|
1989
|
+
json_output = self._request_json(
|
|
1990
|
+
"POST",
|
|
1991
|
+
API_ENDPOINT_DEVICE_EMAIL_ALERT,
|
|
1992
|
+
data=data,
|
|
1993
|
+
retry_401=True,
|
|
1994
|
+
max_retries=max_retries,
|
|
1995
|
+
)
|
|
1996
|
+
self._ensure_ok(json_output, "Could not save device email alert state")
|
|
1997
|
+
return json_output
|
|
1998
|
+
|
|
1121
1999
|
def get_group_defence_mode(self, max_retries: int = 0) -> Any:
|
|
1122
2000
|
"""Get group arm status. The alarm arm/disarm concept on 1st page of app."""
|
|
1123
2001
|
if max_retries > MAX_RETRIES:
|
|
@@ -1298,6 +2176,48 @@ class EzvizClient:
|
|
|
1298
2176
|
return records
|
|
1299
2177
|
return records.get(serial) or devices.get(serial, {})
|
|
1300
2178
|
|
|
2179
|
+
def get_accessory(
|
|
2180
|
+
self,
|
|
2181
|
+
serial: str,
|
|
2182
|
+
local_index: str,
|
|
2183
|
+
*,
|
|
2184
|
+
max_retries: int = 0,
|
|
2185
|
+
) -> dict:
|
|
2186
|
+
"""Retrieve accessory information linked to a device."""
|
|
2187
|
+
|
|
2188
|
+
path = (
|
|
2189
|
+
f"{API_ENDPOINT_DEVICE_ACCESSORY_LINK}{serial}/{local_index}/1/linked/info"
|
|
2190
|
+
)
|
|
2191
|
+
json_output = self._request_json(
|
|
2192
|
+
"GET",
|
|
2193
|
+
path,
|
|
2194
|
+
retry_401=True,
|
|
2195
|
+
max_retries=max_retries,
|
|
2196
|
+
)
|
|
2197
|
+
self._ensure_ok(json_output, "Could not get accessory info")
|
|
2198
|
+
return json_output
|
|
2199
|
+
|
|
2200
|
+
def get_dev_config(
|
|
2201
|
+
self,
|
|
2202
|
+
serial: str,
|
|
2203
|
+
channel: int,
|
|
2204
|
+
key: str,
|
|
2205
|
+
*,
|
|
2206
|
+
max_retries: int = 0,
|
|
2207
|
+
) -> dict:
|
|
2208
|
+
"""Retrieve a devconfig value by key."""
|
|
2209
|
+
|
|
2210
|
+
params = {"key": key}
|
|
2211
|
+
json_output = self._request_json(
|
|
2212
|
+
"GET",
|
|
2213
|
+
f"{API_ENDPOINT_DEVCONFIG_BY_KEY}{serial}/{channel}/op",
|
|
2214
|
+
params=params,
|
|
2215
|
+
retry_401=True,
|
|
2216
|
+
max_retries=max_retries,
|
|
2217
|
+
)
|
|
2218
|
+
self._ensure_ok(json_output, "Could not get devconfig value")
|
|
2219
|
+
return json_output
|
|
2220
|
+
|
|
1301
2221
|
def ptz_control(
|
|
1302
2222
|
self, command: str, serial: str, action: str, speed: int = 5
|
|
1303
2223
|
) -> Any:
|
|
@@ -1330,6 +2250,25 @@ class EzvizClient:
|
|
|
1330
2250
|
|
|
1331
2251
|
return True
|
|
1332
2252
|
|
|
2253
|
+
def capture_picture(
|
|
2254
|
+
self,
|
|
2255
|
+
serial: str,
|
|
2256
|
+
channel: int,
|
|
2257
|
+
*,
|
|
2258
|
+
max_retries: int = 0,
|
|
2259
|
+
) -> dict:
|
|
2260
|
+
"""Trigger a snapshot capture on the device."""
|
|
2261
|
+
|
|
2262
|
+
path = f"/v3/devconfig/v1/{serial}/{channel}/capture"
|
|
2263
|
+
json_output = self._request_json(
|
|
2264
|
+
"PUT",
|
|
2265
|
+
path,
|
|
2266
|
+
retry_401=True,
|
|
2267
|
+
max_retries=max_retries,
|
|
2268
|
+
)
|
|
2269
|
+
self._ensure_ok(json_output, "Could not capture picture")
|
|
2270
|
+
return json_output
|
|
2271
|
+
|
|
1333
2272
|
def get_cam_key(
|
|
1334
2273
|
self, serial: str, smscode: int | None = None, max_retries: int = 0
|
|
1335
2274
|
) -> Any:
|
|
@@ -1590,6 +2529,23 @@ class EzvizClient:
|
|
|
1590
2529
|
|
|
1591
2530
|
return True
|
|
1592
2531
|
|
|
2532
|
+
def get_door_lock_users(
|
|
2533
|
+
self,
|
|
2534
|
+
serial: str,
|
|
2535
|
+
*,
|
|
2536
|
+
max_retries: int = 0,
|
|
2537
|
+
) -> dict:
|
|
2538
|
+
"""Retrieve users associated with a door lock device."""
|
|
2539
|
+
|
|
2540
|
+
json_output = self._request_json(
|
|
2541
|
+
"GET",
|
|
2542
|
+
f"{API_ENDPOINT_DOORLOCK_USERS}{serial}/users",
|
|
2543
|
+
retry_401=True,
|
|
2544
|
+
max_retries=max_retries,
|
|
2545
|
+
)
|
|
2546
|
+
self._ensure_ok(json_output, "Could not get door lock users")
|
|
2547
|
+
return json_output
|
|
2548
|
+
|
|
1593
2549
|
def remote_unlock(self, serial: str, user_id: str, lock_no: int) -> bool:
|
|
1594
2550
|
"""Sends a remote command to unlock a specific lock.
|
|
1595
2551
|
|
|
@@ -1629,6 +2585,23 @@ class EzvizClient:
|
|
|
1629
2585
|
)
|
|
1630
2586
|
return True
|
|
1631
2587
|
|
|
2588
|
+
def get_remote_unbind_progress(
|
|
2589
|
+
self,
|
|
2590
|
+
serial: str,
|
|
2591
|
+
*,
|
|
2592
|
+
max_retries: int = 0,
|
|
2593
|
+
) -> dict:
|
|
2594
|
+
"""Check progress of a remote unbind request."""
|
|
2595
|
+
|
|
2596
|
+
json_output = self._request_json(
|
|
2597
|
+
"GET",
|
|
2598
|
+
f"{API_ENDPOINT_REMOTE_UNBIND_PROGRESS}{serial}/progress",
|
|
2599
|
+
retry_401=True,
|
|
2600
|
+
max_retries=max_retries,
|
|
2601
|
+
)
|
|
2602
|
+
self._ensure_ok(json_output, "Could not get unbind progress")
|
|
2603
|
+
return json_output
|
|
2604
|
+
|
|
1632
2605
|
def login(self, sms_code: int | None = None) -> dict[Any, Any]:
|
|
1633
2606
|
"""Get or refresh ezviz login token."""
|
|
1634
2607
|
if self._token["session_id"] and self._token["rf_session_id"]:
|
|
@@ -1767,18 +2740,64 @@ class EzvizClient:
|
|
|
1767
2740
|
raise PyEzvizError(f"Could not set the schedule: Got {json_output})")
|
|
1768
2741
|
return True
|
|
1769
2742
|
|
|
1770
|
-
def api_set_defence_mode(
|
|
2743
|
+
def api_set_defence_mode(
|
|
2744
|
+
self,
|
|
2745
|
+
mode: DefenseModeType | int,
|
|
2746
|
+
*,
|
|
2747
|
+
visual_alarm: int | None = None,
|
|
2748
|
+
sound_mode: int | None = None,
|
|
2749
|
+
max_retries: int = 0,
|
|
2750
|
+
) -> bool:
|
|
1771
2751
|
"""Set defence mode for all devices. The alarm panel from main page is used."""
|
|
2752
|
+
data: dict[str, Any] = {
|
|
2753
|
+
"groupId": -1,
|
|
2754
|
+
"mode": int(mode.value if isinstance(mode, DefenseModeType) else mode),
|
|
2755
|
+
}
|
|
2756
|
+
if visual_alarm is not None:
|
|
2757
|
+
data["visualAlarm"] = visual_alarm
|
|
2758
|
+
if sound_mode is not None:
|
|
2759
|
+
data["soundMode"] = sound_mode
|
|
2760
|
+
|
|
1772
2761
|
json_output = self._request_json(
|
|
1773
2762
|
"POST",
|
|
1774
2763
|
API_ENDPOINT_SWITCH_DEFENCE_MODE,
|
|
1775
|
-
data=
|
|
2764
|
+
data=data,
|
|
1776
2765
|
retry_401=True,
|
|
1777
2766
|
max_retries=max_retries,
|
|
1778
2767
|
)
|
|
1779
2768
|
self._ensure_ok(json_output, "Could not set defence mode")
|
|
1780
2769
|
return True
|
|
1781
2770
|
|
|
2771
|
+
def switch_defence_mode(
|
|
2772
|
+
self,
|
|
2773
|
+
group_id: int,
|
|
2774
|
+
mode: int,
|
|
2775
|
+
*,
|
|
2776
|
+
visual_alarm: int | None = None,
|
|
2777
|
+
sound_mode: int | None = None,
|
|
2778
|
+
max_retries: int = 0,
|
|
2779
|
+
) -> dict:
|
|
2780
|
+
"""Set defence mode for a specific group with optional sound/visual flags."""
|
|
2781
|
+
|
|
2782
|
+
data: dict[str, Any] = {
|
|
2783
|
+
"groupId": group_id,
|
|
2784
|
+
"mode": mode,
|
|
2785
|
+
}
|
|
2786
|
+
if visual_alarm is not None:
|
|
2787
|
+
data["visualAlarm"] = visual_alarm
|
|
2788
|
+
if sound_mode is not None:
|
|
2789
|
+
data["soundMode"] = sound_mode
|
|
2790
|
+
|
|
2791
|
+
json_output = self._request_json(
|
|
2792
|
+
"POST",
|
|
2793
|
+
API_ENDPOINT_SWITCH_DEFENCE_MODE,
|
|
2794
|
+
data=data,
|
|
2795
|
+
retry_401=True,
|
|
2796
|
+
max_retries=max_retries,
|
|
2797
|
+
)
|
|
2798
|
+
self._ensure_ok(json_output, "Could not switch defence mode")
|
|
2799
|
+
return json_output
|
|
2800
|
+
|
|
1782
2801
|
def do_not_disturb(
|
|
1783
2802
|
self,
|
|
1784
2803
|
serial: str,
|
|
@@ -1908,6 +2927,26 @@ class EzvizClient:
|
|
|
1908
2927
|
max_retries=max_retries,
|
|
1909
2928
|
)
|
|
1910
2929
|
|
|
2930
|
+
def device_mirror(
|
|
2931
|
+
self,
|
|
2932
|
+
serial: str,
|
|
2933
|
+
channel: int,
|
|
2934
|
+
command: str,
|
|
2935
|
+
*,
|
|
2936
|
+
max_retries: int = 0,
|
|
2937
|
+
) -> dict:
|
|
2938
|
+
"""Send a mirror command using the basics API."""
|
|
2939
|
+
|
|
2940
|
+
path = f"{API_ENDPOINT_DEVICE_BASICS}{serial}/{channel}/{command}/mirror"
|
|
2941
|
+
json_output = self._request_json(
|
|
2942
|
+
"PUT",
|
|
2943
|
+
path,
|
|
2944
|
+
retry_401=True,
|
|
2945
|
+
max_retries=max_retries,
|
|
2946
|
+
)
|
|
2947
|
+
self._ensure_ok(json_output, "Could not set mirror state")
|
|
2948
|
+
return json_output
|
|
2949
|
+
|
|
1911
2950
|
def flip_image(
|
|
1912
2951
|
self,
|
|
1913
2952
|
serial: str,
|
|
@@ -1939,33 +2978,109 @@ class EzvizClient:
|
|
|
1939
2978
|
|
|
1940
2979
|
return True
|
|
1941
2980
|
|
|
2981
|
+
def _resolve_osd_text(
|
|
2982
|
+
self,
|
|
2983
|
+
serial: str,
|
|
2984
|
+
*,
|
|
2985
|
+
name: str | None = None,
|
|
2986
|
+
camera_data: Mapping[str, Any] | None = None,
|
|
2987
|
+
) -> str:
|
|
2988
|
+
"""Return the preferred OSD label for a camera."""
|
|
2989
|
+
|
|
2990
|
+
if isinstance(name, str) and name.strip():
|
|
2991
|
+
return name.strip()
|
|
2992
|
+
|
|
2993
|
+
candidates: list[Mapping[str, Any]] = []
|
|
2994
|
+
|
|
2995
|
+
if isinstance(camera_data, Mapping):
|
|
2996
|
+
candidates.append(camera_data)
|
|
2997
|
+
|
|
2998
|
+
cached = self._cameras.get(serial)
|
|
2999
|
+
if isinstance(cached, Mapping):
|
|
3000
|
+
candidates.append(cached)
|
|
3001
|
+
|
|
3002
|
+
for data in candidates:
|
|
3003
|
+
direct = data.get("name")
|
|
3004
|
+
if isinstance(direct, str) and direct.strip():
|
|
3005
|
+
return direct.strip()
|
|
3006
|
+
|
|
3007
|
+
device_info = data.get("deviceInfos")
|
|
3008
|
+
if isinstance(device_info, Mapping):
|
|
3009
|
+
alt = device_info.get("name")
|
|
3010
|
+
if isinstance(alt, str) and alt.strip():
|
|
3011
|
+
return alt.strip()
|
|
3012
|
+
|
|
3013
|
+
optionals = optionals_mapping(data)
|
|
3014
|
+
osd_entries = optionals.get("OSD")
|
|
3015
|
+
if isinstance(osd_entries, Mapping):
|
|
3016
|
+
osd_entries = [osd_entries]
|
|
3017
|
+
if isinstance(osd_entries, list):
|
|
3018
|
+
for entry in osd_entries:
|
|
3019
|
+
if not isinstance(entry, Mapping):
|
|
3020
|
+
continue
|
|
3021
|
+
text = entry.get("name")
|
|
3022
|
+
if isinstance(text, str) and text.strip():
|
|
3023
|
+
return text.strip()
|
|
3024
|
+
|
|
3025
|
+
return serial
|
|
3026
|
+
|
|
1942
3027
|
def set_camera_osd(
|
|
1943
3028
|
self,
|
|
1944
3029
|
serial: str,
|
|
1945
|
-
text: str =
|
|
3030
|
+
text: str | None = None,
|
|
3031
|
+
*,
|
|
3032
|
+
enabled: bool | None = None,
|
|
3033
|
+
name: str | None = None,
|
|
3034
|
+
camera_data: Mapping[str, Any] | None = None,
|
|
1946
3035
|
channel: int = 1,
|
|
1947
3036
|
max_retries: int = 0,
|
|
1948
3037
|
) -> bool:
|
|
1949
|
-
"""Set
|
|
3038
|
+
"""Set or clear the on-screen display text for a camera.
|
|
1950
3039
|
|
|
1951
3040
|
Args:
|
|
1952
|
-
serial
|
|
1953
|
-
text
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
3041
|
+
serial: Camera serial number that should receive the update.
|
|
3042
|
+
text: Explicit OSD label to apply. If provided it takes precedence over
|
|
3043
|
+
all other inputs and `enabled` is ignored.
|
|
3044
|
+
enabled: Convenience flag used when `text` is omitted. When set to
|
|
3045
|
+
`True`, the client derives a label automatically (optionally using
|
|
3046
|
+
`name`/`camera_data`). When `False`, the overlay is cleared.
|
|
3047
|
+
name: Optional friendly name to favour when building the automatic
|
|
3048
|
+
overlay text.
|
|
3049
|
+
camera_data: Optional camera payload (matching coordinator data) that
|
|
3050
|
+
can be inspected for existing OSD labels and names.
|
|
3051
|
+
channel: Camera channel identifier (defaults to the primary channel).
|
|
3052
|
+
max_retries: Number of retry attempts for transient API failures.
|
|
1960
3053
|
|
|
1961
3054
|
Returns:
|
|
1962
|
-
bool: True
|
|
1963
|
-
|
|
3055
|
+
bool: ``True`` when the request is accepted by the Ezviz backend.
|
|
1964
3056
|
"""
|
|
3057
|
+
|
|
3058
|
+
if text is not None:
|
|
3059
|
+
resolved = text
|
|
3060
|
+
elif enabled is False:
|
|
3061
|
+
resolved = ""
|
|
3062
|
+
else:
|
|
3063
|
+
if camera_data is None:
|
|
3064
|
+
camera_data = self._cameras.get(serial)
|
|
3065
|
+
if camera_data is None:
|
|
3066
|
+
raise PyEzvizError(
|
|
3067
|
+
"Camera data unavailable; call load_devices() before setting the OSD"
|
|
3068
|
+
)
|
|
3069
|
+
|
|
3070
|
+
resolved = (
|
|
3071
|
+
self._resolve_osd_text(
|
|
3072
|
+
serial,
|
|
3073
|
+
name=name,
|
|
3074
|
+
camera_data=camera_data,
|
|
3075
|
+
)
|
|
3076
|
+
if enabled
|
|
3077
|
+
else ""
|
|
3078
|
+
)
|
|
3079
|
+
|
|
1965
3080
|
json_output = self._request_json(
|
|
1966
3081
|
"PUT",
|
|
1967
3082
|
f"{API_ENDPOINT_OSD}{serial}/{channel}/osd",
|
|
1968
|
-
data={"osd":
|
|
3083
|
+
data={"osd": resolved},
|
|
1969
3084
|
retry_401=True,
|
|
1970
3085
|
max_retries=max_retries,
|
|
1971
3086
|
)
|
|
@@ -2100,6 +3215,42 @@ class EzvizClient:
|
|
|
2100
3215
|
|
|
2101
3216
|
return True
|
|
2102
3217
|
|
|
3218
|
+
def get_motion_detect_sensitivity(
|
|
3219
|
+
self,
|
|
3220
|
+
serial: str,
|
|
3221
|
+
channel: int,
|
|
3222
|
+
*,
|
|
3223
|
+
max_retries: int = 0,
|
|
3224
|
+
) -> dict:
|
|
3225
|
+
"""Get motion detection sensitivity via v1 devconfig endpoint."""
|
|
3226
|
+
|
|
3227
|
+
json_output = self._request_json(
|
|
3228
|
+
"GET",
|
|
3229
|
+
f"{API_ENDPOINT_SENSITIVITY}{serial}/{channel}",
|
|
3230
|
+
retry_401=True,
|
|
3231
|
+
max_retries=max_retries,
|
|
3232
|
+
)
|
|
3233
|
+
self._ensure_ok(json_output, "Could not get motion detect sensitivity")
|
|
3234
|
+
return json_output
|
|
3235
|
+
|
|
3236
|
+
def get_motion_detect_sensitivity_dp1s(
|
|
3237
|
+
self,
|
|
3238
|
+
serial: str,
|
|
3239
|
+
channel: int,
|
|
3240
|
+
*,
|
|
3241
|
+
max_retries: int = 0,
|
|
3242
|
+
) -> dict:
|
|
3243
|
+
"""Get motion detection sensitivity for DP1S devices."""
|
|
3244
|
+
|
|
3245
|
+
json_output = self._request_json(
|
|
3246
|
+
"GET",
|
|
3247
|
+
f"{API_ENDPOINT_DEVICES}{serial}/{channel}/sensitivity",
|
|
3248
|
+
retry_401=True,
|
|
3249
|
+
max_retries=max_retries,
|
|
3250
|
+
)
|
|
3251
|
+
self._ensure_ok(json_output, "Could not get DP1S motion sensitivity")
|
|
3252
|
+
return json_output
|
|
3253
|
+
|
|
2103
3254
|
def set_detection_sensitivity(
|
|
2104
3255
|
self,
|
|
2105
3256
|
serial: str,
|
|
@@ -2159,18 +3310,1017 @@ class EzvizClient:
|
|
|
2159
3310
|
|
|
2160
3311
|
return None
|
|
2161
3312
|
|
|
3313
|
+
def get_detector_setting_info(
|
|
3314
|
+
self,
|
|
3315
|
+
device_serial: str,
|
|
3316
|
+
detector_serial: str,
|
|
3317
|
+
key: str,
|
|
3318
|
+
*,
|
|
3319
|
+
max_retries: int = 0,
|
|
3320
|
+
) -> dict:
|
|
3321
|
+
"""Fetch a specific configuration key for an A1S detector."""
|
|
3322
|
+
|
|
3323
|
+
path = (
|
|
3324
|
+
f"{API_ENDPOINT_SPECIAL_BIZS_A1S}{device_serial}/detector/"
|
|
3325
|
+
f"{detector_serial}/{key}"
|
|
3326
|
+
)
|
|
3327
|
+
json_output = self._request_json(
|
|
3328
|
+
"GET",
|
|
3329
|
+
path,
|
|
3330
|
+
retry_401=True,
|
|
3331
|
+
max_retries=max_retries,
|
|
3332
|
+
)
|
|
3333
|
+
self._ensure_ok(json_output, "Could not get detector setting info")
|
|
3334
|
+
return json_output
|
|
3335
|
+
|
|
3336
|
+
def set_detector_setting_info(
|
|
3337
|
+
self,
|
|
3338
|
+
device_serial: str,
|
|
3339
|
+
detector_serial: str,
|
|
3340
|
+
key: str,
|
|
3341
|
+
value: int,
|
|
3342
|
+
*,
|
|
3343
|
+
max_retries: int = 0,
|
|
3344
|
+
) -> dict:
|
|
3345
|
+
"""Update a configuration key for an A1S detector."""
|
|
3346
|
+
|
|
3347
|
+
path = (
|
|
3348
|
+
f"{API_ENDPOINT_SPECIAL_BIZS_A1S}{device_serial}/detector/{detector_serial}"
|
|
3349
|
+
)
|
|
3350
|
+
json_output = self._request_json(
|
|
3351
|
+
"POST",
|
|
3352
|
+
path,
|
|
3353
|
+
params={"key": key},
|
|
3354
|
+
data={"value": value},
|
|
3355
|
+
retry_401=True,
|
|
3356
|
+
max_retries=max_retries,
|
|
3357
|
+
)
|
|
3358
|
+
self._ensure_ok(json_output, "Could not set detector setting info")
|
|
3359
|
+
return json_output
|
|
3360
|
+
|
|
3361
|
+
def get_detector_info(
|
|
3362
|
+
self,
|
|
3363
|
+
detector_serial: str,
|
|
3364
|
+
*,
|
|
3365
|
+
max_retries: int = 0,
|
|
3366
|
+
) -> dict:
|
|
3367
|
+
"""Retrieve status/details for an A1S detector."""
|
|
3368
|
+
|
|
3369
|
+
path = f"{API_ENDPOINT_SPECIAL_BIZS_A1S}detector/{detector_serial}"
|
|
3370
|
+
json_output = self._request_json(
|
|
3371
|
+
"GET",
|
|
3372
|
+
path,
|
|
3373
|
+
retry_401=True,
|
|
3374
|
+
max_retries=max_retries,
|
|
3375
|
+
)
|
|
3376
|
+
self._ensure_ok(json_output, "Could not get detector info")
|
|
3377
|
+
return json_output
|
|
3378
|
+
|
|
3379
|
+
def get_radio_signals(
|
|
3380
|
+
self,
|
|
3381
|
+
device_serial: str,
|
|
3382
|
+
child_device_serial: str,
|
|
3383
|
+
*,
|
|
3384
|
+
max_retries: int = 0,
|
|
3385
|
+
) -> dict:
|
|
3386
|
+
"""Return radio signal metrics for a detector connected to a device."""
|
|
3387
|
+
|
|
3388
|
+
path = f"{API_ENDPOINT_SPECIAL_BIZS_A1S}{device_serial}/radioSignal"
|
|
3389
|
+
json_output = self._request_json(
|
|
3390
|
+
"GET",
|
|
3391
|
+
path,
|
|
3392
|
+
params={"childDevSerial": child_device_serial},
|
|
3393
|
+
retry_401=True,
|
|
3394
|
+
max_retries=max_retries,
|
|
3395
|
+
)
|
|
3396
|
+
self._ensure_ok(json_output, "Could not get radio signals")
|
|
3397
|
+
return json_output
|
|
3398
|
+
|
|
3399
|
+
def get_voice_config(
|
|
3400
|
+
self,
|
|
3401
|
+
product_id: str,
|
|
3402
|
+
version: str,
|
|
3403
|
+
*,
|
|
3404
|
+
max_retries: int = 0,
|
|
3405
|
+
) -> dict:
|
|
3406
|
+
"""Fetch voice configuration metadata for a product."""
|
|
3407
|
+
|
|
3408
|
+
params = {"productId": product_id, "version": version}
|
|
3409
|
+
json_output = self._request_json(
|
|
3410
|
+
"GET",
|
|
3411
|
+
API_ENDPOINT_IOT_FEATURE_PRODUCT_VOICE_CONFIG,
|
|
3412
|
+
params=params,
|
|
3413
|
+
retry_401=True,
|
|
3414
|
+
max_retries=max_retries,
|
|
3415
|
+
)
|
|
3416
|
+
self._ensure_ok(json_output, "Could not get voice config")
|
|
3417
|
+
return json_output
|
|
3418
|
+
|
|
2162
3419
|
# soundtype: 0 = normal, 1 = intensive, 2 = disabled ... don't ask me why...
|
|
2163
|
-
def
|
|
2164
|
-
self,
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
3420
|
+
def get_voice_info(
|
|
3421
|
+
self,
|
|
3422
|
+
serial: str,
|
|
3423
|
+
*,
|
|
3424
|
+
local_index: str | None = None,
|
|
3425
|
+
max_retries: int = 0,
|
|
3426
|
+
) -> dict:
|
|
3427
|
+
"""Retrieve uploaded custom voice prompts for a device."""
|
|
2169
3428
|
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
3429
|
+
params: dict[str, Any] = {"deviceSerial": serial}
|
|
3430
|
+
if local_index is not None:
|
|
3431
|
+
params["localIndex"] = local_index
|
|
3432
|
+
|
|
3433
|
+
json_output = self._request_json(
|
|
3434
|
+
"GET",
|
|
3435
|
+
API_ENDPOINT_SPECIAL_BIZS_VOICES,
|
|
3436
|
+
params=params,
|
|
3437
|
+
retry_401=True,
|
|
3438
|
+
max_retries=max_retries,
|
|
3439
|
+
)
|
|
3440
|
+
self._ensure_ok(json_output, "Could not get voice list")
|
|
3441
|
+
return json_output
|
|
3442
|
+
|
|
3443
|
+
def add_voice_info(
|
|
3444
|
+
self,
|
|
3445
|
+
serial: str,
|
|
3446
|
+
voice_name: str,
|
|
3447
|
+
voice_url: str,
|
|
3448
|
+
*,
|
|
3449
|
+
local_index: str | None = None,
|
|
3450
|
+
max_retries: int = 0,
|
|
3451
|
+
) -> dict:
|
|
3452
|
+
"""Upload metadata for a new custom voice prompt."""
|
|
3453
|
+
|
|
3454
|
+
data: dict[str, Any] = {
|
|
3455
|
+
"deviceSerial": serial,
|
|
3456
|
+
"voiceName": voice_name,
|
|
3457
|
+
"voiceUrl": voice_url,
|
|
3458
|
+
}
|
|
3459
|
+
if local_index is not None:
|
|
3460
|
+
data["localIndex"] = local_index
|
|
3461
|
+
|
|
3462
|
+
json_output = self._request_json(
|
|
3463
|
+
"POST",
|
|
3464
|
+
API_ENDPOINT_SPECIAL_BIZS_VOICES,
|
|
3465
|
+
data=data,
|
|
3466
|
+
retry_401=True,
|
|
3467
|
+
max_retries=max_retries,
|
|
3468
|
+
)
|
|
3469
|
+
self._ensure_ok(json_output, "Could not add voice info")
|
|
3470
|
+
return json_output
|
|
3471
|
+
|
|
3472
|
+
def add_shared_voice_info(
|
|
3473
|
+
self,
|
|
3474
|
+
serial: str,
|
|
3475
|
+
voice_name: str,
|
|
3476
|
+
voice_url: str,
|
|
3477
|
+
local_index: str,
|
|
3478
|
+
*,
|
|
3479
|
+
max_retries: int = 0,
|
|
3480
|
+
) -> dict:
|
|
3481
|
+
"""Upload a shared voice with explicit local index, mirroring the mobile API."""
|
|
3482
|
+
|
|
3483
|
+
return self.add_voice_info(
|
|
3484
|
+
serial,
|
|
3485
|
+
voice_name,
|
|
3486
|
+
voice_url,
|
|
3487
|
+
local_index=local_index,
|
|
3488
|
+
max_retries=max_retries,
|
|
3489
|
+
)
|
|
3490
|
+
|
|
3491
|
+
def set_voice_info(
|
|
3492
|
+
self,
|
|
3493
|
+
serial: str,
|
|
3494
|
+
voice_id: int,
|
|
3495
|
+
voice_name: str,
|
|
3496
|
+
*,
|
|
3497
|
+
local_index: str | None = None,
|
|
3498
|
+
max_retries: int = 0,
|
|
3499
|
+
) -> dict:
|
|
3500
|
+
"""Update metadata for an existing voice prompt."""
|
|
3501
|
+
|
|
3502
|
+
data: dict[str, Any] = {
|
|
3503
|
+
"deviceSerial": serial,
|
|
3504
|
+
"voiceId": voice_id,
|
|
3505
|
+
"voiceName": voice_name,
|
|
3506
|
+
}
|
|
3507
|
+
if local_index is not None:
|
|
3508
|
+
data["localIndex"] = local_index
|
|
3509
|
+
|
|
3510
|
+
json_output = self._request_json(
|
|
3511
|
+
"PUT",
|
|
3512
|
+
API_ENDPOINT_SPECIAL_BIZS_VOICES,
|
|
3513
|
+
data=data,
|
|
3514
|
+
retry_401=True,
|
|
3515
|
+
max_retries=max_retries,
|
|
3516
|
+
)
|
|
3517
|
+
self._ensure_ok(json_output, "Could not update voice info")
|
|
3518
|
+
return json_output
|
|
3519
|
+
|
|
3520
|
+
def set_shared_voice_info(
|
|
3521
|
+
self,
|
|
3522
|
+
serial: str,
|
|
3523
|
+
voice_id: int,
|
|
3524
|
+
voice_name: str,
|
|
3525
|
+
local_index: str,
|
|
3526
|
+
*,
|
|
3527
|
+
max_retries: int = 0,
|
|
3528
|
+
) -> dict:
|
|
3529
|
+
"""Alias for updating shared voices that ensures local index is supplied."""
|
|
3530
|
+
|
|
3531
|
+
return self.set_voice_info(
|
|
3532
|
+
serial,
|
|
3533
|
+
voice_id,
|
|
3534
|
+
voice_name,
|
|
3535
|
+
local_index=local_index,
|
|
3536
|
+
max_retries=max_retries,
|
|
3537
|
+
)
|
|
3538
|
+
|
|
3539
|
+
def delete_voice_info(
|
|
3540
|
+
self,
|
|
3541
|
+
serial: str,
|
|
3542
|
+
voice_id: int,
|
|
3543
|
+
*,
|
|
3544
|
+
voice_url: str | None = None,
|
|
3545
|
+
local_index: str | None = None,
|
|
3546
|
+
max_retries: int = 0,
|
|
3547
|
+
) -> dict:
|
|
3548
|
+
"""Remove a voice prompt from a device."""
|
|
3549
|
+
|
|
3550
|
+
params: dict[str, Any] = {
|
|
3551
|
+
"deviceSerial": serial,
|
|
3552
|
+
"voiceId": voice_id,
|
|
3553
|
+
}
|
|
3554
|
+
if voice_url is not None:
|
|
3555
|
+
params["voiceUrl"] = voice_url
|
|
3556
|
+
if local_index is not None:
|
|
3557
|
+
params["localIndex"] = local_index
|
|
3558
|
+
|
|
3559
|
+
json_output = self._request_json(
|
|
3560
|
+
"DELETE",
|
|
3561
|
+
API_ENDPOINT_SPECIAL_BIZS_VOICES,
|
|
3562
|
+
params=params,
|
|
3563
|
+
retry_401=True,
|
|
3564
|
+
max_retries=max_retries,
|
|
3565
|
+
)
|
|
3566
|
+
self._ensure_ok(json_output, "Could not delete voice info")
|
|
3567
|
+
return json_output
|
|
3568
|
+
|
|
3569
|
+
def delete_shared_voice_info(
|
|
3570
|
+
self,
|
|
3571
|
+
serial: str,
|
|
3572
|
+
voice_id: int,
|
|
3573
|
+
voice_url: str,
|
|
3574
|
+
local_index: str,
|
|
3575
|
+
*,
|
|
3576
|
+
max_retries: int = 0,
|
|
3577
|
+
) -> dict:
|
|
3578
|
+
"""Alias for deleting shared voices with required parameters."""
|
|
3579
|
+
|
|
3580
|
+
return self.delete_voice_info(
|
|
3581
|
+
serial,
|
|
3582
|
+
voice_id,
|
|
3583
|
+
voice_url=voice_url,
|
|
3584
|
+
local_index=local_index,
|
|
3585
|
+
max_retries=max_retries,
|
|
3586
|
+
)
|
|
3587
|
+
|
|
3588
|
+
def get_whistle_status_by_channel(
|
|
3589
|
+
self,
|
|
3590
|
+
serial: str,
|
|
3591
|
+
*,
|
|
3592
|
+
max_retries: int = 0,
|
|
3593
|
+
) -> dict:
|
|
3594
|
+
"""Return whistle configuration per channel for a device."""
|
|
3595
|
+
|
|
3596
|
+
json_output = self._request_json(
|
|
3597
|
+
"GET",
|
|
3598
|
+
f"{API_ENDPOINT_DEVICES}{serial}{API_ENDPOINT_ALARM_GET_WHISTLE_STATUS_BY_CHANNEL}",
|
|
3599
|
+
retry_401=True,
|
|
3600
|
+
max_retries=max_retries,
|
|
3601
|
+
)
|
|
3602
|
+
self._ensure_ok(json_output, "Could not get whistle status by channel")
|
|
3603
|
+
return json_output
|
|
3604
|
+
|
|
3605
|
+
def get_whistle_status_by_device(
|
|
3606
|
+
self,
|
|
3607
|
+
serial: str,
|
|
3608
|
+
*,
|
|
3609
|
+
max_retries: int = 0,
|
|
3610
|
+
) -> dict:
|
|
3611
|
+
"""Return whistle configuration at the device level."""
|
|
3612
|
+
|
|
3613
|
+
json_output = self._request_json(
|
|
3614
|
+
"GET",
|
|
3615
|
+
f"{API_ENDPOINT_DEVICES}{serial}{API_ENDPOINT_ALARM_GET_WHISTLE_STATUS_BY_DEVICE}",
|
|
3616
|
+
retry_401=True,
|
|
3617
|
+
max_retries=max_retries,
|
|
3618
|
+
)
|
|
3619
|
+
self._ensure_ok(json_output, "Could not get whistle status by device")
|
|
3620
|
+
return json_output
|
|
3621
|
+
|
|
3622
|
+
def set_channel_whistle(
|
|
3623
|
+
self,
|
|
3624
|
+
serial: str,
|
|
3625
|
+
channel_whistles: list[Mapping[str, Any]] | list[dict[str, Any]],
|
|
3626
|
+
*,
|
|
3627
|
+
max_retries: int = 0,
|
|
3628
|
+
) -> dict:
|
|
3629
|
+
"""Configure whistle behaviour for individual channels."""
|
|
3630
|
+
|
|
3631
|
+
if not channel_whistles:
|
|
3632
|
+
raise PyEzvizError("channel_whistles must contain at least one entry")
|
|
3633
|
+
|
|
3634
|
+
entries: list[dict[str, Any]] = []
|
|
3635
|
+
required_fields = {"channel", "status", "duration", "volume"}
|
|
3636
|
+
for item in channel_whistles:
|
|
3637
|
+
entry = dict(item)
|
|
3638
|
+
entry.setdefault("deviceSerial", serial)
|
|
3639
|
+
missing = [field for field in required_fields if field not in entry]
|
|
3640
|
+
if missing:
|
|
3641
|
+
raise PyEzvizError(
|
|
3642
|
+
"channel_whistles entries must include " + ", ".join(missing)
|
|
3643
|
+
)
|
|
3644
|
+
entries.append(entry)
|
|
3645
|
+
|
|
3646
|
+
payload = {"channelWhistleList": entries}
|
|
3647
|
+
|
|
3648
|
+
json_output = self._request_json(
|
|
3649
|
+
"POST",
|
|
3650
|
+
f"{API_ENDPOINT_DEVICES}{serial}{API_ENDPOINT_ALARM_SET_CHANNEL_WHISTLE}",
|
|
3651
|
+
json_body=payload,
|
|
3652
|
+
retry_401=True,
|
|
3653
|
+
max_retries=max_retries,
|
|
3654
|
+
)
|
|
3655
|
+
self._ensure_ok(json_output, "Could not set channel whistle")
|
|
3656
|
+
return json_output
|
|
3657
|
+
|
|
3658
|
+
def set_device_whistle(
|
|
3659
|
+
self,
|
|
3660
|
+
serial: str,
|
|
3661
|
+
*,
|
|
3662
|
+
status: int,
|
|
3663
|
+
duration: int,
|
|
3664
|
+
volume: int,
|
|
3665
|
+
max_retries: int = 0,
|
|
3666
|
+
) -> dict:
|
|
3667
|
+
"""Configure whistle behaviour at the device level."""
|
|
3668
|
+
|
|
3669
|
+
params = {
|
|
3670
|
+
"status": status,
|
|
3671
|
+
"duration": duration,
|
|
3672
|
+
"volume": volume,
|
|
3673
|
+
}
|
|
3674
|
+
|
|
3675
|
+
json_output = self._request_json(
|
|
3676
|
+
"PUT",
|
|
3677
|
+
f"{API_ENDPOINT_DEVICES}{serial}{API_ENDPOINT_ALARM_SET_DEVICE_WHISTLE}",
|
|
3678
|
+
params=params,
|
|
3679
|
+
retry_401=True,
|
|
3680
|
+
max_retries=max_retries,
|
|
3681
|
+
)
|
|
3682
|
+
self._ensure_ok(json_output, "Could not set device whistle")
|
|
3683
|
+
return json_output
|
|
3684
|
+
|
|
3685
|
+
def stop_whistle(
|
|
3686
|
+
self,
|
|
3687
|
+
serial: str,
|
|
3688
|
+
*,
|
|
3689
|
+
max_retries: int = 0,
|
|
3690
|
+
) -> dict:
|
|
3691
|
+
"""Stop any ongoing whistle sound."""
|
|
3692
|
+
|
|
3693
|
+
json_output = self._request_json(
|
|
3694
|
+
"PUT",
|
|
3695
|
+
f"{API_ENDPOINT_DEVICES}{serial}{API_ENDPOINT_ALARM_STOP_WHISTLE}",
|
|
3696
|
+
retry_401=True,
|
|
3697
|
+
max_retries=max_retries,
|
|
3698
|
+
)
|
|
3699
|
+
self._ensure_ok(json_output, "Could not stop whistle")
|
|
3700
|
+
return json_output
|
|
3701
|
+
|
|
3702
|
+
def delay_battery_device_sleep(
|
|
3703
|
+
self,
|
|
3704
|
+
serial: str,
|
|
3705
|
+
channel: int,
|
|
3706
|
+
sleep_type: int,
|
|
3707
|
+
*,
|
|
3708
|
+
max_retries: int = 0,
|
|
3709
|
+
) -> dict:
|
|
3710
|
+
"""Request additional awake time for a battery-powered device."""
|
|
3711
|
+
|
|
3712
|
+
path = f"{API_ENDPOINT_SPECIAL_BIZS_V1_BATTERY}{serial}/{channel}/{sleep_type}/sleep"
|
|
3713
|
+
json_output = self._request_json(
|
|
3714
|
+
"PUT",
|
|
3715
|
+
path,
|
|
3716
|
+
retry_401=True,
|
|
3717
|
+
max_retries=max_retries,
|
|
3718
|
+
)
|
|
3719
|
+
self._ensure_ok(json_output, "Could not delay battery device sleep")
|
|
3720
|
+
return json_output
|
|
3721
|
+
|
|
3722
|
+
def get_device_chime_info(
|
|
3723
|
+
self,
|
|
3724
|
+
serial: str,
|
|
3725
|
+
channel: int,
|
|
3726
|
+
*,
|
|
3727
|
+
max_retries: int = 0,
|
|
3728
|
+
) -> dict:
|
|
3729
|
+
"""Fetch chime configuration for a specific channel."""
|
|
3730
|
+
|
|
3731
|
+
json_output = self._request_json(
|
|
3732
|
+
"GET",
|
|
3733
|
+
f"{API_ENDPOINT_ALARM_DEVICE_CHIME}{serial}/{channel}",
|
|
3734
|
+
retry_401=True,
|
|
3735
|
+
max_retries=max_retries,
|
|
3736
|
+
)
|
|
3737
|
+
self._ensure_ok(json_output, "Could not get chime info")
|
|
3738
|
+
return json_output
|
|
3739
|
+
|
|
3740
|
+
def set_device_chime_info(
|
|
3741
|
+
self,
|
|
3742
|
+
serial: str,
|
|
3743
|
+
channel: int,
|
|
3744
|
+
*,
|
|
3745
|
+
sound_type: int,
|
|
3746
|
+
duration: int,
|
|
3747
|
+
max_retries: int = 0,
|
|
3748
|
+
) -> dict:
|
|
3749
|
+
"""Update chime type and duration for a channel."""
|
|
3750
|
+
|
|
3751
|
+
data = {
|
|
3752
|
+
"type": sound_type,
|
|
3753
|
+
"duration": duration,
|
|
3754
|
+
}
|
|
3755
|
+
|
|
3756
|
+
json_output = self._request_json(
|
|
3757
|
+
"POST",
|
|
3758
|
+
f"{API_ENDPOINT_ALARM_DEVICE_CHIME}{serial}/{channel}",
|
|
3759
|
+
data=data,
|
|
3760
|
+
retry_401=True,
|
|
3761
|
+
max_retries=max_retries,
|
|
3762
|
+
)
|
|
3763
|
+
self._ensure_ok(json_output, "Could not set chime info")
|
|
3764
|
+
return json_output
|
|
3765
|
+
|
|
3766
|
+
def set_switch_enable_req(
|
|
3767
|
+
self,
|
|
3768
|
+
serial: str,
|
|
3769
|
+
channel: int,
|
|
3770
|
+
enable: int,
|
|
3771
|
+
switch_type: int,
|
|
3772
|
+
*,
|
|
3773
|
+
max_retries: int = 0,
|
|
3774
|
+
) -> dict:
|
|
3775
|
+
"""Call the legacy setSwitchEnableReq endpoint."""
|
|
3776
|
+
|
|
3777
|
+
params = {
|
|
3778
|
+
"enable": enable,
|
|
3779
|
+
"type": switch_type,
|
|
3780
|
+
}
|
|
3781
|
+
json_output = self._request_json(
|
|
3782
|
+
"PUT",
|
|
3783
|
+
f"{API_ENDPOINT_DEVICES}{serial}/{channel}{API_ENDPOINT_DEVICES_SET_SWITCH_ENABLE}",
|
|
3784
|
+
params=params,
|
|
3785
|
+
retry_401=True,
|
|
3786
|
+
max_retries=max_retries,
|
|
3787
|
+
)
|
|
3788
|
+
self._ensure_ok(json_output, "Could not set switch enable request")
|
|
3789
|
+
return json_output
|
|
3790
|
+
|
|
3791
|
+
def get_managed_device_info(
|
|
3792
|
+
self,
|
|
3793
|
+
serial: str,
|
|
3794
|
+
*,
|
|
3795
|
+
max_retries: int = 0,
|
|
3796
|
+
) -> dict:
|
|
3797
|
+
"""Return metadata for a managed device (e.g. base station)."""
|
|
3798
|
+
|
|
3799
|
+
path = f"{API_ENDPOINT_MANAGED_DEVICE_BASE}{serial}/base"
|
|
3800
|
+
json_output = self._request_json(
|
|
3801
|
+
"GET",
|
|
3802
|
+
path,
|
|
3803
|
+
retry_401=True,
|
|
3804
|
+
max_retries=max_retries,
|
|
3805
|
+
)
|
|
3806
|
+
self._ensure_ok(json_output, "Could not get managed device info")
|
|
3807
|
+
return json_output
|
|
3808
|
+
|
|
3809
|
+
def get_managed_device_ipcs(
|
|
3810
|
+
self,
|
|
3811
|
+
serial: str,
|
|
3812
|
+
*,
|
|
3813
|
+
max_retries: int = 0,
|
|
3814
|
+
) -> dict:
|
|
3815
|
+
"""List IPC sub-devices that belong to a managed device."""
|
|
3816
|
+
|
|
3817
|
+
path = f"{API_ENDPOINT_MANAGED_DEVICE_BASE}{serial}/ipcs"
|
|
3818
|
+
json_output = self._request_json(
|
|
3819
|
+
"GET",
|
|
3820
|
+
path,
|
|
3821
|
+
retry_401=True,
|
|
3822
|
+
max_retries=max_retries,
|
|
3823
|
+
)
|
|
3824
|
+
self._ensure_ok(json_output, "Could not get managed IPC list")
|
|
3825
|
+
return json_output
|
|
3826
|
+
|
|
3827
|
+
def get_devices_status(
|
|
3828
|
+
self,
|
|
3829
|
+
serials: list[str] | str,
|
|
3830
|
+
*,
|
|
3831
|
+
max_retries: int = 0,
|
|
3832
|
+
) -> dict:
|
|
3833
|
+
"""Fetch online/offline status for one or more devices."""
|
|
3834
|
+
|
|
3835
|
+
if isinstance(serials, (list, tuple, set)):
|
|
3836
|
+
serial_param = ",".join(sorted({str(s) for s in serials}))
|
|
3837
|
+
else:
|
|
3838
|
+
serial_param = str(serials)
|
|
3839
|
+
|
|
3840
|
+
json_output = self._request_json(
|
|
3841
|
+
"GET",
|
|
3842
|
+
API_ENDPOINT_USERDEVICES_STATUS,
|
|
3843
|
+
params={"deviceSerials": serial_param},
|
|
3844
|
+
retry_401=True,
|
|
3845
|
+
max_retries=max_retries,
|
|
3846
|
+
)
|
|
3847
|
+
self._ensure_ok(json_output, "Could not get device status")
|
|
3848
|
+
return json_output
|
|
3849
|
+
|
|
3850
|
+
def get_device_secret_key_info(
|
|
3851
|
+
self,
|
|
3852
|
+
serials: list[str] | str,
|
|
3853
|
+
*,
|
|
3854
|
+
max_retries: int = 0,
|
|
3855
|
+
) -> dict:
|
|
3856
|
+
"""Retrieve KMS secret key metadata for devices."""
|
|
3857
|
+
|
|
3858
|
+
if isinstance(serials, (list, tuple, set)):
|
|
3859
|
+
serial_param = ",".join(sorted({str(s) for s in serials}))
|
|
3860
|
+
else:
|
|
3861
|
+
serial_param = str(serials)
|
|
3862
|
+
|
|
3863
|
+
json_output = self._request_json(
|
|
3864
|
+
"GET",
|
|
3865
|
+
API_ENDPOINT_USERDEVICES_KMS,
|
|
3866
|
+
params={"deviceSerials": serial_param},
|
|
3867
|
+
retry_401=True,
|
|
3868
|
+
max_retries=max_retries,
|
|
3869
|
+
)
|
|
3870
|
+
self._ensure_ok(json_output, "Could not get device secret key info")
|
|
3871
|
+
return json_output
|
|
3872
|
+
|
|
3873
|
+
def get_device_list_encrypt_key(
|
|
3874
|
+
self,
|
|
3875
|
+
area_id: int,
|
|
3876
|
+
form_data: Mapping[str, Any] | bytes | bytearray | str,
|
|
3877
|
+
*,
|
|
3878
|
+
max_retries: int = 0,
|
|
3879
|
+
) -> dict:
|
|
3880
|
+
"""Batch query encrypt keys for devices, matching the mobile client's risk API."""
|
|
3881
|
+
|
|
3882
|
+
headers = {
|
|
3883
|
+
**self._session.headers,
|
|
3884
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
3885
|
+
"areaId": str(area_id),
|
|
3886
|
+
}
|
|
3887
|
+
if isinstance(form_data, (bytes, bytearray, str)):
|
|
3888
|
+
body = form_data
|
|
3889
|
+
else:
|
|
3890
|
+
body = urlencode(form_data, doseq=True)
|
|
3891
|
+
req = requests.Request(
|
|
3892
|
+
method="POST",
|
|
3893
|
+
url=self._url(API_ENDPOINT_DEVICES_ENCRYPTKEY_BATCH),
|
|
3894
|
+
headers=headers,
|
|
3895
|
+
data=body,
|
|
3896
|
+
).prepare()
|
|
3897
|
+
|
|
3898
|
+
resp = self._send_prepared(
|
|
3899
|
+
req,
|
|
3900
|
+
retry_401=True,
|
|
3901
|
+
max_retries=max_retries,
|
|
3902
|
+
)
|
|
3903
|
+
json_output = self._parse_json(resp)
|
|
3904
|
+
if not self._meta_ok(json_output):
|
|
3905
|
+
raise PyEzvizError(
|
|
3906
|
+
f"Could not get device encrypt key list: Got {json_output})"
|
|
3907
|
+
)
|
|
3908
|
+
return json_output
|
|
3909
|
+
|
|
3910
|
+
def get_p2p_info(
|
|
3911
|
+
self,
|
|
3912
|
+
serials: list[str] | str,
|
|
3913
|
+
*,
|
|
3914
|
+
max_retries: int = 0,
|
|
3915
|
+
) -> dict:
|
|
3916
|
+
"""Retrieve P2P info via the device-scoped endpoint."""
|
|
3917
|
+
|
|
3918
|
+
if isinstance(serials, (list, tuple, set)):
|
|
3919
|
+
serial_param = ",".join(sorted({str(s) for s in serials}))
|
|
3920
|
+
else:
|
|
3921
|
+
serial_param = str(serials)
|
|
3922
|
+
|
|
3923
|
+
json_output = self._request_json(
|
|
3924
|
+
"GET",
|
|
3925
|
+
API_ENDPOINT_DEVICES_P2P_INFO,
|
|
3926
|
+
params={"deviceSerials": serial_param},
|
|
3927
|
+
retry_401=True,
|
|
3928
|
+
max_retries=max_retries,
|
|
3929
|
+
)
|
|
3930
|
+
self._ensure_ok(json_output, "Could not get P2P info")
|
|
3931
|
+
return json_output
|
|
3932
|
+
|
|
3933
|
+
def get_p2p_server_info(
|
|
3934
|
+
self,
|
|
3935
|
+
serials: list[str] | str,
|
|
3936
|
+
*,
|
|
3937
|
+
max_retries: int = 0,
|
|
3938
|
+
) -> dict:
|
|
3939
|
+
"""Retrieve P2P server info via the userdevices endpoint."""
|
|
3940
|
+
|
|
3941
|
+
if isinstance(serials, (list, tuple, set)):
|
|
3942
|
+
serial_param = ",".join(sorted({str(s) for s in serials}))
|
|
3943
|
+
else:
|
|
3944
|
+
serial_param = str(serials)
|
|
3945
|
+
|
|
3946
|
+
json_output = self._request_json(
|
|
3947
|
+
"GET",
|
|
3948
|
+
API_ENDPOINT_USERDEVICES_P2P_INFO,
|
|
3949
|
+
params={"deviceSerials": serial_param},
|
|
3950
|
+
retry_401=True,
|
|
3951
|
+
max_retries=max_retries,
|
|
3952
|
+
)
|
|
3953
|
+
self._ensure_ok(json_output, "Could not get P2P server info")
|
|
3954
|
+
return json_output
|
|
3955
|
+
|
|
3956
|
+
def check_device_upgrade_rule(
|
|
3957
|
+
self,
|
|
3958
|
+
*,
|
|
3959
|
+
max_retries: int = 0,
|
|
3960
|
+
) -> dict:
|
|
3961
|
+
"""Check firmware upgrade eligibility rules."""
|
|
3962
|
+
|
|
3963
|
+
json_output = self._request_json(
|
|
3964
|
+
"GET",
|
|
3965
|
+
API_ENDPOINT_UPGRADE_RULE,
|
|
3966
|
+
retry_401=True,
|
|
3967
|
+
max_retries=max_retries,
|
|
3968
|
+
)
|
|
3969
|
+
self._ensure_ok(json_output, "Could not get upgrade rules")
|
|
3970
|
+
return json_output
|
|
3971
|
+
|
|
3972
|
+
def get_autoupgrade_switch(
|
|
3973
|
+
self,
|
|
3974
|
+
*,
|
|
3975
|
+
max_retries: int = 0,
|
|
3976
|
+
) -> dict:
|
|
3977
|
+
"""Return the current auto-upgrade switch settings."""
|
|
3978
|
+
|
|
3979
|
+
json_output = self._request_json(
|
|
3980
|
+
"GET",
|
|
3981
|
+
API_ENDPOINT_AUTOUPGRADE_SWITCH,
|
|
3982
|
+
retry_401=True,
|
|
3983
|
+
max_retries=max_retries,
|
|
3984
|
+
)
|
|
3985
|
+
self._ensure_ok(json_output, "Could not get auto-upgrade switch")
|
|
3986
|
+
return json_output
|
|
3987
|
+
|
|
3988
|
+
def set_autoupgrade_switch(
|
|
3989
|
+
self,
|
|
3990
|
+
auto_upgrade: int,
|
|
3991
|
+
time_type: int,
|
|
3992
|
+
*,
|
|
3993
|
+
max_retries: int = 0,
|
|
3994
|
+
) -> dict:
|
|
3995
|
+
"""Update the auto-upgrade switch configuration."""
|
|
3996
|
+
|
|
3997
|
+
data = {
|
|
3998
|
+
"autoUpgrade": auto_upgrade,
|
|
3999
|
+
"timeType": time_type,
|
|
4000
|
+
}
|
|
4001
|
+
|
|
4002
|
+
json_output = self._request_json(
|
|
4003
|
+
"PUT",
|
|
4004
|
+
API_ENDPOINT_AUTOUPGRADE_SWITCH,
|
|
4005
|
+
data=data,
|
|
4006
|
+
retry_401=True,
|
|
4007
|
+
max_retries=max_retries,
|
|
4008
|
+
)
|
|
4009
|
+
self._ensure_ok(json_output, "Could not set auto-upgrade switch")
|
|
4010
|
+
return json_output
|
|
4011
|
+
|
|
4012
|
+
def get_black_level_list(
|
|
4013
|
+
self,
|
|
4014
|
+
serial: str,
|
|
4015
|
+
*,
|
|
4016
|
+
max_retries: int = 0,
|
|
4017
|
+
) -> dict:
|
|
4018
|
+
"""Retrieve SD-card black level data for a device."""
|
|
4019
|
+
|
|
4020
|
+
json_output = self._request_json(
|
|
4021
|
+
"GET",
|
|
4022
|
+
f"{API_ENDPOINT_SDCARD_BLACK_LEVEL}{serial}",
|
|
4023
|
+
retry_401=True,
|
|
4024
|
+
max_retries=max_retries,
|
|
4025
|
+
)
|
|
4026
|
+
self._ensure_ok(json_output, "Could not get black level list")
|
|
4027
|
+
return json_output
|
|
4028
|
+
|
|
4029
|
+
def get_time_plan_infos(
|
|
4030
|
+
self,
|
|
4031
|
+
serial: str,
|
|
4032
|
+
channel: int,
|
|
4033
|
+
timing_plan_type: int,
|
|
4034
|
+
*,
|
|
4035
|
+
max_retries: int = 0,
|
|
4036
|
+
) -> dict:
|
|
4037
|
+
"""Fetch timing plan information for a device/channel."""
|
|
4038
|
+
|
|
4039
|
+
params = {
|
|
4040
|
+
"deviceSerial": serial,
|
|
4041
|
+
"channelNo": channel,
|
|
4042
|
+
"timingPlanType": timing_plan_type,
|
|
4043
|
+
}
|
|
4044
|
+
json_output = self._request_json(
|
|
4045
|
+
"GET",
|
|
4046
|
+
API_ENDPOINT_TIME_PLAN_INFOS,
|
|
4047
|
+
params=params,
|
|
4048
|
+
retry_401=True,
|
|
4049
|
+
max_retries=max_retries,
|
|
4050
|
+
)
|
|
4051
|
+
self._ensure_ok(json_output, "Could not get time plan infos")
|
|
4052
|
+
return json_output
|
|
4053
|
+
|
|
4054
|
+
def set_time_plan_infos(
|
|
4055
|
+
self,
|
|
4056
|
+
serial: str,
|
|
4057
|
+
channel: int,
|
|
4058
|
+
timing_plan_type: int,
|
|
4059
|
+
enable: int,
|
|
4060
|
+
timer_defence_qos: Any,
|
|
4061
|
+
*,
|
|
4062
|
+
max_retries: int = 0,
|
|
4063
|
+
) -> dict:
|
|
4064
|
+
"""Update timing plan configuration."""
|
|
4065
|
+
|
|
4066
|
+
params: dict[str, Any] = {
|
|
4067
|
+
"deviceSerial": serial,
|
|
4068
|
+
"channelNo": channel,
|
|
4069
|
+
"timingPlanType": timing_plan_type,
|
|
4070
|
+
"enable": enable,
|
|
4071
|
+
}
|
|
4072
|
+
if not isinstance(timer_defence_qos, str):
|
|
4073
|
+
params["timerDefenceQos"] = json.dumps(timer_defence_qos)
|
|
4074
|
+
else:
|
|
4075
|
+
params["timerDefenceQos"] = timer_defence_qos
|
|
4076
|
+
|
|
4077
|
+
json_output = self._request_json(
|
|
4078
|
+
"PUT",
|
|
4079
|
+
API_ENDPOINT_TIME_PLAN_INFOS,
|
|
4080
|
+
params=params,
|
|
4081
|
+
retry_401=True,
|
|
4082
|
+
max_retries=max_retries,
|
|
4083
|
+
)
|
|
4084
|
+
self._ensure_ok(json_output, "Could not set time plan infos")
|
|
4085
|
+
return json_output
|
|
4086
|
+
|
|
4087
|
+
def search_records(
|
|
4088
|
+
self,
|
|
4089
|
+
serial: str,
|
|
4090
|
+
channel: int,
|
|
4091
|
+
channel_serial: str,
|
|
4092
|
+
start_time: str,
|
|
4093
|
+
stop_time: str,
|
|
4094
|
+
*,
|
|
4095
|
+
size: int = 20,
|
|
4096
|
+
max_retries: int = 0,
|
|
4097
|
+
) -> dict:
|
|
4098
|
+
"""Search recorded video clips for a device."""
|
|
4099
|
+
|
|
4100
|
+
params = {
|
|
4101
|
+
"deviceSerial": serial,
|
|
4102
|
+
"channelNo": channel,
|
|
4103
|
+
"channelSerial": channel_serial,
|
|
4104
|
+
"startTime": start_time,
|
|
4105
|
+
"stopTime": stop_time,
|
|
4106
|
+
"size": size,
|
|
4107
|
+
}
|
|
4108
|
+
json_output = self._request_json(
|
|
4109
|
+
"GET",
|
|
4110
|
+
API_ENDPOINT_STREAMING_RECORDS,
|
|
4111
|
+
params=params,
|
|
4112
|
+
retry_401=True,
|
|
4113
|
+
max_retries=max_retries,
|
|
4114
|
+
)
|
|
4115
|
+
self._ensure_ok(json_output, "Could not search records")
|
|
4116
|
+
return json_output
|
|
4117
|
+
|
|
4118
|
+
def search_device(
|
|
4119
|
+
self,
|
|
4120
|
+
serial: str,
|
|
4121
|
+
*,
|
|
4122
|
+
user_ssid: str | None = None,
|
|
4123
|
+
max_retries: int = 0,
|
|
4124
|
+
) -> dict:
|
|
4125
|
+
"""Find device information by serial."""
|
|
4126
|
+
|
|
4127
|
+
headers = dict(self._session.headers)
|
|
4128
|
+
if user_ssid is not None:
|
|
4129
|
+
headers["userSsid"] = user_ssid
|
|
4130
|
+
|
|
4131
|
+
params = {"deviceSerial": serial}
|
|
4132
|
+
req = requests.Request(
|
|
4133
|
+
method="GET",
|
|
4134
|
+
url=self._url(API_ENDPOINT_USERDEVICES_SEARCH),
|
|
4135
|
+
headers=headers,
|
|
4136
|
+
params=params,
|
|
4137
|
+
).prepare()
|
|
4138
|
+
|
|
4139
|
+
resp = self._send_prepared(
|
|
4140
|
+
req,
|
|
4141
|
+
retry_401=True,
|
|
4142
|
+
max_retries=max_retries,
|
|
4143
|
+
)
|
|
4144
|
+
json_output = self._parse_json(resp)
|
|
4145
|
+
if not self._meta_ok(json_output):
|
|
4146
|
+
raise PyEzvizError(f"Could not search device: Got {json_output})")
|
|
4147
|
+
return json_output
|
|
4148
|
+
|
|
4149
|
+
def get_socket_log_info(
|
|
4150
|
+
self,
|
|
4151
|
+
serial: str,
|
|
4152
|
+
start: str,
|
|
4153
|
+
end: str,
|
|
4154
|
+
*,
|
|
4155
|
+
max_retries: int = 0,
|
|
4156
|
+
) -> dict:
|
|
4157
|
+
"""Fetch smart outlet switch logs within a time range."""
|
|
4158
|
+
|
|
4159
|
+
path = API_ENDPOINT_SMARTHOME_OUTLET_LOG.format(**{"from": start, "to": end})
|
|
4160
|
+
json_output = self._request_json(
|
|
4161
|
+
"GET",
|
|
4162
|
+
path,
|
|
4163
|
+
params={"deviceSerial": serial},
|
|
4164
|
+
retry_401=True,
|
|
4165
|
+
max_retries=max_retries,
|
|
4166
|
+
)
|
|
4167
|
+
self._ensure_ok(json_output, "Could not get socket log info")
|
|
4168
|
+
return json_output
|
|
4169
|
+
|
|
4170
|
+
def linked_cameras(
|
|
4171
|
+
self,
|
|
4172
|
+
serial: str,
|
|
4173
|
+
detector_serial: str,
|
|
4174
|
+
*,
|
|
4175
|
+
max_retries: int = 0,
|
|
4176
|
+
) -> dict:
|
|
4177
|
+
"""List cameras linked to a detector device."""
|
|
4178
|
+
|
|
4179
|
+
params = {
|
|
4180
|
+
"deviceSerial": serial,
|
|
4181
|
+
"detectorDeviceSerial": detector_serial,
|
|
4182
|
+
}
|
|
4183
|
+
json_output = self._request_json(
|
|
4184
|
+
"GET",
|
|
4185
|
+
API_ENDPOINT_DEVICES_ASSOCIATION_LINKED_IPC,
|
|
4186
|
+
params=params,
|
|
4187
|
+
retry_401=True,
|
|
4188
|
+
max_retries=max_retries,
|
|
4189
|
+
)
|
|
4190
|
+
self._ensure_ok(json_output, "Could not get linked cameras")
|
|
4191
|
+
return json_output
|
|
4192
|
+
|
|
4193
|
+
def set_microscope(
|
|
4194
|
+
self,
|
|
4195
|
+
serial: str,
|
|
4196
|
+
multiple: float,
|
|
4197
|
+
x: int,
|
|
4198
|
+
y: int,
|
|
4199
|
+
index: int,
|
|
4200
|
+
*,
|
|
4201
|
+
max_retries: int = 0,
|
|
4202
|
+
) -> dict:
|
|
4203
|
+
"""Configure microscope lens parameters."""
|
|
4204
|
+
|
|
4205
|
+
data = {
|
|
4206
|
+
"multiple": multiple,
|
|
4207
|
+
"x": x,
|
|
4208
|
+
"y": y,
|
|
4209
|
+
"index": index,
|
|
4210
|
+
}
|
|
4211
|
+
json_output = self._request_json(
|
|
4212
|
+
"PUT",
|
|
4213
|
+
f"{API_ENDPOINT_DEVICES}{serial}/microscope",
|
|
4214
|
+
data=data,
|
|
4215
|
+
retry_401=True,
|
|
4216
|
+
max_retries=max_retries,
|
|
4217
|
+
)
|
|
4218
|
+
self._ensure_ok(json_output, "Could not set microscope")
|
|
4219
|
+
return json_output
|
|
4220
|
+
|
|
4221
|
+
def share_accept(
|
|
4222
|
+
self,
|
|
4223
|
+
serial: str,
|
|
4224
|
+
*,
|
|
4225
|
+
max_retries: int = 0,
|
|
4226
|
+
) -> dict:
|
|
4227
|
+
"""Accept a device share invitation."""
|
|
4228
|
+
|
|
4229
|
+
json_output = self._request_json(
|
|
4230
|
+
"POST",
|
|
4231
|
+
API_ENDPOINT_SHARE_ACCEPT,
|
|
4232
|
+
data={"deviceSerial": serial},
|
|
4233
|
+
retry_401=True,
|
|
4234
|
+
max_retries=max_retries,
|
|
4235
|
+
)
|
|
4236
|
+
self._ensure_ok(json_output, "Could not accept share")
|
|
4237
|
+
return json_output
|
|
4238
|
+
|
|
4239
|
+
def share_quit(
|
|
4240
|
+
self,
|
|
4241
|
+
serial: str,
|
|
4242
|
+
*,
|
|
4243
|
+
max_retries: int = 0,
|
|
4244
|
+
) -> dict:
|
|
4245
|
+
"""Leave a shared device."""
|
|
4246
|
+
|
|
4247
|
+
json_output = self._request_json(
|
|
4248
|
+
"DELETE",
|
|
4249
|
+
API_ENDPOINT_SHARE_QUIT,
|
|
4250
|
+
params={"deviceSerial": serial},
|
|
4251
|
+
retry_401=True,
|
|
4252
|
+
max_retries=max_retries,
|
|
4253
|
+
)
|
|
4254
|
+
self._ensure_ok(json_output, "Could not quit share")
|
|
4255
|
+
return json_output
|
|
4256
|
+
|
|
4257
|
+
def send_feedback(
|
|
4258
|
+
self,
|
|
4259
|
+
*,
|
|
4260
|
+
email: str,
|
|
4261
|
+
account: str,
|
|
4262
|
+
score: int,
|
|
4263
|
+
feedback: str,
|
|
4264
|
+
pic_url: str | None = None,
|
|
4265
|
+
max_retries: int = 0,
|
|
4266
|
+
) -> dict:
|
|
4267
|
+
"""Submit feedback to Ezviz support."""
|
|
4268
|
+
|
|
4269
|
+
params: dict[str, Any] = {
|
|
4270
|
+
"email": email,
|
|
4271
|
+
"account": account,
|
|
4272
|
+
"score": score,
|
|
4273
|
+
"feedback": feedback,
|
|
4274
|
+
}
|
|
4275
|
+
if pic_url is not None:
|
|
4276
|
+
params["picUrl"] = pic_url
|
|
4277
|
+
|
|
4278
|
+
json_output = self._request_json(
|
|
4279
|
+
"POST",
|
|
4280
|
+
API_ENDPOINT_FEEDBACK,
|
|
4281
|
+
params=params,
|
|
4282
|
+
retry_401=True,
|
|
4283
|
+
max_retries=max_retries,
|
|
4284
|
+
)
|
|
4285
|
+
self._ensure_ok(json_output, "Could not send feedback")
|
|
4286
|
+
return json_output
|
|
4287
|
+
|
|
4288
|
+
def upload_device_log(
|
|
4289
|
+
self,
|
|
4290
|
+
serial: str,
|
|
4291
|
+
*,
|
|
4292
|
+
max_retries: int = 0,
|
|
4293
|
+
) -> dict:
|
|
4294
|
+
"""Trigger device log upload to Ezviz cloud."""
|
|
4295
|
+
|
|
4296
|
+
json_output = self._request_json(
|
|
4297
|
+
"POST",
|
|
4298
|
+
"/v3/devconfig/dump/app/trigger",
|
|
4299
|
+
data={"deviceSerial": serial},
|
|
4300
|
+
retry_401=True,
|
|
4301
|
+
max_retries=max_retries,
|
|
4302
|
+
)
|
|
4303
|
+
self._ensure_ok(json_output, "Could not upload device log")
|
|
4304
|
+
return json_output
|
|
4305
|
+
|
|
4306
|
+
def alarm_sound(
|
|
4307
|
+
self,
|
|
4308
|
+
serial: str,
|
|
4309
|
+
sound_type: int,
|
|
4310
|
+
enable: int = 1,
|
|
4311
|
+
voice_id: int | None = None,
|
|
4312
|
+
max_retries: int = 0,
|
|
4313
|
+
) -> bool:
|
|
4314
|
+
"""Enable alarm sound by API."""
|
|
4315
|
+
if max_retries > MAX_RETRIES:
|
|
4316
|
+
raise PyEzvizError("Can't gather proper data. Max retries exceeded.")
|
|
4317
|
+
|
|
4318
|
+
if sound_type not in [0, 1, 2]:
|
|
4319
|
+
raise PyEzvizError(
|
|
4320
|
+
"Invalid sound_type, should be 0,1,2: " + str(sound_type)
|
|
4321
|
+
)
|
|
4322
|
+
|
|
4323
|
+
voice_id_value = 0 if voice_id is None else voice_id
|
|
2174
4324
|
|
|
2175
4325
|
response_json = self._request_json(
|
|
2176
4326
|
"PUT",
|
|
@@ -2178,7 +4328,7 @@ class EzvizClient:
|
|
|
2178
4328
|
data={
|
|
2179
4329
|
"enable": enable,
|
|
2180
4330
|
"soundType": sound_type,
|
|
2181
|
-
"voiceId":
|
|
4331
|
+
"voiceId": voice_id_value,
|
|
2182
4332
|
"deviceSerial": serial,
|
|
2183
4333
|
},
|
|
2184
4334
|
retry_401=True,
|