pydiagral 1.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.
- pydiagral/__init__.py +28 -0
- pydiagral/api.py +1249 -0
- pydiagral/constants.py +4 -0
- pydiagral/exceptions.py +39 -0
- pydiagral/models.py +1498 -0
- pydiagral/utils.py +14 -0
- pydiagral-1.0.0.dist-info/METADATA +828 -0
- pydiagral-1.0.0.dist-info/RECORD +10 -0
- pydiagral-1.0.0.dist-info/WHEEL +4 -0
- pydiagral-1.0.0.dist-info/licenses/LICENSE +674 -0
pydiagral/api.py
ADDED
@@ -0,0 +1,1249 @@
|
|
1
|
+
"""Module for interacting with the Diagral API.
|
2
|
+
|
3
|
+
This module provides a DiagralAPI class that encapsulates all the functionality
|
4
|
+
for communicating with the Diagral alarm system API, including authentication,
|
5
|
+
retrieving system status, and controlling various aspects of the alarm system.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from __future__ import annotations
|
9
|
+
|
10
|
+
from datetime import datetime
|
11
|
+
import logging
|
12
|
+
import re
|
13
|
+
import time
|
14
|
+
from typing import Any, Self
|
15
|
+
|
16
|
+
import aiohttp
|
17
|
+
|
18
|
+
from .constants import API_VERSION, BASE_URL
|
19
|
+
from .exceptions import (
|
20
|
+
AuthenticationError,
|
21
|
+
ClientError,
|
22
|
+
ConfigurationError,
|
23
|
+
DiagralAPIError,
|
24
|
+
ServerError,
|
25
|
+
SessionError,
|
26
|
+
ValidationError,
|
27
|
+
)
|
28
|
+
from .models import (
|
29
|
+
AlarmConfiguration,
|
30
|
+
Anomalies,
|
31
|
+
ApiKeys,
|
32
|
+
ApiKeyWithSecret,
|
33
|
+
DeviceList,
|
34
|
+
HTTPErrorResponse,
|
35
|
+
HTTPValidationError,
|
36
|
+
LoginResponse,
|
37
|
+
Rudes,
|
38
|
+
SystemDetails,
|
39
|
+
SystemStatus,
|
40
|
+
Webhook,
|
41
|
+
)
|
42
|
+
from .utils import generate_hmac_signature
|
43
|
+
|
44
|
+
_LOGGER = logging.getLogger(__name__)
|
45
|
+
|
46
|
+
# Minimum Python version: 3.10
|
47
|
+
|
48
|
+
|
49
|
+
class DiagralAPI:
|
50
|
+
"""Provide interface for interacting with the Diagral API.
|
51
|
+
|
52
|
+
This class encapsulates all the functionality for communicating with
|
53
|
+
the Diagral alarm system API, including authentication, retrieving
|
54
|
+
system status, and controlling various aspects of the alarm system.
|
55
|
+
"""
|
56
|
+
|
57
|
+
def __init__(
|
58
|
+
self,
|
59
|
+
username: str,
|
60
|
+
password: str,
|
61
|
+
serial_id: str,
|
62
|
+
apikey: str | None = None,
|
63
|
+
secret_key: str | None = None,
|
64
|
+
pincode: int | None = None,
|
65
|
+
) -> None:
|
66
|
+
"""Initialize the DiagralAPI instance.
|
67
|
+
|
68
|
+
Args:
|
69
|
+
username (str): The email address for Diagral API authentication.
|
70
|
+
password (str): The password for Diagral API authentication.
|
71
|
+
serial_id (str): The serial ID of the Diagral system.
|
72
|
+
apikey (str | None, optional): The API key for additional authentication. Defaults to None.
|
73
|
+
secret_key (str | None, optional): The secret key for additional authentication. Defaults to None.
|
74
|
+
pincode (int | None, optional): The PIN code for the Diagral system. Defaults to None.
|
75
|
+
|
76
|
+
Raises:
|
77
|
+
ConfigurationError: If any required field is empty or invalid.
|
78
|
+
|
79
|
+
"""
|
80
|
+
# Validate username as an email
|
81
|
+
if (
|
82
|
+
not username
|
83
|
+
or not isinstance(username, str)
|
84
|
+
or not self.__is_valid_email(username)
|
85
|
+
):
|
86
|
+
raise ConfigurationError("username must be a valid non-empty email address")
|
87
|
+
self.username = username
|
88
|
+
|
89
|
+
# Validate password
|
90
|
+
if not password or not isinstance(password, str):
|
91
|
+
raise ConfigurationError("password must be a non-empty string")
|
92
|
+
self.__password = password
|
93
|
+
|
94
|
+
# Validate serial_id
|
95
|
+
if not serial_id or not isinstance(serial_id, str):
|
96
|
+
raise ConfigurationError("serial_id must be a non-empty string")
|
97
|
+
self.serial_id = serial_id
|
98
|
+
|
99
|
+
# Set apikey and secret_key
|
100
|
+
self.__apikey = apikey
|
101
|
+
self.__secret_key = secret_key
|
102
|
+
|
103
|
+
# Validate pincode
|
104
|
+
if pincode is not None:
|
105
|
+
if not isinstance(pincode, int):
|
106
|
+
raise ConfigurationError("pincode must be an integer")
|
107
|
+
self.__pincode = pincode
|
108
|
+
|
109
|
+
# Initialize session and access_token
|
110
|
+
self.session: aiohttp.ClientSession | None = None
|
111
|
+
self.__access_token: str | None = None
|
112
|
+
self.access_token_expires: datetime | None = None
|
113
|
+
|
114
|
+
# Set default values for other attributes
|
115
|
+
self.alarm_configuration: AlarmConfiguration | None = None
|
116
|
+
|
117
|
+
async def __aenter__(self) -> Self:
|
118
|
+
"""Initialize the aiohttp ClientSession."""
|
119
|
+
self.session = aiohttp.ClientSession()
|
120
|
+
_LOGGER.info("Successfully initialized DiagralAPI session")
|
121
|
+
return self
|
122
|
+
|
123
|
+
async def __aexit__(self, exc_type, exc, tb):
|
124
|
+
"""Close the aiohttp ClientSession."""
|
125
|
+
if self.session:
|
126
|
+
await self.session.close()
|
127
|
+
|
128
|
+
async def _request(
|
129
|
+
self, method: str, endpoint: str, timeout: float = 30, **kwargs
|
130
|
+
) -> tuple[dict[str, Any], int]:
|
131
|
+
"""Make an asynchronous HTTP request to the specified endpoint.
|
132
|
+
|
133
|
+
Args:
|
134
|
+
method (str): The HTTP method to use for the request (e.g., 'GET', 'POST').
|
135
|
+
endpoint (str): The API endpoint to send the request to.
|
136
|
+
timeout (float, optional): The timeout for the request in seconds. Defaults to 30.
|
137
|
+
**kwargs: Additional keyword arguments to pass to the request.
|
138
|
+
|
139
|
+
Returns:
|
140
|
+
tuple[dict[str, Any], int]: A tuple containing:
|
141
|
+
- dict[str, Any]: The JSON response from the API.
|
142
|
+
- int: The HTTP status code of the response.
|
143
|
+
|
144
|
+
Raises:
|
145
|
+
SessionError: If the session is not initialized.
|
146
|
+
DiagralAPIError: If the request results in a 400 status code or other API errors.
|
147
|
+
AuthenticationError: If the request results in a 401 or 403 status code.
|
148
|
+
ValidationError: If the request results in a 422 status code.
|
149
|
+
ServerError: If the request results in a 500 or 503 status code.
|
150
|
+
ClientError: If there is a network error.
|
151
|
+
aiohttp.ContentTypeError: If the response is not valid JSON.
|
152
|
+
|
153
|
+
"""
|
154
|
+
if not self.session:
|
155
|
+
error_msg = "Session not initialized from __aenter__."
|
156
|
+
_LOGGER.error(error_msg)
|
157
|
+
raise SessionError(error_msg)
|
158
|
+
|
159
|
+
url = f"{BASE_URL}/{API_VERSION}/{endpoint}"
|
160
|
+
headers = kwargs.pop("headers", {})
|
161
|
+
_LOGGER.debug(
|
162
|
+
"Sending %s request to %s with headers %s and data %s",
|
163
|
+
method,
|
164
|
+
url,
|
165
|
+
headers,
|
166
|
+
kwargs.get("json", {}),
|
167
|
+
)
|
168
|
+
|
169
|
+
try:
|
170
|
+
async with self.session.request(
|
171
|
+
method, url, headers=headers, timeout=timeout, **kwargs
|
172
|
+
) as response:
|
173
|
+
response_data = await response.json()
|
174
|
+
if response.status == 400:
|
175
|
+
response = HTTPErrorResponse(**response_data)
|
176
|
+
raise DiagralAPIError(
|
177
|
+
f"Bad request - Detail : {response.detail}",
|
178
|
+
)
|
179
|
+
if response.status == 401:
|
180
|
+
response = HTTPErrorResponse(**response_data)
|
181
|
+
raise AuthenticationError(
|
182
|
+
f"Unauthorized - Invalid or expired token - Detail : {response.detail}"
|
183
|
+
)
|
184
|
+
if response.status == 403:
|
185
|
+
response = HTTPErrorResponse(**response_data)
|
186
|
+
raise AuthenticationError(
|
187
|
+
f"Forbidden - The user does not have the right permissions - Detail : {response.detail}"
|
188
|
+
)
|
189
|
+
if response.status == 422:
|
190
|
+
_LOGGER.debug("test: %s", response_data)
|
191
|
+
response = HTTPValidationError(**response_data)
|
192
|
+
raise ValidationError(
|
193
|
+
f"Validation Error - Detail : {response.detail}"
|
194
|
+
)
|
195
|
+
if response.status == 500:
|
196
|
+
response = HTTPErrorResponse(**response_data)
|
197
|
+
raise ServerError(
|
198
|
+
f"Internal Server Error - Detail : {response.detail}"
|
199
|
+
)
|
200
|
+
if response.status == 503:
|
201
|
+
response = HTTPErrorResponse(**response_data)
|
202
|
+
raise ServerError(
|
203
|
+
f"Service temporarily unavailable - Detail : {response.detail}"
|
204
|
+
)
|
205
|
+
if response.status >= 400:
|
206
|
+
_LOGGER.debug(
|
207
|
+
"Received response with status code: %d and content %s",
|
208
|
+
response.status,
|
209
|
+
await response.json(),
|
210
|
+
)
|
211
|
+
raise DiagralAPIError(
|
212
|
+
f"API error: {response.status} - Error Message : {await response.text()}",
|
213
|
+
status_code=response.status,
|
214
|
+
)
|
215
|
+
return await response.json(), response.status
|
216
|
+
except aiohttp.ContentTypeError as e:
|
217
|
+
raise ValidationError(f"Invalid JSON response: {e}") from e
|
218
|
+
except aiohttp.ClientError as e:
|
219
|
+
raise ClientError(f"Network error: {e}") from e
|
220
|
+
|
221
|
+
async def login(self) -> None:
|
222
|
+
"""Asynchronously logs in to the Diagral API using the provided username and password.
|
223
|
+
|
224
|
+
This method sends a POST request to the authentication endpoint with the necessary credentials.
|
225
|
+
If the login is successful, it retrieves and stores the access token.
|
226
|
+
If the login fails, it raises an appropriate error.
|
227
|
+
|
228
|
+
Raises:
|
229
|
+
AuthenticationError: If the login fails or the access token cannot be obtained.
|
230
|
+
|
231
|
+
"""
|
232
|
+
|
233
|
+
if not self.session:
|
234
|
+
error_msg = "Session not initialized from __aenter__."
|
235
|
+
_LOGGER.error(error_msg)
|
236
|
+
raise SessionError(error_msg)
|
237
|
+
|
238
|
+
_LOGGER.debug("Attempting to login to Diagral API")
|
239
|
+
_DATA = {"username": self.username, "password": self.__password}
|
240
|
+
try:
|
241
|
+
response_data, *_ = await self._request(
|
242
|
+
"POST", "users/authenticate/login?vendor=DIAGRAL", json=_DATA
|
243
|
+
)
|
244
|
+
_LOGGER.debug("Login Response data: %s", response_data)
|
245
|
+
login_response = LoginResponse.from_dict(response_data)
|
246
|
+
_LOGGER.debug("Login response: %s", login_response)
|
247
|
+
|
248
|
+
self.__access_token = login_response.access_token
|
249
|
+
if not self.__access_token:
|
250
|
+
error_msg = "Failed to obtain authentication access_token"
|
251
|
+
_LOGGER.error(error_msg)
|
252
|
+
raise AuthenticationError(error_msg)
|
253
|
+
|
254
|
+
_LOGGER.info("Successfully logged in to Diagral API")
|
255
|
+
except DiagralAPIError as e:
|
256
|
+
error_msg = f"Failed to login : {e!s}"
|
257
|
+
_LOGGER.error(error_msg)
|
258
|
+
raise AuthenticationError(error_msg) from e
|
259
|
+
|
260
|
+
async def set_apikey(self) -> ApiKeyWithSecret:
|
261
|
+
"""Asynchronously set the API key for the Diagral API.
|
262
|
+
|
263
|
+
This method first ensures that the access token is valid and not expired.
|
264
|
+
If the access token is expired, it attempts to log in again to refresh it.
|
265
|
+
Then, it sends a request to create a new API key using the current access token.
|
266
|
+
If the API key is successfully created, it verifies the API key to ensure its validity.
|
267
|
+
|
268
|
+
Returns:
|
269
|
+
ApiKeyWithSecret: An instance of ApiKeyWithSecret containing the created API key and secret key.
|
270
|
+
|
271
|
+
Raises:
|
272
|
+
AuthenticationError: If the API key is not found in the response or if the created API key fails validation.
|
273
|
+
|
274
|
+
"""
|
275
|
+
|
276
|
+
if not self.__access_token:
|
277
|
+
await self.login()
|
278
|
+
|
279
|
+
if (
|
280
|
+
self.__access_token
|
281
|
+
and self.access_token_expires
|
282
|
+
and datetime.now() >= self.access_token_expires
|
283
|
+
):
|
284
|
+
_LOGGER.warning("Access token has expired, attempting to login again")
|
285
|
+
await self.login()
|
286
|
+
|
287
|
+
_DATA = {"serial_id": self.serial_id}
|
288
|
+
_HEADERS = {
|
289
|
+
"Authorization": f"Bearer {self.__access_token}",
|
290
|
+
}
|
291
|
+
|
292
|
+
try:
|
293
|
+
response_data, *_ = await self._request(
|
294
|
+
"POST", "users/api_key", json=_DATA, headers=_HEADERS
|
295
|
+
)
|
296
|
+
set_apikey_response = ApiKeyWithSecret.from_dict(response_data)
|
297
|
+
self.__apikey = set_apikey_response.api_key
|
298
|
+
if not self.__apikey:
|
299
|
+
error_msg = "API key not found in response"
|
300
|
+
_LOGGER.error(error_msg)
|
301
|
+
raise AuthenticationError(error_msg)
|
302
|
+
self.__secret_key = set_apikey_response.secret_key
|
303
|
+
if not self.__secret_key:
|
304
|
+
error_msg = "Secret key not found in response"
|
305
|
+
_LOGGER.error(error_msg)
|
306
|
+
raise AuthenticationError(error_msg)
|
307
|
+
|
308
|
+
_LOGGER.info("Successfully created new API key: ...%s", self.__apikey[-4:])
|
309
|
+
# Verify if the API key is valid
|
310
|
+
try:
|
311
|
+
await self.validate_apikey()
|
312
|
+
_LOGGER.info(
|
313
|
+
"Successfully verified new API key: ...%s", self.__apikey[-4:]
|
314
|
+
)
|
315
|
+
except AuthenticationError as e:
|
316
|
+
_LOGGER.error("Created API key failed validation: %s", e)
|
317
|
+
self.__apikey = None
|
318
|
+
raise
|
319
|
+
except DiagralAPIError as e:
|
320
|
+
error_msg = f"Failed to create API key: {e!s}"
|
321
|
+
_LOGGER.error(error_msg)
|
322
|
+
raise AuthenticationError(error_msg) from e
|
323
|
+
|
324
|
+
return ApiKeyWithSecret(api_key=self.__apikey, secret_key=self.__secret_key)
|
325
|
+
|
326
|
+
async def validate_apikey(self, apikey: str | None = None) -> None:
|
327
|
+
"""Validate the current or provided API key by checking it against the list of valid keys.
|
328
|
+
|
329
|
+
This method performs the following steps:
|
330
|
+
1. Checks if the API key is available. If not, logs a warning and raises an AuthenticationError.
|
331
|
+
2. Ensures that an access token is available by calling the login method if necessary.
|
332
|
+
3. Sends a GET request to retrieve the list of valid API keys associated with the user's system.
|
333
|
+
4. Checks if the current or provided API key is in the list of valid keys.
|
334
|
+
5. Logs a success message if the API key is valid, otherwise raises an AuthenticationError.
|
335
|
+
|
336
|
+
Args:
|
337
|
+
apikey (str | None, optional): The API key to validate. If not provided, the instance's API key is used. Defaults to None.
|
338
|
+
|
339
|
+
Raises:
|
340
|
+
ConfigurationError: If no API key is provided or if the API key is invalid.
|
341
|
+
|
342
|
+
"""
|
343
|
+
|
344
|
+
apikey_to_validate = apikey or self.__apikey
|
345
|
+
|
346
|
+
if not apikey_to_validate:
|
347
|
+
_LOGGER.warning("No API key provided to validate")
|
348
|
+
raise ConfigurationError("No API key provided to validate")
|
349
|
+
|
350
|
+
if not self.__access_token:
|
351
|
+
await self.login()
|
352
|
+
|
353
|
+
_HEADERS = {
|
354
|
+
"Authorization": f"Bearer {self.__access_token}",
|
355
|
+
}
|
356
|
+
response_data, *_ = await self._request(
|
357
|
+
"GET",
|
358
|
+
f"users/systems/{self.serial_id}/api_keys",
|
359
|
+
headers=_HEADERS,
|
360
|
+
)
|
361
|
+
validate_apikey_response = ApiKeys.from_dict(response_data)
|
362
|
+
is_valid = any(
|
363
|
+
key_info.api_key == apikey_to_validate
|
364
|
+
for key_info in validate_apikey_response.api_keys
|
365
|
+
)
|
366
|
+
if is_valid:
|
367
|
+
_LOGGER.info("API key successfully validated")
|
368
|
+
else:
|
369
|
+
raise AuthenticationError(
|
370
|
+
"API key is invalid or not found in the list of valid keys"
|
371
|
+
)
|
372
|
+
|
373
|
+
async def delete_apikey(self, apikey: str | None = None) -> None:
|
374
|
+
"""Asynchronously delete the specified or current API key.
|
375
|
+
|
376
|
+
This method deletes the API key associated with the instance or the provided API key.
|
377
|
+
If the API key is not available, it raises an AuthenticationError. If the access token
|
378
|
+
is not available, it attempts to log in to obtain one. The method then sends a DELETE
|
379
|
+
request to the appropriate endpoint to delete the API key. Upon successful deletion,
|
380
|
+
it logs an informational message and sets the `apikey` and `secret_key` attributes to None.
|
381
|
+
|
382
|
+
Args:
|
383
|
+
apikey (str | None, optional): The API key to delete. If not provided, the instance's API key is used. Defaults to None.
|
384
|
+
|
385
|
+
Raises:
|
386
|
+
AuthenticationError: If the API key is not available.
|
387
|
+
|
388
|
+
"""
|
389
|
+
|
390
|
+
apikey_to_delete = apikey or self.__apikey
|
391
|
+
|
392
|
+
if not apikey_to_delete:
|
393
|
+
raise AuthenticationError("An API key is required to delete it")
|
394
|
+
|
395
|
+
if not self.__access_token:
|
396
|
+
await self.login()
|
397
|
+
|
398
|
+
_HEADERS = {
|
399
|
+
"Authorization": f"Bearer {self.__access_token}",
|
400
|
+
}
|
401
|
+
await self._request(
|
402
|
+
"DELETE",
|
403
|
+
f"users/systems/{self.serial_id}/api_keys/{apikey_to_delete}",
|
404
|
+
headers=_HEADERS,
|
405
|
+
)
|
406
|
+
_LOGGER.info("Successfully deleted API key: ...%s", apikey_to_delete[-4:])
|
407
|
+
|
408
|
+
if apikey is None:
|
409
|
+
self.__apikey = None
|
410
|
+
self.__secret_key = None
|
411
|
+
|
412
|
+
async def get_configuration(self) -> None:
|
413
|
+
"""Asynchronously retrieve the configuration of the Diagral system.
|
414
|
+
|
415
|
+
This method retrieves the configuration of the Diagral system by sending a GET
|
416
|
+
request to the appropriate endpoint. If the access token is not available, it
|
417
|
+
attempts to log in to obtain one. Upon successful retrieval, it logs an informational
|
418
|
+
message with the configuration details. The retrieved configuration is stored in
|
419
|
+
the self.alarm_configuration attribute, allowing it to be reused within the same
|
420
|
+
session without needing to collect it multiple times.
|
421
|
+
|
422
|
+
Returns:
|
423
|
+
AlarmConfiguration: The configuration details of the Diagral system.
|
424
|
+
|
425
|
+
Raises:
|
426
|
+
AuthenticationError: If the access token is not available.
|
427
|
+
|
428
|
+
"""
|
429
|
+
|
430
|
+
if not self.__apikey or not self.__secret_key:
|
431
|
+
raise AuthenticationError(
|
432
|
+
"API key and secret key required to get configuration"
|
433
|
+
)
|
434
|
+
|
435
|
+
_TIMESTAMP = str(int(time.time()))
|
436
|
+
_HMAC = generate_hmac_signature(
|
437
|
+
timestamp=_TIMESTAMP,
|
438
|
+
serial_id=self.serial_id,
|
439
|
+
api_key=self.__apikey,
|
440
|
+
secret_key=self.__secret_key,
|
441
|
+
)
|
442
|
+
|
443
|
+
_HEADERS = {
|
444
|
+
"X-HMAC": _HMAC,
|
445
|
+
"X-TIMESTAMP": _TIMESTAMP,
|
446
|
+
"X-APIKEY": self.__apikey,
|
447
|
+
}
|
448
|
+
response_data, *_ = await self._request(
|
449
|
+
"GET", f"systems/{self.serial_id}/configurations", headers=_HEADERS
|
450
|
+
)
|
451
|
+
self.alarm_configuration = AlarmConfiguration.from_dict(response_data)
|
452
|
+
_LOGGER.debug(
|
453
|
+
"Successfully retrieved configuration: %s", self.alarm_configuration
|
454
|
+
)
|
455
|
+
return self.alarm_configuration
|
456
|
+
|
457
|
+
async def get_devices_info(self) -> DeviceList:
|
458
|
+
"""Asynchronously retrieves information about various device types from the alarm configuration.
|
459
|
+
|
460
|
+
The method retrieve information for each device type (cameras, commands, sensors, sirens,
|
461
|
+
transmitters) from the alarm configuration, and compiles this information into a dictionary.
|
462
|
+
|
463
|
+
Returns:
|
464
|
+
dict: A dictionary where the keys are device types and the values are lists of dictionaries
|
465
|
+
containing device information (index and label).
|
466
|
+
|
467
|
+
Raises:
|
468
|
+
ConfigurationError: If the alarm configuration cannot be retrieved.
|
469
|
+
|
470
|
+
"""
|
471
|
+
|
472
|
+
if not self.alarm_configuration:
|
473
|
+
await self.get_configuration()
|
474
|
+
|
475
|
+
if not self.alarm_configuration:
|
476
|
+
raise ConfigurationError("Failed to retrieve alarm configuration")
|
477
|
+
|
478
|
+
device_types = sorted(
|
479
|
+
["cameras", "commands", "sensors", "sirens", "transmitters"]
|
480
|
+
)
|
481
|
+
devices_infos = {}
|
482
|
+
for device_type in device_types:
|
483
|
+
_LOGGER.debug("Retrieving devices information for %s", device_type)
|
484
|
+
devices = getattr(self.alarm_configuration, device_type, None)
|
485
|
+
if devices is not None:
|
486
|
+
devices_infos[device_type] = [
|
487
|
+
{"index": device.index, "label": device.label} for device in devices
|
488
|
+
]
|
489
|
+
else:
|
490
|
+
devices_infos[device_type] = []
|
491
|
+
_LOGGER.debug("Successfully retrieved devices information: %s", devices_infos)
|
492
|
+
return DeviceList.from_dict(devices_infos)
|
493
|
+
|
494
|
+
async def get_system_details(self) -> SystemDetails:
|
495
|
+
"""Asynchronously retrieves the system details.
|
496
|
+
|
497
|
+
This method fetches the system details using the provided API key, secret key,
|
498
|
+
and PIN code. It generates the necessary HMAC signature and includes it in the
|
499
|
+
request headers.
|
500
|
+
|
501
|
+
Returns:
|
502
|
+
SystemDetails: An instance of SystemDetails containing the retrieved system information.
|
503
|
+
|
504
|
+
Raises:
|
505
|
+
AuthenticationError: If the API key, secret key, or PIN code is not provided.
|
506
|
+
|
507
|
+
"""
|
508
|
+
|
509
|
+
if not self.__apikey or not self.__secret_key:
|
510
|
+
raise AuthenticationError(
|
511
|
+
"API key and secret key required to get system details"
|
512
|
+
)
|
513
|
+
|
514
|
+
if not self.__pincode:
|
515
|
+
raise AuthenticationError("PIN code required to get system details")
|
516
|
+
|
517
|
+
_TIMESTAMP = str(int(time.time()))
|
518
|
+
_HMAC = generate_hmac_signature(
|
519
|
+
timestamp=_TIMESTAMP,
|
520
|
+
serial_id=self.serial_id,
|
521
|
+
api_key=self.__apikey,
|
522
|
+
secret_key=self.__secret_key,
|
523
|
+
)
|
524
|
+
|
525
|
+
_HEADERS = {
|
526
|
+
"X-PIN-CODE": str(self.__pincode),
|
527
|
+
"X-HMAC": _HMAC,
|
528
|
+
"X-TIMESTAMP": _TIMESTAMP,
|
529
|
+
"X-APIKEY": self.__apikey,
|
530
|
+
}
|
531
|
+
response_data, *_ = await self._request(
|
532
|
+
"GET", f"systems/{self.serial_id}", headers=_HEADERS
|
533
|
+
)
|
534
|
+
_LOGGER.debug("Successfully retrieved system details: %s", response_data)
|
535
|
+
return SystemDetails.from_dict(response_data)
|
536
|
+
|
537
|
+
async def get_system_status(self) -> SystemStatus:
|
538
|
+
"""Asynchronously retrieves the system status.
|
539
|
+
|
540
|
+
This method fetches the current status of the system using the provided API key,
|
541
|
+
secret key, and PIN code. It generates an HMAC signature for authentication and
|
542
|
+
sends a GET request to the system status endpoint.
|
543
|
+
|
544
|
+
Returns:
|
545
|
+
SystemStatus: An instance of SystemStatus containing the retrieved system status.
|
546
|
+
|
547
|
+
Raises:
|
548
|
+
AuthenticationError: If the API key, secret key, or PIN code is not provided.
|
549
|
+
|
550
|
+
"""
|
551
|
+
|
552
|
+
if not self.__apikey or not self.__secret_key:
|
553
|
+
raise AuthenticationError(
|
554
|
+
"API key and secret key required to get system details"
|
555
|
+
)
|
556
|
+
|
557
|
+
if not self.__pincode:
|
558
|
+
raise AuthenticationError("PIN code required to get system details")
|
559
|
+
|
560
|
+
_TIMESTAMP = str(int(time.time()))
|
561
|
+
_HMAC = generate_hmac_signature(
|
562
|
+
timestamp=_TIMESTAMP,
|
563
|
+
serial_id=self.serial_id,
|
564
|
+
api_key=self.__apikey,
|
565
|
+
secret_key=self.__secret_key,
|
566
|
+
)
|
567
|
+
|
568
|
+
_HEADERS = {
|
569
|
+
"X-PIN-CODE": str(self.__pincode),
|
570
|
+
"X-HMAC": _HMAC,
|
571
|
+
"X-TIMESTAMP": _TIMESTAMP,
|
572
|
+
"X-APIKEY": self.__apikey,
|
573
|
+
}
|
574
|
+
response_data, *_ = await self._request(
|
575
|
+
"GET", f"systems/{self.serial_id}/status", headers=_HEADERS
|
576
|
+
)
|
577
|
+
_LOGGER.debug("Successfully retrieved system status: %s", response_data)
|
578
|
+
return SystemStatus.from_dict(response_data)
|
579
|
+
|
580
|
+
async def __system_action(self, action: str) -> SystemStatus:
|
581
|
+
"""Perform a system action such as start, stop, presence, partial start 1, or partial start 2.
|
582
|
+
|
583
|
+
Args:
|
584
|
+
action (str): The action to perform. Must be one of 'start', 'stop', 'presence', 'partial_start_1', or 'partial_start_2'.
|
585
|
+
|
586
|
+
Returns:
|
587
|
+
SystemStatus: An instance of SystemStatus containing the system status after performing the action.
|
588
|
+
|
589
|
+
Raises:
|
590
|
+
ConfigurationError: If the action is not one of the allowed actions.
|
591
|
+
AuthenticationError: If the API key, secret key, or PIN code is missing.
|
592
|
+
All other exceptions from _request function are propagated.
|
593
|
+
|
594
|
+
"""
|
595
|
+
|
596
|
+
if action not in [
|
597
|
+
"start",
|
598
|
+
"stop",
|
599
|
+
"presence",
|
600
|
+
"partial_start_1",
|
601
|
+
"partial_start_2",
|
602
|
+
]:
|
603
|
+
raise ConfigurationError(
|
604
|
+
"Action must be one of 'start', 'stop', 'presence', 'partial_start_1', or 'partial_start_2'"
|
605
|
+
)
|
606
|
+
|
607
|
+
if not self.__apikey or not self.__secret_key:
|
608
|
+
raise AuthenticationError(
|
609
|
+
"API key and secret key required to get system details"
|
610
|
+
)
|
611
|
+
|
612
|
+
if not self.__pincode:
|
613
|
+
raise AuthenticationError(f"PIN code required to do system action {action}")
|
614
|
+
|
615
|
+
_TIMESTAMP = str(int(time.time()))
|
616
|
+
_HMAC = generate_hmac_signature(
|
617
|
+
timestamp=_TIMESTAMP,
|
618
|
+
serial_id=self.serial_id,
|
619
|
+
api_key=self.__apikey,
|
620
|
+
secret_key=self.__secret_key,
|
621
|
+
)
|
622
|
+
|
623
|
+
_HEADERS = {
|
624
|
+
"X-PIN-CODE": str(self.__pincode),
|
625
|
+
"X-HMAC": _HMAC,
|
626
|
+
"X-TIMESTAMP": _TIMESTAMP,
|
627
|
+
"X-APIKEY": self.__apikey,
|
628
|
+
}
|
629
|
+
response_data, *_ = await self._request(
|
630
|
+
"POST", f"systems/{self.serial_id}/{action}", headers=_HEADERS
|
631
|
+
)
|
632
|
+
_LOGGER.debug(
|
633
|
+
"Successfully performed action %s: %s", action.upper(), response_data
|
634
|
+
)
|
635
|
+
return SystemStatus.from_dict(response_data)
|
636
|
+
|
637
|
+
async def start_system(self) -> SystemStatus:
|
638
|
+
"""Asynchronously starts the system.
|
639
|
+
|
640
|
+
This method sends a request to start the system and returns the system status.
|
641
|
+
|
642
|
+
Returns:
|
643
|
+
SystemStatus: An instance of SystemStatus containing the system status after starting the system.
|
644
|
+
|
645
|
+
Raises:
|
646
|
+
All exceptions from __system_action function are propagated.
|
647
|
+
|
648
|
+
"""
|
649
|
+
|
650
|
+
return await self.__system_action("start")
|
651
|
+
|
652
|
+
async def stop_system(self) -> SystemStatus:
|
653
|
+
"""Asynchronously stops the system.
|
654
|
+
|
655
|
+
This method sends a request to stop the system and returns the system status.
|
656
|
+
|
657
|
+
Returns:
|
658
|
+
SystemStatus: An instance of SystemStatus containing the system status after stopping the system.
|
659
|
+
|
660
|
+
Raises:
|
661
|
+
All exceptions from __system_action function are propagated.
|
662
|
+
|
663
|
+
"""
|
664
|
+
|
665
|
+
return await self.__system_action("stop")
|
666
|
+
|
667
|
+
async def presence(self) -> SystemStatus:
|
668
|
+
"""Asynchronously starts the system in presence mode.
|
669
|
+
|
670
|
+
Returns:
|
671
|
+
SystemStatus: An instance of SystemStatus containing the system status after starting the system in presence mode.
|
672
|
+
|
673
|
+
Raises:
|
674
|
+
All exceptions from __system_action function are propagated.
|
675
|
+
|
676
|
+
"""
|
677
|
+
|
678
|
+
return await self.__system_action("presence")
|
679
|
+
|
680
|
+
async def partial_start_system(self, id: int = 1) -> SystemStatus: # NOT-TESTED
|
681
|
+
"""Initiate a partial start of the system.
|
682
|
+
|
683
|
+
Args:
|
684
|
+
id (int, optional): The ID of the partial start. Must be either 1 or 2. Defaults to 1.
|
685
|
+
|
686
|
+
Returns:
|
687
|
+
SystemStatus: An instance of SystemStatus containing the system status after performing the partial start.
|
688
|
+
|
689
|
+
Raises:
|
690
|
+
ConfigurationError: If the provided ID is not 1 or 2.
|
691
|
+
All other exceptions from __system_action function are propagated.
|
692
|
+
|
693
|
+
"""
|
694
|
+
|
695
|
+
if id not in [1, 2]:
|
696
|
+
raise ConfigurationError("Partial Start Id must be 1 or 2")
|
697
|
+
|
698
|
+
return await self.__system_action(f"partial_start_{id}")
|
699
|
+
|
700
|
+
async def __action_group_system(
|
701
|
+
self, action: str, groups: list[int]
|
702
|
+
) -> SystemStatus: # TO-TEST
|
703
|
+
"""Perform an action on a group of systems.
|
704
|
+
|
705
|
+
This method activates or disables a group of systems based on the provided action.
|
706
|
+
|
707
|
+
Args:
|
708
|
+
action (str): The action to perform. Must be either 'activate_group' or 'disable_group'.
|
709
|
+
groups (list[int]): A list of group indices to perform the action on.
|
710
|
+
|
711
|
+
Returns:
|
712
|
+
SystemStatus: An instance of SystemStatus containing the system status after performing the action.
|
713
|
+
|
714
|
+
Raises:
|
715
|
+
ConfigurationError: If the action is not 'activate_group' or 'disable_group', or if the groups are invalid.
|
716
|
+
AuthenticationError: If the API key, secret key, or PIN code is not provided.
|
717
|
+
All other exceptions from _request function are propagated.
|
718
|
+
|
719
|
+
"""
|
720
|
+
|
721
|
+
if action not in ["activate_group", "disable_group"]:
|
722
|
+
raise ConfigurationError(
|
723
|
+
"Action must be either 'activate_group' or 'disable_group'"
|
724
|
+
)
|
725
|
+
|
726
|
+
if not self.__apikey or not self.__secret_key:
|
727
|
+
raise AuthenticationError(
|
728
|
+
"API key and secret key required to get system details"
|
729
|
+
)
|
730
|
+
|
731
|
+
if not self.__pincode:
|
732
|
+
raise AuthenticationError("PIN code required to get system details")
|
733
|
+
|
734
|
+
# Get the configuration if it is not already available
|
735
|
+
if not self.alarm_configuration:
|
736
|
+
await self.get_configuration()
|
737
|
+
|
738
|
+
# Check if the groups are valid
|
739
|
+
invalid_groups = [
|
740
|
+
group
|
741
|
+
for group in groups
|
742
|
+
if group not in [g.index for g in self.alarm_configuration.groups]
|
743
|
+
]
|
744
|
+
if invalid_groups:
|
745
|
+
raise ConfigurationError(
|
746
|
+
f"The following groups do not exist in your system: {invalid_groups}"
|
747
|
+
)
|
748
|
+
|
749
|
+
_TIMESTAMP = str(int(time.time()))
|
750
|
+
_HMAC = generate_hmac_signature(
|
751
|
+
timestamp=_TIMESTAMP,
|
752
|
+
serial_id=self.serial_id,
|
753
|
+
api_key=self.__apikey,
|
754
|
+
secret_key=self.__secret_key,
|
755
|
+
)
|
756
|
+
|
757
|
+
_HEADERS = {
|
758
|
+
"X-PIN-CODE": str(self.__pincode),
|
759
|
+
"X-HMAC": _HMAC,
|
760
|
+
"X-TIMESTAMP": _TIMESTAMP,
|
761
|
+
"X-APIKEY": self.__apikey,
|
762
|
+
}
|
763
|
+
data = {"groups": groups}
|
764
|
+
response_data, *_ = await self._request(
|
765
|
+
"POST",
|
766
|
+
f"systems/{self.serial_id}/{action}",
|
767
|
+
headers=_HEADERS,
|
768
|
+
json=data,
|
769
|
+
)
|
770
|
+
_LOGGER.debug(
|
771
|
+
"Successfully %s %s: %s", action.replace("_", " "), groups, response_data
|
772
|
+
)
|
773
|
+
return SystemStatus.from_dict(response_data)
|
774
|
+
|
775
|
+
async def activate_group(self, groups: list[int]) -> SystemStatus:
|
776
|
+
"""Asynchronously activates a group of systems.
|
777
|
+
|
778
|
+
Args:
|
779
|
+
groups (list[int]): A list of integers representing the groups to be activated.
|
780
|
+
|
781
|
+
Returns:
|
782
|
+
SystemStatus: An instance of SystemStatus containing the system status after activating the groups.
|
783
|
+
|
784
|
+
Raises:
|
785
|
+
ConfigurationError: If the action is not one of the allowed actions.
|
786
|
+
All other exceptions from __action_group_system function are propagated.
|
787
|
+
|
788
|
+
"""
|
789
|
+
|
790
|
+
if not isinstance(groups, list) or not all(
|
791
|
+
isinstance(item, int) for item in groups
|
792
|
+
):
|
793
|
+
raise ConfigurationError("Groups must be a list of integers")
|
794
|
+
|
795
|
+
return await self.__action_group_system("activate_group", groups)
|
796
|
+
|
797
|
+
async def disable_group(self, groups: list[int]) -> SystemStatus:
|
798
|
+
"""Asynchronously disables a group of systems.
|
799
|
+
|
800
|
+
Args:
|
801
|
+
groups (list[int]): A list of integers representing the groups to disable.
|
802
|
+
|
803
|
+
Returns:
|
804
|
+
SystemStatus: An instance of SystemStatus containing the system status after disabling the groups.
|
805
|
+
|
806
|
+
Raises:
|
807
|
+
ConfigurationError: If the action is not one of the allowed actions.
|
808
|
+
All other exceptions from __action_group_system function are propagated.
|
809
|
+
|
810
|
+
"""
|
811
|
+
|
812
|
+
if not isinstance(groups, list) or not all(
|
813
|
+
isinstance(item, int) for item in groups
|
814
|
+
):
|
815
|
+
raise ConfigurationError("Groups must be a list of integers")
|
816
|
+
|
817
|
+
return await self.__action_group_system("disable_group", groups)
|
818
|
+
|
819
|
+
async def __action_product(
|
820
|
+
self, action: str, type: str, product_id: int
|
821
|
+
) -> SystemStatus: # NOT-TESTED
|
822
|
+
"""Perform an action on a product in the Diagral system.
|
823
|
+
|
824
|
+
This method enables or disables a product based on the provided action.
|
825
|
+
|
826
|
+
Args:
|
827
|
+
action (str): The action to perform. Must be either 'enable' or 'disable'.
|
828
|
+
type (str): The type of product to perform the action on.
|
829
|
+
product_id (int): The ID of the product to perform the action on.
|
830
|
+
|
831
|
+
Returns:
|
832
|
+
SystemStatus: An instance of SystemStatus containing the system status after performing the action.
|
833
|
+
|
834
|
+
Raises:
|
835
|
+
ConfigurationError: If the action is not 'enable' or 'disable', or if the product type is invalid.
|
836
|
+
AuthenticationError: If the API key, secret key, or PIN code is not provided.
|
837
|
+
All other exceptions from _request function are propagated.
|
838
|
+
|
839
|
+
"""
|
840
|
+
|
841
|
+
if action not in ["enable", "disable"]:
|
842
|
+
raise ConfigurationError("Action must be either 'enable' or 'disable'")
|
843
|
+
|
844
|
+
if type not in ["CENTRAL", "SENSOR", "COMMAND", "ALARM", "BOX", "PLUG"]:
|
845
|
+
raise ConfigurationError(
|
846
|
+
"Product type must be one of 'CENTRAL', 'SENSOR', 'COMMAND', 'ALARM', 'BOX', or 'PLUG'"
|
847
|
+
)
|
848
|
+
|
849
|
+
if not self.__apikey or not self.__secret_key:
|
850
|
+
raise AuthenticationError(
|
851
|
+
"API key and secret key required to get system details"
|
852
|
+
)
|
853
|
+
|
854
|
+
if not self.__pincode:
|
855
|
+
raise AuthenticationError("PIN code required to get system details")
|
856
|
+
|
857
|
+
_TIMESTAMP = str(int(time.time()))
|
858
|
+
_HMAC = generate_hmac_signature(
|
859
|
+
timestamp=_TIMESTAMP,
|
860
|
+
serial_id=self.serial_id,
|
861
|
+
api_key=self.__apikey,
|
862
|
+
secret_key=self.__secret_key,
|
863
|
+
)
|
864
|
+
|
865
|
+
_HEADERS = {
|
866
|
+
"X-PIN-CODE": str(self.__pincode),
|
867
|
+
"X-HMAC": _HMAC,
|
868
|
+
"X-TIMESTAMP": _TIMESTAMP,
|
869
|
+
"X-APIKEY": self.__apikey,
|
870
|
+
}
|
871
|
+
response_data, *_ = await self._request(
|
872
|
+
"POST",
|
873
|
+
f"systems/{self.serial_id}/{type}/{product_id}/{action}",
|
874
|
+
headers=_HEADERS,
|
875
|
+
)
|
876
|
+
return SystemStatus.from_dict(response_data)
|
877
|
+
|
878
|
+
async def enable_product(
|
879
|
+
self, type: str, product_id: int
|
880
|
+
) -> SystemStatus: # NOT-TESTED
|
881
|
+
"""Asynchronously enables a product in the system.
|
882
|
+
|
883
|
+
Args:
|
884
|
+
type (str): The type of the product to enable.
|
885
|
+
product_id (int): The unique identifier of the product to enable.
|
886
|
+
|
887
|
+
Returns:
|
888
|
+
SystemStatus: An instance of SystemStatus containing the system status after enabling the product.
|
889
|
+
|
890
|
+
Raises:
|
891
|
+
All exceptions from __action_product function are propagated.
|
892
|
+
|
893
|
+
"""
|
894
|
+
return await self.__action_product(
|
895
|
+
action="enable", type=type, product_id=product_id
|
896
|
+
)
|
897
|
+
|
898
|
+
async def disable_product(
|
899
|
+
self, type: str, product_id: int
|
900
|
+
) -> SystemStatus: # NOT-TESTED
|
901
|
+
"""Asynchronously enables a product in the system.
|
902
|
+
|
903
|
+
Args:
|
904
|
+
type (str): The type of the product to enable.
|
905
|
+
product_id (int): The unique identifier of the product to enable.
|
906
|
+
|
907
|
+
Returns:
|
908
|
+
SystemStatus: An instance of SystemStatus containing the system status after disabling the product.
|
909
|
+
|
910
|
+
Raises:
|
911
|
+
All exceptions from __action_product function are propagated.
|
912
|
+
|
913
|
+
"""
|
914
|
+
return await self.__action_product(
|
915
|
+
action="disable", type=type, product_id=product_id
|
916
|
+
)
|
917
|
+
|
918
|
+
async def get_anomalies(self) -> Anomalies | dict:
|
919
|
+
"""Asynchronously retrieves anomalies for the system.
|
920
|
+
|
921
|
+
This method fetches the anomalies associated with the system identified by the serial ID. It requires valid API key and secret key for authentication.
|
922
|
+
|
923
|
+
Returns:
|
924
|
+
Anomalies | dict: An instance of the Anomalies class populated with the data retrieved from the API, or an empty dictionary if no anomalies are found.
|
925
|
+
|
926
|
+
Raises:
|
927
|
+
AuthenticationError: If the API key or secret key is not provided.
|
928
|
+
|
929
|
+
"""
|
930
|
+
|
931
|
+
if not self.__apikey or not self.__secret_key:
|
932
|
+
raise AuthenticationError(
|
933
|
+
"API key and secret key required to get system details"
|
934
|
+
)
|
935
|
+
|
936
|
+
_TIMESTAMP = str(int(time.time()))
|
937
|
+
_HMAC = generate_hmac_signature(
|
938
|
+
timestamp=_TIMESTAMP,
|
939
|
+
serial_id=self.serial_id,
|
940
|
+
api_key=self.__apikey,
|
941
|
+
secret_key=self.__secret_key,
|
942
|
+
)
|
943
|
+
|
944
|
+
_HEADERS = {
|
945
|
+
"X-HMAC": _HMAC,
|
946
|
+
"X-TIMESTAMP": _TIMESTAMP,
|
947
|
+
"X-APIKEY": self.__apikey,
|
948
|
+
}
|
949
|
+
try:
|
950
|
+
response_data, *_ = await self._request(
|
951
|
+
"GET",
|
952
|
+
f"systems/{self.serial_id}/anomalies",
|
953
|
+
headers=_HEADERS,
|
954
|
+
)
|
955
|
+
except DiagralAPIError as e:
|
956
|
+
if e.status_code == 404 and "No anomalies found" in e.message:
|
957
|
+
_LOGGER.info("No anomalies found for the system")
|
958
|
+
return {}
|
959
|
+
return Anomalies.from_dict(response_data)
|
960
|
+
|
961
|
+
async def delete_anomalies(self, anomaly_id: int) -> None: # NOT-IMPLEMENTED
|
962
|
+
"""Asynchronously delete the list of anomalies.
|
963
|
+
|
964
|
+
This method is currently not implemented.
|
965
|
+
|
966
|
+
Raises:
|
967
|
+
NotImplementedError: This method is not yet implemented.
|
968
|
+
|
969
|
+
"""
|
970
|
+
raise NotImplementedError("Method not yet implemented")
|
971
|
+
|
972
|
+
async def get_automatism_rudes(self) -> Rudes: # NOT-TESTED
|
973
|
+
"""Asynchronously retrieves the automatism Rudes for the system.
|
974
|
+
|
975
|
+
This method fetches the Rudes data for the system identified by the serial ID.
|
976
|
+
It requires valid API and secret keys for authentication.
|
977
|
+
|
978
|
+
Returns:
|
979
|
+
Rudes: An instance of the Rudes class populated with the data from the response.
|
980
|
+
|
981
|
+
Raises:
|
982
|
+
AuthenticationError: If the API key or secret key is not provided.
|
983
|
+
All other exceptions from _request function are propagated.
|
984
|
+
|
985
|
+
"""
|
986
|
+
|
987
|
+
if not self.__apikey or not self.__secret_key:
|
988
|
+
raise AuthenticationError(
|
989
|
+
"API key and secret key required to get system details"
|
990
|
+
)
|
991
|
+
|
992
|
+
_TIMESTAMP = str(int(time.time()))
|
993
|
+
_HMAC = generate_hmac_signature(
|
994
|
+
timestamp=_TIMESTAMP,
|
995
|
+
serial_id=self.serial_id,
|
996
|
+
api_key=self.__apikey,
|
997
|
+
secret_key=self.__secret_key,
|
998
|
+
)
|
999
|
+
|
1000
|
+
_HEADERS = {
|
1001
|
+
"X-HMAC": _HMAC,
|
1002
|
+
"X-TIMESTAMP": _TIMESTAMP,
|
1003
|
+
"X-APIKEY": self.__apikey,
|
1004
|
+
}
|
1005
|
+
response_data, *_ = await self._request(
|
1006
|
+
"GET",
|
1007
|
+
f"systems/{self.serial_id}/rudes",
|
1008
|
+
headers=_HEADERS,
|
1009
|
+
)
|
1010
|
+
return Rudes.from_dict(response_data)
|
1011
|
+
|
1012
|
+
async def set_automatism_action(
|
1013
|
+
self, canal: str, action: str
|
1014
|
+
) -> None: # NOT-IMPLEMENTED
|
1015
|
+
"""Set the automatism action.
|
1016
|
+
|
1017
|
+
This method is currently not implemented.
|
1018
|
+
|
1019
|
+
Args:
|
1020
|
+
canal (str): The canal for the automatism action.
|
1021
|
+
action (str): The action to be set for the automatism.
|
1022
|
+
|
1023
|
+
Raises:
|
1024
|
+
NotImplementedError: Always, as this method is not yet implemented.
|
1025
|
+
|
1026
|
+
"""
|
1027
|
+
raise NotImplementedError("Method not yet implemented")
|
1028
|
+
|
1029
|
+
async def get_webhook(self) -> Webhook:
|
1030
|
+
"""Retrieve the webhook subscription details for the system.
|
1031
|
+
|
1032
|
+
Returns:
|
1033
|
+
Webhook: An instance of the Webhook class containing the subscription details.
|
1034
|
+
|
1035
|
+
Raises:
|
1036
|
+
AuthenticationError: If the API key or secret key is not provided.
|
1037
|
+
|
1038
|
+
"""
|
1039
|
+
|
1040
|
+
if not self.__apikey or not self.__secret_key:
|
1041
|
+
raise AuthenticationError(
|
1042
|
+
"API key and secret key required to get system details"
|
1043
|
+
)
|
1044
|
+
|
1045
|
+
_TIMESTAMP = str(int(time.time()))
|
1046
|
+
_HMAC = generate_hmac_signature(
|
1047
|
+
timestamp=_TIMESTAMP,
|
1048
|
+
serial_id=self.serial_id,
|
1049
|
+
api_key=self.__apikey,
|
1050
|
+
secret_key=self.__secret_key,
|
1051
|
+
)
|
1052
|
+
|
1053
|
+
_HEADERS = {
|
1054
|
+
"X-HMAC": _HMAC,
|
1055
|
+
"X-TIMESTAMP": _TIMESTAMP,
|
1056
|
+
"X-APIKEY": self.__apikey,
|
1057
|
+
}
|
1058
|
+
response_data, *_ = await self._request(
|
1059
|
+
"GET",
|
1060
|
+
f"webhooks/{self.serial_id}/subscription",
|
1061
|
+
headers=_HEADERS,
|
1062
|
+
)
|
1063
|
+
return Webhook.from_dict(response_data)
|
1064
|
+
|
1065
|
+
async def __webhook_action_create_update(
|
1066
|
+
self,
|
1067
|
+
action: str,
|
1068
|
+
webhook_url: str,
|
1069
|
+
subscribe_to_anomaly: bool = False,
|
1070
|
+
subscribe_to_alert: bool = False,
|
1071
|
+
subscribe_to_state: bool = False,
|
1072
|
+
) -> Webhook | None:
|
1073
|
+
"""Create or update a webhook subscription.
|
1074
|
+
|
1075
|
+
Args:
|
1076
|
+
action (str): The action to perform, either 'register' or 'update'.
|
1077
|
+
webhook_url (str): The URL of the webhook to register or update.
|
1078
|
+
subscribe_to_anomaly (bool, optional): Whether to subscribe to anomaly notifications. Defaults to False.
|
1079
|
+
subscribe_to_alert (bool, optional): Whether to subscribe to alert notifications. Defaults to False.
|
1080
|
+
subscribe_to_state (bool, optional): Whether to subscribe to state notifications. Defaults to False.
|
1081
|
+
|
1082
|
+
Returns:
|
1083
|
+
Webhook | None: The created or updated Webhook object, or None if registration is skipped.
|
1084
|
+
|
1085
|
+
Raises:
|
1086
|
+
ConfigurationError: If the action is not 'register' or 'update'.
|
1087
|
+
AuthenticationError: If the API key or secret key is missing.
|
1088
|
+
ValidationError: If the webhook URL is invalid or if the subscription already exists.
|
1089
|
+
|
1090
|
+
"""
|
1091
|
+
if action not in ["register", "update"]:
|
1092
|
+
raise ConfigurationError("Action must be either 'register' or 'update'")
|
1093
|
+
|
1094
|
+
if not self.__apikey or not self.__secret_key:
|
1095
|
+
raise AuthenticationError(
|
1096
|
+
"API key and secret key required to get system details"
|
1097
|
+
)
|
1098
|
+
|
1099
|
+
if action == "register" and not any(
|
1100
|
+
[subscribe_to_anomaly, subscribe_to_alert, subscribe_to_state]
|
1101
|
+
):
|
1102
|
+
_LOGGER.warning("No subscriptions selected, skipping webhook registration")
|
1103
|
+
return None
|
1104
|
+
|
1105
|
+
if not re.match(r"^https?://", webhook_url):
|
1106
|
+
raise ValidationError(
|
1107
|
+
"Invalid webhook URL. Must start with http:// or https://"
|
1108
|
+
)
|
1109
|
+
|
1110
|
+
_TIMESTAMP = str(int(time.time()))
|
1111
|
+
_HMAC = generate_hmac_signature(
|
1112
|
+
timestamp=_TIMESTAMP,
|
1113
|
+
serial_id=self.serial_id,
|
1114
|
+
api_key=self.__apikey,
|
1115
|
+
secret_key=self.__secret_key,
|
1116
|
+
)
|
1117
|
+
|
1118
|
+
_HEADERS = {
|
1119
|
+
"X-HMAC": _HMAC,
|
1120
|
+
"X-TIMESTAMP": _TIMESTAMP,
|
1121
|
+
"X-APIKEY": self.__apikey,
|
1122
|
+
}
|
1123
|
+
data = {
|
1124
|
+
"webhook_url": webhook_url,
|
1125
|
+
"subscribe_to_anomaly": subscribe_to_anomaly,
|
1126
|
+
"subscribe_to_alert": subscribe_to_alert,
|
1127
|
+
"subscribe_to_state": subscribe_to_state,
|
1128
|
+
}
|
1129
|
+
method = "POST" if action == "register" else "PUT"
|
1130
|
+
try:
|
1131
|
+
response_data, *_ = await self._request(
|
1132
|
+
method,
|
1133
|
+
f"webhooks/{self.serial_id}/subscription",
|
1134
|
+
headers=_HEADERS,
|
1135
|
+
json=data,
|
1136
|
+
)
|
1137
|
+
except DiagralAPIError as e:
|
1138
|
+
if "Subscription already exists" in str(e):
|
1139
|
+
raise DiagralAPIError(
|
1140
|
+
"Webhook subscription already exists. Please use the 'update' action to modify it."
|
1141
|
+
) from e
|
1142
|
+
if "There is no subscription for" in str(e):
|
1143
|
+
raise DiagralAPIError(
|
1144
|
+
"No subscription found for the specified serial ID"
|
1145
|
+
) from e
|
1146
|
+
return Webhook.from_dict(response_data)
|
1147
|
+
|
1148
|
+
async def register_webhook(
|
1149
|
+
self,
|
1150
|
+
webhook_url: str,
|
1151
|
+
subscribe_to_anomaly: bool = False,
|
1152
|
+
subscribe_to_alert: bool = False,
|
1153
|
+
subscribe_to_state: bool = False,
|
1154
|
+
) -> Webhook | None:
|
1155
|
+
"""Register a webhook with the specified URL and subscription options.
|
1156
|
+
|
1157
|
+
Args:
|
1158
|
+
webhook_url (str): The URL to which the webhook will send data.
|
1159
|
+
subscribe_to_anomaly (bool, optional): If True, subscribe to anomaly events. Defaults to False.
|
1160
|
+
subscribe_to_alert (bool, optional): If True, subscribe to alert events. Defaults to False.
|
1161
|
+
subscribe_to_state (bool, optional): If True, subscribe to state events. Defaults to False.
|
1162
|
+
|
1163
|
+
Returns:
|
1164
|
+
Webhook | None: The registered Webhook object if successful, otherwise None.
|
1165
|
+
|
1166
|
+
"""
|
1167
|
+
return await self.__webhook_action_create_update(
|
1168
|
+
"register",
|
1169
|
+
webhook_url,
|
1170
|
+
subscribe_to_anomaly,
|
1171
|
+
subscribe_to_alert,
|
1172
|
+
subscribe_to_state,
|
1173
|
+
)
|
1174
|
+
|
1175
|
+
async def update_webhook(
|
1176
|
+
self,
|
1177
|
+
webhook_url: str,
|
1178
|
+
subscribe_to_anomaly: bool = False,
|
1179
|
+
subscribe_to_alert: bool = False,
|
1180
|
+
subscribe_to_state: bool = False,
|
1181
|
+
) -> Webhook | None:
|
1182
|
+
"""Update the webhook configuration with the specified parameters.
|
1183
|
+
|
1184
|
+
Args:
|
1185
|
+
webhook_url (str): The URL of the webhook to update.
|
1186
|
+
subscribe_to_anomaly (bool, optional): Whether to subscribe to anomaly notifications. Defaults to False.
|
1187
|
+
subscribe_to_alert (bool, optional): Whether to subscribe to alert notifications. Defaults to False.
|
1188
|
+
subscribe_to_state (bool, optional): Whether to subscribe to state notifications. Defaults to False.
|
1189
|
+
|
1190
|
+
Returns:
|
1191
|
+
Webhook | None: The registered Webhook object if successful, otherwise None.
|
1192
|
+
|
1193
|
+
"""
|
1194
|
+
return await self.__webhook_action_create_update(
|
1195
|
+
"update",
|
1196
|
+
webhook_url,
|
1197
|
+
subscribe_to_anomaly,
|
1198
|
+
subscribe_to_alert,
|
1199
|
+
subscribe_to_state,
|
1200
|
+
)
|
1201
|
+
|
1202
|
+
async def delete_webhook(self) -> None:
|
1203
|
+
"""Asynchronously deletes the webhook subscription for the current system.
|
1204
|
+
|
1205
|
+
Raises:
|
1206
|
+
AuthenticationError: If the API key or secret key is not provided.
|
1207
|
+
|
1208
|
+
Returns:
|
1209
|
+
None
|
1210
|
+
|
1211
|
+
"""
|
1212
|
+
|
1213
|
+
if not self.__apikey or not self.__secret_key:
|
1214
|
+
raise AuthenticationError(
|
1215
|
+
"API key and secret key required to get system details"
|
1216
|
+
)
|
1217
|
+
|
1218
|
+
_TIMESTAMP = str(int(time.time()))
|
1219
|
+
_HMAC = generate_hmac_signature(
|
1220
|
+
timestamp=_TIMESTAMP,
|
1221
|
+
serial_id=self.serial_id,
|
1222
|
+
api_key=self.__apikey,
|
1223
|
+
secret_key=self.__secret_key,
|
1224
|
+
)
|
1225
|
+
|
1226
|
+
_HEADERS = {
|
1227
|
+
"X-HMAC": _HMAC,
|
1228
|
+
"X-TIMESTAMP": _TIMESTAMP,
|
1229
|
+
"X-APIKEY": self.__apikey,
|
1230
|
+
}
|
1231
|
+
response_data, *_ = await self._request(
|
1232
|
+
"DELETE",
|
1233
|
+
f"webhooks/{self.serial_id}/subscription",
|
1234
|
+
headers=_HEADERS,
|
1235
|
+
)
|
1236
|
+
|
1237
|
+
@staticmethod
|
1238
|
+
def __is_valid_email(email: str) -> bool:
|
1239
|
+
"""Validate the format of an email address.
|
1240
|
+
|
1241
|
+
Args:
|
1242
|
+
email (str): The email address to validate.
|
1243
|
+
|
1244
|
+
Returns:
|
1245
|
+
bool: True if the email is valid, False otherwise.
|
1246
|
+
|
1247
|
+
"""
|
1248
|
+
email_regex = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
|
1249
|
+
return re.match(email_regex, email) is not None
|