pyezvizapi 1.0.3.0__py3-none-any.whl → 1.0.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pyezvizapi might be problematic. Click here for more details.

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