pydiagral 1.0.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
pydiagral/models.py ADDED
@@ -0,0 +1,1498 @@
1
+ """Module containing data models for interacting with the Diagral API.
2
+
3
+ The models include representations for login responses, API key creation and validation,
4
+ and other related data structures.
5
+ """
6
+
7
+ # Minimum Python version: 3.10
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass, field, fields
12
+ from datetime import datetime, timezone
13
+ import logging
14
+ import re
15
+ import types
16
+ from typing import TypeVar, Union, get_args, get_origin, get_type_hints
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ #######################################################
22
+ # Class for converting between camelCase and snake_case
23
+ #######################################################
24
+
25
+
26
+ class CamelCaseModel:
27
+ """CamelCaseModel is a base class for models that need to convert between snake_case and camelCase keys.
28
+
29
+ Methods:
30
+ to_dict() -> dict:
31
+ Convert the model instance to a dictionary with camelCase keys.
32
+ _from_dict_recursive(cls, data: dict, target_cls: type[T]) -> T:
33
+ Recursively create an instance of the target class from a dictionary.
34
+ from_dict(cls: type[T], data: dict) -> T:
35
+ Create an instance of the model from a dictionary.
36
+ snake_to_camel(string: str) -> str:
37
+ Convert a snake_case string to camelCase.
38
+ camel_to_snake(string: str) -> str:
39
+ Convert a camelCase string to snake_case.
40
+
41
+ Examples:
42
+ >>> @dataclass
43
+ ... class ExampleModel(CamelCaseModel):
44
+ ... first_name: str
45
+ ... last_name: str
46
+ ...
47
+ >>> example = ExampleModel(first_name="Luke", last_name="Skywalker")
48
+ >>> example_dict = example.to_dict()
49
+ >>> print(example_dict)
50
+ {'firstName': 'Luke', 'lastName': 'Skywalker'}
51
+ >>> new_example = ExampleModel.from_dict(example_dict)
52
+ >>> print(new_example)
53
+ ExampleModel(first_name='Luke', last_name='Skywalker')
54
+
55
+ """
56
+
57
+ # Type variable for the class itself
58
+ T = TypeVar("T", bound="CamelCaseModel")
59
+
60
+ def to_dict(self) -> dict:
61
+ """Convert the instance attributes to a dictionary, transforming attribute names.
62
+
63
+ from snake_case to camelCase and handling nested CamelCaseModel instances.
64
+
65
+ Returns:
66
+ dict: A dictionary representation of the instance with camelCase keys.
67
+
68
+ Example:
69
+ >>> class ExampleModel(CamelCaseModel):
70
+ ... first_name: str
71
+ ... last_name: str
72
+ ...
73
+ >>> example = ExampleModel(first_name="Luke", last_name="Skywalker")
74
+ >>> example.to_dict()
75
+ {'firstName': 'Luke', 'lastName': 'Skywalker'}
76
+
77
+ """
78
+
79
+ result = {}
80
+ for k, v in self.__dict__.items():
81
+ if v is not None:
82
+ if isinstance(v, CamelCaseModel):
83
+ v = v.to_dict()
84
+ elif isinstance(v, list) and v and isinstance(v[0], CamelCaseModel):
85
+ v = [item.to_dict() for item in v]
86
+ key = getattr(self.__class__, k).metadata.get(
87
+ "alias", self.snake_to_camel(k)
88
+ )
89
+ result[key] = v
90
+ return result
91
+
92
+ @classmethod
93
+ def _from_dict_recursive(cls, data: dict, target_cls: type[T]) -> T:
94
+ """Recursively converts a dictionary to an instance of the specified target class.
95
+
96
+ This method handles nested dictionaries and lists, converting them to the appropriate
97
+ types as specified by the target class's type hints. It also supports optional fields
98
+ by handling `Union` types and removing `None` from the type hints.
99
+
100
+ Args:
101
+ cls: The class that this method is a part of.
102
+ data (dict): The dictionary to convert.
103
+ target_cls (type[T]): The target class to convert the dictionary to.
104
+
105
+ Returns:
106
+ T: An instance of the target class populated with the data from the dictionary.
107
+
108
+ Raises:
109
+ TypeError: If the target class cannot be instantiated with the provided data.
110
+
111
+ Notes:
112
+ - The method assumes that the target class and its nested classes (if any) are
113
+ annotated with type hints.
114
+ - The method uses snake_case to camelCase conversion for dictionary keys to match
115
+ the field names in the target class.
116
+ - The method logs detailed debug information about the conversion process.
117
+
118
+ """
119
+
120
+ logger.debug("Converting data: %s to %s", data, target_cls)
121
+
122
+ logger.debug("Extracted target_cls: %s", target_cls)
123
+ if get_origin(target_cls) is Union:
124
+ # Extract the real type by removing None
125
+ target_cls = next(t for t in get_args(target_cls) if t is not type(None))
126
+ logger.debug("Extracted target_cls: %s", target_cls)
127
+
128
+ init_values = {}
129
+ fields_dict = {field.name: field for field in fields(target_cls)}
130
+ for field_name, field_type in get_type_hints(target_cls).items():
131
+ field = fields_dict.get(field_name)
132
+ logger.debug("Field Metadata: %s", field.metadata if field else {})
133
+ # alias = cls.snake_to_camel(field_name) # Old version who don't support field with underscore and without alias
134
+ alias = field.metadata.get("alias", field_name)
135
+ logger.debug(
136
+ "Processing field: %s (alias: %s, type: %s)",
137
+ field_name,
138
+ alias,
139
+ field_type,
140
+ )
141
+
142
+ logger.debug("Extracted field_type: %s", field_type)
143
+ if get_origin(field_type) is types.UnionType:
144
+ # Extract the real type by removing None
145
+ field_type = next(
146
+ t for t in get_args(field_type) if t is not type(None)
147
+ )
148
+ logger.debug("Extracted field_type: %s", field_type)
149
+
150
+ logger.debug("Checking if alias %s is in data: %s", alias, data)
151
+ if any(alias.lower() == key.lower() for key in data):
152
+ alias = next(key for key in data if alias.lower() == key.lower())
153
+ value = data[alias]
154
+ logger.debug("Found value for %s: %s", alias, value)
155
+
156
+ if (
157
+ isinstance(value, dict)
158
+ and isinstance(field_type, type)
159
+ and issubclass(field_type, CamelCaseModel)
160
+ ):
161
+ logger.debug(
162
+ "Recursively converting nested dict for field: %s", field_name
163
+ )
164
+ init_values[field_name] = cls._from_dict_recursive(
165
+ value, field_type
166
+ )
167
+ elif isinstance(value, list) and get_origin(field_type) is list:
168
+ item_type = get_args(field_type)[0]
169
+ logger.debug(
170
+ "Recursively converting list for field: %s with item type: %s",
171
+ field_name,
172
+ item_type,
173
+ )
174
+ init_values[field_name] = [
175
+ cls._from_dict_recursive(item, item_type)
176
+ if isinstance(item, dict)
177
+ else item
178
+ for item in value
179
+ ]
180
+ else:
181
+ init_values[field_name] = value
182
+ else:
183
+ init_values[field_name] = None
184
+ logger.debug("No value found for %s, setting to None", alias)
185
+
186
+ logger.debug("Initialized values for %s: %s", target_cls, init_values)
187
+ return target_cls(**init_values)
188
+
189
+ @classmethod
190
+ def from_dict(cls: type[T], data: dict) -> T:
191
+ """Create an instance of the class from a dictionary.
192
+
193
+ Args:
194
+ cls (type[T]): The class type to instantiate.
195
+ data (dict): The dictionary containing the data to populate the instance.
196
+
197
+ Returns:
198
+ T: An instance of the class populated with the data from the dictionary.
199
+
200
+ Example:
201
+ >>> data = {"diagral_id": 123, "user_id": 456, "access_token": "abc123"}
202
+ >>> login_response = LoginResponse.from_dict(data)
203
+ >>> login_response.diagral_id
204
+ 123
205
+ >>> login_response.user_id
206
+ 456
207
+ >>> login_response.access_token
208
+ 'abc123'
209
+
210
+ """
211
+
212
+ return cls._from_dict_recursive(data, cls)
213
+
214
+ @staticmethod
215
+ def snake_to_camel(string: str) -> str:
216
+ """Convert a snake_case string to camelCase.
217
+
218
+ Args:
219
+ string (str): The snake_case string to be converted.
220
+
221
+ Returns:
222
+ str: The converted camelCase string.
223
+
224
+ Example:
225
+ >>> snake_to_camel("example_string")
226
+ 'exampleString'
227
+
228
+ """
229
+
230
+ components = string.split("_")
231
+ return components[0] + "".join(x.title() for x in components[1:])
232
+
233
+ @staticmethod
234
+ def camel_to_snake(string: str) -> str:
235
+ """Convert a CamelCase string to snake_case.
236
+
237
+ Args:
238
+ string (str): The CamelCase string to be converted.
239
+
240
+ Returns:
241
+ str: The converted snake_case string.
242
+
243
+ Example:
244
+ >>> camel_to_snake("CamelCaseString")
245
+ 'camel_case_string'
246
+
247
+ """
248
+
249
+ # Replace capital letters with _ followed by the lowercase letter
250
+ s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", string)
251
+ # Handle cases where multiple capitals are together
252
+ s2 = re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1)
253
+ return s2.lower()
254
+
255
+
256
+ ##############################################
257
+ # Data models for Diagral API Authentification
258
+ ##############################################
259
+
260
+
261
+ @dataclass
262
+ class LoginResponse(CamelCaseModel):
263
+ """LoginResponse model represents the response received after a successful login.
264
+
265
+ Attributes:
266
+ access_token (str): The access token provided for authentication.
267
+
268
+ Example:
269
+ >>> response = LoginResponse(
270
+ ... access_token="abc123",
271
+ ... )
272
+ >>> print(response.access_token)
273
+ abc123
274
+
275
+ """
276
+
277
+ access_token: str
278
+
279
+
280
+ @dataclass
281
+ class ApiKeyWithSecret(CamelCaseModel):
282
+ """ApiKeyWithSecret is a model that represents an API key and its corresponding secret key.
283
+
284
+ Attributes:
285
+ api_key (str): The API key, which must be a non-empty string.
286
+ secret_key (str): The secret key associated with the API key, which must also be a non-empty string.
287
+
288
+ Methods:
289
+ __post_init__(): Post-initialization processing to validate the API key and secret key.
290
+
291
+ Example:
292
+ >>> api_key_with_secret = ApiKeyWithSecret(api_key="your_api_key", secret_key="your_secret_key")
293
+ >>> print(api_key_with_secret.api_key)
294
+ your_api_key
295
+ >>> print(api_key_with_secret.secret_key)
296
+ your_secret_key
297
+
298
+ """
299
+
300
+ api_key: str
301
+ secret_key: str
302
+
303
+ def __post_init__(self):
304
+ """Post-initialization processing to validate API key and secret key."""
305
+ if not self.api_key or not isinstance(self.api_key, str):
306
+ raise ValueError("api_key must be a non-empty string")
307
+ if not self.secret_key or not isinstance(self.secret_key, str):
308
+ raise ValueError("secret_key must be a non-empty string")
309
+
310
+
311
+ @dataclass
312
+ class ApiKey(CamelCaseModel):
313
+ """Represents an API key model.
314
+
315
+ Attributes:
316
+ api_key (str): The API key as a string.
317
+
318
+ Example:
319
+ >>> api_key = ApiKey(api_key="your_api_key")
320
+ >>> print(api_key.api_key)
321
+ your_api_key
322
+
323
+ """
324
+
325
+ api_key: str
326
+
327
+
328
+ @dataclass
329
+ class ApiKeys(CamelCaseModel):
330
+ """ApiKeys model to represent a collection of API keys.
331
+
332
+ Attributes:
333
+ api_keys (list[ApiKey]): A list of ApiKey instances.
334
+
335
+ Methods:
336
+ from_dict(data: dict) -> ApiKeys:
337
+ Class method to create an instance of ApiKeys from a dictionary.
338
+
339
+ Example:
340
+ >>> data = {"api_keys": [{"api_key": "key1"}, {"api_key": "key2"}]}
341
+ >>> api_keys = ApiKeys.from_dict(data)
342
+ >>> print(api_keys.api_keys)
343
+ [ApiKey(api_key='key1'), ApiKey(api_key='key2')]
344
+
345
+ """
346
+
347
+ api_keys: list[ApiKey]
348
+
349
+ @classmethod
350
+ def from_dict(cls, data: dict) -> ApiKeys:
351
+ """Create an instance of ApiKeys from a dictionary."""
352
+
353
+ return cls(
354
+ api_keys=[ApiKey(**key_info) for key_info in data.get("api_keys", [])]
355
+ )
356
+
357
+
358
+ #####################################
359
+ # Data models for alarm configuration
360
+ #####################################
361
+
362
+
363
+ @dataclass
364
+ class FirmwareModel(CamelCaseModel):
365
+ """FirmwareModel represents the firmware details of a device.
366
+
367
+ Attributes:
368
+ box (str | None): The firmware version of the box, aliased as "BOX".
369
+ central (str | None): The firmware version of the central unit, aliased as "CENTRAL".
370
+ centralradio (str | None): The firmware version of the central radio unit, aliased as "CENTRALRADIO".
371
+
372
+ Example:
373
+ >>> firmware = FirmwareModel(box="1.0.0", central="2.0.0", centralradio="3.0.0")
374
+ >>> print(firmware.box)
375
+ '1.0.0'
376
+ >>> print(firmware.central)
377
+ '2.0.0'
378
+ >>> print(firmware.centralradio)
379
+ '3.0.0'
380
+
381
+ """
382
+
383
+ box: str | None = field(default=None, metadata={"alias": "BOX"})
384
+ central: str | None = field(default=None, metadata={"alias": "CENTRAL"})
385
+ centralradio: str | None = field(default=None, metadata={"alias": "CENTRALRADIO"})
386
+
387
+
388
+ @dataclass
389
+ class CentralPlugModel(CamelCaseModel):
390
+ """CentralPlugModel represents the central plug device.
391
+
392
+ Attributes:
393
+ name (str | None): The name of the central plug device.
394
+ serial (str | None): The serial number of the central plug device.
395
+ vendor (str | None): The vendor of the central plug device.
396
+ firmwares (FirmwareModel | None): The firmware information of the central plug device.
397
+
398
+ Example:
399
+ >>> firmware = FirmwareModel(box="1.0.0", central="2.0.0", centralradio="3.0.0")
400
+ >>> central_plug = CentralPlugModel(
401
+ ... name="Central Plug 1",
402
+ ... serial="123456789",
403
+ ... vendor="VendorName",
404
+ ... firmwares=firmware
405
+ ... )
406
+ >>> print(central_plug.name)
407
+ Central Plug 1
408
+ >>> print(central_plug.serial)
409
+ 123456789
410
+ >>> print(central_plug.vendor)
411
+ VendorName
412
+ >>> print(central_plug.firmwares.box)
413
+ 1.0.0
414
+
415
+ """
416
+
417
+ name: str | None = None
418
+ serial: str | None = None
419
+ vendor: str | None = None
420
+ firmwares: FirmwareModel | None = None
421
+
422
+
423
+ @dataclass
424
+ class Group(CamelCaseModel):
425
+ """Represents a Group model.
426
+
427
+ Attributes:
428
+ name (str | None): The name of the group. Defaults to None.
429
+ index (int | None): The index of the group. Defaults to None.
430
+ input_delay (int | None): The input delay of the group, aliased as 'inputDelay'. Defaults to None.
431
+ output_delay (int | None): The output delay of the group, aliased as 'outputDelay'. Defaults to None.
432
+
433
+ Example:
434
+ >>> group = Group(name="Group A", index=1, input_delay=10, output_delay=20)
435
+ >>> print(group.name)
436
+ Group A
437
+ >>> print(group.index)
438
+ 1
439
+ >>> print(group.input_delay)
440
+ 10
441
+ >>> print(group.output_delay)
442
+ 20
443
+
444
+ """
445
+
446
+ name: str | None = None
447
+ index: int | None = None
448
+ input_delay: int | None = field(default=None, metadata={"alias": "inputDelay"})
449
+ output_delay: int | None = field(default=None, metadata={"alias": "outputDelay"})
450
+
451
+
452
+ @dataclass
453
+ class ConfAnomaliesModel(CamelCaseModel):
454
+ """ConfAnomaliesModel is a data model that represents various configuration anomalies in a system.
455
+
456
+ Attributes:
457
+ radio_alert (bool | None): Indicates if there is a radio alert. Alias: "radioAlert".
458
+ power_supply_alert (bool | None): Indicates if there is a power supply alert. Alias: "powerSupplyAlert".
459
+ autoprotection_mechanical_alert (bool | None): Indicates if there is an autoprotection mechanical alert. Alias: "autoprotectionMechanicalAlert".
460
+ loop_alert (bool | None): Indicates if there is a loop alert. Alias: "loopAlert".
461
+ mask_alert (bool | None): Indicates if there is a mask alert. Alias: "maskAlert".
462
+ sensor_alert (bool | None): Indicates if there is a sensor alert. Alias: "sensorAlert".
463
+ media_gsm_alert (bool | None): Indicates if there is a GSM media alert. Alias: "mediaGSMAlert".
464
+ media_rtc_alert (bool | None): Indicates if there is an RTC media alert. Alias: "mediaRTCAlert".
465
+ media_adsl_alert (bool | None): Indicates if there is an ADSL media alert. Alias: "mediaADSLAlert".
466
+ out_of_order_alert (bool | None): Indicates if there is an out of order alert. Alias: "outOfOrderAlert".
467
+ main_power_supply_alert (bool | None): Indicates if there is a main power supply alert. Alias: "mainPowerSupplyAlert".
468
+ secondary_power_supply_alert (bool | None): Indicates if there is a secondary power supply alert. Alias: "secondaryPowerSupplyAlert".
469
+ default_media_alert (bool | None): Indicates if there is a default media alert. Alias: "defaultMediaAlert".
470
+ autoprotection_wired_alert (bool | None): Indicates if there is an autoprotection wired alert. Alias: "autoprotectionWiredAlert".
471
+
472
+ Example:
473
+ >>> anomalies = ConfAnomaliesModel(
474
+ ... radio_alert=True,
475
+ ... power_supply_alert=False,
476
+ ... autoprotection_mechanical_alert=True,
477
+ ... loop_alert=False,
478
+ ... mask_alert=True,
479
+ ... sensor_alert=False,
480
+ ... media_gsm_alert=True,
481
+ ... media_rtc_alert=False,
482
+ ... media_adsl_alert=True,
483
+ ... out_of_order_alert=False,
484
+ ... main_power_supply_alert=True,
485
+ ... secondary_power_supply_alert=False,
486
+ ... default_media_alert=True,
487
+ ... autoprotection_wired_alert=False
488
+ ... )
489
+ >>> print(anomalies.radio_alert)
490
+ True
491
+ >>> print(anomalies.power_supply_alert)
492
+ False
493
+
494
+ """
495
+
496
+ radio_alert: bool | None = field(default=None, metadata={"alias": "radioAlert"})
497
+ power_supply_alert: bool | None = field(
498
+ default=None, metadata={"alias": "powerSupplyAlert"}
499
+ )
500
+ autoprotection_mechanical_alert: bool | None = field(
501
+ default=None, metadata={"alias": "autoprotectionMechanicalAlert"}
502
+ )
503
+ loop_alert: bool | None = field(default=None, metadata={"alias": "loopAlert"})
504
+ mask_alert: bool | None = field(default=None, metadata={"alias": "maskAlert"})
505
+ sensor_alert: bool | None = field(default=None, metadata={"alias": "sensorAlert"})
506
+ media_gsm_alert: bool | None = field(
507
+ default=None, metadata={"alias": "mediaGSMAlert"}
508
+ )
509
+ media_rtc_alert: bool | None = field(
510
+ default=None, metadata={"alias": "mediaRTCAlert"}
511
+ )
512
+ media_adsl_alert: bool | None = field(
513
+ default=None, metadata={"alias": "mediaADSLAlert"}
514
+ )
515
+ out_of_order_alert: bool | None = field(
516
+ default=None, metadata={"alias": "outOfOrderAlert"}
517
+ )
518
+ main_power_supply_alert: bool | None = field(
519
+ default=None, metadata={"alias": "mainPowerSupplyAlert"}
520
+ )
521
+ secondary_power_supply_alert: bool | None = field(
522
+ default=None, metadata={"alias": "secondaryPowerSupplyAlert"}
523
+ )
524
+ default_media_alert: bool | None = field(
525
+ default=None, metadata={"alias": "defaultMediaAlert"}
526
+ )
527
+ autoprotection_wired_alert: bool | None = field(
528
+ default=None, metadata={"alias": "autoprotectionWiredAlert"}
529
+ )
530
+
531
+
532
+ @dataclass
533
+ class SensorModel(CamelCaseModel):
534
+ """SensorModel represents the data structure for a sensor.
535
+
536
+ Attributes:
537
+ uid (str | None): Unique identifier for the sensor.
538
+ type (int | None): Type of the sensor.
539
+ gamme (int | None): Range or category of the sensor.
540
+ group (int | None): Group to which the sensor belongs.
541
+ index (int | None): Index of the sensor.
542
+ label (str | None): Label or name of the sensor.
543
+ serial (str | None): Serial number of the sensor.
544
+ is_video (bool | None): Indicates if the sensor is a video sensor (alias: isVideo).
545
+ ref_code (str | None): Reference code of the sensor (alias: refCode).
546
+ subtype (int | None): Subtype of the sensor.
547
+ anomalies (ConfAnomaliesModel | None): Configuration anomalies associated with the sensor.
548
+ inhibited (bool | None): Indicates if the sensor is inhibited.
549
+ can_inhibit (bool | None): Indicates if the sensor can be inhibited (alias: canInhibit).
550
+
551
+ Example:
552
+ >>> anomalies = ConfAnomaliesModel(radio_alert=True)
553
+ >>> sensor = SensorModel(
554
+ ... uid="12345",
555
+ ... type=1,
556
+ ... gamme=2,
557
+ ... group=3,
558
+ ... index=4,
559
+ ... label="Sensor 1",
560
+ ... serial="SN12345",
561
+ ... is_video=True,
562
+ ... ref_code="RC123",
563
+ ... subtype=5,
564
+ ... anomalies=anomalies,
565
+ ... inhibited=False,
566
+ ... can_inhibit=True
567
+ ... )
568
+ >>> print(sensor.uid)
569
+ 12345
570
+
571
+ """
572
+
573
+ uid: str | None = None
574
+ type: int | None = None
575
+ gamme: int | None = None
576
+ group: int | None = None
577
+ index: int | None = None
578
+ label: str | None = None
579
+ serial: str | None = None
580
+ is_video: bool | None = field(default=None, metadata={"alias": "isVideo"})
581
+ ref_code: str | None = field(default=None, metadata={"alias": "refCode"})
582
+ subtype: int | None = None
583
+ anomalies: ConfAnomaliesModel | None = None
584
+ inhibited: bool | None = None
585
+ can_inhibit: bool | None = field(default=None, metadata={"alias": "canInhibit"})
586
+
587
+
588
+ @dataclass
589
+ class Cameras(SensorModel):
590
+ """Cameras model representing a sensor with an installation date, inheriting from SensorModel.
591
+
592
+ Attributes:
593
+ installation_date (datetime | None): The date when the camera was installed.
594
+ Defaults to None. This attribute is aliased as 'installationDate' in metadata.
595
+
596
+ Example:
597
+ >>> camera = Cameras(
598
+ ... uid="12345",
599
+ ... type=1,
600
+ ... gamme=2,
601
+ ... group=3,
602
+ ... index=4,
603
+ ... label="Camera 1",
604
+ ... serial="SN12345",
605
+ ... is_video=True,
606
+ ... ref_code="RC123",
607
+ ... subtype=5,
608
+ ... anomalies=None,
609
+ ... inhibited=False,
610
+ ... can_inhibit=True,
611
+ ... installation_date=datetime(2023, 10, 1)
612
+ ... )
613
+ >>> print(camera.installation_date)
614
+ 2023-10-01 00:00:00
615
+
616
+ """
617
+
618
+ installation_date: datetime | None = field(
619
+ default=None, metadata={"alias": "installationDate"}
620
+ )
621
+
622
+
623
+ @dataclass
624
+ class TransceiverModel(SensorModel):
625
+ """TransceiverModel represents a model for a transceiver device, inheriting from SensorModel.
626
+
627
+ Attributes:
628
+ firmwares (FirmwareModel | None): An optional attribute that holds the firmware information associated with the transceiver.
629
+
630
+ Example:
631
+ >>> firmware = FirmwareModel(box="1.0.0", central="2.0.0", centralradio="3.0.0")
632
+ >>> transceiver = TransceiverModel(
633
+ ... uid="12345",
634
+ ... type=1,
635
+ ... gamme=2,
636
+ ... group=3,
637
+ ... index=4,
638
+ ... label="Transceiver 1",
639
+ ... serial="SN12345",
640
+ ... is_video=True,
641
+ ... ref_code="RC123",
642
+ ... subtype=5,
643
+ ... anomalies=None,
644
+ ... inhibited=False,
645
+ ... can_inhibit=True,
646
+ ... firmwares=firmware
647
+ ... )
648
+ >>> print(transceiver.uid)
649
+ 12345
650
+
651
+ """
652
+
653
+ firmwares: FirmwareModel | None = None
654
+
655
+
656
+ @dataclass
657
+ class TransmitterModel(SensorModel):
658
+ """TransmitterModel represents a model for a transmitter device, inheriting from SensorModel.
659
+
660
+ Attributes:
661
+ firmwares (FirmwareModel | None): The firmware associated with the transmitter, if any.
662
+ is_plug (bool | None): Indicates whether the transmitter is a plug, with metadata alias "isPlug".
663
+
664
+ Example:
665
+ >>> firmware = FirmwareModel(box="1.0.0", central="2.0.0", centralradio="3.0.0")
666
+ >>> transmitter = TransmitterModel(
667
+ ... uid="12345",
668
+ ... type=1,
669
+ ... gamme=2,
670
+ ... group=3,
671
+ ... index=4,
672
+ ... label="Transmitter 1",
673
+ ... serial="SN12345",
674
+ ... is_video=True,
675
+ ... ref_code="RC123",
676
+ ... subtype=5,
677
+ ... anomalies=None,
678
+ ... inhibited=False,
679
+ ... can_inhibit=True,
680
+ ... firmwares=firmware,
681
+ ... is_plug=True
682
+ ... )
683
+ >>> print(transmitter.uid)
684
+ 12345
685
+
686
+ """
687
+
688
+ firmwares: FirmwareModel | None = None
689
+ is_plug: bool | None = field(default=None, metadata={"alias": "isPlug"})
690
+
691
+
692
+ @dataclass
693
+ class CentralInformation(CamelCaseModel):
694
+ """CentralInformation model represents the central unit's configuration and status information.
695
+
696
+ Attributes:
697
+ has_plug (bool | None): Indicates if the central unit has a plug. Alias: "hasPlug".
698
+ plug_gsm (bool | None): Indicates if the central unit has a GSM plug. Alias: "plugGSM".
699
+ plug_rtc (bool | None): Indicates if the central unit has an RTC plug. Alias: "plugRTC".
700
+ plug_adsl (bool | None): Indicates if the central unit has an ADSL plug. Alias: "plugADSL".
701
+ anomalies (ConfAnomaliesModel | None): Represents the configuration anomalies of the central unit.
702
+ firmwares (FirmwareModel | None): Represents the firmware information of the central unit.
703
+ relay_card (bool | None): Indicates if the central unit has a relay card. Alias: "relayCard".
704
+ can_inhibit (bool | None): Indicates if the central unit can be inhibited. Alias: "canInhibit".
705
+ parameter_gsm_saved (bool | None): Indicates if the GSM parameters are saved. Alias: "parameterGsmSaved".
706
+
707
+ Example:
708
+ >>> anomalies = ConfAnomaliesModel(radio_alert=True)
709
+ >>> firmware = FirmwareModel(box="1.0.0", central="2.0.0", centralradio="3.0.0")
710
+ >>> central_info = CentralInformation(
711
+ ... has_plug=True,
712
+ ... plug_gsm=True,
713
+ ... plug_rtc=False,
714
+ ... plug_adsl=True,
715
+ ... anomalies=anomalies,
716
+ ... firmwares=firmware,
717
+ ... relay_card=True,
718
+ ... can_inhibit=True,
719
+ ... parameter_gsm_saved=False
720
+ ... )
721
+ >>> print(central_info.has_plug)
722
+ True
723
+
724
+ """
725
+
726
+ has_plug: bool | None = field(default=None, metadata={"alias": "hasPlug"})
727
+ plug_gsm: bool | None = field(default=None, metadata={"alias": "plugGSM"})
728
+ plug_rtc: bool | None = field(default=None, metadata={"alias": "plugRTC"})
729
+ plug_adsl: bool | None = field(default=None, metadata={"alias": "plugADSL"})
730
+ anomalies: ConfAnomaliesModel | None = None
731
+ firmwares: FirmwareModel | None = None
732
+ relay_card: bool | None = field(default=None, metadata={"alias": "relayCard"})
733
+ can_inhibit: bool | None = field(default=None, metadata={"alias": "canInhibit"})
734
+ parameter_gsm_saved: bool | None = field(
735
+ default=None, metadata={"alias": "parameterGsmSaved"}
736
+ )
737
+
738
+
739
+ @dataclass
740
+ class BoxModel(CamelCaseModel):
741
+ """BoxModel represents a model for a box with various attributes.
742
+
743
+ Attributes:
744
+ name (str | None): The name of the box. Defaults to None.
745
+ serial (str | None): The serial number of the box. Defaults to None.
746
+ vendor (str | None): The vendor of the box. Defaults to None.
747
+ firmwares (FirmwareModel | None): The firmware model associated with the box. Defaults to None.
748
+
749
+ Example:
750
+ >>> firmware = FirmwareModel(box="1.0.0", central="2.0.0", centralradio="3.0.0")
751
+ >>> box = BoxModel(name="Box 1", serial="123456789", vendor="VendorName", firmwares=firmware)
752
+ >>> print(box.name)
753
+ Box 1
754
+
755
+ """
756
+
757
+ name: str | None = None
758
+ serial: str | None = None
759
+ vendor: str | None = None
760
+ firmwares: FirmwareModel | None = None
761
+
762
+
763
+ @dataclass
764
+ class AlarmModel(CamelCaseModel):
765
+ """AlarmModel represents the configuration and state of an alarm system.
766
+
767
+ Attributes:
768
+ box (BoxModel | None): The box model associated with the alarm system.
769
+ plug (CentralPlugModel | None): The central plug model for the alarm system.
770
+ tls (bool | None): Indicates if TLS (Transport Layer Security) is enabled.
771
+ name (str | None): The name of the alarm system.
772
+ central (CentralPlugModel | None): The central plug model for the alarm system.
773
+ force_push_config (bool | None): Indicates if the configuration should be forcefully pushed.
774
+ This attribute is aliased as "forcePushConfig".
775
+
776
+ Example:
777
+ >>> box = BoxModel(name="Box 1", serial="123456789", vendor="VendorName")
778
+ >>> plug = CentralPlugModel(name="Central Plug 1", serial="987654321", vendor="VendorName")
779
+ >>> alarm = AlarmModel(box=box, plug=plug, tls=True, name="Home Alarm", central=plug, force_push_config=True)
780
+ >>> print(alarm.name)
781
+ Home Alarm
782
+
783
+ """
784
+
785
+ box: BoxModel | None = None
786
+ plug: CentralPlugModel | None = None
787
+ tls: bool | None = None
788
+ name: str | None = None
789
+ central: CentralPlugModel | None = None
790
+ force_push_config: bool | None = field(
791
+ default=None, metadata={"alias": "forcePushConfig"}
792
+ )
793
+
794
+
795
+ @dataclass
796
+ class AlarmConfiguration(CamelCaseModel):
797
+ """AlarmConfiguration model represents the configuration of an alarm system.
798
+
799
+ Attributes:
800
+ alarm (AlarmModel | None): The alarm model associated with the configuration.
801
+ groups (list[Group] | None): A list of groups associated with the alarm configuration.
802
+ sirens (list[SensorModel] | None): A list of siren sensor models.
803
+ cameras (list[Cameras] | None): A list of camera models.
804
+ sensors (list[SensorModel] | None): A list of sensor models.
805
+ commands (list[SensorModel] | None): A list of command sensor models.
806
+ reading_date (datetime | None): The date when the configuration was read, aliased as "readingDate".
807
+ transceivers (list[TransceiverModel] | None): A list of transceiver models.
808
+ transmitters (list[TransmitterModel] | None): A list of transmitter models.
809
+ grp_marche_presence (list[int] | None): A list of group marche presence, aliased as "grpMarchePresence".
810
+ installation_state (int | None): The state of the installation, aliased as "installationState".
811
+ central_information (CentralInformation | None): Information about the central unit, aliased as "centralInformation".
812
+ grp_marche_partielle1 (list[int] | None): A list of group marche partielle 1, aliased as "grpMarchePartielle1".
813
+ grp_marche_partielle2 (list[int] | None): A list of group marche partielle 2, aliased as "grpMarchePartielle2".
814
+
815
+ Example:
816
+ >>> alarm_config = AlarmConfiguration(
817
+ ... alarm=AlarmModel(name="Home Alarm"),
818
+ ... groups=[Group(name="Group A", index=1)],
819
+ ... sirens=[SensorModel(uid="12345", type=1)],
820
+ ... cameras=[Cameras(uid="67890", type=2)],
821
+ ... sensors=[SensorModel(uid="54321", type=3)],
822
+ ... commands=[SensorModel(uid="98765", type=4)],
823
+ ... reading_date=datetime(2023, 10, 1),
824
+ ... transceivers=[TransceiverModel(uid="11223", type=5)],
825
+ ... transmitters=[TransmitterModel(uid="44556", type=6)],
826
+ ... grp_marche_presence=[1, 2, 3],
827
+ ... installation_state=1,
828
+ ... central_information=CentralInformation(has_plug=True),
829
+ ... grp_marche_partielle1=[4, 5, 6],
830
+ ... grp_marche_partielle2=[7, 8, 9]
831
+ ... )
832
+ >>> print(alarm_config.alarm.name)
833
+ Home Alarm
834
+
835
+ """
836
+
837
+ alarm: AlarmModel | None = None
838
+ # badges: list[] # Not yet implemented. No enough information in documentation
839
+ groups: list[Group] | None = None
840
+ sirens: list[SensorModel] | None = None
841
+ cameras: list[Cameras] | None = None
842
+ sensors: list[SensorModel] | None = None
843
+ commands: list[SensorModel] | None = None
844
+ reading_date: datetime | None = field(
845
+ default=None, metadata={"alias": "readingDate"}
846
+ )
847
+ transceivers: list[TransceiverModel] | None = None
848
+ transmitters: list[TransmitterModel] | None = None
849
+ grp_marche_presence: list[int] | None = field(
850
+ default=None, metadata={"alias": "grpMarchePresence"}
851
+ )
852
+ installation_state: int | None = field(
853
+ default=None, metadata={"alias": "installationState"}
854
+ )
855
+ central_information: CentralInformation | None = field(
856
+ default=None, metadata={"alias": "centralInformation"}
857
+ )
858
+ grp_marche_partielle1: list[int] | None = field(
859
+ default=None, metadata={"alias": "grpMarchePartielle1"}
860
+ )
861
+ grp_marche_partielle2: list[int] | None = field(
862
+ default=None, metadata={"alias": "grpMarchePartielle2"}
863
+ )
864
+
865
+
866
+ ############################
867
+ # Data model for device list
868
+ ############################
869
+
870
+
871
+ @dataclass
872
+ class DeviceInfos(CamelCaseModel):
873
+ """DeviceInfos model represents the information of a device.
874
+
875
+ Attributes:
876
+ index (int): The index of the device.
877
+ label (str): The label or name of the device.
878
+
879
+ Example:
880
+ >>> device_info = DeviceInfos(index=1, label="Sensor 1")
881
+ >>> print(device_info.index)
882
+ 1
883
+ >>> print(device_info.label)
884
+ Sensor 1
885
+
886
+ """
887
+
888
+ index: int
889
+ label: str
890
+
891
+
892
+ @dataclass
893
+ class DeviceList(CamelCaseModel):
894
+ """DeviceList model representing a collection of various device types.
895
+
896
+ Attributes:
897
+ cameras (list[DeviceInfos] | None): A list of camera devices or None if not available.
898
+ commands (list[DeviceInfos] | None): A list of command devices or None if not available.
899
+ sensors (list[DeviceInfos] | None): A list of sensor devices or None if not available.
900
+ sirens (list[DeviceInfos] | None): A list of siren devices or None if not available.
901
+ transmitters (list[DeviceInfos] | None): A list of transmitter devices or None if not available.
902
+
903
+ Example:
904
+ >>> device_list = DeviceList(
905
+ ... cameras=[DeviceInfos(index=1, label="Camera 1")],
906
+ ... commands=[DeviceInfos(index=2, label="Command 1")],
907
+ ... sensors=[DeviceInfos(index=3, label="Sensor 1")],
908
+ ... sirens=[DeviceInfos(index=4, label="Siren 1")],
909
+ ... transmitters=[DeviceInfos(index=5, label="Transmitter 1")]
910
+ ... )
911
+ >>> print(device_list.cameras[0].label)
912
+ Camera 1
913
+
914
+ """
915
+
916
+ cameras: list[DeviceInfos] | None = None
917
+ commands: list[DeviceInfos] | None = None
918
+ sensors: list[DeviceInfos] | None = None
919
+ sirens: list[DeviceInfos] | None = None
920
+ transmitters: list[DeviceInfos] | None = None
921
+
922
+
923
+ ###############################
924
+ # Data model for system details
925
+ ###############################
926
+
927
+
928
+ @dataclass
929
+ class SystemDetails(CamelCaseModel):
930
+ """SystemDetails model represents the details of a system with various attributes.
931
+
932
+ Attributes:
933
+ device_type (str): The type of the device.
934
+ firmware_version (str): The firmware version of the device.
935
+ ip_address (str): The IP address of the device.
936
+ ipoda_version (str): The version of the IPODA.
937
+ mode (str): The mode of the device.
938
+ first_vocal_contact (str): The first vocal contact information.
939
+ is_alarm_file_present (bool): Indicates if the alarm file is present.
940
+ is_mjpeg_archive_video_supported (str): Indicates if MJPEG archive video is supported.
941
+ is_mass_storage_present (str): Indicates if mass storage is present.
942
+ is_remote_startup_shutdown_allowed (str): Indicates if remote startup/shutdown is allowed.
943
+ is_video_password_protected (str): Indicates if the video is password protected.
944
+
945
+ Example:
946
+ >>> system_details = SystemDetails(
947
+ ... device_type="Camera",
948
+ ... firmware_version="1.0.0",
949
+ ... ip_address="192.168.1.1",
950
+ ... ipoda_version="2.0.0",
951
+ ... mode="Active",
952
+ ... first_vocal_contact="2023-10-01T12:00:00Z",
953
+ ... is_alarm_file_present=True,
954
+ ... is_mjpeg_archive_video_supported="Yes",
955
+ ... is_mass_storage_present="Yes",
956
+ ... is_remote_startup_shutdown_allowed="No",
957
+ ... is_video_password_protected="Yes"
958
+ ... )
959
+ >>> print(system_details.device_type)
960
+ Camera
961
+
962
+ """
963
+
964
+ device_type: str = field(metadata={"alias": "DeviceType"})
965
+ firmware_version: str = field(metadata={"alias": "FirmwareVersion"})
966
+ ip_address: str = field(metadata={"alias": "IpAddress"})
967
+ ipoda_version: str = field(metadata={"alias": "IpodaVersion"})
968
+ mode: str = field(metadata={"alias": "Mode"})
969
+ first_vocal_contact: str = field(metadata={"alias": "FirstVocalContact"})
970
+ is_alarm_file_present: bool = field(metadata={"alias": "IsAlarmFilePresent"})
971
+ is_mjpeg_archive_video_supported: str = field(
972
+ metadata={"alias": "IsMJPEGArchiveVideoSupported"}
973
+ )
974
+ is_mass_storage_present: str = field(metadata={"alias": "IsMassStoragePresent"})
975
+ is_remote_startup_shutdown_allowed: str = field(
976
+ metadata={"alias": "IsRemoteStartupShutdownAllowed"}
977
+ )
978
+ is_video_password_protected: str = field(
979
+ metadata={"alias": "IsVideoPasswordProtected"}
980
+ )
981
+
982
+
983
+ ##############################
984
+ # Data model for system status
985
+ ##############################
986
+
987
+
988
+ @dataclass
989
+ class SystemStatus(CamelCaseModel):
990
+ """SystemStatus represents the status of a system with various attributes.
991
+
992
+ Attributes:
993
+ status (str): The current status of the system.
994
+ activated_groups (list[int]): A list of IDs representing the activated groups within the system.
995
+
996
+ Example:
997
+ >>> system_status = SystemStatus(status="Active", activated_groups=[1, 2, 3])
998
+ >>> print(system_status.status)
999
+ Active
1000
+ >>> print(system_status.activated_groups)
1001
+ [1, 2, 3]
1002
+
1003
+ """
1004
+
1005
+ status: str
1006
+ activated_groups: list[int]
1007
+
1008
+
1009
+ #######################################
1010
+ # Data model for anomalies informations
1011
+ #######################################
1012
+
1013
+
1014
+ @dataclass
1015
+ class AnomalyName(CamelCaseModel):
1016
+ """AnomalyName model representing an anomaly with an identifier and a name.
1017
+
1018
+ Attributes:
1019
+ id (int): The unique identifier for the anomaly.
1020
+ name (str): The name of the anomaly.
1021
+
1022
+ Example:
1023
+ >>> anomaly = AnomalyName(id=1, name="Low Battery")
1024
+ >>> print(anomaly.id)
1025
+ 1
1026
+ >>> print(anomaly.name)
1027
+ Low Battery
1028
+
1029
+ """
1030
+
1031
+ id: int
1032
+ name: str
1033
+
1034
+
1035
+ @dataclass
1036
+ class AnomalyDetail(CamelCaseModel):
1037
+ """AnomalyDetail represents detailed information about an anomaly.
1038
+
1039
+ Attributes:
1040
+ anomaly_names (list[AnomalyName]): A list of anomaly names associated with this detail.
1041
+ serial (str | None): An optional serial number associated with the anomaly.
1042
+ index (int | None): An optional index value for the anomaly.
1043
+ group (int | None): An optional group identifier for the anomaly.
1044
+ label (str | None): An optional label describing the anomaly.
1045
+
1046
+ Example:
1047
+ >>> anomaly_names = [AnomalyName(id=1, name="Low Battery")]
1048
+ >>> anomaly_detail = AnomalyDetail(
1049
+ ... anomaly_names=anomaly_names,
1050
+ ... serial="SN12345",
1051
+ ... index=1,
1052
+ ... group=2,
1053
+ ... label="Sensor Anomaly"
1054
+ ... )
1055
+ >>> print(anomaly_detail.serial)
1056
+ SN12345
1057
+
1058
+ """
1059
+
1060
+ anomaly_names: list[AnomalyName]
1061
+ serial: str | None = None
1062
+ index: int | None = None
1063
+ group: int | None = None
1064
+ label: str | None = None
1065
+
1066
+
1067
+ @dataclass
1068
+ class Anomalies(CamelCaseModel):
1069
+ """A model representing anomalies detected in various devices.
1070
+
1071
+ Attributes:
1072
+ created_at (datetime): The timestamp when the anomalies were created.
1073
+ sensors (list[AnomalyDetail] | None): A list of anomaly details for sensors, or None if no anomalies.
1074
+ badges (list[AnomalyDetail] | None): A list of anomaly details for badges, or None if no anomalies.
1075
+ sirens (list[AnomalyDetail] | None): A list of anomaly details for sirens, or None if no anomalies.
1076
+ cameras (list[AnomalyDetail] | None): A list of anomaly details for cameras, or None if no anomalies.
1077
+ commands (list[AnomalyDetail] | None): A list of anomaly details for commands, or None if no anomalies.
1078
+ transceivers (list[AnomalyDetail] | None): A list of anomaly details for transceivers, or None if no anomalies.
1079
+ transmitters (list[AnomalyDetail] | None): A list of anomaly details for transmitters, or None if no anomalies.
1080
+ central (list[AnomalyDetail] | None): A list of anomaly details for central devices, or None if no anomalies.
1081
+
1082
+ Methods:
1083
+ from_dict(data: dict) -> Anomalies:
1084
+ Create an instance of Anomalies from a dictionary.
1085
+
1086
+ Example:
1087
+ >>> data = {
1088
+ ... "created_at": "2025-02-16T10:15:12.625165",
1089
+ ... "sensors": [
1090
+ ... {
1091
+ ... "serial": "SN12345",
1092
+ ... "index": 1,
1093
+ ... "group": 2,
1094
+ ... "label": "Sensor Anomaly",
1095
+ ... "anomaly_names": [{"id": 1, "name": "Low Battery"}]
1096
+ ... }
1097
+ ... ]
1098
+ ... }
1099
+ >>> anomalies = Anomalies.from_dict(data)
1100
+ >>> print(anomalies.created_at)
1101
+ 2025-02-16 10:15:12.625165+00:00
1102
+ >>> print(anomalies.sensors[0].serial)
1103
+ SN12345
1104
+
1105
+ """
1106
+
1107
+ created_at: datetime
1108
+ sensors: list[AnomalyDetail] | None = None
1109
+ badges: list[AnomalyDetail] | None = None
1110
+ sirens: list[AnomalyDetail] | None = None
1111
+ cameras: list[AnomalyDetail] | None = None
1112
+ commands: list[AnomalyDetail] | None = None
1113
+ transceivers: list[AnomalyDetail] | None = None
1114
+ transmitters: list[AnomalyDetail] | None = None
1115
+ central: list[AnomalyDetail] | None = None
1116
+
1117
+ @classmethod
1118
+ def from_dict(cls, data: dict) -> Anomalies:
1119
+ """Create an instance of Anomalies from a dictionary."""
1120
+
1121
+ def create_devices(device_data):
1122
+ return [
1123
+ AnomalyDetail(
1124
+ serial=data.get("serial"),
1125
+ index=data.get("index"),
1126
+ group=data.get("group"),
1127
+ label=data.get("label"),
1128
+ anomaly_names=[AnomalyName(**a) for a in data.get("anomaly_names")],
1129
+ )
1130
+ for data in device_data
1131
+ ]
1132
+
1133
+ # Convert the created_at field to a datetime object with UTC timezone
1134
+ created_at = datetime.fromisoformat(data["created_at"]).replace(
1135
+ tzinfo=timezone.utc
1136
+ )
1137
+
1138
+ return cls(
1139
+ created_at=created_at,
1140
+ sensors=create_devices(data.get("sensors", [])),
1141
+ badges=create_devices(data.get("badges", [])),
1142
+ sirens=create_devices(data.get("sirens", [])),
1143
+ cameras=create_devices(data.get("cameras", [])),
1144
+ commands=create_devices(data.get("commands", [])),
1145
+ transceivers=create_devices(data.get("transceivers", [])),
1146
+ transmitters=create_devices(data.get("transmitters", [])),
1147
+ central=create_devices(data.get("central", [])),
1148
+ )
1149
+
1150
+
1151
+ ############################
1152
+ # Data Model for webhooks
1153
+ ############################
1154
+
1155
+
1156
+ @dataclass
1157
+ class WebhookSubscription(CamelCaseModel):
1158
+ """Represents a subscription to webhook notifications.
1159
+
1160
+ Attributes:
1161
+ anomaly (bool): Indicates whether anomaly notifications are enabled.
1162
+ alert (bool): Indicates whether alert notifications are enabled.
1163
+ state (bool): Indicates whether state notifications are enabled.
1164
+
1165
+ Example:
1166
+ >>> subscription = WebhookSubscription(anomaly=True, alert=False, state=True)
1167
+ >>> print(subscription.anomaly)
1168
+ True
1169
+
1170
+ """
1171
+
1172
+ anomaly: bool
1173
+ alert: bool
1174
+ state: bool
1175
+
1176
+
1177
+ @dataclass
1178
+ class Webhook(CamelCaseModel):
1179
+ """Represents a Webhook model.
1180
+
1181
+ Attributes:
1182
+ transmitter_id (str): The unique identifier for the transmitter.
1183
+ webhook_url (str): The URL to which the webhook will send data.
1184
+ subscriptions (WebhookSubscription): The subscription details for the webhook.
1185
+
1186
+ Example:
1187
+ >>> subscription = WebhookSubscription(anomaly=True, alert=False, state=True)
1188
+ >>> webhook = Webhook(transmitter_id="12345", webhook_url="https://example.com/webhook", subscriptions=subscription)
1189
+ >>> print(webhook.transmitter_id)
1190
+ 12345
1191
+
1192
+ """
1193
+
1194
+ transmitter_id: str
1195
+ webhook_url: str
1196
+ subscriptions: WebhookSubscription
1197
+
1198
+
1199
+ @dataclass
1200
+ class WebHookNotificationDetail(CamelCaseModel):
1201
+ """WebHookNotificationDetail model represents the details of a webhook notification.
1202
+
1203
+ Attributes:
1204
+ device_type (str): The type of the device.
1205
+ device_index (str): The index of the device.
1206
+ device_label (str | None): The label of the device, which is optional.
1207
+
1208
+ Example:
1209
+ >>> detail = WebHookNotificationDetail(device_type="Sensor", device_index="1", device_label="Front Door Sensor")
1210
+ >>> print(detail.device_type)
1211
+ Sensor
1212
+ >>> print(detail.device_index)
1213
+ 1
1214
+ >>> print(detail.device_label)
1215
+ Front Door Sensor
1216
+
1217
+ """
1218
+
1219
+ device_type: str
1220
+ device_index: str
1221
+ device_label: str | None = None
1222
+
1223
+
1224
+ @dataclass
1225
+ class WebHookNotificationUser(CamelCaseModel):
1226
+ """WebHookNotificationUser represents a user who receives webhook notifications.
1227
+
1228
+ Attributes:
1229
+ username (str): The username of the user.
1230
+ user_type (str): The type of the user.
1231
+
1232
+ Example:
1233
+ >>> user = WebHookNotificationUser(username="Dark Vador", user_type="owner")
1234
+ >>> print(user.username)
1235
+ Dark Vador
1236
+ >>> print(detail.user_type)
1237
+ owner
1238
+
1239
+ """
1240
+
1241
+ username: str
1242
+ user_type: str
1243
+
1244
+
1245
+ @dataclass
1246
+ class WebHookNotification(CamelCaseModel):
1247
+ """A model representing a webhook notification.
1248
+
1249
+ Attributes:
1250
+ transmitter_id (str): The ID of the transmitter sending the notification.
1251
+ alarm_type (str): The type of alarm, determined based on the alarm code.
1252
+ alarm_code (str): The code representing the specific alarm.
1253
+ alarm_description (str): A description of the alarm.
1254
+ group_index (str): The index of the group associated with the alarm.
1255
+ detail (WebHookNotificationDetail): Detailed information about the webhook notification.
1256
+ Only during anomaly/alert notification.
1257
+ user (WebHookNotificationUser): The user who trigger the notification.
1258
+ Only during change state notification.
1259
+ date_time (datetime): The date and time when the notification was generated.
1260
+
1261
+ Methods:
1262
+ from_dict(data: dict) -> WebHookNotification:
1263
+ Create an instance of WebHookNotification from a dictionary.
1264
+
1265
+ Example:
1266
+ >>> data = {
1267
+ ... "transmitter_id": "12345",
1268
+ ... "alarm_code": "1130",
1269
+ ... "alarm_description": "Intrusion detected",
1270
+ ... "group_index": "01",
1271
+ ... "detail": {
1272
+ ... "device_type": "Sensor",
1273
+ ... "device_index": "1",
1274
+ ... "device_label": "Front Door Sensor"
1275
+ ... },
1276
+ ... "date_time": "2023-10-01T12:00:00Z"
1277
+ ... }
1278
+ >>> notification = WebHookNotification.from_dict(data)
1279
+ >>> print(notification.transmitter_id)
1280
+ 12345
1281
+
1282
+ """
1283
+
1284
+ transmitter_id: str
1285
+ alarm_type: str # Not included in Diagral answer. Added with below function
1286
+ alarm_code: str
1287
+ alarm_description: str
1288
+ group_index: str
1289
+ detail: WebHookNotificationDetail
1290
+ user: WebHookNotificationUser
1291
+ date_time: datetime
1292
+
1293
+ @classmethod
1294
+ def from_dict(cls, data: dict) -> WebHookNotification:
1295
+ """Create an instance of WebHookNotification from a dictionary."""
1296
+
1297
+ def alarm_type(alarm_code):
1298
+ """Determine the type of alarm based on the alarm code."""
1299
+ ANOMALY_CODES = [
1300
+ 1301,
1301
+ 3301,
1302
+ 1137,
1303
+ 3137,
1304
+ 1355,
1305
+ 3355,
1306
+ 1381,
1307
+ 3381,
1308
+ 1144,
1309
+ 3144,
1310
+ 1302,
1311
+ 1384,
1312
+ 1570,
1313
+ 3570,
1314
+ 1352,
1315
+ 3352,
1316
+ 1351,
1317
+ 3351,
1318
+ 1573,
1319
+ ]
1320
+ ALERT_CODES = [
1321
+ 1130,
1322
+ 1110,
1323
+ 1111,
1324
+ 1117,
1325
+ 1158,
1326
+ 1139,
1327
+ 1344,
1328
+ 1120,
1329
+ 1122,
1330
+ 1159,
1331
+ 1152,
1332
+ 1154,
1333
+ 1150,
1334
+ 1140,
1335
+ 1141,
1336
+ 1142,
1337
+ 1143,
1338
+ 3391,
1339
+ 1391,
1340
+ ]
1341
+ STATUS_CODES = [1306, 3401, 3407, 1401, 1407]
1342
+
1343
+ if int(alarm_code) in ANOMALY_CODES:
1344
+ return "ANOMALY"
1345
+ if int(alarm_code) in ALERT_CODES:
1346
+ return "ALERT"
1347
+ if int(alarm_code) in STATUS_CODES:
1348
+ return "STATUS"
1349
+ return "UNKNOWN"
1350
+
1351
+ return cls(
1352
+ transmitter_id=data.get("transmitter_id"),
1353
+ alarm_type=alarm_type(data.get("alarm_code")),
1354
+ alarm_code=data.get("alarm_code"),
1355
+ alarm_description=data.get("alarm_description"),
1356
+ group_index=data.get("group_index"),
1357
+ detail=WebHookNotificationDetail(
1358
+ device_type=data.get("detail", {}).get("device_type", None),
1359
+ device_index=data.get("detail", {}).get("device_index", None),
1360
+ device_label=data.get("detail", {}).get("device_label", None),
1361
+ ),
1362
+ user=WebHookNotificationUser(
1363
+ username=data.get("user", {}).get("username", None),
1364
+ user_type=data.get("user", {}).get("user_type", None),
1365
+ ),
1366
+ date_time=datetime.fromisoformat(data["date_time"].replace("Z", "+00:00")),
1367
+ )
1368
+
1369
+
1370
+ ############################
1371
+ # Data Model for automations
1372
+ ############################
1373
+
1374
+
1375
+ @dataclass
1376
+ class Rude(CamelCaseModel):
1377
+ """Rude model representing a device with a name, canal, and mode.
1378
+
1379
+ Attributes:
1380
+ name (str): The name of the device.
1381
+ canal (str): The canal associated with the device.
1382
+ mode (str): The mode of operation for the device. Must be one of {"ON", "PULSE", "SWITCH", "TIMER"}.
1383
+
1384
+ Methods:
1385
+ __post_init__(): Post-initialization processing to validate the mode attribute.
1386
+
1387
+ Example:
1388
+ >>> rude = Rude(name="Device1", canal="Canal1", mode="ON")
1389
+ >>> print(rude.name)
1390
+ Device1
1391
+
1392
+ """
1393
+
1394
+ name: str
1395
+ canal: str
1396
+ mode: str
1397
+
1398
+ def __post_init__(self):
1399
+ """Post-initialization processing to validate mode."""
1400
+ valid_modes = {"ON", "PULSE", "SWITCH", "TIMER"}
1401
+ if self.mode not in valid_modes:
1402
+ raise ValueError(f"mode must be one of {valid_modes}")
1403
+
1404
+
1405
+ @dataclass
1406
+ class Rudes(CamelCaseModel):
1407
+ """Rudes model representing a collection of Rude instances.
1408
+
1409
+ Attributes:
1410
+ rudes (list[Rude]): A list of Rude objects.
1411
+
1412
+ Example:
1413
+ >>> rude1 = Rude(name="Device1", canal="Canal1", mode="ON")
1414
+ >>> rude2 = Rude(name="Device2", canal="Canal2", mode="PULSE")
1415
+ >>> rudes = Rudes(rudes=[rude1, rude2])
1416
+ >>> print(rudes.rudes[0].name)
1417
+ Device1
1418
+
1419
+ """
1420
+
1421
+ rudes: list[Rude]
1422
+
1423
+
1424
+ ###########################
1425
+ # Data Model for exceptions
1426
+ ###########################
1427
+
1428
+
1429
+ @dataclass
1430
+ class ValidationError(CamelCaseModel):
1431
+ """ValidationError model represents an error that occurs during validation.
1432
+
1433
+ Attributes:
1434
+ loc (list[str] | None): The location of the error, typically indicating the field or attribute that caused the error.
1435
+ message (str | None): A human-readable message describing the error.
1436
+ type (str | None): The type or category of the error.
1437
+ input (str | None): The input value that caused the error.
1438
+ url (str | None): A URL providing more information about the error.
1439
+
1440
+ Example:
1441
+ >>> error = ValidationError(
1442
+ ... loc=["body", "username"],
1443
+ ... message="Username is required",
1444
+ ... type="value_error.missing",
1445
+ ... input=None,
1446
+ ... url="https://example.com/errors/username-required"
1447
+ ... )
1448
+ >>> print(error.message)
1449
+ Username is required
1450
+
1451
+ """
1452
+
1453
+ loc: list[str] | None = None
1454
+ message: str | None = None
1455
+ type: str | None = None
1456
+ input: str | None = None
1457
+ url: str | None = None
1458
+
1459
+
1460
+ @dataclass
1461
+ class HTTPValidationError(ValidationError):
1462
+ """HTTPValidationError is a subclass of ValidationError that represents an HTTP validation error.
1463
+
1464
+ Attributes:
1465
+ detail (list[ValidationError] | None): A list of ValidationError instances or None, providing detailed information about the validation errors.
1466
+
1467
+ Example:
1468
+ >>> error_detail = ValidationError(
1469
+ ... loc=["body", "username"],
1470
+ ... message="Username is required",
1471
+ ... type="value_error.missing",
1472
+ ... input=None,
1473
+ ... url="https://example.com/errors/username-required"
1474
+ ... )
1475
+ >>> http_error = HTTPValidationError(detail=[error_detail])
1476
+ >>> print(http_error.detail[0].message)
1477
+ Username is required
1478
+
1479
+ """
1480
+
1481
+ detail: list[ValidationError] | None = None
1482
+
1483
+
1484
+ @dataclass
1485
+ class HTTPErrorResponse(CamelCaseModel):
1486
+ """HTTPErrorResponse is a model that represents an HTTP error response.
1487
+
1488
+ Attributes:
1489
+ detail (str): A detailed message describing the error.
1490
+
1491
+ Example:
1492
+ >>> error_response = HTTPErrorResponse(detail="Not Found")
1493
+ >>> print(error_response.detail)
1494
+ Not Found
1495
+
1496
+ """
1497
+
1498
+ detail: str