pyezvizapi 1.0.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pyezvizapi might be problematic. Click here for more details.
- pyezvizapi/__init__.py +54 -0
- pyezvizapi/__main__.py +459 -0
- pyezvizapi/api_endpoints.py +52 -0
- pyezvizapi/camera.py +258 -0
- pyezvizapi/cas.py +169 -0
- pyezvizapi/client.py +2089 -0
- pyezvizapi/constants.py +381 -0
- pyezvizapi/exceptions.py +29 -0
- pyezvizapi/light_bulb.py +126 -0
- pyezvizapi/mqtt.py +259 -0
- pyezvizapi/test_cam_rtsp.py +149 -0
- pyezvizapi/utils.py +160 -0
- pyezvizapi-1.0.0.0.dist-info/LICENSE +201 -0
- pyezvizapi-1.0.0.0.dist-info/LICENSE.md +201 -0
- pyezvizapi-1.0.0.0.dist-info/METADATA +18 -0
- pyezvizapi-1.0.0.0.dist-info/RECORD +19 -0
- pyezvizapi-1.0.0.0.dist-info/WHEEL +5 -0
- pyezvizapi-1.0.0.0.dist-info/entry_points.txt +2 -0
- pyezvizapi-1.0.0.0.dist-info/top_level.txt +1 -0
pyezvizapi/client.py
ADDED
|
@@ -0,0 +1,2089 @@
|
|
|
1
|
+
"""Ezviz API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any
|
|
10
|
+
import urllib.parse
|
|
11
|
+
from uuid import uuid4
|
|
12
|
+
|
|
13
|
+
import requests
|
|
14
|
+
|
|
15
|
+
from .api_endpoints import (
|
|
16
|
+
API_ENDPOINT_ALARM_SOUND,
|
|
17
|
+
API_ENDPOINT_ALARMINFO_GET,
|
|
18
|
+
API_ENDPOINT_CAM_ENCRYPTKEY,
|
|
19
|
+
API_ENDPOINT_CANCEL_ALARM,
|
|
20
|
+
API_ENDPOINT_CHANGE_DEFENCE_STATUS,
|
|
21
|
+
API_ENDPOINT_CREATE_PANORAMIC,
|
|
22
|
+
API_ENDPOINT_DETECTION_SENSIBILITY,
|
|
23
|
+
API_ENDPOINT_DETECTION_SENSIBILITY_GET,
|
|
24
|
+
API_ENDPOINT_DEVCONFIG_BY_KEY,
|
|
25
|
+
API_ENDPOINT_DEVICE_STORAGE_STATUS,
|
|
26
|
+
API_ENDPOINT_DEVICE_SYS_OPERATION,
|
|
27
|
+
API_ENDPOINT_DEVICES,
|
|
28
|
+
API_ENDPOINT_DO_NOT_DISTURB,
|
|
29
|
+
API_ENDPOINT_GROUP_DEFENCE_MODE,
|
|
30
|
+
API_ENDPOINT_IOT_FEATURE,
|
|
31
|
+
API_ENDPOINT_LOGIN,
|
|
32
|
+
API_ENDPOINT_LOGOUT,
|
|
33
|
+
API_ENDPOINT_PAGELIST,
|
|
34
|
+
API_ENDPOINT_PANORAMIC_DEVICES_OPERATION,
|
|
35
|
+
API_ENDPOINT_PTZCONTROL,
|
|
36
|
+
API_ENDPOINT_REFRESH_SESSION_ID,
|
|
37
|
+
API_ENDPOINT_RETURN_PANORAMIC,
|
|
38
|
+
API_ENDPOINT_SEND_CODE,
|
|
39
|
+
API_ENDPOINT_SERVER_INFO,
|
|
40
|
+
API_ENDPOINT_SET_DEFENCE_SCHEDULE,
|
|
41
|
+
API_ENDPOINT_SET_LUMINANCE,
|
|
42
|
+
API_ENDPOINT_SWITCH_DEFENCE_MODE,
|
|
43
|
+
API_ENDPOINT_SWITCH_OTHER,
|
|
44
|
+
API_ENDPOINT_SWITCH_SOUND_ALARM,
|
|
45
|
+
API_ENDPOINT_SWITCH_STATUS,
|
|
46
|
+
API_ENDPOINT_UNIFIEDMSG_LIST_GET,
|
|
47
|
+
API_ENDPOINT_UPGRADE_DEVICE,
|
|
48
|
+
API_ENDPOINT_USER_ID,
|
|
49
|
+
API_ENDPOINT_V3_ALARMS,
|
|
50
|
+
API_ENDPOINT_VIDEO_ENCRYPT,
|
|
51
|
+
)
|
|
52
|
+
from .camera import EzvizCamera
|
|
53
|
+
from .cas import EzvizCAS
|
|
54
|
+
from .constants import (
|
|
55
|
+
DEFAULT_TIMEOUT,
|
|
56
|
+
FEATURE_CODE,
|
|
57
|
+
MAX_RETRIES,
|
|
58
|
+
REQUEST_HEADER,
|
|
59
|
+
DefenseModeType,
|
|
60
|
+
DeviceCatagories,
|
|
61
|
+
DeviceSwitchType,
|
|
62
|
+
MessageFilterType,
|
|
63
|
+
)
|
|
64
|
+
from .exceptions import (
|
|
65
|
+
EzvizAuthTokenExpired,
|
|
66
|
+
EzvizAuthVerificationCode,
|
|
67
|
+
HTTPError,
|
|
68
|
+
InvalidURL,
|
|
69
|
+
PyEzvizError,
|
|
70
|
+
)
|
|
71
|
+
from .light_bulb import EzvizLightBulb
|
|
72
|
+
from .utils import convert_to_dict, deep_merge
|
|
73
|
+
|
|
74
|
+
_LOGGER = logging.getLogger(__name__)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class EzvizClient:
|
|
78
|
+
"""Initialize api client object."""
|
|
79
|
+
|
|
80
|
+
def __init__(
|
|
81
|
+
self,
|
|
82
|
+
account: str | None = None,
|
|
83
|
+
password: str | None = None,
|
|
84
|
+
url: str = "apiieu.ezvizlife.com",
|
|
85
|
+
timeout: int = DEFAULT_TIMEOUT,
|
|
86
|
+
token: dict | None = None,
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Initialize the client object."""
|
|
89
|
+
self.account = account
|
|
90
|
+
self.password = (
|
|
91
|
+
hashlib.md5(password.encode("utf-8")).hexdigest() if password else None
|
|
92
|
+
) # Ezviz API sends md5 of password
|
|
93
|
+
self._session = requests.session()
|
|
94
|
+
self._session.headers.update(REQUEST_HEADER)
|
|
95
|
+
self._session.headers["sessionId"] = token["session_id"] if token else None
|
|
96
|
+
self._token = token or {
|
|
97
|
+
"session_id": None,
|
|
98
|
+
"rf_session_id": None,
|
|
99
|
+
"username": None,
|
|
100
|
+
"api_url": url,
|
|
101
|
+
}
|
|
102
|
+
self._timeout = timeout
|
|
103
|
+
self._cameras: dict[str, Any] = {}
|
|
104
|
+
self._light_bulbs: dict[str, Any] = {}
|
|
105
|
+
|
|
106
|
+
def _login(self, smscode: int | None = None) -> dict[Any, Any]:
|
|
107
|
+
"""Login to Ezviz API."""
|
|
108
|
+
|
|
109
|
+
# Region code to url.
|
|
110
|
+
if len(self._token["api_url"].split(".")) == 1:
|
|
111
|
+
self._token["api_url"] = "apii" + self._token["api_url"] + ".ezvizlife.com"
|
|
112
|
+
|
|
113
|
+
payload = {
|
|
114
|
+
"account": self.account,
|
|
115
|
+
"password": self.password,
|
|
116
|
+
"featureCode": FEATURE_CODE,
|
|
117
|
+
"msgType": "3" if smscode else "0",
|
|
118
|
+
"bizType": "TERMINAL_BIND" if smscode else "",
|
|
119
|
+
"cuName": "SGFzc2lv", # hassio base64 encoded
|
|
120
|
+
"smsCode": smscode,
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
req = self._session.post(
|
|
125
|
+
"https://" + self._token["api_url"] + API_ENDPOINT_LOGIN,
|
|
126
|
+
allow_redirects=False,
|
|
127
|
+
data=payload,
|
|
128
|
+
timeout=self._timeout,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
req.raise_for_status()
|
|
132
|
+
|
|
133
|
+
except requests.ConnectionError as err:
|
|
134
|
+
raise InvalidURL("A Invalid URL or Proxy error occurred") from err
|
|
135
|
+
|
|
136
|
+
except requests.HTTPError as err:
|
|
137
|
+
raise HTTPError from err
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
json_result = req.json()
|
|
141
|
+
|
|
142
|
+
except ValueError as err:
|
|
143
|
+
raise PyEzvizError(
|
|
144
|
+
"Impossible to decode response: "
|
|
145
|
+
+ str(err)
|
|
146
|
+
+ "\nResponse was: "
|
|
147
|
+
+ str(req.text)
|
|
148
|
+
) from err
|
|
149
|
+
|
|
150
|
+
if json_result["meta"]["code"] == 200:
|
|
151
|
+
self._session.headers["sessionId"] = json_result["loginSession"][
|
|
152
|
+
"sessionId"
|
|
153
|
+
]
|
|
154
|
+
self._token = {
|
|
155
|
+
"session_id": str(json_result["loginSession"]["sessionId"]),
|
|
156
|
+
"rf_session_id": str(json_result["loginSession"]["rfSessionId"]),
|
|
157
|
+
"username": str(json_result["loginUser"]["username"]),
|
|
158
|
+
"api_url": str(json_result["loginArea"]["apiDomain"]),
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
self._token["service_urls"] = self.get_service_urls()
|
|
162
|
+
|
|
163
|
+
return self._token
|
|
164
|
+
|
|
165
|
+
if json_result["meta"]["code"] == 1100:
|
|
166
|
+
self._token["api_url"] = json_result["loginArea"]["apiDomain"]
|
|
167
|
+
_LOGGER.warning("Region incorrect!")
|
|
168
|
+
_LOGGER.warning("Your region url: %s", self._token["api_url"])
|
|
169
|
+
return self.login()
|
|
170
|
+
|
|
171
|
+
if json_result["meta"]["code"] == 1012:
|
|
172
|
+
raise PyEzvizError("The MFA code is invalid, please try again.")
|
|
173
|
+
|
|
174
|
+
if json_result["meta"]["code"] == 1013:
|
|
175
|
+
raise PyEzvizError("Incorrect Username.")
|
|
176
|
+
|
|
177
|
+
if json_result["meta"]["code"] == 1014:
|
|
178
|
+
raise PyEzvizError("Incorrect Password.")
|
|
179
|
+
|
|
180
|
+
if json_result["meta"]["code"] == 1015:
|
|
181
|
+
raise PyEzvizError("The user is locked.")
|
|
182
|
+
|
|
183
|
+
if json_result["meta"]["code"] == 6002:
|
|
184
|
+
self.send_mfa_code()
|
|
185
|
+
raise EzvizAuthVerificationCode(
|
|
186
|
+
"MFA enabled on account. Please retry with code."
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
raise PyEzvizError(f"Login error: {json_result['meta']}")
|
|
190
|
+
|
|
191
|
+
def send_mfa_code(self) -> bool:
|
|
192
|
+
"""Send verification code."""
|
|
193
|
+
try:
|
|
194
|
+
req = self._session.post(
|
|
195
|
+
"https://" + self._token["api_url"] + API_ENDPOINT_SEND_CODE,
|
|
196
|
+
data={
|
|
197
|
+
"from": self.account,
|
|
198
|
+
"bizType": "TERMINAL_BIND",
|
|
199
|
+
},
|
|
200
|
+
timeout=self._timeout,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
req.raise_for_status()
|
|
204
|
+
|
|
205
|
+
except requests.HTTPError as err:
|
|
206
|
+
raise HTTPError from err
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
json_output = req.json()
|
|
210
|
+
|
|
211
|
+
except ValueError as err:
|
|
212
|
+
raise PyEzvizError(
|
|
213
|
+
"Impossible to decode response: "
|
|
214
|
+
+ str(err)
|
|
215
|
+
+ "\nResponse was: "
|
|
216
|
+
+ str(req.text)
|
|
217
|
+
) from err
|
|
218
|
+
|
|
219
|
+
if json_output["meta"]["code"] != 200:
|
|
220
|
+
raise PyEzvizError(f"Could not request MFA code: Got {json_output})")
|
|
221
|
+
|
|
222
|
+
return True
|
|
223
|
+
|
|
224
|
+
def get_service_urls(self) -> Any:
|
|
225
|
+
"""Get Ezviz service urls."""
|
|
226
|
+
|
|
227
|
+
if not self._token["session_id"]:
|
|
228
|
+
raise PyEzvizError("No Login token present!")
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
req = self._session.get(
|
|
232
|
+
f"https://{self._token['api_url']}{API_ENDPOINT_SERVER_INFO}",
|
|
233
|
+
timeout=self._timeout,
|
|
234
|
+
)
|
|
235
|
+
req.raise_for_status()
|
|
236
|
+
|
|
237
|
+
except requests.ConnectionError as err:
|
|
238
|
+
raise InvalidURL("A Invalid URL or Proxy error occurred") from err
|
|
239
|
+
|
|
240
|
+
except requests.HTTPError as err:
|
|
241
|
+
raise HTTPError from err
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
json_output = req.json()
|
|
245
|
+
|
|
246
|
+
except ValueError as err:
|
|
247
|
+
raise PyEzvizError(
|
|
248
|
+
"Impossible to decode response: "
|
|
249
|
+
+ str(err)
|
|
250
|
+
+ "\nResponse was: "
|
|
251
|
+
+ str(req.text)
|
|
252
|
+
) from err
|
|
253
|
+
|
|
254
|
+
if json_output["meta"]["code"] != 200:
|
|
255
|
+
raise PyEzvizError(f"Error getting Service URLs: {json_output}")
|
|
256
|
+
|
|
257
|
+
service_urls = json_output["systemConfigInfo"]
|
|
258
|
+
service_urls["sysConf"] = service_urls["sysConf"].split("|")
|
|
259
|
+
|
|
260
|
+
return service_urls
|
|
261
|
+
|
|
262
|
+
def _api_get_pagelist(
|
|
263
|
+
self,
|
|
264
|
+
page_filter: str,
|
|
265
|
+
json_key: str | None = None,
|
|
266
|
+
group_id: int = -1,
|
|
267
|
+
limit: int = 30,
|
|
268
|
+
offset: int = 0,
|
|
269
|
+
max_retries: int = 0,
|
|
270
|
+
) -> Any:
|
|
271
|
+
"""Get data from pagelist API."""
|
|
272
|
+
|
|
273
|
+
if max_retries > MAX_RETRIES:
|
|
274
|
+
raise PyEzvizError("Can't gather proper data. Max retries exceeded.")
|
|
275
|
+
|
|
276
|
+
if page_filter is None:
|
|
277
|
+
raise PyEzvizError("Trying to call get_pagelist without filter")
|
|
278
|
+
|
|
279
|
+
params: dict[str, int | str] = {
|
|
280
|
+
"groupId": group_id,
|
|
281
|
+
"limit": limit,
|
|
282
|
+
"offset": offset,
|
|
283
|
+
"filter": page_filter,
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
try:
|
|
287
|
+
req = self._session.get(
|
|
288
|
+
"https://" + self._token["api_url"] + API_ENDPOINT_PAGELIST,
|
|
289
|
+
params=params,
|
|
290
|
+
timeout=self._timeout,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
req.raise_for_status()
|
|
294
|
+
|
|
295
|
+
except requests.HTTPError as err:
|
|
296
|
+
if err.response.status_code == 401:
|
|
297
|
+
# session is wrong, need to relogin
|
|
298
|
+
self.login()
|
|
299
|
+
return self._api_get_pagelist(
|
|
300
|
+
page_filter, json_key, group_id, limit, offset, max_retries + 1
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
raise HTTPError from err
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
json_output = req.json()
|
|
307
|
+
|
|
308
|
+
except ValueError as err:
|
|
309
|
+
raise PyEzvizError(
|
|
310
|
+
"Impossible to decode response: "
|
|
311
|
+
+ str(err)
|
|
312
|
+
+ "\nResponse was: "
|
|
313
|
+
+ str(req.text)
|
|
314
|
+
) from err
|
|
315
|
+
|
|
316
|
+
if json_output["meta"]["code"] != 200:
|
|
317
|
+
# session is wrong, need to relogin
|
|
318
|
+
self.login()
|
|
319
|
+
_LOGGER.warning(
|
|
320
|
+
"Could not get pagelist, relogging (max retries: %s), got: %s",
|
|
321
|
+
str(max_retries),
|
|
322
|
+
json_output,
|
|
323
|
+
)
|
|
324
|
+
return self._api_get_pagelist(
|
|
325
|
+
page_filter, json_key, group_id, limit, offset, max_retries + 1
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
next_page = json_output["page"].get("hasNext", False)
|
|
329
|
+
|
|
330
|
+
data = json_output[json_key] if json_key else json_output
|
|
331
|
+
|
|
332
|
+
if next_page:
|
|
333
|
+
next_offset = offset + limit
|
|
334
|
+
# Recursive call to fetch next page
|
|
335
|
+
next_data = self._api_get_pagelist(
|
|
336
|
+
page_filter, json_key, group_id, limit, next_offset, max_retries
|
|
337
|
+
)
|
|
338
|
+
# Merge data from next page into current data
|
|
339
|
+
data = deep_merge(data, next_data)
|
|
340
|
+
|
|
341
|
+
return data
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def get_alarminfo(self, serial: str, limit: int = 1, max_retries: int = 0) -> dict:
|
|
345
|
+
"""Get data from alarm info API for camera serial."""
|
|
346
|
+
if max_retries > MAX_RETRIES:
|
|
347
|
+
raise PyEzvizError("Can't gather proper data. Max retries exceeded.")
|
|
348
|
+
|
|
349
|
+
params: dict[str, int | str] = {
|
|
350
|
+
"deviceSerials": serial,
|
|
351
|
+
"queryType": -1,
|
|
352
|
+
"limit": limit,
|
|
353
|
+
"stype": -1,
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
try:
|
|
357
|
+
req = self._session.get(
|
|
358
|
+
"https://" + self._token["api_url"] + API_ENDPOINT_ALARMINFO_GET,
|
|
359
|
+
params=params,
|
|
360
|
+
timeout=self._timeout,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
req.raise_for_status()
|
|
364
|
+
|
|
365
|
+
except requests.HTTPError as err:
|
|
366
|
+
if err.response.status_code == 401:
|
|
367
|
+
# session is wrong, need to relogin
|
|
368
|
+
self.login()
|
|
369
|
+
return self.get_alarminfo(serial, limit, max_retries + 1)
|
|
370
|
+
|
|
371
|
+
raise HTTPError from err
|
|
372
|
+
|
|
373
|
+
try:
|
|
374
|
+
json_output: dict = req.json()
|
|
375
|
+
|
|
376
|
+
except ValueError as err:
|
|
377
|
+
raise PyEzvizError(
|
|
378
|
+
"Impossible to decode response: "
|
|
379
|
+
+ str(err)
|
|
380
|
+
+ "\nResponse was: "
|
|
381
|
+
+ str(req.text)
|
|
382
|
+
) from err
|
|
383
|
+
|
|
384
|
+
if json_output["meta"]["code"] != 200:
|
|
385
|
+
if json_output["meta"]["code"] == 500:
|
|
386
|
+
_LOGGER.debug(
|
|
387
|
+
"Retry getting alarm info, server returned busy: %s",
|
|
388
|
+
json_output,
|
|
389
|
+
)
|
|
390
|
+
return self.get_alarminfo(serial, limit, max_retries + 1)
|
|
391
|
+
|
|
392
|
+
raise PyEzvizError(f"Could not get data from alarm api: Got {json_output})")
|
|
393
|
+
|
|
394
|
+
return json_output
|
|
395
|
+
|
|
396
|
+
def get_device_messages_list(
|
|
397
|
+
self,
|
|
398
|
+
serials: str | None = None,
|
|
399
|
+
s_type: int = MessageFilterType.FILTER_TYPE_ALL_ALARM.value,
|
|
400
|
+
limit: int | None = 20, # 50 is the max even if you set it higher
|
|
401
|
+
date: str = datetime.today().strftime("%Y%m%d"),
|
|
402
|
+
end_time: str | None = None,
|
|
403
|
+
tags: str = "ALL",
|
|
404
|
+
max_retries: int = 0,
|
|
405
|
+
) -> dict:
|
|
406
|
+
"""Get data from Unified message list API."""
|
|
407
|
+
if max_retries > MAX_RETRIES:
|
|
408
|
+
raise PyEzvizError("Can't gather proper data. Max retries exceeded.")
|
|
409
|
+
|
|
410
|
+
params: dict[str, int | str | None] = {
|
|
411
|
+
"serials:": serials,
|
|
412
|
+
"stype": s_type,
|
|
413
|
+
"limit": limit,
|
|
414
|
+
"date": date,
|
|
415
|
+
"endTime": end_time,
|
|
416
|
+
"tags": tags,
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
try:
|
|
420
|
+
req = self._session.get(
|
|
421
|
+
"https://" + self._token["api_url"] + API_ENDPOINT_UNIFIEDMSG_LIST_GET,
|
|
422
|
+
params=params,
|
|
423
|
+
timeout=self._timeout,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
req.raise_for_status()
|
|
427
|
+
|
|
428
|
+
except requests.HTTPError as err:
|
|
429
|
+
if err.response.status_code == 401:
|
|
430
|
+
# session is wrong, need to relogin
|
|
431
|
+
self.login()
|
|
432
|
+
return self.get_device_messages_list(
|
|
433
|
+
serials, s_type, limit, date, end_time, tags, max_retries + 1
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
raise HTTPError from err
|
|
437
|
+
|
|
438
|
+
try:
|
|
439
|
+
json_output: dict = req.json()
|
|
440
|
+
|
|
441
|
+
except ValueError as err:
|
|
442
|
+
raise PyEzvizError(
|
|
443
|
+
"Impossible to decode response: "
|
|
444
|
+
+ str(err)
|
|
445
|
+
+ "\nResponse was: "
|
|
446
|
+
+ str(req.text)
|
|
447
|
+
) from err
|
|
448
|
+
|
|
449
|
+
if json_output["meta"]["code"] != 200:
|
|
450
|
+
raise PyEzvizError(
|
|
451
|
+
f"Could not get unified message list: Got {json_output})"
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
return json_output
|
|
455
|
+
|
|
456
|
+
def switch_status(
|
|
457
|
+
self,
|
|
458
|
+
serial: str,
|
|
459
|
+
status_type: int,
|
|
460
|
+
enable: int,
|
|
461
|
+
channel_no: int = 0,
|
|
462
|
+
max_retries: int = 0,
|
|
463
|
+
) -> bool:
|
|
464
|
+
"""Camera features are represented as switches. Switch them on or off."""
|
|
465
|
+
if max_retries > MAX_RETRIES:
|
|
466
|
+
raise PyEzvizError("Can't gather proper data. Max retries exceeded.")
|
|
467
|
+
|
|
468
|
+
try:
|
|
469
|
+
req = self._session.put(
|
|
470
|
+
url=f"https://{self._token['api_url']}{API_ENDPOINT_DEVICES}{serial}/{channel_no}/{enable}/{status_type}{API_ENDPOINT_SWITCH_STATUS}",
|
|
471
|
+
timeout=self._timeout,
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
req.raise_for_status()
|
|
475
|
+
|
|
476
|
+
except requests.HTTPError as err:
|
|
477
|
+
if err.response.status_code == 401:
|
|
478
|
+
# session is wrong, need to relogin
|
|
479
|
+
self.login()
|
|
480
|
+
return self.switch_status(serial, status_type, enable, max_retries + 1)
|
|
481
|
+
|
|
482
|
+
raise HTTPError from err
|
|
483
|
+
|
|
484
|
+
try:
|
|
485
|
+
json_output = req.json()
|
|
486
|
+
|
|
487
|
+
except ValueError as err:
|
|
488
|
+
raise PyEzvizError(
|
|
489
|
+
"Impossible to decode response: "
|
|
490
|
+
+ str(err)
|
|
491
|
+
+ "\nResponse was: "
|
|
492
|
+
+ str(req.text)
|
|
493
|
+
) from err
|
|
494
|
+
|
|
495
|
+
if json_output["meta"]["code"] != 200:
|
|
496
|
+
raise PyEzvizError(f"Could not set the switch: Got {json_output})")
|
|
497
|
+
|
|
498
|
+
if self._cameras.get(serial):
|
|
499
|
+
self._cameras[serial]["switches"][status_type] = bool(enable)
|
|
500
|
+
|
|
501
|
+
return True
|
|
502
|
+
|
|
503
|
+
def switch_status_other(
|
|
504
|
+
self,
|
|
505
|
+
serial: str,
|
|
506
|
+
status_type: int,
|
|
507
|
+
enable: int,
|
|
508
|
+
channel_number: int = 1,
|
|
509
|
+
max_retries: int = 0,
|
|
510
|
+
) -> bool:
|
|
511
|
+
"""Features are represented as switches. This api is for alternative switch types to turn them on or off.
|
|
512
|
+
|
|
513
|
+
All day recording is a good example.
|
|
514
|
+
"""
|
|
515
|
+
if max_retries > MAX_RETRIES:
|
|
516
|
+
raise PyEzvizError("Can't gather proper data. Max retries exceeded.")
|
|
517
|
+
|
|
518
|
+
try:
|
|
519
|
+
req = self._session.put(
|
|
520
|
+
url=f"https://{self._token['api_url']}{API_ENDPOINT_DEVICES}{serial}{API_ENDPOINT_SWITCH_OTHER}",
|
|
521
|
+
timeout=self._timeout,
|
|
522
|
+
params={
|
|
523
|
+
"channelNo": channel_number,
|
|
524
|
+
"enable": enable,
|
|
525
|
+
"switchType": status_type,
|
|
526
|
+
},
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
req.raise_for_status()
|
|
530
|
+
|
|
531
|
+
except requests.HTTPError as err:
|
|
532
|
+
if err.response.status_code == 401:
|
|
533
|
+
# session is wrong, need to relogin
|
|
534
|
+
self.login()
|
|
535
|
+
return self.switch_status_other(
|
|
536
|
+
serial, status_type, enable, channel_number, max_retries + 1
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
raise HTTPError from err
|
|
540
|
+
|
|
541
|
+
try:
|
|
542
|
+
json_output = req.json()
|
|
543
|
+
|
|
544
|
+
except ValueError as err:
|
|
545
|
+
raise PyEzvizError(
|
|
546
|
+
"Impossible to decode response: "
|
|
547
|
+
+ str(err)
|
|
548
|
+
+ "\nResponse was: "
|
|
549
|
+
+ str(req.text)
|
|
550
|
+
) from err
|
|
551
|
+
|
|
552
|
+
if json_output["meta"]["code"] != 200:
|
|
553
|
+
raise PyEzvizError(f"Could not set the switch: Got {json_output})")
|
|
554
|
+
|
|
555
|
+
return True
|
|
556
|
+
|
|
557
|
+
def set_camera_defence(
|
|
558
|
+
self,
|
|
559
|
+
serial: str,
|
|
560
|
+
enable: int,
|
|
561
|
+
channel_no: int = 1,
|
|
562
|
+
arm_type: str = "Global",
|
|
563
|
+
actor: str = "V",
|
|
564
|
+
max_retries: int = 0,
|
|
565
|
+
) -> bool:
|
|
566
|
+
"""Enable/Disable motion detection on camera."""
|
|
567
|
+
|
|
568
|
+
if max_retries > MAX_RETRIES:
|
|
569
|
+
raise PyEzvizError("Can't gather proper data. Max retries exceeded.")
|
|
570
|
+
|
|
571
|
+
try:
|
|
572
|
+
req = self._session.put(
|
|
573
|
+
url=f"https://{self._token['api_url']}{API_ENDPOINT_DEVICES}{serial}/{channel_no}/{API_ENDPOINT_CHANGE_DEFENCE_STATUS}",
|
|
574
|
+
timeout=self._timeout,
|
|
575
|
+
data={
|
|
576
|
+
"type": arm_type,
|
|
577
|
+
"status": enable,
|
|
578
|
+
"actor": actor,
|
|
579
|
+
},
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
req.raise_for_status()
|
|
583
|
+
|
|
584
|
+
except requests.HTTPError as err:
|
|
585
|
+
if err.response.status_code == 401:
|
|
586
|
+
# session is wrong, need to relogin
|
|
587
|
+
self.login()
|
|
588
|
+
return self.set_camera_defence(serial, enable, max_retries + 1)
|
|
589
|
+
|
|
590
|
+
raise HTTPError from err
|
|
591
|
+
|
|
592
|
+
try:
|
|
593
|
+
json_output = req.json()
|
|
594
|
+
|
|
595
|
+
except ValueError as err:
|
|
596
|
+
raise PyEzvizError(
|
|
597
|
+
"Impossible to decode response: "
|
|
598
|
+
+ str(err)
|
|
599
|
+
+ "\nResponse was: "
|
|
600
|
+
+ str(req.text)
|
|
601
|
+
) from err
|
|
602
|
+
|
|
603
|
+
if json_output["meta"]["code"] != 200:
|
|
604
|
+
if json_output["meta"]["code"] == 504:
|
|
605
|
+
_LOGGER.warning(
|
|
606
|
+
"Arm or disarm for camera %s timed out. Retrying %s of %s",
|
|
607
|
+
serial,
|
|
608
|
+
max_retries,
|
|
609
|
+
MAX_RETRIES,
|
|
610
|
+
)
|
|
611
|
+
return self.set_camera_defence(serial, enable, max_retries + 1)
|
|
612
|
+
|
|
613
|
+
raise PyEzvizError(
|
|
614
|
+
f"Could not arm or disarm Camera {serial}: Got {json_output})"
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
return True
|
|
618
|
+
|
|
619
|
+
def set_battery_camera_work_mode(self, serial: str, value: int) -> bool:
|
|
620
|
+
"""Set battery camera work mode."""
|
|
621
|
+
return self.set_device_config_by_key(serial, value, key="batteryCameraWorkMode")
|
|
622
|
+
|
|
623
|
+
def set_detection_mode(self, serial: str, value: int) -> bool:
|
|
624
|
+
"""Set detection mode."""
|
|
625
|
+
return self.set_device_config_by_key(
|
|
626
|
+
serial, value=f'{{"type":{value}}}', key="Alarm_DetectHumanCar"
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
def set_night_vision_mode(
|
|
630
|
+
self, serial: str, mode: int, luminance: int = 100
|
|
631
|
+
) -> bool:
|
|
632
|
+
"""Set night vision mode."""
|
|
633
|
+
return self.set_device_config_by_key(
|
|
634
|
+
serial,
|
|
635
|
+
value=f'{{"graphicType":{mode},"luminance":{luminance}}}',
|
|
636
|
+
key="NightVision_Model",
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
def set_display_mode(self, serial: str, mode: int) -> bool:
|
|
640
|
+
"""Change video color and saturation mode."""
|
|
641
|
+
return self.set_device_config_by_key(
|
|
642
|
+
serial, value=f'{{"mode":{mode}}}', key="display_mode"
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
def set_device_config_by_key(
|
|
646
|
+
self,
|
|
647
|
+
serial: str,
|
|
648
|
+
value: Any,
|
|
649
|
+
key: str,
|
|
650
|
+
max_retries: int = 0,
|
|
651
|
+
) -> bool:
|
|
652
|
+
"""Change value on device by setting key."""
|
|
653
|
+
if max_retries > MAX_RETRIES:
|
|
654
|
+
raise PyEzvizError("Can't gather proper data. Max retries exceeded.")
|
|
655
|
+
|
|
656
|
+
params = {"key": key, "value": value}
|
|
657
|
+
params_str = urllib.parse.urlencode(
|
|
658
|
+
params, safe="}{:"
|
|
659
|
+
) # not encode curly braces and colon
|
|
660
|
+
|
|
661
|
+
full_url = f'https://{self._token["api_url"]}{API_ENDPOINT_DEVCONFIG_BY_KEY}{serial}/1/op'
|
|
662
|
+
|
|
663
|
+
# EZVIZ api request needs {}: in the url, but requests lib doesn't allow it
|
|
664
|
+
# so we need to manually prepare it
|
|
665
|
+
req_prep = requests.Request(
|
|
666
|
+
method="PUT", url=full_url, headers=self._session.headers
|
|
667
|
+
).prepare()
|
|
668
|
+
req_prep.url = full_url + "?" + params_str
|
|
669
|
+
|
|
670
|
+
try:
|
|
671
|
+
req = self._session.send(
|
|
672
|
+
request=req_prep,
|
|
673
|
+
timeout=self._timeout,
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
req.raise_for_status()
|
|
677
|
+
|
|
678
|
+
except requests.HTTPError as err:
|
|
679
|
+
if err.response.status_code == 401:
|
|
680
|
+
# session is wrong, need to relogin
|
|
681
|
+
self.login()
|
|
682
|
+
return self.set_device_config_by_key(
|
|
683
|
+
serial, value, key, max_retries + 1
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
raise HTTPError from err
|
|
687
|
+
|
|
688
|
+
try:
|
|
689
|
+
json_output = req.json()
|
|
690
|
+
|
|
691
|
+
except ValueError as err:
|
|
692
|
+
raise PyEzvizError(
|
|
693
|
+
"Impossible to decode response: "
|
|
694
|
+
+ str(err)
|
|
695
|
+
+ "\nResponse was: "
|
|
696
|
+
+ str(req.text)
|
|
697
|
+
) from err
|
|
698
|
+
|
|
699
|
+
if json_output["meta"]["code"] != 200:
|
|
700
|
+
raise PyEzvizError(f"Could not set config key '${key}': Got {json_output})")
|
|
701
|
+
|
|
702
|
+
return True
|
|
703
|
+
|
|
704
|
+
def set_device_feature_by_key(
|
|
705
|
+
self,
|
|
706
|
+
serial: str,
|
|
707
|
+
product_id: str,
|
|
708
|
+
value: Any,
|
|
709
|
+
key: str,
|
|
710
|
+
max_retries: int = 0,
|
|
711
|
+
) -> bool:
|
|
712
|
+
"""Change value on device by setting the iot-feature's key.
|
|
713
|
+
|
|
714
|
+
The FEATURE key that is part of 'device info' holds
|
|
715
|
+
information about the device's functions (for example light_switch, brightness etc.).
|
|
716
|
+
"""
|
|
717
|
+
if max_retries > MAX_RETRIES:
|
|
718
|
+
raise PyEzvizError("Can't gather proper data. Max retries exceeded.")
|
|
719
|
+
|
|
720
|
+
payload = json.dumps({"itemKey": key, "productId": product_id, "value": value})
|
|
721
|
+
|
|
722
|
+
full_url = f'https://{self._token["api_url"]}{API_ENDPOINT_IOT_FEATURE}{serial.upper()}/0'
|
|
723
|
+
|
|
724
|
+
headers = self._session.headers
|
|
725
|
+
headers.update({"Content-Type": "application/json"})
|
|
726
|
+
|
|
727
|
+
req_prep = requests.Request(
|
|
728
|
+
method="PUT", url=full_url, headers=headers, data=payload
|
|
729
|
+
).prepare()
|
|
730
|
+
|
|
731
|
+
try:
|
|
732
|
+
req = self._session.send(
|
|
733
|
+
request=req_prep,
|
|
734
|
+
timeout=self._timeout,
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
req.raise_for_status()
|
|
738
|
+
|
|
739
|
+
except requests.HTTPError as err:
|
|
740
|
+
if err.response.status_code == 401:
|
|
741
|
+
# session is wrong, need to relogin
|
|
742
|
+
self.login()
|
|
743
|
+
return self.set_device_feature_by_key(
|
|
744
|
+
serial, product_id, value, key, max_retries + 1
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
raise HTTPError from err
|
|
748
|
+
|
|
749
|
+
try:
|
|
750
|
+
json_output = req.json()
|
|
751
|
+
|
|
752
|
+
except ValueError as err:
|
|
753
|
+
raise PyEzvizError(
|
|
754
|
+
"Impossible to decode response: "
|
|
755
|
+
+ str(err)
|
|
756
|
+
+ "\nResponse was: "
|
|
757
|
+
+ str(req.text)
|
|
758
|
+
) from err
|
|
759
|
+
|
|
760
|
+
if json_output["meta"]["code"] != 200:
|
|
761
|
+
raise PyEzvizError(
|
|
762
|
+
f"Could not set iot-feature key '${key}': Got {json_output})"
|
|
763
|
+
)
|
|
764
|
+
|
|
765
|
+
return True
|
|
766
|
+
|
|
767
|
+
def upgrade_device(self, serial: str, max_retries: int = 0) -> bool:
|
|
768
|
+
"""Upgrade device firmware."""
|
|
769
|
+
if max_retries > MAX_RETRIES:
|
|
770
|
+
raise PyEzvizError("Can't gather proper data. Max retries exceeded.")
|
|
771
|
+
|
|
772
|
+
try:
|
|
773
|
+
req = self._session.put(
|
|
774
|
+
"https://"
|
|
775
|
+
+ self._token["api_url"]
|
|
776
|
+
+ API_ENDPOINT_UPGRADE_DEVICE
|
|
777
|
+
+ serial
|
|
778
|
+
+ "/0/upgrade",
|
|
779
|
+
timeout=self._timeout,
|
|
780
|
+
)
|
|
781
|
+
|
|
782
|
+
req.raise_for_status()
|
|
783
|
+
|
|
784
|
+
except requests.HTTPError as err:
|
|
785
|
+
if err.response.status_code == 401:
|
|
786
|
+
# session is wrong, need to relogin
|
|
787
|
+
self.login()
|
|
788
|
+
return self.upgrade_device(serial, max_retries + 1)
|
|
789
|
+
|
|
790
|
+
raise HTTPError from err
|
|
791
|
+
|
|
792
|
+
try:
|
|
793
|
+
json_output = req.json()
|
|
794
|
+
|
|
795
|
+
except ValueError as err:
|
|
796
|
+
raise PyEzvizError(
|
|
797
|
+
"Impossible to decode response: "
|
|
798
|
+
+ str(err)
|
|
799
|
+
+ "\nResponse was: "
|
|
800
|
+
+ str(req.text)
|
|
801
|
+
) from err
|
|
802
|
+
|
|
803
|
+
if json_output["meta"]["code"] != 200:
|
|
804
|
+
raise PyEzvizError(
|
|
805
|
+
f"Could not initiate firmware upgrade: Got {json_output})"
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
return True
|
|
809
|
+
|
|
810
|
+
def get_storage_status(self, serial: str, max_retries: int = 0) -> Any:
|
|
811
|
+
"""Get device storage status."""
|
|
812
|
+
if max_retries > MAX_RETRIES:
|
|
813
|
+
raise PyEzvizError("Can't gather proper data. Max retries exceeded.")
|
|
814
|
+
|
|
815
|
+
try:
|
|
816
|
+
req = self._session.post(
|
|
817
|
+
url=f"https://{self._token['api_url']}{API_ENDPOINT_DEVICE_STORAGE_STATUS}",
|
|
818
|
+
data={"subSerial": serial},
|
|
819
|
+
timeout=self._timeout,
|
|
820
|
+
)
|
|
821
|
+
|
|
822
|
+
req.raise_for_status()
|
|
823
|
+
|
|
824
|
+
except requests.HTTPError as err:
|
|
825
|
+
if err.response.status_code == 401:
|
|
826
|
+
# session is wrong, need to relogin
|
|
827
|
+
self.login()
|
|
828
|
+
return self.get_storage_status(serial, max_retries + 1)
|
|
829
|
+
|
|
830
|
+
raise HTTPError from err
|
|
831
|
+
|
|
832
|
+
try:
|
|
833
|
+
json_output = req.json()
|
|
834
|
+
|
|
835
|
+
except ValueError as err:
|
|
836
|
+
raise PyEzvizError(
|
|
837
|
+
"Impossible to decode response: "
|
|
838
|
+
+ str(err)
|
|
839
|
+
+ "\nResponse was: "
|
|
840
|
+
+ str(req.text)
|
|
841
|
+
) from err
|
|
842
|
+
|
|
843
|
+
if json_output["resultCode"] != "0":
|
|
844
|
+
if json_output["resultCode"] == "-1":
|
|
845
|
+
_LOGGER.warning(
|
|
846
|
+
"Can't get storage status from device %s, retrying %s of %s",
|
|
847
|
+
serial,
|
|
848
|
+
max_retries,
|
|
849
|
+
MAX_RETRIES,
|
|
850
|
+
)
|
|
851
|
+
return self.get_storage_status(serial, max_retries + 1)
|
|
852
|
+
raise PyEzvizError(
|
|
853
|
+
f"Could not get device storage status: Got {json_output})"
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
return json_output["storageStatus"]
|
|
857
|
+
|
|
858
|
+
def sound_alarm(self, serial: str, enable: int = 1, max_retries: int = 0) -> bool:
|
|
859
|
+
"""Sound alarm on a device."""
|
|
860
|
+
if max_retries > MAX_RETRIES:
|
|
861
|
+
raise PyEzvizError("Can't gather proper data. Max retries exceeded.")
|
|
862
|
+
|
|
863
|
+
try:
|
|
864
|
+
req = self._session.put(
|
|
865
|
+
"https://"
|
|
866
|
+
+ self._token["api_url"]
|
|
867
|
+
+ API_ENDPOINT_DEVICES
|
|
868
|
+
+ serial
|
|
869
|
+
+ "/0"
|
|
870
|
+
+ API_ENDPOINT_SWITCH_SOUND_ALARM,
|
|
871
|
+
data={
|
|
872
|
+
"enable": enable,
|
|
873
|
+
},
|
|
874
|
+
timeout=self._timeout,
|
|
875
|
+
)
|
|
876
|
+
|
|
877
|
+
req.raise_for_status()
|
|
878
|
+
|
|
879
|
+
except requests.HTTPError as err:
|
|
880
|
+
if err.response.status_code == 401:
|
|
881
|
+
# session is wrong, need to relogin
|
|
882
|
+
self.login()
|
|
883
|
+
return self.sound_alarm(serial, enable, max_retries + 1)
|
|
884
|
+
|
|
885
|
+
raise HTTPError from err
|
|
886
|
+
|
|
887
|
+
try:
|
|
888
|
+
json_output = req.json()
|
|
889
|
+
|
|
890
|
+
except ValueError as err:
|
|
891
|
+
raise PyEzvizError(
|
|
892
|
+
"Impossible to decode response: "
|
|
893
|
+
+ str(err)
|
|
894
|
+
+ "\nResponse was: "
|
|
895
|
+
+ str(req.text)
|
|
896
|
+
) from err
|
|
897
|
+
|
|
898
|
+
if json_output["meta"]["code"] != 200:
|
|
899
|
+
raise PyEzvizError(f"Could not set the alarm sound: Got {json_output})")
|
|
900
|
+
|
|
901
|
+
return True
|
|
902
|
+
|
|
903
|
+
def get_user_id(self, max_retries: int = 0) -> Any:
|
|
904
|
+
"""Get Ezviz userid, used by restricted api endpoints."""
|
|
905
|
+
|
|
906
|
+
if max_retries > MAX_RETRIES:
|
|
907
|
+
raise PyEzvizError("Can't gather proper data. Max retries exceeded.")
|
|
908
|
+
|
|
909
|
+
try:
|
|
910
|
+
req = self._session.get(
|
|
911
|
+
f"https://{self._token['api_url']}{API_ENDPOINT_USER_ID}",
|
|
912
|
+
timeout=self._timeout,
|
|
913
|
+
)
|
|
914
|
+
req.raise_for_status()
|
|
915
|
+
|
|
916
|
+
except requests.HTTPError as err:
|
|
917
|
+
if err.response.status_code == 401:
|
|
918
|
+
# session is wrong, need to relogin
|
|
919
|
+
self.login()
|
|
920
|
+
return self.get_user_id(max_retries + 1)
|
|
921
|
+
|
|
922
|
+
raise HTTPError from err
|
|
923
|
+
|
|
924
|
+
try:
|
|
925
|
+
json_output = req.json()
|
|
926
|
+
|
|
927
|
+
except ValueError as err:
|
|
928
|
+
raise PyEzvizError(
|
|
929
|
+
"Impossible to decode response: "
|
|
930
|
+
+ str(err)
|
|
931
|
+
+ "\nResponse was: "
|
|
932
|
+
+ str(req.text)
|
|
933
|
+
) from err
|
|
934
|
+
|
|
935
|
+
if json_output["meta"]["code"] != 200:
|
|
936
|
+
raise PyEzvizError(f"Could get user id, Got: {json_output})")
|
|
937
|
+
|
|
938
|
+
return json_output["deviceTokenInfo"]
|
|
939
|
+
|
|
940
|
+
def set_video_enc(
|
|
941
|
+
self,
|
|
942
|
+
serial: str,
|
|
943
|
+
enable: int = 1,
|
|
944
|
+
camera_verification_code: str | None = None,
|
|
945
|
+
new_password: str | None = None,
|
|
946
|
+
old_password: str | None = None,
|
|
947
|
+
max_retries: int = 0,
|
|
948
|
+
) -> bool:
|
|
949
|
+
"""Enable or Disable video encryption."""
|
|
950
|
+
if max_retries > MAX_RETRIES:
|
|
951
|
+
raise PyEzvizError("Can't gather proper data. Max retries exceeded.")
|
|
952
|
+
|
|
953
|
+
device_token_info = self.get_user_id()
|
|
954
|
+
cookies = {
|
|
955
|
+
"clientType": "3",
|
|
956
|
+
"clientVersion": "5.12.1.0517",
|
|
957
|
+
"userId": device_token_info["userId"],
|
|
958
|
+
"ASG_DisplayName": "home",
|
|
959
|
+
"C_SS": self._session.headers["sessionId"],
|
|
960
|
+
"lang": "en_US",
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
try:
|
|
964
|
+
req = self._session.put(
|
|
965
|
+
"https://"
|
|
966
|
+
+ self._token["api_url"]
|
|
967
|
+
+ API_ENDPOINT_DEVICES
|
|
968
|
+
+ API_ENDPOINT_VIDEO_ENCRYPT,
|
|
969
|
+
data={
|
|
970
|
+
"deviceSerial": serial,
|
|
971
|
+
"isEncrypt": enable,
|
|
972
|
+
"oldPassword": old_password,
|
|
973
|
+
"password": new_password,
|
|
974
|
+
"featureCode": FEATURE_CODE,
|
|
975
|
+
"validateCode": camera_verification_code,
|
|
976
|
+
"msgType": -1,
|
|
977
|
+
},
|
|
978
|
+
cookies=cookies,
|
|
979
|
+
timeout=self._timeout,
|
|
980
|
+
)
|
|
981
|
+
|
|
982
|
+
req.raise_for_status()
|
|
983
|
+
|
|
984
|
+
except requests.HTTPError as err:
|
|
985
|
+
if err.response.status_code == 401:
|
|
986
|
+
# session is wrong, need to relogin
|
|
987
|
+
self.login()
|
|
988
|
+
return self.set_video_enc(
|
|
989
|
+
serial,
|
|
990
|
+
enable,
|
|
991
|
+
camera_verification_code,
|
|
992
|
+
new_password,
|
|
993
|
+
old_password,
|
|
994
|
+
max_retries + 1,
|
|
995
|
+
)
|
|
996
|
+
|
|
997
|
+
raise HTTPError from err
|
|
998
|
+
|
|
999
|
+
try:
|
|
1000
|
+
json_output = req.json()
|
|
1001
|
+
|
|
1002
|
+
except ValueError as err:
|
|
1003
|
+
raise PyEzvizError(
|
|
1004
|
+
"Impossible to decode response: "
|
|
1005
|
+
+ str(err)
|
|
1006
|
+
+ "\nResponse was: "
|
|
1007
|
+
+ str(req.text)
|
|
1008
|
+
) from err
|
|
1009
|
+
|
|
1010
|
+
if json_output["meta"]["code"] != 200:
|
|
1011
|
+
raise PyEzvizError(f"Could not set video encryption: Got {json_output})")
|
|
1012
|
+
|
|
1013
|
+
return True
|
|
1014
|
+
|
|
1015
|
+
def reboot_camera(
|
|
1016
|
+
self,
|
|
1017
|
+
serial: str,
|
|
1018
|
+
delay: int = 1,
|
|
1019
|
+
operation: int = 1,
|
|
1020
|
+
max_retries: int = 0,
|
|
1021
|
+
) -> bool:
|
|
1022
|
+
"""Reboot camera."""
|
|
1023
|
+
if max_retries > MAX_RETRIES:
|
|
1024
|
+
raise PyEzvizError("Can't gather proper data. Max retries exceeded.")
|
|
1025
|
+
|
|
1026
|
+
try:
|
|
1027
|
+
req = self._session.post(
|
|
1028
|
+
url=f'https://{self._token["api_url"]}{API_ENDPOINT_DEVICE_SYS_OPERATION}{serial}',
|
|
1029
|
+
data={
|
|
1030
|
+
"oper": operation,
|
|
1031
|
+
"deviceSerial": serial,
|
|
1032
|
+
"delay": delay,
|
|
1033
|
+
},
|
|
1034
|
+
timeout=self._timeout,
|
|
1035
|
+
)
|
|
1036
|
+
|
|
1037
|
+
req.raise_for_status()
|
|
1038
|
+
|
|
1039
|
+
except requests.HTTPError as err:
|
|
1040
|
+
if err.response.status_code == 401:
|
|
1041
|
+
# session is wrong, need to relogin
|
|
1042
|
+
self.login()
|
|
1043
|
+
return self.reboot_camera(
|
|
1044
|
+
serial,
|
|
1045
|
+
delay,
|
|
1046
|
+
operation,
|
|
1047
|
+
max_retries + 1,
|
|
1048
|
+
)
|
|
1049
|
+
|
|
1050
|
+
raise HTTPError from err
|
|
1051
|
+
|
|
1052
|
+
try:
|
|
1053
|
+
json_output = req.json()
|
|
1054
|
+
|
|
1055
|
+
except ValueError as err:
|
|
1056
|
+
raise PyEzvizError(
|
|
1057
|
+
"Impossible to decode response: "
|
|
1058
|
+
+ str(err)
|
|
1059
|
+
+ "\nResponse was: "
|
|
1060
|
+
+ str(req.text)
|
|
1061
|
+
) from err
|
|
1062
|
+
|
|
1063
|
+
if json_output["resultCode"] != "0":
|
|
1064
|
+
if json_output["resultCode"] == "-1":
|
|
1065
|
+
_LOGGER.warning(
|
|
1066
|
+
"Unable to reboot camera, camera %s is unreachable, retrying %s of %s",
|
|
1067
|
+
serial,
|
|
1068
|
+
max_retries,
|
|
1069
|
+
MAX_RETRIES,
|
|
1070
|
+
)
|
|
1071
|
+
return self.reboot_camera(serial, delay, operation, max_retries + 1)
|
|
1072
|
+
raise PyEzvizError(f"Could not reboot device {json_output})")
|
|
1073
|
+
|
|
1074
|
+
return True
|
|
1075
|
+
|
|
1076
|
+
def get_group_defence_mode(self, max_retries: int = 0) -> Any:
|
|
1077
|
+
"""Get group arm status. The alarm arm/disarm concept on 1st page of app."""
|
|
1078
|
+
|
|
1079
|
+
if max_retries > MAX_RETRIES:
|
|
1080
|
+
raise PyEzvizError("Can't gather proper data. Max retries exceeded.")
|
|
1081
|
+
|
|
1082
|
+
try:
|
|
1083
|
+
req = self._session.get(
|
|
1084
|
+
"https://" + self._token["api_url"] + API_ENDPOINT_GROUP_DEFENCE_MODE,
|
|
1085
|
+
params={
|
|
1086
|
+
"groupId": -1,
|
|
1087
|
+
},
|
|
1088
|
+
timeout=self._timeout,
|
|
1089
|
+
)
|
|
1090
|
+
|
|
1091
|
+
req.raise_for_status()
|
|
1092
|
+
|
|
1093
|
+
except requests.HTTPError as err:
|
|
1094
|
+
if err.response.status_code == 401:
|
|
1095
|
+
# session is wrong, need to relogin
|
|
1096
|
+
self.login()
|
|
1097
|
+
return self.get_group_defence_mode(max_retries + 1)
|
|
1098
|
+
|
|
1099
|
+
raise HTTPError from err
|
|
1100
|
+
|
|
1101
|
+
try:
|
|
1102
|
+
json_output = req.json()
|
|
1103
|
+
|
|
1104
|
+
except ValueError as err:
|
|
1105
|
+
raise PyEzvizError(
|
|
1106
|
+
"Impossible to decode response: "
|
|
1107
|
+
+ str(err)
|
|
1108
|
+
+ "\nResponse was: "
|
|
1109
|
+
+ str(req.text)
|
|
1110
|
+
) from err
|
|
1111
|
+
|
|
1112
|
+
if json_output["meta"]["code"] != 200:
|
|
1113
|
+
raise PyEzvizError(
|
|
1114
|
+
f"Could not get group defence status: Got {json_output})"
|
|
1115
|
+
)
|
|
1116
|
+
|
|
1117
|
+
return json_output["mode"]
|
|
1118
|
+
|
|
1119
|
+
# Not tested
|
|
1120
|
+
def cancel_alarm_device(self, serial: str, max_retries: int = 0) -> bool:
|
|
1121
|
+
"""Cacnel alarm on an Alarm device."""
|
|
1122
|
+
if max_retries > MAX_RETRIES:
|
|
1123
|
+
raise PyEzvizError("Can't gather proper data. Max retries exceeded.")
|
|
1124
|
+
|
|
1125
|
+
try:
|
|
1126
|
+
req = self._session.post(
|
|
1127
|
+
"https://" + self._token["api_url"] + API_ENDPOINT_CANCEL_ALARM,
|
|
1128
|
+
data={"subSerial": serial},
|
|
1129
|
+
timeout=self._timeout,
|
|
1130
|
+
)
|
|
1131
|
+
|
|
1132
|
+
req.raise_for_status()
|
|
1133
|
+
|
|
1134
|
+
except requests.HTTPError as err:
|
|
1135
|
+
if err.response.status_code == 401:
|
|
1136
|
+
# session is wrong, need to relogin
|
|
1137
|
+
self.login()
|
|
1138
|
+
return self.sound_alarm(serial, max_retries + 1)
|
|
1139
|
+
|
|
1140
|
+
raise HTTPError from err
|
|
1141
|
+
|
|
1142
|
+
try:
|
|
1143
|
+
json_output = req.json()
|
|
1144
|
+
|
|
1145
|
+
except ValueError as err:
|
|
1146
|
+
raise PyEzvizError(
|
|
1147
|
+
"Impossible to decode response: "
|
|
1148
|
+
+ str(err)
|
|
1149
|
+
+ "\nResponse was: "
|
|
1150
|
+
+ str(req.text)
|
|
1151
|
+
) from err
|
|
1152
|
+
|
|
1153
|
+
if json_output["meta"]["code"] != 200:
|
|
1154
|
+
raise PyEzvizError(f"Could not cancel alarm siren: Got {json_output})")
|
|
1155
|
+
|
|
1156
|
+
return True
|
|
1157
|
+
|
|
1158
|
+
def load_devices(self) -> dict[Any, Any]:
|
|
1159
|
+
"""Load and return all cameras and light bulb objects."""
|
|
1160
|
+
|
|
1161
|
+
devices = self.get_device_infos()
|
|
1162
|
+
supported_categories = [
|
|
1163
|
+
DeviceCatagories.COMMON_DEVICE_CATEGORY.value,
|
|
1164
|
+
DeviceCatagories.CAMERA_DEVICE_CATEGORY.value,
|
|
1165
|
+
DeviceCatagories.BATTERY_CAMERA_DEVICE_CATEGORY.value,
|
|
1166
|
+
DeviceCatagories.DOORBELL_DEVICE_CATEGORY.value,
|
|
1167
|
+
DeviceCatagories.BASE_STATION_DEVICE_CATEGORY.value,
|
|
1168
|
+
DeviceCatagories.CAT_EYE_CATEGORY.value,
|
|
1169
|
+
DeviceCatagories.LIGHTING.value,
|
|
1170
|
+
]
|
|
1171
|
+
|
|
1172
|
+
for device, data in devices.items():
|
|
1173
|
+
if data["deviceInfos"]["deviceCategory"] in supported_categories:
|
|
1174
|
+
# Add support for connected HikVision cameras
|
|
1175
|
+
if (
|
|
1176
|
+
data["deviceInfos"]["deviceCategory"]
|
|
1177
|
+
== DeviceCatagories.COMMON_DEVICE_CATEGORY.value
|
|
1178
|
+
and not data["deviceInfos"]["hik"]
|
|
1179
|
+
):
|
|
1180
|
+
continue
|
|
1181
|
+
|
|
1182
|
+
if (
|
|
1183
|
+
data["deviceInfos"]["deviceCategory"]
|
|
1184
|
+
== DeviceCatagories.LIGHTING.value
|
|
1185
|
+
):
|
|
1186
|
+
# Create a light bulb object
|
|
1187
|
+
self._light_bulbs[device] = EzvizLightBulb(
|
|
1188
|
+
self, device, data
|
|
1189
|
+
).status()
|
|
1190
|
+
else:
|
|
1191
|
+
# Create camera object
|
|
1192
|
+
self._cameras[device] = EzvizCamera(self, device, data).status()
|
|
1193
|
+
|
|
1194
|
+
return {**self._cameras, **self._light_bulbs}
|
|
1195
|
+
|
|
1196
|
+
def load_cameras(self) -> dict[Any, Any]:
|
|
1197
|
+
"""Load and return all cameras objects."""
|
|
1198
|
+
|
|
1199
|
+
self.load_devices()
|
|
1200
|
+
return self._cameras
|
|
1201
|
+
|
|
1202
|
+
def load_light_bulbs(self) -> dict[Any, Any]:
|
|
1203
|
+
"""Load light bulbs."""
|
|
1204
|
+
|
|
1205
|
+
self.load_devices()
|
|
1206
|
+
return self._light_bulbs
|
|
1207
|
+
|
|
1208
|
+
def get_device_infos(self, serial: str | None = None) -> dict[Any, Any]:
|
|
1209
|
+
"""Load all devices and build dict per device serial."""
|
|
1210
|
+
|
|
1211
|
+
devices = self._get_page_list()
|
|
1212
|
+
result: dict[str, Any] = {}
|
|
1213
|
+
_res_id = "NONE"
|
|
1214
|
+
|
|
1215
|
+
for device in devices["deviceInfos"]:
|
|
1216
|
+
_serial = device["deviceSerial"]
|
|
1217
|
+
_res_id_list = {
|
|
1218
|
+
item
|
|
1219
|
+
for item in devices.get("CLOUD", {})
|
|
1220
|
+
if devices["CLOUD"][item].get("deviceSerial") == _serial
|
|
1221
|
+
}
|
|
1222
|
+
_res_id = _res_id_list.pop() if len(_res_id_list) else "NONE"
|
|
1223
|
+
|
|
1224
|
+
result[_serial] = {
|
|
1225
|
+
"CLOUD": {_res_id: devices.get("CLOUD", {}).get(_res_id, {})},
|
|
1226
|
+
"VTM": {_res_id: devices.get("VTM", {}).get(_res_id, {})},
|
|
1227
|
+
"P2P": devices.get("P2P", {}).get(_serial, {}),
|
|
1228
|
+
"CONNECTION": devices.get("CONNECTION", {}).get(_serial, {}),
|
|
1229
|
+
"KMS": devices.get("KMS", {}).get(_serial, {}),
|
|
1230
|
+
"STATUS": devices.get("STATUS", {}).get(_serial, {}),
|
|
1231
|
+
"TIME_PLAN": devices.get("TIME_PLAN", {}).get(_serial, {}),
|
|
1232
|
+
"CHANNEL": {_res_id: devices.get("CHANNEL", {}).get(_res_id, {})},
|
|
1233
|
+
"QOS": devices.get("QOS", {}).get(_serial, {}),
|
|
1234
|
+
"NODISTURB": devices.get("NODISTURB", {}).get(_serial, {}),
|
|
1235
|
+
"FEATURE": devices.get("FEATURE", {}).get(_serial, {}),
|
|
1236
|
+
"UPGRADE": devices.get("UPGRADE", {}).get(_serial, {}),
|
|
1237
|
+
"FEATURE_INFO": devices.get("FEATURE_INFO", {}).get(_serial, {}),
|
|
1238
|
+
"SWITCH": devices.get("SWITCH", {}).get(_serial, {}),
|
|
1239
|
+
"CUSTOM_TAG": devices.get("CUSTOM_TAG", {}).get(_serial, {}),
|
|
1240
|
+
"VIDEO_QUALITY": {
|
|
1241
|
+
_res_id: devices.get("VIDEO_QUALITY", {}).get(_res_id, {})
|
|
1242
|
+
},
|
|
1243
|
+
"resourceInfos": [
|
|
1244
|
+
item
|
|
1245
|
+
for item in devices.get("resourceInfos")
|
|
1246
|
+
if item.get("deviceSerial") == _serial
|
|
1247
|
+
], # Could be more than one
|
|
1248
|
+
"WIFI": devices.get("WIFI", {}).get(_serial, {}),
|
|
1249
|
+
"deviceInfos": device,
|
|
1250
|
+
}
|
|
1251
|
+
# Nested keys are still encoded as JSON strings
|
|
1252
|
+
result[_serial]["deviceInfos"]["supportExt"] = json.loads(
|
|
1253
|
+
result[_serial]["deviceInfos"]["supportExt"]
|
|
1254
|
+
)
|
|
1255
|
+
convert_to_dict(result[_serial]["STATUS"].get("optionals"))
|
|
1256
|
+
|
|
1257
|
+
if not serial:
|
|
1258
|
+
return result
|
|
1259
|
+
|
|
1260
|
+
return result.get(serial, {})
|
|
1261
|
+
|
|
1262
|
+
def ptz_control(
|
|
1263
|
+
self, command: str, serial: str, action: str, speed: int = 5
|
|
1264
|
+
) -> Any:
|
|
1265
|
+
"""PTZ Control by API."""
|
|
1266
|
+
if command is None:
|
|
1267
|
+
raise PyEzvizError("Trying to call ptzControl without command")
|
|
1268
|
+
if action is None:
|
|
1269
|
+
raise PyEzvizError("Trying to call ptzControl without action")
|
|
1270
|
+
|
|
1271
|
+
try:
|
|
1272
|
+
req = self._session.put(
|
|
1273
|
+
"https://"
|
|
1274
|
+
+ self._token["api_url"]
|
|
1275
|
+
+ API_ENDPOINT_DEVICES
|
|
1276
|
+
+ serial
|
|
1277
|
+
+ API_ENDPOINT_PTZCONTROL,
|
|
1278
|
+
data={
|
|
1279
|
+
"command": command,
|
|
1280
|
+
"action": action,
|
|
1281
|
+
"channelNo": "1",
|
|
1282
|
+
"speed": speed,
|
|
1283
|
+
"uuid": str(uuid4()),
|
|
1284
|
+
"serial": serial,
|
|
1285
|
+
},
|
|
1286
|
+
timeout=self._timeout,
|
|
1287
|
+
)
|
|
1288
|
+
|
|
1289
|
+
req.raise_for_status()
|
|
1290
|
+
|
|
1291
|
+
except requests.HTTPError as err:
|
|
1292
|
+
raise HTTPError from err
|
|
1293
|
+
|
|
1294
|
+
try:
|
|
1295
|
+
json_output = req.json()
|
|
1296
|
+
|
|
1297
|
+
except ValueError as err:
|
|
1298
|
+
raise PyEzvizError(
|
|
1299
|
+
"Impossible to decode response: "
|
|
1300
|
+
+ str(err)
|
|
1301
|
+
+ "\nResponse was: "
|
|
1302
|
+
+ str(req.text)
|
|
1303
|
+
) from err
|
|
1304
|
+
|
|
1305
|
+
_LOGGER.debug("PTZ Control: %s", json_output)
|
|
1306
|
+
|
|
1307
|
+
return True
|
|
1308
|
+
|
|
1309
|
+
def get_cam_key(
|
|
1310
|
+
self, serial: str, smscode: int | None = None, max_retries: int = 0
|
|
1311
|
+
) -> Any:
|
|
1312
|
+
"""Get Camera encryption key. The key that is set after the camera is added to the account."""
|
|
1313
|
+
|
|
1314
|
+
if max_retries > MAX_RETRIES:
|
|
1315
|
+
raise PyEzvizError("Can't gather proper data. Max retries exceeded.")
|
|
1316
|
+
|
|
1317
|
+
try:
|
|
1318
|
+
req = self._session.post(
|
|
1319
|
+
"https://" + self._token["api_url"] + API_ENDPOINT_CAM_ENCRYPTKEY,
|
|
1320
|
+
data={
|
|
1321
|
+
"checkcode": smscode,
|
|
1322
|
+
"serial": serial,
|
|
1323
|
+
"clientNo": "web_site",
|
|
1324
|
+
"clientType": 3,
|
|
1325
|
+
"netType": "WIFI",
|
|
1326
|
+
"featureCode": FEATURE_CODE,
|
|
1327
|
+
"sessionId": self._token["session_id"],
|
|
1328
|
+
},
|
|
1329
|
+
timeout=self._timeout,
|
|
1330
|
+
)
|
|
1331
|
+
|
|
1332
|
+
req.raise_for_status()
|
|
1333
|
+
|
|
1334
|
+
except requests.HTTPError as err:
|
|
1335
|
+
if err.response.status_code == 401:
|
|
1336
|
+
# session is wrong, need to relogin
|
|
1337
|
+
self.login()
|
|
1338
|
+
return self.get_cam_key(serial, smscode, max_retries + 1)
|
|
1339
|
+
|
|
1340
|
+
raise HTTPError from err
|
|
1341
|
+
|
|
1342
|
+
try:
|
|
1343
|
+
json_output = req.json()
|
|
1344
|
+
|
|
1345
|
+
except ValueError as err:
|
|
1346
|
+
raise PyEzvizError(
|
|
1347
|
+
"Impossible to decode response: "
|
|
1348
|
+
+ str(err)
|
|
1349
|
+
+ "\nResponse was: "
|
|
1350
|
+
+ str(req.text)
|
|
1351
|
+
) from err
|
|
1352
|
+
|
|
1353
|
+
if json_output["resultCode"] == "20002":
|
|
1354
|
+
raise EzvizAuthVerificationCode(f"MFA code required: Got {json_output})")
|
|
1355
|
+
|
|
1356
|
+
if json_output["resultCode"] != "0":
|
|
1357
|
+
if json_output["resultCode"] == "-1":
|
|
1358
|
+
_LOGGER.warning(
|
|
1359
|
+
"Camera %s encryption key not found, retrying %s of %s",
|
|
1360
|
+
serial,
|
|
1361
|
+
max_retries,
|
|
1362
|
+
MAX_RETRIES,
|
|
1363
|
+
)
|
|
1364
|
+
return self.get_cam_key(serial, smscode, max_retries + 1)
|
|
1365
|
+
raise PyEzvizError(
|
|
1366
|
+
f"Could not get camera encryption key: Got {json_output})"
|
|
1367
|
+
)
|
|
1368
|
+
|
|
1369
|
+
return json_output["encryptkey"]
|
|
1370
|
+
|
|
1371
|
+
def create_panoramic(self, serial: str, max_retries: int = 0) -> Any:
|
|
1372
|
+
"""Create panoramic image."""
|
|
1373
|
+
|
|
1374
|
+
if max_retries > MAX_RETRIES:
|
|
1375
|
+
raise PyEzvizError("Can't gather proper data. Max retries exceeded.")
|
|
1376
|
+
|
|
1377
|
+
try:
|
|
1378
|
+
req = self._session.post(
|
|
1379
|
+
"https://" + self._token["api_url"] + API_ENDPOINT_CREATE_PANORAMIC,
|
|
1380
|
+
data={"deviceSerial": serial},
|
|
1381
|
+
timeout=self._timeout,
|
|
1382
|
+
)
|
|
1383
|
+
|
|
1384
|
+
req.raise_for_status()
|
|
1385
|
+
|
|
1386
|
+
except requests.HTTPError as err:
|
|
1387
|
+
if err.response.status_code == 401:
|
|
1388
|
+
# session is wrong, need to relogin
|
|
1389
|
+
self.login()
|
|
1390
|
+
return self.create_panoramic(serial, max_retries + 1)
|
|
1391
|
+
|
|
1392
|
+
raise HTTPError from err
|
|
1393
|
+
|
|
1394
|
+
try:
|
|
1395
|
+
json_output = req.json()
|
|
1396
|
+
|
|
1397
|
+
except ValueError as err:
|
|
1398
|
+
raise PyEzvizError(
|
|
1399
|
+
"Impossible to decode response: "
|
|
1400
|
+
+ str(err)
|
|
1401
|
+
+ "\nResponse was: "
|
|
1402
|
+
+ str(req.text)
|
|
1403
|
+
) from err
|
|
1404
|
+
|
|
1405
|
+
if json_output["resultCode"] != "0":
|
|
1406
|
+
if json_output["resultCode"] == "-1":
|
|
1407
|
+
_LOGGER.warning(
|
|
1408
|
+
"Create panoramic failed on device %s retrying %s",
|
|
1409
|
+
serial,
|
|
1410
|
+
max_retries,
|
|
1411
|
+
)
|
|
1412
|
+
return self.create_panoramic(serial, max_retries + 1)
|
|
1413
|
+
raise PyEzvizError(
|
|
1414
|
+
f"Could not send command to create panoramic photo: Got {json_output})"
|
|
1415
|
+
)
|
|
1416
|
+
|
|
1417
|
+
return json_output
|
|
1418
|
+
|
|
1419
|
+
def return_panoramic(self, serial: str, max_retries: int = 0) -> Any:
|
|
1420
|
+
"""Return panoramic image url list."""
|
|
1421
|
+
|
|
1422
|
+
if max_retries > MAX_RETRIES:
|
|
1423
|
+
raise PyEzvizError("Can't gather proper data. Max retries exceeded.")
|
|
1424
|
+
|
|
1425
|
+
try:
|
|
1426
|
+
req = self._session.post(
|
|
1427
|
+
"https://" + self._token["api_url"] + API_ENDPOINT_RETURN_PANORAMIC,
|
|
1428
|
+
data={"deviceSerial": serial},
|
|
1429
|
+
timeout=self._timeout,
|
|
1430
|
+
)
|
|
1431
|
+
|
|
1432
|
+
req.raise_for_status()
|
|
1433
|
+
|
|
1434
|
+
except requests.HTTPError as err:
|
|
1435
|
+
if err.response.status_code == 401:
|
|
1436
|
+
# session is wrong, need to relogin
|
|
1437
|
+
self.login()
|
|
1438
|
+
return self.return_panoramic(serial, max_retries + 1)
|
|
1439
|
+
|
|
1440
|
+
raise HTTPError from err
|
|
1441
|
+
|
|
1442
|
+
try:
|
|
1443
|
+
json_output = req.json()
|
|
1444
|
+
|
|
1445
|
+
except ValueError as err:
|
|
1446
|
+
raise PyEzvizError(
|
|
1447
|
+
"Impossible to decode response: "
|
|
1448
|
+
+ str(err)
|
|
1449
|
+
+ "\nResponse was: "
|
|
1450
|
+
+ str(req.text)
|
|
1451
|
+
) from err
|
|
1452
|
+
|
|
1453
|
+
if json_output["resultCode"] != "0":
|
|
1454
|
+
if json_output["resultCode"] == "-1":
|
|
1455
|
+
_LOGGER.warning(
|
|
1456
|
+
"Camera %s busy or unreachable, retrying %s of %s",
|
|
1457
|
+
serial,
|
|
1458
|
+
max_retries,
|
|
1459
|
+
MAX_RETRIES,
|
|
1460
|
+
)
|
|
1461
|
+
return self.return_panoramic(serial, max_retries + 1)
|
|
1462
|
+
raise PyEzvizError(f"Could retrieve panoramic photo: Got {json_output})")
|
|
1463
|
+
|
|
1464
|
+
return json_output
|
|
1465
|
+
|
|
1466
|
+
def ptz_control_coordinates(
|
|
1467
|
+
self, serial: str, x_axis: float, y_axis: float
|
|
1468
|
+
) -> bool:
|
|
1469
|
+
"""PTZ Coordinate Move."""
|
|
1470
|
+
if 0 < x_axis > 1:
|
|
1471
|
+
raise PyEzvizError(
|
|
1472
|
+
f"Invalid X coordinate: {x_axis}: Should be between 0 and 1 inclusive"
|
|
1473
|
+
)
|
|
1474
|
+
|
|
1475
|
+
if 0 < y_axis > 1:
|
|
1476
|
+
raise PyEzvizError(
|
|
1477
|
+
f"Invalid Y coordinate: {y_axis}: Should be between 0 and 1 inclusive"
|
|
1478
|
+
)
|
|
1479
|
+
|
|
1480
|
+
try:
|
|
1481
|
+
req = self._session.post(
|
|
1482
|
+
"https://"
|
|
1483
|
+
+ self._token["api_url"]
|
|
1484
|
+
+ API_ENDPOINT_PANORAMIC_DEVICES_OPERATION,
|
|
1485
|
+
data={
|
|
1486
|
+
"x": f"{x_axis:.6f}",
|
|
1487
|
+
"y": f"{y_axis:.6f}",
|
|
1488
|
+
"deviceSerial": serial,
|
|
1489
|
+
},
|
|
1490
|
+
timeout=self._timeout,
|
|
1491
|
+
)
|
|
1492
|
+
|
|
1493
|
+
req.raise_for_status()
|
|
1494
|
+
|
|
1495
|
+
except requests.HTTPError as err:
|
|
1496
|
+
raise HTTPError from err
|
|
1497
|
+
|
|
1498
|
+
try:
|
|
1499
|
+
json_result = req.json()
|
|
1500
|
+
|
|
1501
|
+
except ValueError as err:
|
|
1502
|
+
raise PyEzvizError(
|
|
1503
|
+
"Impossible to decode response: "
|
|
1504
|
+
+ str(err)
|
|
1505
|
+
+ "\nResponse was: "
|
|
1506
|
+
+ str(req.text)
|
|
1507
|
+
) from err
|
|
1508
|
+
|
|
1509
|
+
_LOGGER.debug("PTZ control coordinates: %s", json_result)
|
|
1510
|
+
|
|
1511
|
+
return True
|
|
1512
|
+
|
|
1513
|
+
def login(self, sms_code: int | None = None) -> dict[Any, Any]:
|
|
1514
|
+
"""Get or refresh ezviz login token."""
|
|
1515
|
+
if self._token["session_id"] and self._token["rf_session_id"]:
|
|
1516
|
+
try:
|
|
1517
|
+
req = self._session.put(
|
|
1518
|
+
"https://"
|
|
1519
|
+
+ self._token["api_url"]
|
|
1520
|
+
+ API_ENDPOINT_REFRESH_SESSION_ID,
|
|
1521
|
+
data={
|
|
1522
|
+
"refreshSessionId": self._token["rf_session_id"],
|
|
1523
|
+
"featureCode": FEATURE_CODE,
|
|
1524
|
+
},
|
|
1525
|
+
timeout=self._timeout,
|
|
1526
|
+
)
|
|
1527
|
+
req.raise_for_status()
|
|
1528
|
+
|
|
1529
|
+
except requests.HTTPError as err:
|
|
1530
|
+
raise HTTPError from err
|
|
1531
|
+
|
|
1532
|
+
try:
|
|
1533
|
+
json_result = req.json()
|
|
1534
|
+
|
|
1535
|
+
except ValueError as err:
|
|
1536
|
+
raise PyEzvizError(
|
|
1537
|
+
"Impossible to decode response: "
|
|
1538
|
+
+ str(err)
|
|
1539
|
+
+ "\nResponse was: "
|
|
1540
|
+
+ str(req.text)
|
|
1541
|
+
) from err
|
|
1542
|
+
|
|
1543
|
+
if json_result["meta"]["code"] == 200:
|
|
1544
|
+
self._session.headers["sessionId"] = json_result["sessionInfo"][
|
|
1545
|
+
"sessionId"
|
|
1546
|
+
]
|
|
1547
|
+
self._token["session_id"] = str(json_result["sessionInfo"]["sessionId"])
|
|
1548
|
+
self._token["rf_session_id"] = str(
|
|
1549
|
+
json_result["sessionInfo"]["refreshSessionId"]
|
|
1550
|
+
)
|
|
1551
|
+
|
|
1552
|
+
if not self._token.get("service_urls"):
|
|
1553
|
+
self._token["service_urls"] = self.get_service_urls()
|
|
1554
|
+
|
|
1555
|
+
return self._token
|
|
1556
|
+
|
|
1557
|
+
if json_result["meta"]["code"] == 403:
|
|
1558
|
+
if self.account and self.password:
|
|
1559
|
+
self._token = {
|
|
1560
|
+
"session_id": None,
|
|
1561
|
+
"rf_session_id": None,
|
|
1562
|
+
"username": None,
|
|
1563
|
+
"api_url": self._token["api_url"],
|
|
1564
|
+
}
|
|
1565
|
+
return self.login()
|
|
1566
|
+
|
|
1567
|
+
raise EzvizAuthTokenExpired(
|
|
1568
|
+
f"Token expired, Login with username and password required: {req.text}"
|
|
1569
|
+
)
|
|
1570
|
+
|
|
1571
|
+
raise PyEzvizError(f"Error renewing login token: {json_result['meta']}")
|
|
1572
|
+
|
|
1573
|
+
if self.account and self.password:
|
|
1574
|
+
return self._login(sms_code)
|
|
1575
|
+
|
|
1576
|
+
raise PyEzvizError("Login with account and password required")
|
|
1577
|
+
|
|
1578
|
+
def logout(self) -> bool:
|
|
1579
|
+
"""Close Ezviz session and remove login session from ezviz servers."""
|
|
1580
|
+
try:
|
|
1581
|
+
req = self._session.delete(
|
|
1582
|
+
"https://" + self._token["api_url"] + API_ENDPOINT_LOGOUT,
|
|
1583
|
+
timeout=self._timeout,
|
|
1584
|
+
)
|
|
1585
|
+
req.raise_for_status()
|
|
1586
|
+
|
|
1587
|
+
except requests.HTTPError as err:
|
|
1588
|
+
if err.response.status_code == 401:
|
|
1589
|
+
_LOGGER.warning("Token is no longer valid. Already logged out?")
|
|
1590
|
+
return True
|
|
1591
|
+
raise HTTPError from err
|
|
1592
|
+
|
|
1593
|
+
try:
|
|
1594
|
+
json_result = req.json()
|
|
1595
|
+
|
|
1596
|
+
except ValueError as err:
|
|
1597
|
+
raise PyEzvizError(
|
|
1598
|
+
"Impossible to decode response: "
|
|
1599
|
+
+ str(err)
|
|
1600
|
+
+ "\nResponse was: "
|
|
1601
|
+
+ str(req.text)
|
|
1602
|
+
) from err
|
|
1603
|
+
|
|
1604
|
+
self.close_session()
|
|
1605
|
+
|
|
1606
|
+
return bool(json_result["meta"]["code"] == 200)
|
|
1607
|
+
|
|
1608
|
+
def set_camera_defence_old(self, serial: str, enable: int) -> bool:
|
|
1609
|
+
"""Enable/Disable motion detection on camera."""
|
|
1610
|
+
cas_client = EzvizCAS(self._token)
|
|
1611
|
+
cas_client.set_camera_defence_state(serial, enable)
|
|
1612
|
+
|
|
1613
|
+
return True
|
|
1614
|
+
|
|
1615
|
+
def api_set_defence_schedule(
|
|
1616
|
+
self, serial: str, schedule: str, enable: int, max_retries: int = 0
|
|
1617
|
+
) -> bool:
|
|
1618
|
+
"""Set defence schedules."""
|
|
1619
|
+
if max_retries > MAX_RETRIES:
|
|
1620
|
+
raise PyEzvizError("Can't gather proper data. Max retries exceeded.")
|
|
1621
|
+
|
|
1622
|
+
schedulestring = (
|
|
1623
|
+
'{"CN":0,"EL":'
|
|
1624
|
+
+ str(enable)
|
|
1625
|
+
+ ',"SS":"'
|
|
1626
|
+
+ serial
|
|
1627
|
+
+ '","WP":['
|
|
1628
|
+
+ schedule
|
|
1629
|
+
+ "]}]}"
|
|
1630
|
+
)
|
|
1631
|
+
try:
|
|
1632
|
+
req = self._session.post(
|
|
1633
|
+
"https://" + self._token["api_url"] + API_ENDPOINT_SET_DEFENCE_SCHEDULE,
|
|
1634
|
+
data={
|
|
1635
|
+
"devTimingPlan": schedulestring,
|
|
1636
|
+
},
|
|
1637
|
+
timeout=self._timeout,
|
|
1638
|
+
)
|
|
1639
|
+
|
|
1640
|
+
req.raise_for_status()
|
|
1641
|
+
|
|
1642
|
+
except requests.HTTPError as err:
|
|
1643
|
+
if err.response.status_code == 401:
|
|
1644
|
+
# session is wrong, need to relogin
|
|
1645
|
+
self.login()
|
|
1646
|
+
return self.api_set_defence_schedule(
|
|
1647
|
+
serial, schedule, enable, max_retries + 1
|
|
1648
|
+
)
|
|
1649
|
+
|
|
1650
|
+
raise HTTPError from err
|
|
1651
|
+
|
|
1652
|
+
try:
|
|
1653
|
+
json_output = req.json()
|
|
1654
|
+
|
|
1655
|
+
except ValueError as err:
|
|
1656
|
+
raise PyEzvizError(
|
|
1657
|
+
"Impossible to decode response: "
|
|
1658
|
+
+ str(err)
|
|
1659
|
+
+ "\nResponse was: "
|
|
1660
|
+
+ str(req.text)
|
|
1661
|
+
) from err
|
|
1662
|
+
|
|
1663
|
+
if json_output["resultCode"] != "0":
|
|
1664
|
+
if json_output["resultCode"] == "-1":
|
|
1665
|
+
_LOGGER.warning(
|
|
1666
|
+
"Camara %s offline or unreachable, retrying %s of %s",
|
|
1667
|
+
serial,
|
|
1668
|
+
max_retries,
|
|
1669
|
+
MAX_RETRIES,
|
|
1670
|
+
)
|
|
1671
|
+
return self.api_set_defence_schedule(
|
|
1672
|
+
serial, schedule, enable, max_retries + 1
|
|
1673
|
+
)
|
|
1674
|
+
raise PyEzvizError(f"Could not set the schedule: Got {json_output})")
|
|
1675
|
+
|
|
1676
|
+
return True
|
|
1677
|
+
|
|
1678
|
+
def api_set_defence_mode(self, mode: DefenseModeType, max_retries: int = 0) -> bool:
|
|
1679
|
+
"""Set defence mode for all devices. The alarm panel from main page is used."""
|
|
1680
|
+
if max_retries > MAX_RETRIES:
|
|
1681
|
+
raise PyEzvizError("Can't gather proper data. Max retries exceeded.")
|
|
1682
|
+
|
|
1683
|
+
try:
|
|
1684
|
+
req = self._session.post(
|
|
1685
|
+
"https://" + self._token["api_url"] + API_ENDPOINT_SWITCH_DEFENCE_MODE,
|
|
1686
|
+
data={
|
|
1687
|
+
"groupId": -1,
|
|
1688
|
+
"mode": mode,
|
|
1689
|
+
},
|
|
1690
|
+
timeout=self._timeout,
|
|
1691
|
+
)
|
|
1692
|
+
|
|
1693
|
+
req.raise_for_status()
|
|
1694
|
+
|
|
1695
|
+
except requests.HTTPError as err:
|
|
1696
|
+
if err.response.status_code == 401:
|
|
1697
|
+
# session is wrong, need to relogin
|
|
1698
|
+
self.login()
|
|
1699
|
+
return self.api_set_defence_mode(mode, max_retries + 1)
|
|
1700
|
+
|
|
1701
|
+
raise HTTPError from err
|
|
1702
|
+
|
|
1703
|
+
try:
|
|
1704
|
+
json_output = req.json()
|
|
1705
|
+
|
|
1706
|
+
except ValueError as err:
|
|
1707
|
+
raise PyEzvizError(
|
|
1708
|
+
"Impossible to decode response: "
|
|
1709
|
+
+ str(err)
|
|
1710
|
+
+ "\nResponse was: "
|
|
1711
|
+
+ str(req.text)
|
|
1712
|
+
) from err
|
|
1713
|
+
|
|
1714
|
+
if json_output["meta"]["code"] != 200:
|
|
1715
|
+
raise PyEzvizError(f"Could not set defence mode: Got {json_output})")
|
|
1716
|
+
|
|
1717
|
+
return True
|
|
1718
|
+
|
|
1719
|
+
def do_not_disturb(
|
|
1720
|
+
self,
|
|
1721
|
+
serial: str,
|
|
1722
|
+
enable: int = 1,
|
|
1723
|
+
channelno: int = 1,
|
|
1724
|
+
max_retries: int = 0,
|
|
1725
|
+
) -> bool:
|
|
1726
|
+
"""Set do not disturb on camera with specified serial."""
|
|
1727
|
+
if max_retries > MAX_RETRIES:
|
|
1728
|
+
raise PyEzvizError("Can't gather proper data. Max retries exceeded.")
|
|
1729
|
+
|
|
1730
|
+
try:
|
|
1731
|
+
req = self._session.put(
|
|
1732
|
+
"https://"
|
|
1733
|
+
+ self._token["api_url"]
|
|
1734
|
+
+ API_ENDPOINT_V3_ALARMS
|
|
1735
|
+
+ serial
|
|
1736
|
+
+ "/"
|
|
1737
|
+
+ channelno
|
|
1738
|
+
+ API_ENDPOINT_DO_NOT_DISTURB,
|
|
1739
|
+
data={"enable": enable, "channelNo": channelno, "deviceSerial": serial},
|
|
1740
|
+
timeout=self._timeout,
|
|
1741
|
+
)
|
|
1742
|
+
req.raise_for_status()
|
|
1743
|
+
|
|
1744
|
+
except requests.HTTPError as err:
|
|
1745
|
+
if err.response.status_code == 401:
|
|
1746
|
+
# session is wrong, need to re-log-in
|
|
1747
|
+
self.login()
|
|
1748
|
+
return self.do_not_disturb(serial, enable, channelno, max_retries + 1)
|
|
1749
|
+
|
|
1750
|
+
raise HTTPError from err
|
|
1751
|
+
|
|
1752
|
+
try:
|
|
1753
|
+
json_output = req.json()
|
|
1754
|
+
|
|
1755
|
+
except ValueError as err:
|
|
1756
|
+
raise PyEzvizError("Could not decode response:" + str(err)) from err
|
|
1757
|
+
|
|
1758
|
+
if json_output["meta"]["code"] != 200:
|
|
1759
|
+
raise PyEzvizError(f"Could not set do not disturb: Got {json_output})")
|
|
1760
|
+
|
|
1761
|
+
return True
|
|
1762
|
+
|
|
1763
|
+
def set_floodlight_brightness(
|
|
1764
|
+
self,
|
|
1765
|
+
serial: str,
|
|
1766
|
+
luminance: int = 50,
|
|
1767
|
+
channelno: str = "1",
|
|
1768
|
+
max_retries: int = 0,
|
|
1769
|
+
) -> bool | str:
|
|
1770
|
+
"""Set brightness on camera with adjustable light."""
|
|
1771
|
+
if max_retries > MAX_RETRIES:
|
|
1772
|
+
raise PyEzvizError("Can't gather proper data. Max retries exceeded.")
|
|
1773
|
+
|
|
1774
|
+
if luminance not in range(1, 100):
|
|
1775
|
+
raise PyEzvizError(
|
|
1776
|
+
"Range of luminance is 1-100, got " + str(luminance) + "."
|
|
1777
|
+
)
|
|
1778
|
+
|
|
1779
|
+
try:
|
|
1780
|
+
req = self._session.post(
|
|
1781
|
+
"https://"
|
|
1782
|
+
+ self._token["api_url"]
|
|
1783
|
+
+ API_ENDPOINT_SET_LUMINANCE
|
|
1784
|
+
+ "/"
|
|
1785
|
+
+ serial
|
|
1786
|
+
+ "/"
|
|
1787
|
+
+ channelno,
|
|
1788
|
+
data={
|
|
1789
|
+
"luminance": luminance,
|
|
1790
|
+
},
|
|
1791
|
+
timeout=self._timeout,
|
|
1792
|
+
)
|
|
1793
|
+
|
|
1794
|
+
req.raise_for_status()
|
|
1795
|
+
|
|
1796
|
+
except requests.HTTPError as err:
|
|
1797
|
+
if err.response.status_code == 401:
|
|
1798
|
+
# session is wrong, need to re-log-in
|
|
1799
|
+
self.login()
|
|
1800
|
+
return self.set_floodlight_brightness(
|
|
1801
|
+
serial, luminance, channelno, max_retries + 1
|
|
1802
|
+
)
|
|
1803
|
+
|
|
1804
|
+
raise HTTPError from err
|
|
1805
|
+
|
|
1806
|
+
try:
|
|
1807
|
+
response_json = req.json()
|
|
1808
|
+
|
|
1809
|
+
except ValueError as err:
|
|
1810
|
+
raise PyEzvizError("Could not decode response:" + str(err)) from err
|
|
1811
|
+
|
|
1812
|
+
if response_json["meta"]["code"] != 200:
|
|
1813
|
+
raise PyEzvizError(f"Unable to set brightness, got: {response_json}")
|
|
1814
|
+
|
|
1815
|
+
return True
|
|
1816
|
+
|
|
1817
|
+
def set_brightness(
|
|
1818
|
+
self,
|
|
1819
|
+
serial: str,
|
|
1820
|
+
luminance: int = 50,
|
|
1821
|
+
channelno: str = "1",
|
|
1822
|
+
max_retries: int = 0,
|
|
1823
|
+
) -> bool | str:
|
|
1824
|
+
"""Facade that changes the brightness to light bulbs or cameras' light."""
|
|
1825
|
+
|
|
1826
|
+
device = self._light_bulbs.get(serial)
|
|
1827
|
+
if device:
|
|
1828
|
+
# the device is a light bulb
|
|
1829
|
+
return self.set_device_feature_by_key(
|
|
1830
|
+
serial, device["productId"], luminance, "brightness", max_retries
|
|
1831
|
+
)
|
|
1832
|
+
|
|
1833
|
+
# assume the device is a camera
|
|
1834
|
+
return self.set_floodlight_brightness(serial, luminance, channelno, max_retries)
|
|
1835
|
+
|
|
1836
|
+
def switch_light_status(
|
|
1837
|
+
self,
|
|
1838
|
+
serial: str,
|
|
1839
|
+
enable: int,
|
|
1840
|
+
channel_no: int = 0,
|
|
1841
|
+
max_retries: int = 0,
|
|
1842
|
+
) -> bool:
|
|
1843
|
+
"""Facade that turns on/off light bulbs or cameras' light."""
|
|
1844
|
+
|
|
1845
|
+
device = self._light_bulbs.get(serial)
|
|
1846
|
+
if device:
|
|
1847
|
+
# the device is a light bulb
|
|
1848
|
+
return self.set_device_feature_by_key(
|
|
1849
|
+
serial, device["productId"], bool(enable), "light_switch", max_retries
|
|
1850
|
+
)
|
|
1851
|
+
|
|
1852
|
+
# assume the device is a camera
|
|
1853
|
+
return self.switch_status(
|
|
1854
|
+
serial, DeviceSwitchType.ALARM_LIGHT.value, enable, channel_no, max_retries
|
|
1855
|
+
)
|
|
1856
|
+
|
|
1857
|
+
def detection_sensibility(
|
|
1858
|
+
self,
|
|
1859
|
+
serial: str,
|
|
1860
|
+
sensibility: int = 3,
|
|
1861
|
+
type_value: int = 3,
|
|
1862
|
+
max_retries: int = 0,
|
|
1863
|
+
) -> bool | str:
|
|
1864
|
+
"""Set detection sensibility."""
|
|
1865
|
+
if max_retries > MAX_RETRIES:
|
|
1866
|
+
raise PyEzvizError("Can't gather proper data. Max retries exceeded.")
|
|
1867
|
+
|
|
1868
|
+
if sensibility not in [0, 1, 2, 3, 4, 5, 6] and type_value == 0:
|
|
1869
|
+
raise PyEzvizError(
|
|
1870
|
+
"Unproper sensibility for type 0 (should be within 1 to 6)."
|
|
1871
|
+
)
|
|
1872
|
+
|
|
1873
|
+
try:
|
|
1874
|
+
req = self._session.post(
|
|
1875
|
+
"https://"
|
|
1876
|
+
+ self._token["api_url"]
|
|
1877
|
+
+ API_ENDPOINT_DETECTION_SENSIBILITY,
|
|
1878
|
+
data={
|
|
1879
|
+
"subSerial": serial,
|
|
1880
|
+
"type": type_value,
|
|
1881
|
+
"channelNo": "1",
|
|
1882
|
+
"value": sensibility,
|
|
1883
|
+
},
|
|
1884
|
+
timeout=self._timeout,
|
|
1885
|
+
)
|
|
1886
|
+
|
|
1887
|
+
req.raise_for_status()
|
|
1888
|
+
|
|
1889
|
+
except requests.HTTPError as err:
|
|
1890
|
+
if err.response.status_code == 401:
|
|
1891
|
+
# session is wrong, need to re-log-in
|
|
1892
|
+
self.login()
|
|
1893
|
+
return self.detection_sensibility(
|
|
1894
|
+
serial, sensibility, type_value, max_retries + 1
|
|
1895
|
+
)
|
|
1896
|
+
|
|
1897
|
+
raise HTTPError from err
|
|
1898
|
+
|
|
1899
|
+
try:
|
|
1900
|
+
response_json = req.json()
|
|
1901
|
+
|
|
1902
|
+
except ValueError as err:
|
|
1903
|
+
raise PyEzvizError("Could not decode response:" + str(err)) from err
|
|
1904
|
+
|
|
1905
|
+
if response_json["resultCode"] != "0":
|
|
1906
|
+
if response_json["resultCode"] == "-1":
|
|
1907
|
+
_LOGGER.warning(
|
|
1908
|
+
"Camera %s is offline or unreachable, can't set sensitivity, retrying %s of %s",
|
|
1909
|
+
serial,
|
|
1910
|
+
max_retries,
|
|
1911
|
+
MAX_RETRIES,
|
|
1912
|
+
)
|
|
1913
|
+
return self.detection_sensibility(
|
|
1914
|
+
serial, sensibility, type_value, max_retries + 1
|
|
1915
|
+
)
|
|
1916
|
+
raise PyEzvizError(
|
|
1917
|
+
f"Unable to set detection sensibility. Got: {response_json}"
|
|
1918
|
+
)
|
|
1919
|
+
|
|
1920
|
+
return True
|
|
1921
|
+
|
|
1922
|
+
def get_detection_sensibility(
|
|
1923
|
+
self, serial: str, type_value: str = "0", max_retries: int = 0
|
|
1924
|
+
) -> Any:
|
|
1925
|
+
"""Get detection sensibility notifications."""
|
|
1926
|
+
if max_retries > MAX_RETRIES:
|
|
1927
|
+
raise PyEzvizError("Can't gather proper data. Max retries exceeded.")
|
|
1928
|
+
|
|
1929
|
+
try:
|
|
1930
|
+
req = self._session.post(
|
|
1931
|
+
"https://"
|
|
1932
|
+
+ self._token["api_url"]
|
|
1933
|
+
+ API_ENDPOINT_DETECTION_SENSIBILITY_GET,
|
|
1934
|
+
data={
|
|
1935
|
+
"subSerial": serial,
|
|
1936
|
+
},
|
|
1937
|
+
timeout=self._timeout,
|
|
1938
|
+
)
|
|
1939
|
+
|
|
1940
|
+
req.raise_for_status()
|
|
1941
|
+
|
|
1942
|
+
except requests.HTTPError as err:
|
|
1943
|
+
if err.response.status_code == 401:
|
|
1944
|
+
# session is wrong, need to re-log-in.
|
|
1945
|
+
self.login()
|
|
1946
|
+
return self.get_detection_sensibility(
|
|
1947
|
+
serial, type_value, max_retries + 1
|
|
1948
|
+
)
|
|
1949
|
+
|
|
1950
|
+
raise HTTPError from err
|
|
1951
|
+
|
|
1952
|
+
try:
|
|
1953
|
+
response_json = req.json()
|
|
1954
|
+
|
|
1955
|
+
except ValueError as err:
|
|
1956
|
+
raise PyEzvizError("Could not decode response:" + str(err)) from err
|
|
1957
|
+
|
|
1958
|
+
if response_json["resultCode"] != "0":
|
|
1959
|
+
if response_json["resultCode"] == "-1":
|
|
1960
|
+
_LOGGER.warning(
|
|
1961
|
+
"Camera %s is offline or unreachable, retrying %s of %s",
|
|
1962
|
+
serial,
|
|
1963
|
+
max_retries,
|
|
1964
|
+
MAX_RETRIES,
|
|
1965
|
+
)
|
|
1966
|
+
return self.get_detection_sensibility(
|
|
1967
|
+
serial, type_value, max_retries + 1
|
|
1968
|
+
)
|
|
1969
|
+
raise PyEzvizError(
|
|
1970
|
+
f"Unable to get detection sensibility. Got: {response_json}"
|
|
1971
|
+
)
|
|
1972
|
+
|
|
1973
|
+
if response_json["algorithmConfig"]["algorithmList"]:
|
|
1974
|
+
for idx in response_json["algorithmConfig"]["algorithmList"]:
|
|
1975
|
+
if idx["type"] == type_value:
|
|
1976
|
+
return idx["value"]
|
|
1977
|
+
|
|
1978
|
+
return None
|
|
1979
|
+
|
|
1980
|
+
# soundtype: 0 = normal, 1 = intensive, 2 = disabled ... don't ask me why...
|
|
1981
|
+
def alarm_sound(
|
|
1982
|
+
self, serial: str, sound_type: int, enable: int = 1, max_retries: int = 0
|
|
1983
|
+
) -> bool:
|
|
1984
|
+
"""Enable alarm sound by API."""
|
|
1985
|
+
if max_retries > MAX_RETRIES:
|
|
1986
|
+
raise PyEzvizError("Can't gather proper data. Max retries exceeded.")
|
|
1987
|
+
|
|
1988
|
+
if sound_type not in [0, 1, 2]:
|
|
1989
|
+
raise PyEzvizError(
|
|
1990
|
+
"Invalid sound_type, should be 0,1,2: " + str(sound_type)
|
|
1991
|
+
)
|
|
1992
|
+
|
|
1993
|
+
try:
|
|
1994
|
+
req = self._session.put(
|
|
1995
|
+
"https://"
|
|
1996
|
+
+ self._token["api_url"]
|
|
1997
|
+
+ API_ENDPOINT_DEVICES
|
|
1998
|
+
+ serial
|
|
1999
|
+
+ API_ENDPOINT_ALARM_SOUND,
|
|
2000
|
+
data={
|
|
2001
|
+
"enable": enable,
|
|
2002
|
+
"soundType": sound_type,
|
|
2003
|
+
"voiceId": "0",
|
|
2004
|
+
"deviceSerial": serial,
|
|
2005
|
+
},
|
|
2006
|
+
timeout=self._timeout,
|
|
2007
|
+
)
|
|
2008
|
+
|
|
2009
|
+
req.raise_for_status()
|
|
2010
|
+
|
|
2011
|
+
except requests.HTTPError as err:
|
|
2012
|
+
if err.response.status_code == 401:
|
|
2013
|
+
# session is wrong, need to re-log-in
|
|
2014
|
+
self.login()
|
|
2015
|
+
return self.alarm_sound(serial, sound_type, enable, max_retries + 1)
|
|
2016
|
+
|
|
2017
|
+
raise HTTPError from err
|
|
2018
|
+
|
|
2019
|
+
try:
|
|
2020
|
+
response_json = req.json()
|
|
2021
|
+
|
|
2022
|
+
except ValueError as err:
|
|
2023
|
+
raise PyEzvizError("Could not decode response:" + str(err)) from err
|
|
2024
|
+
|
|
2025
|
+
_LOGGER.debug("Response: %s", response_json)
|
|
2026
|
+
|
|
2027
|
+
return True
|
|
2028
|
+
|
|
2029
|
+
def _get_page_list(self) -> Any:
|
|
2030
|
+
"""Get ezviz device info broken down in sections."""
|
|
2031
|
+
return self._api_get_pagelist(
|
|
2032
|
+
page_filter="CLOUD, TIME_PLAN, CONNECTION, SWITCH,"
|
|
2033
|
+
"STATUS, WIFI, NODISTURB, KMS,"
|
|
2034
|
+
"P2P, TIME_PLAN, CHANNEL, VTM, DETECTOR,"
|
|
2035
|
+
"FEATURE, CUSTOM_TAG, UPGRADE, VIDEO_QUALITY,"
|
|
2036
|
+
"QOS, PRODUCTS_INFO, SIM_CARD, MULTI_UPGRADE_EXT,"
|
|
2037
|
+
"FEATURE_INFO",
|
|
2038
|
+
json_key=None,
|
|
2039
|
+
)
|
|
2040
|
+
|
|
2041
|
+
def get_device(self) -> Any:
|
|
2042
|
+
"""Get ezviz devices filter."""
|
|
2043
|
+
return self._api_get_pagelist(page_filter="CLOUD", json_key="deviceInfos")
|
|
2044
|
+
|
|
2045
|
+
def get_connection(self) -> Any:
|
|
2046
|
+
"""Get ezviz connection infos filter."""
|
|
2047
|
+
return self._api_get_pagelist(
|
|
2048
|
+
page_filter="CONNECTION", json_key="CONNECTION"
|
|
2049
|
+
)
|
|
2050
|
+
|
|
2051
|
+
def _get_status(self) -> Any:
|
|
2052
|
+
"""Get ezviz status infos filter."""
|
|
2053
|
+
return self._api_get_pagelist(page_filter="STATUS", json_key="STATUS")
|
|
2054
|
+
|
|
2055
|
+
def get_switch(self) -> Any:
|
|
2056
|
+
"""Get ezviz switch infos filter."""
|
|
2057
|
+
return self._api_get_pagelist(
|
|
2058
|
+
page_filter="SWITCH", json_key="SWITCH"
|
|
2059
|
+
)
|
|
2060
|
+
|
|
2061
|
+
def _get_wifi(self) -> Any:
|
|
2062
|
+
"""Get ezviz wifi infos filter."""
|
|
2063
|
+
return self._api_get_pagelist(page_filter="WIFI", json_key="WIFI")
|
|
2064
|
+
|
|
2065
|
+
def _get_nodisturb(self) -> Any:
|
|
2066
|
+
"""Get ezviz nodisturb infos filter."""
|
|
2067
|
+
return self._api_get_pagelist(
|
|
2068
|
+
page_filter="NODISTURB", json_key="NODISTURB"
|
|
2069
|
+
)
|
|
2070
|
+
|
|
2071
|
+
def _get_p2p(self) -> Any:
|
|
2072
|
+
"""Get ezviz P2P infos filter."""
|
|
2073
|
+
return self._api_get_pagelist(page_filter="P2P", json_key="P2P")
|
|
2074
|
+
|
|
2075
|
+
def _get_kms(self) -> Any:
|
|
2076
|
+
"""Get ezviz KMS infos filter."""
|
|
2077
|
+
return self._api_get_pagelist(page_filter="KMS", json_key="KMS")
|
|
2078
|
+
|
|
2079
|
+
def _get_time_plan(self) -> Any:
|
|
2080
|
+
"""Get ezviz TIME_PLAN infos filter."""
|
|
2081
|
+
return self._api_get_pagelist(page_filter="TIME_PLAN", json_key="TIME_PLAN")
|
|
2082
|
+
|
|
2083
|
+
def close_session(self) -> None:
|
|
2084
|
+
"""Clear current session."""
|
|
2085
|
+
if self._session:
|
|
2086
|
+
self._session.close()
|
|
2087
|
+
|
|
2088
|
+
self._session = requests.session()
|
|
2089
|
+
self._session.headers.update(REQUEST_HEADER) # Reset session.
|