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 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
- 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.
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
- AuthenticationError: If the API key is not found in the response or if the created API key fails validation.
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
- 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 = {
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(response_data)
297
- self.__apikey = set_apikey_response.api_key
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 AuthenticationError(error_msg)
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 AuthenticationError(error_msg)
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 AuthenticationError as e:
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 AuthenticationError(error_msg) from e
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 AuthenticationError(
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
- AuthenticationError: If the API key, secret key, or PIN code is not provided.
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 AuthenticationError(
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 AuthenticationError("PIN code required to get system details")
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.4.0
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 :: 3 - Alpha
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
- # Documentation pydiagral
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
- ## Requirement
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
- ![How to find your Diagral Serial](docs/how-to-find-diagral-serial.png)
828
+ ![How to find your Diagral Serial](https://raw.githubusercontent.com/mguyard/pydiagral/main/docs/how-to-find-diagral-serial.png)
809
829
 
810
830
  > [!IMPORTANT]
811
831
  >
812
832
  > This code is necessary to use this library and Diagral API.
813
833
 
814
- ## API Structure
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
- For detailed API documentation, please refer to the following sections:
841
+ <details closed>
842
+ <summary>Contributing Guidelines</summary>
817
843
 
818
- - [API Reference](https://XXXXXXXXXXXXXXXX): Comprehensive documentation of the DiagralAPI class and its methods
819
- - [Data Models](https://XXXXXXXXXXXXXXXX): Description of the data structures used
820
- - [Exceptions](https://XXXXXXXXXXXXXXXX): List of package exceptions
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
- ## Contribution
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
- Contributions to pydiagral are welcome! Please check our contribution guidelines for more information on how to participate in the development of this library.
866
+ </details>
825
867
 
826
- ## License
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,,
@@ -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,,