pydiagral 1.4.0__py3-none-any.whl → 1.5.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/api.py +145 -71
- pydiagral/exceptions.py +10 -2
- pydiagral/models.py +25 -1
- pydiagral/utils.py +1 -1
- {pydiagral-1.4.0.dist-info → pydiagral-1.5.0.dist-info}/METADATA +62 -20
- pydiagral-1.5.0.dist-info/RECORD +10 -0
- pydiagral-1.4.0.dist-info/RECORD +0 -10
- {pydiagral-1.4.0.dist-info → pydiagral-1.5.0.dist-info}/WHEEL +0 -0
- {pydiagral-1.4.0.dist-info → pydiagral-1.5.0.dist-info}/licenses/LICENSE +0 -0
pydiagral/api.py
CHANGED
@@ -7,7 +7,6 @@ retrieving system status, and controlling various aspects of the alarm system.
|
|
7
7
|
|
8
8
|
from __future__ import annotations
|
9
9
|
|
10
|
-
from datetime import datetime
|
11
10
|
import logging
|
12
11
|
import re
|
13
12
|
import time
|
@@ -17,6 +16,8 @@ import aiohttp
|
|
17
16
|
|
18
17
|
from .constants import API_VERSION, BASE_URL
|
19
18
|
from .exceptions import (
|
19
|
+
APIKeyCreationError,
|
20
|
+
APIValidationError,
|
20
21
|
AuthenticationError,
|
21
22
|
ClientError,
|
22
23
|
ConfigurationError,
|
@@ -37,11 +38,12 @@ from .models import (
|
|
37
38
|
Rudes,
|
38
39
|
SystemDetails,
|
39
40
|
SystemStatus,
|
41
|
+
TryConnectResult,
|
40
42
|
Webhook,
|
41
43
|
)
|
42
44
|
from .utils import generate_hmac_signature
|
43
45
|
|
44
|
-
_LOGGER = logging.getLogger(__name__)
|
46
|
+
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
45
47
|
|
46
48
|
# Minimum Python version: 3.10
|
47
49
|
|
@@ -84,17 +86,17 @@ class DiagralAPI:
|
|
84
86
|
or not self.__is_valid_email(username)
|
85
87
|
):
|
86
88
|
raise ConfigurationError("username must be a valid non-empty email address")
|
87
|
-
self.username = username
|
89
|
+
self.username: str = username
|
88
90
|
|
89
91
|
# Validate password
|
90
92
|
if not password or not isinstance(password, str):
|
91
93
|
raise ConfigurationError("password must be a non-empty string")
|
92
|
-
self.__password = password
|
94
|
+
self.__password: str = password
|
93
95
|
|
94
96
|
# Validate serial_id
|
95
97
|
if not serial_id or not isinstance(serial_id, str):
|
96
98
|
raise ConfigurationError("serial_id must be a non-empty string")
|
97
|
-
self.serial_id = serial_id
|
99
|
+
self.serial_id: str = serial_id
|
98
100
|
|
99
101
|
# Set apikey and secret_key
|
100
102
|
self.__apikey = apikey
|
@@ -104,12 +106,11 @@ class DiagralAPI:
|
|
104
106
|
if pincode is not None:
|
105
107
|
if not isinstance(pincode, int):
|
106
108
|
raise ConfigurationError("pincode must be an integer")
|
107
|
-
self.__pincode = pincode
|
109
|
+
self.__pincode: int | None = pincode
|
108
110
|
|
109
111
|
# Initialize session and access_token
|
110
112
|
self.session: aiohttp.ClientSession | None = None
|
111
113
|
self.__access_token: str | None = None
|
112
|
-
self.access_token_expires: datetime | None = None
|
113
114
|
|
114
115
|
# Set default values for other attributes
|
115
116
|
self.alarm_configuration: AlarmConfiguration | None = None
|
@@ -120,7 +121,7 @@ class DiagralAPI:
|
|
120
121
|
_LOGGER.info("Successfully initialized DiagralAPI session")
|
121
122
|
return self
|
122
123
|
|
123
|
-
async def __aexit__(self, exc_type, exc, tb):
|
124
|
+
async def __aexit__(self, exc_type, exc, tb) -> None:
|
124
125
|
"""Close the aiohttp ClientSession."""
|
125
126
|
if self.session:
|
126
127
|
await self.session.close()
|
@@ -156,8 +157,8 @@ class DiagralAPI:
|
|
156
157
|
_LOGGER.error(error_msg)
|
157
158
|
raise SessionError(error_msg)
|
158
159
|
|
159
|
-
url = f"{BASE_URL}/{API_VERSION}/{endpoint}"
|
160
|
-
headers = kwargs.pop("headers", {})
|
160
|
+
url: str = f"{BASE_URL}/{API_VERSION}/{endpoint}"
|
161
|
+
headers: Any = kwargs.pop("headers", {})
|
161
162
|
_LOGGER.debug(
|
162
163
|
"Sending %s request to %s with headers %s and data %s",
|
163
164
|
method,
|
@@ -170,7 +171,7 @@ class DiagralAPI:
|
|
170
171
|
async with self.session.request(
|
171
172
|
method, url, headers=headers, timeout=timeout, **kwargs
|
172
173
|
) as response:
|
173
|
-
response_data = await response.json()
|
174
|
+
response_data: Any = await response.json()
|
174
175
|
if response.status == 400:
|
175
176
|
response = HTTPErrorResponse(**response_data)
|
176
177
|
raise DiagralAPIError(
|
@@ -236,13 +237,13 @@ class DiagralAPI:
|
|
236
237
|
raise SessionError(error_msg)
|
237
238
|
|
238
239
|
_LOGGER.debug("Attempting to login to Diagral API")
|
239
|
-
_DATA = {"username": self.username, "password": self.__password}
|
240
|
+
_DATA: dict[str, str] = {"username": self.username, "password": self.__password}
|
240
241
|
try:
|
241
242
|
response_data, *_ = await self._request(
|
242
243
|
"POST", "users/authenticate/login?vendor=DIAGRAL", json=_DATA
|
243
244
|
)
|
244
245
|
_LOGGER.debug("Login Response data: %s", response_data)
|
245
|
-
login_response = LoginResponse.from_dict(response_data)
|
246
|
+
login_response: LoginResponse = LoginResponse.from_dict(response_data)
|
246
247
|
_LOGGER.debug("Login response: %s", login_response)
|
247
248
|
|
248
249
|
self.__access_token = login_response.access_token
|
@@ -253,39 +254,30 @@ class DiagralAPI:
|
|
253
254
|
|
254
255
|
_LOGGER.info("Successfully logged in to Diagral API")
|
255
256
|
except DiagralAPIError as e:
|
256
|
-
error_msg = f"Failed to login : {e!s}"
|
257
|
+
error_msg: str = f"Failed to login : {e!s}"
|
257
258
|
_LOGGER.error(error_msg)
|
258
259
|
raise AuthenticationError(error_msg) from e
|
259
260
|
|
260
261
|
async def set_apikey(self) -> ApiKeyWithSecret:
|
261
262
|
"""Asynchronously set the API key for the Diagral API.
|
262
263
|
|
263
|
-
|
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.
|
264
|
+
It sends a request to create a new API key using the current access token.
|
266
265
|
If the API key is successfully created, it verifies the API key to ensure its validity.
|
267
266
|
|
268
267
|
Returns:
|
269
268
|
ApiKeyWithSecret: An instance of ApiKeyWithSecret containing the created API key and secret key.
|
270
269
|
|
271
270
|
Raises:
|
272
|
-
|
271
|
+
APIKeyCreationError: If the API key creation fails.
|
272
|
+
APIValidationError: If the API key validation fails.
|
273
273
|
|
274
274
|
"""
|
275
275
|
|
276
276
|
if not self.__access_token:
|
277
277
|
await self.login()
|
278
278
|
|
279
|
-
|
280
|
-
|
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 = {
|
279
|
+
_DATA: dict[str, str] = {"serial_id": self.serial_id}
|
280
|
+
_HEADERS: dict[str, str] = {
|
289
281
|
"Authorization": f"Bearer {self.__access_token}",
|
290
282
|
}
|
291
283
|
|
@@ -293,17 +285,19 @@ class DiagralAPI:
|
|
293
285
|
response_data, *_ = await self._request(
|
294
286
|
"POST", "users/api_key", json=_DATA, headers=_HEADERS
|
295
287
|
)
|
296
|
-
set_apikey_response = ApiKeyWithSecret.from_dict(
|
297
|
-
|
288
|
+
set_apikey_response: ApiKeyWithSecret = ApiKeyWithSecret.from_dict(
|
289
|
+
response_data
|
290
|
+
)
|
291
|
+
self.__apikey: str = set_apikey_response.api_key
|
298
292
|
if not self.__apikey:
|
299
293
|
error_msg = "API key not found in response"
|
300
294
|
_LOGGER.error(error_msg)
|
301
|
-
raise
|
302
|
-
self.__secret_key = set_apikey_response.secret_key
|
295
|
+
raise APIKeyCreationError(error_msg)
|
296
|
+
self.__secret_key: str = set_apikey_response.secret_key
|
303
297
|
if not self.__secret_key:
|
304
298
|
error_msg = "Secret key not found in response"
|
305
299
|
_LOGGER.error(error_msg)
|
306
|
-
raise
|
300
|
+
raise APIKeyCreationError(error_msg)
|
307
301
|
|
308
302
|
_LOGGER.info("Successfully created new API key: ...%s", self.__apikey[-4:])
|
309
303
|
# Verify if the API key is valid
|
@@ -312,14 +306,14 @@ class DiagralAPI:
|
|
312
306
|
_LOGGER.info(
|
313
307
|
"Successfully verified new API key: ...%s", self.__apikey[-4:]
|
314
308
|
)
|
315
|
-
except
|
309
|
+
except APIValidationError as e:
|
316
310
|
_LOGGER.error("Created API key failed validation: %s", e)
|
317
311
|
self.__apikey = None
|
318
312
|
raise
|
319
313
|
except DiagralAPIError as e:
|
320
|
-
error_msg = f"Failed to create API key: {e!s}"
|
314
|
+
error_msg: str = f"Failed to create API key: {e!s}"
|
321
315
|
_LOGGER.error(error_msg)
|
322
|
-
raise
|
316
|
+
raise APIKeyCreationError(error_msg) from e
|
323
317
|
|
324
318
|
return ApiKeyWithSecret(api_key=self.__apikey, secret_key=self.__secret_key)
|
325
319
|
|
@@ -338,10 +332,11 @@ class DiagralAPI:
|
|
338
332
|
|
339
333
|
Raises:
|
340
334
|
ConfigurationError: If no API key is provided or if the API key is invalid.
|
335
|
+
APIValidationError: If the API key is invalid or not found in the list of valid keys.
|
341
336
|
|
342
337
|
"""
|
343
338
|
|
344
|
-
apikey_to_validate = apikey or self.__apikey
|
339
|
+
apikey_to_validate: str = apikey or self.__apikey
|
345
340
|
|
346
341
|
if not apikey_to_validate:
|
347
342
|
_LOGGER.warning("No API key provided to validate")
|
@@ -350,7 +345,7 @@ class DiagralAPI:
|
|
350
345
|
if not self.__access_token:
|
351
346
|
await self.login()
|
352
347
|
|
353
|
-
_HEADERS = {
|
348
|
+
_HEADERS: dict[str, str] = {
|
354
349
|
"Authorization": f"Bearer {self.__access_token}",
|
355
350
|
}
|
356
351
|
response_data, *_ = await self._request(
|
@@ -358,7 +353,7 @@ class DiagralAPI:
|
|
358
353
|
f"users/systems/{self.serial_id}/api_keys",
|
359
354
|
headers=_HEADERS,
|
360
355
|
)
|
361
|
-
validate_apikey_response = ApiKeys.from_dict(response_data)
|
356
|
+
validate_apikey_response: ApiKeys = ApiKeys.from_dict(response_data)
|
362
357
|
is_valid = any(
|
363
358
|
key_info.api_key == apikey_to_validate
|
364
359
|
for key_info in validate_apikey_response.api_keys
|
@@ -366,7 +361,7 @@ class DiagralAPI:
|
|
366
361
|
if is_valid:
|
367
362
|
_LOGGER.info("API key successfully validated")
|
368
363
|
else:
|
369
|
-
raise
|
364
|
+
raise APIValidationError(
|
370
365
|
"API key is invalid or not found in the list of valid keys"
|
371
366
|
)
|
372
367
|
|
@@ -387,7 +382,7 @@ class DiagralAPI:
|
|
387
382
|
|
388
383
|
"""
|
389
384
|
|
390
|
-
apikey_to_delete = apikey or self.__apikey
|
385
|
+
apikey_to_delete: str = apikey or self.__apikey
|
391
386
|
|
392
387
|
if not apikey_to_delete:
|
393
388
|
raise AuthenticationError("An API key is required to delete it")
|
@@ -395,7 +390,7 @@ class DiagralAPI:
|
|
395
390
|
if not self.__access_token:
|
396
391
|
await self.login()
|
397
392
|
|
398
|
-
_HEADERS = {
|
393
|
+
_HEADERS: dict[str, str] = {
|
399
394
|
"Authorization": f"Bearer {self.__access_token}",
|
400
395
|
}
|
401
396
|
await self._request(
|
@@ -409,6 +404,63 @@ class DiagralAPI:
|
|
409
404
|
self.__apikey = None
|
410
405
|
self.__secret_key = None
|
411
406
|
|
407
|
+
async def try_connection(self, ephemeral: bool = True) -> TryConnectResult:
|
408
|
+
"""Test connection with the Diagral system.
|
409
|
+
|
410
|
+
This method tests the connection by either using provided API credentials or generating
|
411
|
+
temporary ones. It validates the connection by checking the system status.
|
412
|
+
|
413
|
+
Args:
|
414
|
+
ephemeral (bool, optional): If True, temporary API keys will be deleted after
|
415
|
+
connection test. Defaults to True.
|
416
|
+
|
417
|
+
Returns:
|
418
|
+
TryConnectResult: Object containing connection test results and optionally API keys
|
419
|
+
if non-ephemeral temporary keys were generated.
|
420
|
+
|
421
|
+
Raises:
|
422
|
+
APIKeyCreationError: If creation of temporary API keys fails
|
423
|
+
APIValidationError: If API key validation fails
|
424
|
+
SessionError: If the session is not initialized.
|
425
|
+
DiagralAPIError: If the request results in a 400 status code or other API errors.
|
426
|
+
AuthenticationError: If authentication fails or request results in a 401 or 403 status code.
|
427
|
+
ValidationError: If the request results in a 422 status code.
|
428
|
+
ServerError: If the request results in a 500 or 503 status code.
|
429
|
+
ClientError: If there is a network error.
|
430
|
+
|
431
|
+
Note:
|
432
|
+
If API credentials are not provided during client initialization, temporary
|
433
|
+
keys will be generated (if ephemeral) for the connection test. These keys will be:
|
434
|
+
- Deleted after the test if ephemeral=True
|
435
|
+
- Returned in the result if ephemeral=False
|
436
|
+
|
437
|
+
"""
|
438
|
+
|
439
|
+
result: TryConnectResult = TryConnectResult()
|
440
|
+
api_keys_provided = bool(self.__apikey and self.__secret_key)
|
441
|
+
|
442
|
+
# If API keys are not provided, generate temporary keys
|
443
|
+
if not api_keys_provided:
|
444
|
+
api_key_response: ApiKeyWithSecret = await self.set_apikey()
|
445
|
+
|
446
|
+
# Retrieve system status to validate connection
|
447
|
+
try:
|
448
|
+
await self.get_system_status()
|
449
|
+
except DiagralAPIError:
|
450
|
+
# If connection fails, clean up keys
|
451
|
+
if not api_keys_provided:
|
452
|
+
await self.delete_apikey(apikey=self.__apikey)
|
453
|
+
raise
|
454
|
+
|
455
|
+
# If connection is successful, clean up temporary keys if requested (ephemeral)
|
456
|
+
if ephemeral and not api_keys_provided:
|
457
|
+
await self.delete_apikey(apikey=self.__apikey)
|
458
|
+
elif not ephemeral and not api_keys_provided:
|
459
|
+
result.keys = api_key_response
|
460
|
+
|
461
|
+
result.result = True
|
462
|
+
return result
|
463
|
+
|
412
464
|
async def get_configuration(self) -> None:
|
413
465
|
"""Asynchronously retrieve the configuration of the Diagral system.
|
414
466
|
|
@@ -433,14 +485,14 @@ class DiagralAPI:
|
|
433
485
|
)
|
434
486
|
|
435
487
|
_TIMESTAMP = str(int(time.time()))
|
436
|
-
_HMAC = generate_hmac_signature(
|
488
|
+
_HMAC: str = generate_hmac_signature(
|
437
489
|
timestamp=_TIMESTAMP,
|
438
490
|
serial_id=self.serial_id,
|
439
491
|
api_key=self.__apikey,
|
440
492
|
secret_key=self.__secret_key,
|
441
493
|
)
|
442
494
|
|
443
|
-
_HEADERS = {
|
495
|
+
_HEADERS: dict[str, str] = {
|
444
496
|
"X-HMAC": _HMAC,
|
445
497
|
"X-TIMESTAMP": _TIMESTAMP,
|
446
498
|
"X-APIKEY": self.__apikey,
|
@@ -454,6 +506,28 @@ class DiagralAPI:
|
|
454
506
|
)
|
455
507
|
return self.alarm_configuration
|
456
508
|
|
509
|
+
async def get_alarm_name(self) -> str:
|
510
|
+
"""Get the name of the alarm from the configuration.
|
511
|
+
|
512
|
+
Returns:
|
513
|
+
str: The name of the alarm from the configuration.
|
514
|
+
|
515
|
+
Raises:
|
516
|
+
ConfigurationError: If unable to retrieve the alarm configuration.
|
517
|
+
|
518
|
+
Note:
|
519
|
+
This method will attempt to fetch the configuration if it hasn't been loaded yet.
|
520
|
+
|
521
|
+
"""
|
522
|
+
|
523
|
+
if not self.alarm_configuration:
|
524
|
+
await self.get_configuration()
|
525
|
+
|
526
|
+
if not self.alarm_configuration:
|
527
|
+
raise ConfigurationError("Failed to retrieve alarm configuration")
|
528
|
+
|
529
|
+
return self.alarm_configuration.alarm.name
|
530
|
+
|
457
531
|
async def get_devices_info(self) -> DeviceList:
|
458
532
|
"""Asynchronously retrieves information about various device types from the alarm configuration.
|
459
533
|
|
@@ -475,13 +549,13 @@ class DiagralAPI:
|
|
475
549
|
if not self.alarm_configuration:
|
476
550
|
raise ConfigurationError("Failed to retrieve alarm configuration")
|
477
551
|
|
478
|
-
device_types = sorted(
|
552
|
+
device_types: list[str] = sorted(
|
479
553
|
["cameras", "commands", "sensors", "sirens", "transmitters"]
|
480
554
|
)
|
481
555
|
devices_infos = {}
|
482
556
|
for device_type in device_types:
|
483
557
|
_LOGGER.debug("Retrieving devices information for %s", device_type)
|
484
|
-
devices = getattr(self.alarm_configuration, device_type, None)
|
558
|
+
devices: Any | None = getattr(self.alarm_configuration, device_type, None)
|
485
559
|
if devices is not None:
|
486
560
|
devices_infos[device_type] = [
|
487
561
|
{"index": device.index, "label": device.label} for device in devices
|
@@ -515,14 +589,14 @@ class DiagralAPI:
|
|
515
589
|
raise AuthenticationError("PIN code required to get system details")
|
516
590
|
|
517
591
|
_TIMESTAMP = str(int(time.time()))
|
518
|
-
_HMAC = generate_hmac_signature(
|
592
|
+
_HMAC: str = generate_hmac_signature(
|
519
593
|
timestamp=_TIMESTAMP,
|
520
594
|
serial_id=self.serial_id,
|
521
595
|
api_key=self.__apikey,
|
522
596
|
secret_key=self.__secret_key,
|
523
597
|
)
|
524
598
|
|
525
|
-
_HEADERS = {
|
599
|
+
_HEADERS: dict[str, str] = {
|
526
600
|
"X-PIN-CODE": str(self.__pincode),
|
527
601
|
"X-HMAC": _HMAC,
|
528
602
|
"X-TIMESTAMP": _TIMESTAMP,
|
@@ -545,27 +619,27 @@ class DiagralAPI:
|
|
545
619
|
SystemStatus: An instance of SystemStatus containing the retrieved system status.
|
546
620
|
|
547
621
|
Raises:
|
548
|
-
|
622
|
+
ConfigurationError: If the API key, secret key, or PIN code is not provided.
|
549
623
|
|
550
624
|
"""
|
551
625
|
|
552
626
|
if not self.__apikey or not self.__secret_key:
|
553
|
-
raise
|
627
|
+
raise ConfigurationError(
|
554
628
|
"API key and secret key required to get system details"
|
555
629
|
)
|
556
630
|
|
557
631
|
if not self.__pincode:
|
558
|
-
raise
|
632
|
+
raise ConfigurationError("PIN code required to get system details")
|
559
633
|
|
560
634
|
_TIMESTAMP = str(int(time.time()))
|
561
|
-
_HMAC = generate_hmac_signature(
|
635
|
+
_HMAC: str = generate_hmac_signature(
|
562
636
|
timestamp=_TIMESTAMP,
|
563
637
|
serial_id=self.serial_id,
|
564
638
|
api_key=self.__apikey,
|
565
639
|
secret_key=self.__secret_key,
|
566
640
|
)
|
567
641
|
|
568
|
-
_HEADERS = {
|
642
|
+
_HEADERS: dict[str, str] = {
|
569
643
|
"X-PIN-CODE": str(self.__pincode),
|
570
644
|
"X-HMAC": _HMAC,
|
571
645
|
"X-TIMESTAMP": _TIMESTAMP,
|
@@ -612,14 +686,14 @@ class DiagralAPI:
|
|
612
686
|
raise AuthenticationError(f"PIN code required to do system action {action}")
|
613
687
|
|
614
688
|
_TIMESTAMP = str(int(time.time()))
|
615
|
-
_HMAC = generate_hmac_signature(
|
689
|
+
_HMAC: str = generate_hmac_signature(
|
616
690
|
timestamp=_TIMESTAMP,
|
617
691
|
serial_id=self.serial_id,
|
618
692
|
api_key=self.__apikey,
|
619
693
|
secret_key=self.__secret_key,
|
620
694
|
)
|
621
695
|
|
622
|
-
_HEADERS = {
|
696
|
+
_HEADERS: dict[str, str] = {
|
623
697
|
"X-PIN-CODE": str(self.__pincode),
|
624
698
|
"X-HMAC": _HMAC,
|
625
699
|
"X-TIMESTAMP": _TIMESTAMP,
|
@@ -736,7 +810,7 @@ class DiagralAPI:
|
|
736
810
|
await self.get_configuration()
|
737
811
|
|
738
812
|
# Check if the groups are valid
|
739
|
-
invalid_groups = [
|
813
|
+
invalid_groups: list[int] = [
|
740
814
|
group
|
741
815
|
for group in groups
|
742
816
|
if group not in [g.index for g in self.alarm_configuration.groups]
|
@@ -747,20 +821,20 @@ class DiagralAPI:
|
|
747
821
|
)
|
748
822
|
|
749
823
|
_TIMESTAMP = str(int(time.time()))
|
750
|
-
_HMAC = generate_hmac_signature(
|
824
|
+
_HMAC: str = generate_hmac_signature(
|
751
825
|
timestamp=_TIMESTAMP,
|
752
826
|
serial_id=self.serial_id,
|
753
827
|
api_key=self.__apikey,
|
754
828
|
secret_key=self.__secret_key,
|
755
829
|
)
|
756
830
|
|
757
|
-
_HEADERS = {
|
831
|
+
_HEADERS: dict[str, str] = {
|
758
832
|
"X-PIN-CODE": str(self.__pincode),
|
759
833
|
"X-HMAC": _HMAC,
|
760
834
|
"X-TIMESTAMP": _TIMESTAMP,
|
761
835
|
"X-APIKEY": self.__apikey,
|
762
836
|
}
|
763
|
-
data = {"groups": groups}
|
837
|
+
data: dict[str, list[int]] = {"groups": groups}
|
764
838
|
response_data, *_ = await self._request(
|
765
839
|
"POST",
|
766
840
|
f"systems/{self.serial_id}/{action}",
|
@@ -854,14 +928,14 @@ class DiagralAPI:
|
|
854
928
|
raise AuthenticationError("PIN code required to get system details")
|
855
929
|
|
856
930
|
_TIMESTAMP = str(int(time.time()))
|
857
|
-
_HMAC = generate_hmac_signature(
|
931
|
+
_HMAC: str = generate_hmac_signature(
|
858
932
|
timestamp=_TIMESTAMP,
|
859
933
|
serial_id=self.serial_id,
|
860
934
|
api_key=self.__apikey,
|
861
935
|
secret_key=self.__secret_key,
|
862
936
|
)
|
863
937
|
|
864
|
-
_HEADERS = {
|
938
|
+
_HEADERS: dict[str, str] = {
|
865
939
|
"X-PIN-CODE": str(self.__pincode),
|
866
940
|
"X-HMAC": _HMAC,
|
867
941
|
"X-TIMESTAMP": _TIMESTAMP,
|
@@ -935,14 +1009,14 @@ class DiagralAPI:
|
|
935
1009
|
)
|
936
1010
|
|
937
1011
|
_TIMESTAMP = str(int(time.time()))
|
938
|
-
_HMAC = generate_hmac_signature(
|
1012
|
+
_HMAC: str = generate_hmac_signature(
|
939
1013
|
timestamp=_TIMESTAMP,
|
940
1014
|
serial_id=self.serial_id,
|
941
1015
|
api_key=self.__apikey,
|
942
1016
|
secret_key=self.__secret_key,
|
943
1017
|
)
|
944
1018
|
|
945
|
-
_HEADERS = {
|
1019
|
+
_HEADERS: dict[str, str] = {
|
946
1020
|
"X-HMAC": _HMAC,
|
947
1021
|
"X-TIMESTAMP": _TIMESTAMP,
|
948
1022
|
"X-APIKEY": self.__apikey,
|
@@ -990,14 +1064,14 @@ class DiagralAPI:
|
|
990
1064
|
)
|
991
1065
|
|
992
1066
|
_TIMESTAMP = str(int(time.time()))
|
993
|
-
_HMAC = generate_hmac_signature(
|
1067
|
+
_HMAC: str = generate_hmac_signature(
|
994
1068
|
timestamp=_TIMESTAMP,
|
995
1069
|
serial_id=self.serial_id,
|
996
1070
|
api_key=self.__apikey,
|
997
1071
|
secret_key=self.__secret_key,
|
998
1072
|
)
|
999
1073
|
|
1000
|
-
_HEADERS = {
|
1074
|
+
_HEADERS: dict[str, str] = {
|
1001
1075
|
"X-HMAC": _HMAC,
|
1002
1076
|
"X-TIMESTAMP": _TIMESTAMP,
|
1003
1077
|
"X-APIKEY": self.__apikey,
|
@@ -1043,14 +1117,14 @@ class DiagralAPI:
|
|
1043
1117
|
)
|
1044
1118
|
|
1045
1119
|
_TIMESTAMP = str(int(time.time()))
|
1046
|
-
_HMAC = generate_hmac_signature(
|
1120
|
+
_HMAC: str = generate_hmac_signature(
|
1047
1121
|
timestamp=_TIMESTAMP,
|
1048
1122
|
serial_id=self.serial_id,
|
1049
1123
|
api_key=self.__apikey,
|
1050
1124
|
secret_key=self.__secret_key,
|
1051
1125
|
)
|
1052
1126
|
|
1053
|
-
_HEADERS = {
|
1127
|
+
_HEADERS: dict[str, str] = {
|
1054
1128
|
"X-HMAC": _HMAC,
|
1055
1129
|
"X-TIMESTAMP": _TIMESTAMP,
|
1056
1130
|
"X-APIKEY": self.__apikey,
|
@@ -1108,14 +1182,14 @@ class DiagralAPI:
|
|
1108
1182
|
)
|
1109
1183
|
|
1110
1184
|
_TIMESTAMP = str(int(time.time()))
|
1111
|
-
_HMAC = generate_hmac_signature(
|
1185
|
+
_HMAC: str = generate_hmac_signature(
|
1112
1186
|
timestamp=_TIMESTAMP,
|
1113
1187
|
serial_id=self.serial_id,
|
1114
1188
|
api_key=self.__apikey,
|
1115
1189
|
secret_key=self.__secret_key,
|
1116
1190
|
)
|
1117
1191
|
|
1118
|
-
_HEADERS = {
|
1192
|
+
_HEADERS: dict[str, str] = {
|
1119
1193
|
"X-HMAC": _HMAC,
|
1120
1194
|
"X-TIMESTAMP": _TIMESTAMP,
|
1121
1195
|
"X-APIKEY": self.__apikey,
|
@@ -1216,14 +1290,14 @@ class DiagralAPI:
|
|
1216
1290
|
)
|
1217
1291
|
|
1218
1292
|
_TIMESTAMP = str(int(time.time()))
|
1219
|
-
_HMAC = generate_hmac_signature(
|
1293
|
+
_HMAC: str = generate_hmac_signature(
|
1220
1294
|
timestamp=_TIMESTAMP,
|
1221
1295
|
serial_id=self.serial_id,
|
1222
1296
|
api_key=self.__apikey,
|
1223
1297
|
secret_key=self.__secret_key,
|
1224
1298
|
)
|
1225
1299
|
|
1226
|
-
_HEADERS = {
|
1300
|
+
_HEADERS: dict[str, str] = {
|
1227
1301
|
"X-HMAC": _HMAC,
|
1228
1302
|
"X-TIMESTAMP": _TIMESTAMP,
|
1229
1303
|
"X-APIKEY": self.__apikey,
|
pydiagral/exceptions.py
CHANGED
@@ -10,8 +10,8 @@ class DiagralAPIError(Exception):
|
|
10
10
|
:param message: The error message.
|
11
11
|
:param status_code: The status code of the error, if any.
|
12
12
|
"""
|
13
|
-
self.message = message
|
14
|
-
self.status_code = status_code
|
13
|
+
self.message: str = message
|
14
|
+
self.status_code: int | None = status_code
|
15
15
|
super().__init__(self.message)
|
16
16
|
|
17
17
|
|
@@ -37,3 +37,11 @@ class ServerError(DiagralAPIError):
|
|
37
37
|
|
38
38
|
class ClientError(DiagralAPIError):
|
39
39
|
"""Raised when client returns error."""
|
40
|
+
|
41
|
+
|
42
|
+
class APIKeyCreationError(DiagralAPIError):
|
43
|
+
"""Raised when API key creation fails."""
|
44
|
+
|
45
|
+
|
46
|
+
class APIValidationError(DiagralAPIError):
|
47
|
+
"""Raised when API validation fails."""
|
pydiagral/models.py
CHANGED
@@ -15,7 +15,7 @@ import re
|
|
15
15
|
import types
|
16
16
|
from typing import TypeVar, Union, get_args, get_origin, get_type_hints
|
17
17
|
|
18
|
-
logger = logging.getLogger(__name__)
|
18
|
+
logger: logging.Logger = logging.getLogger(__name__)
|
19
19
|
|
20
20
|
|
21
21
|
#######################################################
|
@@ -355,6 +355,30 @@ class ApiKeys(CamelCaseModel):
|
|
355
355
|
)
|
356
356
|
|
357
357
|
|
358
|
+
@dataclass
|
359
|
+
class TryConnectResult(CamelCaseModel):
|
360
|
+
"""A class representing the result of an API connection attempt.
|
361
|
+
|
362
|
+
This class is used to store the result of an API connection attempt
|
363
|
+
and the associated API keys if the connection was successful.
|
364
|
+
|
365
|
+
Attributes:
|
366
|
+
result (bool | None): Whether the connection attempt was successful. Defaults to False.
|
367
|
+
keys (ApiKeyWithSecret | None): The API keys associated with the successful connection. Defaults to None.
|
368
|
+
|
369
|
+
Example:
|
370
|
+
>>> result = TryConnectResult(result=True, keys=api_key_obj)
|
371
|
+
>>> print(result.result)
|
372
|
+
True
|
373
|
+
>>> print(result.keys)
|
374
|
+
ApiKeyWithSecret(api_key='abc123', api_secret='xyz789')
|
375
|
+
|
376
|
+
"""
|
377
|
+
|
378
|
+
result: bool | None = False
|
379
|
+
keys: ApiKeyWithSecret | None = None
|
380
|
+
|
381
|
+
|
358
382
|
#####################################
|
359
383
|
# Data models for alarm configuration
|
360
384
|
#####################################
|
pydiagral/utils.py
CHANGED
@@ -10,5 +10,5 @@ def generate_hmac_signature(
|
|
10
10
|
) -> str:
|
11
11
|
"""Generate an HMAC signature for the given parameters."""
|
12
12
|
timestamp = str(int(time.time()))
|
13
|
-
message = f"{timestamp}.{serial_id}.{api_key}"
|
13
|
+
message: str = f"{timestamp}.{serial_id}.{api_key}"
|
14
14
|
return hmac.new(secret_key.encode(), message.encode(), hashlib.sha256).hexdigest()
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: pydiagral
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.5.0
|
4
4
|
Summary: A Python library for interacting with Diagral systems
|
5
5
|
Project-URL: Homepage, https://github.com/mguyard/pydiagral
|
6
6
|
Project-URL: Documentation, https://github.com/mguyard/pydiagral
|
@@ -685,7 +685,7 @@ License: GNU GENERAL PUBLIC LICENSE
|
|
685
685
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
686
686
|
License-File: LICENSE
|
687
687
|
Keywords: diagral,home automation,python
|
688
|
-
Classifier: Development Status ::
|
688
|
+
Classifier: Development Status :: 5 - Production/Stable
|
689
689
|
Classifier: Intended Audience :: Developers
|
690
690
|
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
691
691
|
Classifier: Operating System :: OS Independent
|
@@ -702,9 +702,6 @@ Description-Content-Type: text/markdown
|
|
702
702
|
<p align="center">
|
703
703
|
<img src="https://raw.githubusercontent.com/mguyard/pydiagral/main/docs/pydiagral-Logo.png" width="400" />
|
704
704
|
</p>
|
705
|
-
<p align="center">
|
706
|
-
<h1 align="center">PyDiagral</h1>
|
707
|
-
</p>
|
708
705
|
<p align="center">
|
709
706
|
A powerful and easy-to-use Python library for seamless integration with the Diagral alarm system.
|
710
707
|
</p>
|
@@ -729,19 +726,25 @@ Description-Content-Type: text/markdown
|
|
729
726
|
</p>
|
730
727
|
<br /><br />
|
731
728
|
|
732
|
-
#
|
729
|
+
# pydiagral
|
733
730
|
|
734
731
|
Welcome to the documentation for pydiagral, a Python library for interacting with the Diagral API.
|
735
732
|
|
736
|
-
## About pydiagral
|
733
|
+
## 📍 About pydiagral
|
737
734
|
|
738
|
-
pydiagral is an asynchronous Python interface for the Diagral alarm system. This library allows users to control and monitor their Diagral alarm system through the official API.
|
735
|
+
`pydiagral` is an asynchronous Python interface for the Diagral alarm system. This library allows users to control and monitor their Diagral alarm system through the official API.
|
739
736
|
|
740
|
-
|
737
|
+
> [!CAUTION]
|
738
|
+
>
|
739
|
+
> Please note that the Diagral alarm system is a security system, and it may be preferable not to connect it to any automation platform for security reasons.
|
740
|
+
> In no event shall the developer of [`pydiagral`](https://github.com/mguyard/pydiagral) library be held liable for any issues arising from the use of this [`pydiagral`](https://github.com/mguyard/pydiagral) library.
|
741
|
+
> The user installs and uses this integration at their own risk and with full knowledge of the potential implications.
|
742
|
+
|
743
|
+
## ✅ Requirement
|
741
744
|
|
742
745
|
To use this library, which leverages the Diagral APIs, you must have a Diagral box (DIAG56AAX). This box connects your Diagral alarm system to the internet, enabling interaction with the alarm system via the API. You can find more information about the Diagral box [here](https://www.diagral.fr/commande/box-alerte-et-pilotage).
|
743
746
|
|
744
|
-
## Key Features
|
747
|
+
## 📦 Key Features
|
745
748
|
|
746
749
|
The `DiagralAPI` class offers the following functionalities:
|
747
750
|
|
@@ -766,7 +769,7 @@ The `DiagralAPI` class offers the following functionalities:
|
|
766
769
|
- Activate or Desactivate system (partially or globally)
|
767
770
|
- Automatism actions
|
768
771
|
|
769
|
-
## Quick Start
|
772
|
+
## 🚀 Quick Start
|
770
773
|
|
771
774
|
To get started with pydiagral, follow these steps:
|
772
775
|
|
@@ -796,33 +799,72 @@ And run the [example_code.py](https://github.com/mguyard/pydiagral/blob/main/exa
|
|
796
799
|
>
|
797
800
|
> You can customize the actions performed by [example_code.py](https://github.com/mguyard/pydiagral/blob/main/example_code.py) by modifying the parameters in the code, as indicated by the `CUSTOMIZE THE TESTS` section title.
|
798
801
|
|
802
|
+
# 📖 Documentations
|
803
|
+
|
804
|
+
## Package Documentation
|
805
|
+
|
806
|
+
Library documentation is available [here](https://mguyard.github.io/pydiagral/).
|
807
|
+
|
808
|
+
### Package Structure
|
809
|
+
|
810
|
+
For detailed library documentation, please refer to the following sections:
|
811
|
+
|
812
|
+
- [API Reference](https://mguyard.github.io/pydiagral/api/): Comprehensive documentation of the DiagralAPI class and its methods
|
813
|
+
- [Data Models](https://mguyard.github.io/pydiagral/models/): Description of the data structures used
|
814
|
+
- [Exceptions](https://mguyard.github.io/pydiagral/exceptions/): List of package exceptions
|
815
|
+
|
799
816
|
## Diagral API Official documentation
|
800
817
|
|
801
818
|
Official Diagral API is available [here](https://appv3.tt-monitor.com/emerald/redoc).
|
802
819
|
|
820
|
+
# 🙋 FAQ
|
821
|
+
|
803
822
|
## How to find Serial on DIAG56AAX
|
804
823
|
|
805
824
|
The serial number can only be found with physical access to the box. You need to open it, and you will find a label with a QR Code.
|
825
|
+
|
806
826
|
On this label, there is a 15-character code that represents the serial number of the box.
|
807
827
|
|
808
|
-

|
828
|
+

|
809
829
|
|
810
830
|
> [!IMPORTANT]
|
811
831
|
>
|
812
832
|
> This code is necessary to use this library and Diagral API.
|
813
833
|
|
814
|
-
|
834
|
+
# 🤝 Contribution
|
835
|
+
|
836
|
+
Contributions are welcome! Here are several ways you can contribute:
|
837
|
+
|
838
|
+
- **Submit Pull Requests**: Review open PRs, and submit your own PRs.
|
839
|
+
- **Report Issues**: Submit bugs found or log feature requests.
|
815
840
|
|
816
|
-
|
841
|
+
<details closed>
|
842
|
+
<summary>Contributing Guidelines</summary>
|
817
843
|
|
818
|
-
|
819
|
-
|
820
|
-
|
844
|
+
1. **Fork the Repository**: Start by forking the project repository to your GitHub account.
|
845
|
+
2. **Clone Locally**: Clone the forked repository to your local machine using a Git client.
|
846
|
+
```sh
|
847
|
+
git clone https://github.com/mguyard/pydiagral
|
848
|
+
```
|
849
|
+
3. **Create a New Branch**: Always work on a new branch, giving it a descriptive name.
|
850
|
+
```sh
|
851
|
+
git checkout -b new-feature-x
|
852
|
+
```
|
853
|
+
4. **Make Your Changes**: Develop and test your changes locally.
|
854
|
+
5. **Commit Your Changes**: Commit your changes with a clear and concise message that follows the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) guidelines.
|
855
|
+
```sh
|
856
|
+
git commit -m 'feat: Implemented new feature x.'
|
857
|
+
```
|
858
|
+
6. **Push to GitHub**: Push the changes to your forked repository.
|
859
|
+
```sh
|
860
|
+
git push origin new-feature-x
|
861
|
+
```
|
862
|
+
7. **Submit a Pull Request**: Create a PR against the original project repository. Clearly describe the changes and their motivations.
|
821
863
|
|
822
|
-
|
864
|
+
Once your PR is reviewed and approved, it will be merged into the `beta` branch. After final testing, it will be merged into the `main` branch.
|
823
865
|
|
824
|
-
|
866
|
+
</details>
|
825
867
|
|
826
|
-
|
868
|
+
# 📄 License
|
827
869
|
|
828
870
|
pydiagral is distributed under the GPL-v3 License. See the [LICENSE](https://github.com/mguyard/pydiagral/blob/main/LICENSE) file for more details.
|
@@ -0,0 +1,10 @@
|
|
1
|
+
pydiagral/__init__.py,sha256=4uM-RD2GQ6JYJkxu-D6wj3XpqfY5gN2hP8NF6WvRI9k,576
|
2
|
+
pydiagral/api.py,sha256=nEmP1UuZWIMoSpWMxGk4dF2AbejlE7AJHMTBGUeLra8,50129
|
3
|
+
pydiagral/constants.py,sha256=2B0TdKxQHA3cpIBxojo43bMW44wN9xKYsHbBRHWsaBk,119
|
4
|
+
pydiagral/exceptions.py,sha256=vMhGDQW-AhYIH9gcKHK1-4SHV3eZUXeeqXPyznUAKnU,1211
|
5
|
+
pydiagral/models.py,sha256=vUjxuApjVaMtd7H6Iw5LarwABi30O4FfdObhZRbUNuc,54931
|
6
|
+
pydiagral/utils.py,sha256=-VxI-lNaC4bU1K4DSmWDhvbsS2bXv5FAGULGKBf1UMU,449
|
7
|
+
pydiagral-1.5.0.dist-info/METADATA,sha256=znS_27alZXQQyVkcNyvWXDmhHA7V3E84z9O_qlyiYVA,48529
|
8
|
+
pydiagral-1.5.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
9
|
+
pydiagral-1.5.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
10
|
+
pydiagral-1.5.0.dist-info/RECORD,,
|
pydiagral-1.4.0.dist-info/RECORD
DELETED
@@ -1,10 +0,0 @@
|
|
1
|
-
pydiagral/__init__.py,sha256=4uM-RD2GQ6JYJkxu-D6wj3XpqfY5gN2hP8NF6WvRI9k,576
|
2
|
-
pydiagral/api.py,sha256=sX46Fct3Bwei5yKKQ5PnGDKl261BBqZ0CYMFUnXZ5Mw,46753
|
3
|
-
pydiagral/constants.py,sha256=2B0TdKxQHA3cpIBxojo43bMW44wN9xKYsHbBRHWsaBk,119
|
4
|
-
pydiagral/exceptions.py,sha256=PLo85XJ55q4_dzsyaMYXLIaX8Ws7HH6vDUWNWodzCpM,1013
|
5
|
-
pydiagral/models.py,sha256=shnAf7ojh6y5fGp3l5RbGdp4qpuEyOZI-0ePrWAg2l0,54120
|
6
|
-
pydiagral/utils.py,sha256=weccW18dseD_Ypwe4dKvNz6TlZPA7BQbf5l5qk3GXOg,444
|
7
|
-
pydiagral-1.4.0.dist-info/METADATA,sha256=uomaH_YBT9vnk7pplGjfSA4vnxvxq9jBVAsE5Wc2zAc,46568
|
8
|
-
pydiagral-1.4.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
9
|
-
pydiagral-1.4.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
10
|
-
pydiagral-1.4.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|