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/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 = self._session.headers
951
- headers.update({"Content-Type": "application/json"})
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 upgrade_device(self, serial: str, max_retries: int = 0) -> bool:
967
- """Upgrade device firmware."""
968
- json_output = self._request_json(
969
- "PUT",
970
- f"{API_ENDPOINT_UPGRADE_DEVICE}{serial}/0/upgrade",
971
- retry_401=True,
972
- max_retries=max_retries,
973
- )
974
- self._ensure_ok(json_output, "Could not initiate firmware upgrade")
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(self, mode: DefenseModeType, max_retries: int = 0) -> bool:
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={"groupId": -1, "mode": mode},
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 OSD (on screen display) text.
3038
+ """Set or clear the on-screen display text for a camera.
1950
3039
 
1951
3040
  Args:
1952
- serial (str): The camera serial.
1953
- text (str, optional): The osd text to set. The default of "" will clear.
1954
- channel (int, optional): The cammera channel to set this on. Defaults to 1.
1955
- max_retries (int, optional): Number of retries attempted. Defaults to 0.
1956
-
1957
- Raises:
1958
- PyEzvizError: If max retries are exceeded or if the response indicates failure.
1959
- HTTPError: If an HTTP error occurs (other than a 401, which triggers re-login).
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 if the operation was successful.
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": text},
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 alarm_sound(
2164
- self, serial: str, sound_type: int, enable: int = 1, max_retries: int = 0
2165
- ) -> bool:
2166
- """Enable alarm sound by API."""
2167
- if max_retries > MAX_RETRIES:
2168
- raise PyEzvizError("Can't gather proper data. Max retries exceeded.")
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
- if sound_type not in [0, 1, 2]:
2171
- raise PyEzvizError(
2172
- "Invalid sound_type, should be 0,1,2: " + str(sound_type)
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": "0",
4331
+ "voiceId": voice_id_value,
2182
4332
  "deviceSerial": serial,
2183
4333
  },
2184
4334
  retry_401=True,