pydiagral 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pydiagral/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