PyAutomationIO 0.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.
Files changed (138) hide show
  1. automation/__init__.py +46 -0
  2. automation/alarms/__init__.py +563 -0
  3. automation/alarms/states.py +192 -0
  4. automation/alarms/trigger.py +64 -0
  5. automation/buffer.py +132 -0
  6. automation/core.py +1775 -0
  7. automation/dbmodels/__init__.py +23 -0
  8. automation/dbmodels/alarms.py +524 -0
  9. automation/dbmodels/core.py +86 -0
  10. automation/dbmodels/events.py +153 -0
  11. automation/dbmodels/logs.py +155 -0
  12. automation/dbmodels/machines.py +181 -0
  13. automation/dbmodels/opcua.py +81 -0
  14. automation/dbmodels/opcua_server.py +174 -0
  15. automation/dbmodels/tags.py +921 -0
  16. automation/dbmodels/users.py +259 -0
  17. automation/extensions/__init__.py +15 -0
  18. automation/extensions/api.py +149 -0
  19. automation/extensions/cors.py +18 -0
  20. automation/filter/__init__.py +19 -0
  21. automation/iad/__init__.py +3 -0
  22. automation/iad/frozen_data.py +54 -0
  23. automation/iad/out_of_range.py +51 -0
  24. automation/iad/outliers.py +51 -0
  25. automation/logger/__init__.py +0 -0
  26. automation/logger/alarms.py +426 -0
  27. automation/logger/core.py +265 -0
  28. automation/logger/datalogger.py +646 -0
  29. automation/logger/events.py +194 -0
  30. automation/logger/logdict.py +53 -0
  31. automation/logger/logs.py +203 -0
  32. automation/logger/machines.py +248 -0
  33. automation/logger/opcua_server.py +130 -0
  34. automation/logger/users.py +96 -0
  35. automation/managers/__init__.py +4 -0
  36. automation/managers/alarms.py +455 -0
  37. automation/managers/db.py +328 -0
  38. automation/managers/opcua_client.py +186 -0
  39. automation/managers/state_machine.py +183 -0
  40. automation/models.py +174 -0
  41. automation/modules/__init__.py +14 -0
  42. automation/modules/alarms/__init__.py +0 -0
  43. automation/modules/alarms/resources/__init__.py +10 -0
  44. automation/modules/alarms/resources/alarms.py +280 -0
  45. automation/modules/alarms/resources/summary.py +79 -0
  46. automation/modules/events/__init__.py +0 -0
  47. automation/modules/events/resources/__init__.py +10 -0
  48. automation/modules/events/resources/events.py +83 -0
  49. automation/modules/events/resources/logs.py +109 -0
  50. automation/modules/tags/__init__.py +0 -0
  51. automation/modules/tags/resources/__init__.py +8 -0
  52. automation/modules/tags/resources/tags.py +201 -0
  53. automation/modules/users/__init__.py +2 -0
  54. automation/modules/users/resources/__init__.py +10 -0
  55. automation/modules/users/resources/models/__init__.py +2 -0
  56. automation/modules/users/resources/models/roles.py +5 -0
  57. automation/modules/users/resources/models/users.py +14 -0
  58. automation/modules/users/resources/roles.py +38 -0
  59. automation/modules/users/resources/users.py +113 -0
  60. automation/modules/users/roles.py +121 -0
  61. automation/modules/users/users.py +335 -0
  62. automation/opcua/__init__.py +1 -0
  63. automation/opcua/models.py +541 -0
  64. automation/opcua/subscription.py +259 -0
  65. automation/pages/__init__.py +0 -0
  66. automation/pages/alarms.py +34 -0
  67. automation/pages/alarms_history.py +21 -0
  68. automation/pages/assets/styles.css +7 -0
  69. automation/pages/callbacks/__init__.py +28 -0
  70. automation/pages/callbacks/alarms.py +218 -0
  71. automation/pages/callbacks/alarms_summary.py +20 -0
  72. automation/pages/callbacks/db.py +222 -0
  73. automation/pages/callbacks/filter.py +238 -0
  74. automation/pages/callbacks/machines.py +29 -0
  75. automation/pages/callbacks/machines_detailed.py +581 -0
  76. automation/pages/callbacks/opcua.py +266 -0
  77. automation/pages/callbacks/opcua_server.py +244 -0
  78. automation/pages/callbacks/tags.py +495 -0
  79. automation/pages/callbacks/trends.py +119 -0
  80. automation/pages/communications.py +129 -0
  81. automation/pages/components/__init__.py +123 -0
  82. automation/pages/components/alarms.py +151 -0
  83. automation/pages/components/alarms_summary.py +45 -0
  84. automation/pages/components/database.py +128 -0
  85. automation/pages/components/gaussian_filter.py +69 -0
  86. automation/pages/components/machines.py +396 -0
  87. automation/pages/components/opcua.py +384 -0
  88. automation/pages/components/opcua_server.py +53 -0
  89. automation/pages/components/tags.py +253 -0
  90. automation/pages/components/trends.py +66 -0
  91. automation/pages/database.py +26 -0
  92. automation/pages/filter.py +55 -0
  93. automation/pages/machines.py +20 -0
  94. automation/pages/machines_detailed.py +41 -0
  95. automation/pages/main.py +63 -0
  96. automation/pages/opcua_server.py +28 -0
  97. automation/pages/tags.py +40 -0
  98. automation/pages/trends.py +35 -0
  99. automation/singleton.py +30 -0
  100. automation/state_machine.py +1672 -0
  101. automation/tags/__init__.py +2 -0
  102. automation/tags/cvt.py +1198 -0
  103. automation/tags/filter.py +55 -0
  104. automation/tags/tag.py +418 -0
  105. automation/tests/__init__.py +10 -0
  106. automation/tests/test_alarms.py +110 -0
  107. automation/tests/test_core.py +257 -0
  108. automation/tests/test_unit.py +21 -0
  109. automation/tests/test_user.py +155 -0
  110. automation/utils/__init__.py +164 -0
  111. automation/utils/decorators.py +222 -0
  112. automation/utils/npw.py +294 -0
  113. automation/utils/observer.py +21 -0
  114. automation/utils/units.py +118 -0
  115. automation/variables/__init__.py +55 -0
  116. automation/variables/adimentional.py +30 -0
  117. automation/variables/current.py +71 -0
  118. automation/variables/density.py +115 -0
  119. automation/variables/eng_time.py +68 -0
  120. automation/variables/force.py +90 -0
  121. automation/variables/length.py +104 -0
  122. automation/variables/mass.py +80 -0
  123. automation/variables/mass_flow.py +101 -0
  124. automation/variables/percentage.py +30 -0
  125. automation/variables/power.py +113 -0
  126. automation/variables/pressure.py +93 -0
  127. automation/variables/temperature.py +168 -0
  128. automation/variables/volume.py +70 -0
  129. automation/variables/volumetric_flow.py +100 -0
  130. automation/workers/__init__.py +2 -0
  131. automation/workers/logger.py +164 -0
  132. automation/workers/state_machine.py +207 -0
  133. automation/workers/worker.py +36 -0
  134. pyautomationio-0.0.0.dist-info/METADATA +198 -0
  135. pyautomationio-0.0.0.dist-info/RECORD +138 -0
  136. pyautomationio-0.0.0.dist-info/WHEEL +5 -0
  137. pyautomationio-0.0.0.dist-info/licenses/LICENSE +21 -0
  138. pyautomationio-0.0.0.dist-info/top_level.txt +1 -0
automation/tags/cvt.py ADDED
@@ -0,0 +1,1198 @@
1
+ import threading, copy, logging
2
+ from datetime import datetime
3
+ from ..singleton import Singleton
4
+ from ..models import FloatType, StringType, IntegerType, BooleanType
5
+ from ..modules.users.users import User
6
+ from ..modules.users.users import User
7
+ from ..utils.decorators import set_event, logging_error_handler
8
+ from ..filter import filter
9
+ # from ..iad import iad_outlier, iad_frozen_data, iad_out_of_range
10
+ from .tag import Tag
11
+ from flask_socketio import SocketIO
12
+
13
+ class CVT:
14
+ """
15
+ Current Value Table (CVT) class for a tag-based repository.
16
+
17
+ This class is designed to hold in-memory tag-based values and manage observers for the required tags. It is intended to be used exclusively by PyAutomation and should not be used for other purposes.
18
+
19
+ **Usage Example**:
20
+
21
+ .. code-block:: python
22
+
23
+ >>> from automation.tags import CVT
24
+ >>> _cvt = CVT()
25
+
26
+ """
27
+
28
+ def __init__(self):
29
+
30
+ self._tags = dict()
31
+ self.data_types = ["float", "int", "bool", "str"]
32
+ self.sio:SocketIO|None = None
33
+
34
+ def set_socketio(self, sio:SocketIO):
35
+ r"""
36
+ Documentation here
37
+ """
38
+ self.sio:SocketIO = sio
39
+
40
+ @set_event(message=f"Created", classification="Tag", priority=1, criticity=1)
41
+ def set_tag(
42
+ self,
43
+ name:str,
44
+ unit:str,
45
+ data_type:str,
46
+ description:str,
47
+ variable:str,
48
+ display_name:str="",
49
+ display_unit:str="",
50
+ opcua_address:str="",
51
+ node_namespace:str="",
52
+ scan_time:int=None,
53
+ dead_band:float=None,
54
+ process_filter:bool=False,
55
+ gaussian_filter:bool=False,
56
+ gaussian_filter_threshold:float=1.0,
57
+ gaussian_filter_r_value:float=0.0,
58
+ outlier_detection:bool=False,
59
+ out_of_range_detection:bool=False,
60
+ frozen_data_detection:bool=False,
61
+ manufacturer:str="",
62
+ segment:str="",
63
+ id:str=None,
64
+ user:User=None
65
+ )->tuple[Tag, str]:
66
+ """Initialize a new Tag object in the _tags dictionary.
67
+
68
+ # Parameters
69
+ name (str):
70
+ Tag name.
71
+ data_type (str):
72
+ Tag value type ("int", "float", "bool", "str")
73
+ """
74
+ if isinstance(data_type, str):
75
+
76
+ if data_type in self.data_types:
77
+ if data_type == "float":
78
+ value = 0.0
79
+ elif data_type == "int":
80
+ value = 0
81
+ elif data_type == "str":
82
+ value = ""
83
+ else:
84
+ value = False
85
+
86
+ elif isinstance(data_type, (FloatType, IntegerType, StringType, BooleanType)):
87
+
88
+ value = data_type()
89
+ data_type.set(name, value)
90
+ data_type = data_type.__name__
91
+ self.set_data_type(data_type)
92
+
93
+ has_duplicates, message = self.has_duplicates(name=name, display_name=display_name, opcua_address=opcua_address, node_namespace=node_namespace)
94
+ if has_duplicates:
95
+
96
+ return None, message
97
+
98
+ if not display_unit:
99
+
100
+ display_unit = unit
101
+
102
+
103
+ tag = Tag(
104
+ name=name,
105
+ unit=unit,
106
+ data_type=data_type,
107
+ description=description,
108
+ variable=variable,
109
+ display_name=display_name,
110
+ display_unit=display_unit,
111
+ opcua_address=opcua_address,
112
+ node_namespace=node_namespace,
113
+ scan_time=scan_time,
114
+ dead_band=dead_band,
115
+ process_filter=process_filter,
116
+ gaussian_filter=gaussian_filter,
117
+ gaussian_filter_threshold=gaussian_filter_threshold,
118
+ gaussian_filter_r_value=gaussian_filter_r_value,
119
+ outlier_detection=outlier_detection,
120
+ out_of_range_detection=out_of_range_detection,
121
+ frozen_data_detection=frozen_data_detection,
122
+ manufacturer=manufacturer,
123
+ segment=segment,
124
+ id=id
125
+ )
126
+ self._tags[tag.id] = tag
127
+
128
+ return tag, message
129
+
130
+ @set_event(message=f"Updated", classification="Tag", priority=1, criticity=3)
131
+ def update_tag(
132
+ self,
133
+ id:str,
134
+ user:User=None,
135
+ **kwargs
136
+ )->tuple[Tag|None, str]:
137
+ r"""Documentation here
138
+
139
+ # Parameters
140
+
141
+ -
142
+
143
+ # Returns
144
+
145
+ -
146
+ """
147
+ check = dict()
148
+ if "name" in kwargs:
149
+ check["name"] = kwargs["name"]
150
+ if "display_name" in kwargs:
151
+ check["display_name"] = kwargs["display_name"]
152
+ if "node_namespace" in kwargs:
153
+ check["node_namespace"] = kwargs["node_namespace"]
154
+ if "opcua_address" in kwargs:
155
+ check["opcua_address"] = kwargs["opcua_address"]
156
+ has_duplicates, message = self.has_duplicates(**check)
157
+ if has_duplicates:
158
+
159
+ return None, message
160
+
161
+ tag = self._tags[id]
162
+ if "name" in kwargs:
163
+ tag.set_name(name=kwargs["name"])
164
+ if "unit" in kwargs:
165
+ tag.set_unit(unit=kwargs["unit"])
166
+ if "data_type" in kwargs:
167
+ tag.set_data_type(data_type=kwargs["data_type"])
168
+ if "description" in kwargs:
169
+ tag.set_description(description=kwargs["description"])
170
+ if "variable" in kwargs:
171
+ tag.set_variable(variable=kwargs["variable"])
172
+ if "display_name" in kwargs:
173
+ tag.set_display_name(name=kwargs["display_name"])
174
+ if "display_unit" in kwargs:
175
+ tag.set_display_unit(unit=kwargs["display_unit"])
176
+ if "opcua_address" in kwargs:
177
+ tag.set_opcua_address(opcua_address=kwargs["opcua_address"])
178
+ if "node_namespace" in kwargs:
179
+ tag.set_node_namespace(node_namespace=kwargs["node_namespace"])
180
+ if "scan_time" in kwargs:
181
+ if isinstance(kwargs["scan_time"], int):
182
+ tag.set_scan_time(scan_time=kwargs["scan_time"])
183
+ if "dead_band" in kwargs:
184
+ tag.set_dead_band(dead_band=kwargs["dead_band"])
185
+ if "segment" in kwargs:
186
+ tag.segment = kwargs["segment"]
187
+ if "manufacturer" in kwargs:
188
+ tag.manufacturer = kwargs["manufacturer"]
189
+ if "gaussian_filter" in kwargs:
190
+
191
+ if kwargs['gaussian_filter'].lower() in ('1', 'true'):
192
+
193
+ tag.gaussian_filter = True
194
+
195
+ else:
196
+
197
+ tag.gaussian_filter = False
198
+
199
+ if "gaussian_filter_r_value" in kwargs:
200
+
201
+ tag.gaussian_filter_r_value = kwargs['gaussian_filter_r_value']
202
+
203
+ if "gaussian_filter_threshold" in kwargs:
204
+
205
+ tag.gaussian_filter_threshold = kwargs['gaussian_filter_threshold']
206
+
207
+ self._tags[id] = tag
208
+
209
+ return tag, f"Tag: {tag.name}"
210
+
211
+ @set_event(message=f"Updated", classification="Tag", priority=1, criticity=5)
212
+ def delete_tag(self, id:str, user:User):
213
+ r"""Documentation here
214
+
215
+ # Parameters
216
+
217
+ -
218
+
219
+ # Returns
220
+
221
+ -
222
+ """
223
+ tag = self._tags.pop(id)
224
+ return tag, f"Tag: {tag.name}"
225
+
226
+ def get_tag(self, id:str)->Tag|None:
227
+ r"""Documentation here
228
+
229
+ # Parameters
230
+
231
+ -
232
+
233
+ # Returns
234
+
235
+ -
236
+ """
237
+ for _id, tag in self._tags.items():
238
+
239
+ if _id==id:
240
+
241
+ return tag
242
+
243
+ return None
244
+
245
+ def get_unit_by_tag(self, tag:str)->Tag|None:
246
+ r"""Documentation here
247
+
248
+ # Parameters
249
+
250
+ -
251
+
252
+ # Returns
253
+
254
+ -
255
+ """
256
+ for _id, _tag in self._tags.items():
257
+
258
+ if _tag.name==tag:
259
+
260
+ return _tag.unit
261
+
262
+ return None
263
+
264
+ def get_display_unit_by_tag(self, tag:str)->Tag|None:
265
+ r"""Documentation here
266
+
267
+ # Parameters
268
+
269
+ -
270
+
271
+ # Returns
272
+
273
+ -
274
+ """
275
+ for _id, _tag in self._tags.items():
276
+
277
+ if _tag.name==tag:
278
+
279
+ return _tag.display_unit
280
+
281
+ return None
282
+
283
+ def get_tags(self)->list:
284
+ r"""
285
+ Returns a list of the defined tags names.
286
+ """
287
+ if self._tags:
288
+
289
+ return [tag.serialize() for _, tag in self._tags.items()]
290
+
291
+ return list()
292
+
293
+ def get_tags_by_names(self, names:list)->list:
294
+ r"""
295
+ Returns a list of the defined tags names.
296
+ """
297
+ if self._tags:
298
+
299
+ return [tag.serialize() for _, tag in self._tags.items() if tag.name in names]
300
+
301
+ return list()
302
+
303
+ def get_field_tags_names(self)->list:
304
+ r"""
305
+ Returns a list of the defined tags names.
306
+ """
307
+ if self._tags:
308
+
309
+ return [tag.name for _, tag in self._tags.items() if tag.opcua_address and tag.node_namespace]
310
+
311
+ return list()
312
+
313
+ def get_cuasi_field_tags_names(self)->list:
314
+ r"""
315
+ Returns a list of the defined tags names.
316
+ """
317
+ if self._tags:
318
+
319
+ return [tag.name for _, tag in self._tags.items() if tag.opcua_address]
320
+
321
+ return list()
322
+
323
+ def get_tag_by_name(self, name:str)->Tag|None:
324
+ r"""Documentation here
325
+
326
+ # Parameters
327
+
328
+ -
329
+
330
+ # Returns
331
+
332
+ -
333
+ """
334
+ for _, tag in self._tags.items():
335
+
336
+ if tag.get_name()==name:
337
+
338
+ return tag
339
+
340
+ return None
341
+
342
+ def get_tag_by_display_name(self, display_name:str)->Tag|None:
343
+ r"""Documentation here
344
+
345
+ # Parameters
346
+
347
+ -
348
+
349
+ # Returns
350
+
351
+ -
352
+ """
353
+ for _, tag in self._tags.items():
354
+
355
+ if tag.get_display_name()==display_name:
356
+
357
+ return tag
358
+
359
+ return None
360
+
361
+ def get_tag_by_node_namespace(self, node_namespace:str)->Tag|None:
362
+ r"""Documentation here
363
+
364
+ # Parameters
365
+
366
+ -
367
+
368
+ # Returns
369
+
370
+ -
371
+ """
372
+ for _, tag in self._tags.items():
373
+
374
+ if tag.get_node_namespace()==node_namespace:
375
+
376
+ return tag
377
+
378
+ return None
379
+
380
+ def get_value(self, id:str)->str|float|int|bool:
381
+ r"""Documentation here
382
+
383
+ # Parameters
384
+
385
+ -
386
+
387
+ # Returns
388
+
389
+ -
390
+ """
391
+ tag = self._tags[id]
392
+ _new_object = copy.copy(tag.get_value())
393
+ return _new_object
394
+
395
+ def get_timestamp(self, id:str)->datetime:
396
+ r"""
397
+ Documentation here
398
+ """
399
+ tag = self._tags[id]
400
+
401
+ return tag.get_timestamp()
402
+
403
+ def get_value_by_name(self, name:str)->str|float|int|bool:
404
+ r"""Documentation here
405
+
406
+ # Parameters
407
+
408
+ -
409
+
410
+ # Returns
411
+
412
+ -
413
+ """
414
+
415
+ tag = self.get_tag_by_name(name=name)
416
+
417
+ return {
418
+ "value": tag.get_value(),
419
+ "unit": tag.get_unit(),
420
+ "timestamp": tag.get_timestamp()
421
+ }
422
+
423
+ def get_values_by_name(self, names:list[str])->str|float|int|bool:
424
+ r"""Documentation here
425
+
426
+ # Parameters
427
+
428
+ -
429
+
430
+ # Returns
431
+
432
+ -
433
+ """
434
+ data = dict()
435
+
436
+ for name in names:
437
+
438
+ tag = self.get_tag_by_name(name=name)
439
+ data[name] = {
440
+ "value": tag.get_value(),
441
+ "unit": tag.get_unit(),
442
+ "timestamp": tag.get_timestamp()
443
+ }
444
+
445
+ return data
446
+
447
+ @logging_error_handler
448
+ @filter
449
+ # @iad_frozen_data
450
+ # @iad_out_of_range
451
+ # @iad_outlier
452
+ def set_value(self, id:str, value, timestamp:datetime):
453
+ """Sets a new value for a defined tag.
454
+
455
+ # Parameters
456
+ name (str):
457
+ Tag name.
458
+ value (float, int, bool):
459
+ Tag value ("int", "float", "bool")
460
+ """
461
+ from .. import TIMEZONE
462
+ tag = self._tags[id]
463
+
464
+ # Deadband Logic Wrapper for CVT
465
+
466
+
467
+ if tag.dead_band and isinstance(value, (int, float)):
468
+ try:
469
+ current_value = tag.value.value
470
+ if abs(value - current_value) < tag.dead_band:
471
+ return value
472
+ except Exception as e:
473
+ logging.error(f"Error in deadband logic: {e}")
474
+
475
+ tag.set_value(value=value, timestamp=timestamp)
476
+ if self.sio:
477
+ timestamp = timestamp.astimezone(TIMEZONE)
478
+ self._tags[id].timestamp = timestamp
479
+ self.sio.emit("on.tag", data=self._tags[id].serialize())
480
+
481
+ return value
482
+
483
+ def set_data_type(self, data_type):
484
+ r"""Documentation here
485
+
486
+ # Parameters
487
+
488
+ -
489
+
490
+ # Returns
491
+
492
+ -
493
+ """
494
+ self.data_types.append(data_type)
495
+ self.data_types = list(set(self.data_types))
496
+
497
+ def is_tag_defined(self, name:str)->bool:
498
+ r"""Documentation here
499
+
500
+ # Parameters
501
+
502
+ -
503
+
504
+ # Returns
505
+
506
+ -
507
+ """
508
+
509
+ return name in self._tags
510
+
511
+ def attach_observer(self, name, observer):
512
+ r"""Attaches a new observer to a tag object defined by name.
513
+
514
+ # Parameters
515
+ name (str):
516
+ Tag name.
517
+ observer (TagObserver):
518
+ Tag observer object, will update once a tag object is changed.
519
+ """
520
+ tag = self.get_tag_by_name(name)
521
+ if tag:
522
+
523
+ self._tags[tag.id].attach(observer)
524
+
525
+ else:
526
+ logger = logging.getLogger("pyautomation")
527
+ logger.warning(f"{name} tag Not exists in CVT.attach_observer method")
528
+
529
+ def detach_observer(self, name, observer):
530
+ r"""
531
+ Detaches an observer from a tag object defined by name.
532
+
533
+ # Parameters
534
+ name (str):
535
+ Tag name.
536
+ observer (TagObserver):
537
+ Tag observer object.
538
+ """
539
+ tag = self.get_tag_by_name(name)
540
+ self._tags[tag.id].detach(observer)
541
+
542
+ def has_duplicates(self, tag:Tag=None, name:str=None, display_name:str=None, node_namespace:str=None, opcua_address:str=None):
543
+ r"""Documentation here
544
+
545
+ # Parameters
546
+
547
+ -
548
+
549
+ # Returns
550
+
551
+ -
552
+ """
553
+
554
+ for _, _tag in self._tags.items():
555
+
556
+ if name:
557
+
558
+ if _tag.get_name()==name:
559
+
560
+ return True, f"Duplicated Tag Name: {name}"
561
+
562
+ if display_name:
563
+
564
+ if _tag.get_display_name()==display_name:
565
+
566
+ return True, f"Duplicated Display Name: {display_name}"
567
+
568
+ if node_namespace:
569
+
570
+ if _tag.get_node_namespace()==node_namespace:
571
+
572
+ if tag:
573
+
574
+ if _tag.get_opcua_address()==tag.get_opcua_address():
575
+
576
+ return True, f"Duplicated Node Namespace: {node_namespace}"
577
+
578
+ return True, f"Duplicated Node Namespace: {node_namespace}"
579
+
580
+ return False, f"Valid Tag Name: {name} - Display Name: {display_name}"
581
+
582
+ def serialize(self, id:str)->dict:
583
+ r"""Returns a tag type defined by name.
584
+
585
+ # Parameters
586
+ name (str):
587
+ Tag name.
588
+ """
589
+ return self._tags[id].serialize()
590
+
591
+ def serialize_by_tag_name(self, name:str)->dict|None:
592
+ r"""Documentation here
593
+
594
+ # Parameters
595
+
596
+ -
597
+
598
+ # Returns
599
+
600
+ -
601
+ """
602
+ tag = self.get_tag_by_name(name)
603
+
604
+ if tag:
605
+
606
+ return tag.serialize()
607
+
608
+
609
+ class CVTEngine(Singleton):
610
+ """
611
+ Current Value Table (CVT) Engine class for a tag-based, thread-safe repository.
612
+
613
+ This class is designed to hold in-memory tag-based values and manage observers for the required tags. It is implemented as a singleton, ensuring that each sub-thread within the PyAutomation application can access and modify tags in a thread-safe manner.
614
+
615
+ **Usage Example**:
616
+
617
+ .. code-block:: python
618
+
619
+ >>> from automation.tags import CVTEngine
620
+ >>> tag_engine = CVTEngine()
621
+ """
622
+
623
+
624
+ def __init__(self):
625
+
626
+ super(CVTEngine, self).__init__()
627
+ self._cvt = CVT()
628
+ self._request_lock = threading.Lock()
629
+ self._response_lock = threading.Lock()
630
+ self._config = None
631
+ self._response = None
632
+ self._response_lock.acquire()
633
+ self.DATETIME_FORMAT = "%m/%d/%Y, %H:%M:%S.%f"
634
+
635
+ def set_tag(
636
+ self,
637
+ name:str,
638
+ unit:str,
639
+ data_type:str,
640
+ variable:str,
641
+ description:str,
642
+ display_unit:str="",
643
+ display_name:str="",
644
+ opcua_address:str="",
645
+ node_namespace:str="",
646
+ scan_time:int=0,
647
+ dead_band:float=0.0,
648
+ process_filter:bool=False,
649
+ gaussian_filter:bool=False,
650
+ gaussian_filter_threshold:float=1.0,
651
+ gaussian_filter_r_value:float=0.0,
652
+ outlier_detection:bool=False,
653
+ out_of_range_detection:bool=False,
654
+ frozen_data_detection:bool=False,
655
+ manufacturer:str="",
656
+ segment:str="",
657
+ id:str="",
658
+ user:User|None=None
659
+ )->tuple[Tag, str]:
660
+ r"""Documentation here
661
+
662
+ # Parameters
663
+
664
+ -
665
+
666
+ # Returns
667
+
668
+ -
669
+ """
670
+ _query = dict()
671
+ _query["action"] = "set_tag"
672
+ _query["parameters"] = dict()
673
+ _query["parameters"]["name"] = name
674
+ _query["parameters"]["unit"] = unit
675
+ _query["parameters"]["data_type"] = data_type
676
+ _query["parameters"]["variable"] = variable
677
+ _query["parameters"]["description"] = description
678
+ _query["parameters"]["display_unit"] = display_unit
679
+ _query["parameters"]["display_name"] = display_name
680
+ _query["parameters"]["opcua_address"] = opcua_address
681
+ _query["parameters"]["node_namespace"] = node_namespace
682
+ _query["parameters"]["scan_time"] = scan_time
683
+ _query["parameters"]["dead_band"] = dead_band
684
+ _query["parameters"]["process_filter"] = process_filter
685
+ _query["parameters"]["gaussian_filter"] = gaussian_filter
686
+ _query["parameters"]["gaussian_filter_threshold"] = gaussian_filter_threshold
687
+ _query["parameters"]["gaussian_filter_r_value"] = gaussian_filter_r_value
688
+ _query["parameters"]["outlier_detection"] = outlier_detection
689
+ _query["parameters"]["out_of_range_detection"] = out_of_range_detection
690
+ _query["parameters"]["frozen_data_detection"] = frozen_data_detection
691
+ _query["parameters"]["manufacturer"] = manufacturer
692
+ _query["parameters"]["segment"] = segment
693
+ _query["parameters"]["id"] = id
694
+ _query["parameters"]["user"] = user
695
+ return self.__query(_query)
696
+
697
+ def update_tag(
698
+ self,
699
+ id:str,
700
+ user:User=None,
701
+ **kwargs
702
+ ):
703
+ r"""Documentation here
704
+
705
+ # Parameters
706
+
707
+ -
708
+
709
+ # Returns
710
+
711
+ -
712
+ """
713
+ _query = dict()
714
+ _query["action"] = "update_tag"
715
+ _query["parameters"] = dict()
716
+ _query["parameters"]["id"] = id
717
+ _query["parameters"]["user"] = user
718
+ for key, value in kwargs.items():
719
+
720
+ _query["parameters"][key] = value
721
+ return self.__query(_query)
722
+
723
+ def delete_tag(self, id:str, user:User|None=None):
724
+ r"""Documentation here
725
+
726
+ # Parameters
727
+
728
+ -
729
+
730
+ # Returns
731
+
732
+ -
733
+ """
734
+ _query = dict()
735
+ _query["action"] = "delete_tag"
736
+ _query["parameters"] = dict()
737
+ _query["parameters"]["id"] = id
738
+ _query["parameters"]["user"] = user
739
+ return self.__query(_query)
740
+
741
+ def get_tag(
742
+ self,
743
+ id:str=None
744
+ )->Tag:
745
+ r"""Documentation here
746
+
747
+ # Parameters
748
+
749
+ -
750
+
751
+ # Returns
752
+
753
+ -
754
+ """
755
+ _query = dict()
756
+ _query["action"] = "get_tag"
757
+ _query["parameters"] = dict()
758
+ _query["parameters"]["id"] = id
759
+ return self.__query(_query)
760
+
761
+ def get_tags(self):
762
+ r"""Documentation here
763
+
764
+ # Parameters
765
+
766
+ -
767
+
768
+ # Returns
769
+
770
+ -
771
+ """
772
+ _query = dict()
773
+ _query["action"] = "get_tags"
774
+ return self.__query(_query)
775
+
776
+ def get_tags_by_names(self, names:list[str]):
777
+ r"""Documentation here
778
+
779
+ # Parameters
780
+
781
+ -
782
+
783
+ # Returns
784
+
785
+ -
786
+ """
787
+ _query = dict()
788
+ _query["action"] = "get_tags_by_names"
789
+ _query["parameters"] = dict()
790
+ _query["parameters"]["names"] = names
791
+ return self.__query(_query)
792
+
793
+ def get_tag_by_name(self, name:str)->Tag|None:
794
+ r"""Documentation here
795
+
796
+ # Parameters
797
+
798
+ -
799
+
800
+ # Returns
801
+
802
+ -
803
+ """
804
+ _query = dict()
805
+ _query["action"] = "get_tag_by_name"
806
+ _query["parameters"] = dict()
807
+ _query["parameters"]["name"] = name
808
+ return self.__query(_query)
809
+
810
+ def get_tag_by_display_name(self, display_name:str)->Tag|None:
811
+ r"""Documentation here
812
+
813
+ # Parameters
814
+
815
+ -
816
+
817
+ # Returns
818
+
819
+ -
820
+ """
821
+ _query = dict()
822
+ _query["action"] = "get_tag_by_display_name"
823
+ _query["parameters"] = dict()
824
+ _query["parameters"]["display_name"] = display_name
825
+ return self.__query(_query)
826
+
827
+ def get_tag_by_node_namespace(self, node_namespace:str)->Tag|None:
828
+ r"""Documentation here
829
+
830
+ # Parameters
831
+
832
+ -
833
+
834
+ # Returns
835
+
836
+ -
837
+ """
838
+ _query = dict()
839
+ _query["action"] = "get_tag_by_node_namespace"
840
+ _query["parameters"] = dict()
841
+ _query["parameters"]["node_namespace"] = node_namespace
842
+ return self.__query(_query)
843
+
844
+ def get_value(self, id:str)->str|float|int|bool:
845
+ r"""Documentation here
846
+
847
+ # Parameters
848
+
849
+ -
850
+
851
+ # Returns
852
+
853
+ -
854
+ """
855
+ _query = dict()
856
+ _query["action"] = "get_value"
857
+ _query["parameters"] = dict()
858
+ _query["parameters"]["id"] = id
859
+ return self.__query(_query)
860
+
861
+ def get_value_by_name(self, tag_name:str)->dict:
862
+ r"""Documentation here
863
+
864
+ # Parameters
865
+
866
+ -
867
+
868
+ # Returns
869
+
870
+ -
871
+ """
872
+ _query = dict()
873
+ _query["action"] = "get_value_by_name"
874
+ _query["parameters"] = dict()
875
+ _query["parameters"]["name"] = tag_name
876
+ return self.__query(_query)
877
+
878
+ def get_values_by_name(self, tag_names:list[str])->str|float|int|bool:
879
+ r"""Documentation here
880
+
881
+ # Parameters
882
+
883
+ -
884
+
885
+ # Returns
886
+
887
+ -
888
+ """
889
+ _query = dict()
890
+ _query["action"] = "get_values_by_name"
891
+ _query["parameters"] = dict()
892
+ _query["parameters"]["names"] = tag_names
893
+ return self.__query(_query)
894
+
895
+ def get_scan_time(self, id:str)->str|float|int|bool:
896
+ r"""Documentation here
897
+
898
+ # Parameters
899
+
900
+ -
901
+
902
+ # Returns
903
+
904
+ -
905
+ """
906
+ _query = dict()
907
+ _query["action"] = "get_scan_time"
908
+ _query["parameters"] = dict()
909
+ _query["parameters"]["id"] = id
910
+ return self.__query(_query)
911
+
912
+ def get_dead_band(self, id:str)->str|float|int|bool:
913
+ r"""Documentation here
914
+
915
+ # Parameters
916
+
917
+ -
918
+
919
+ # Returns
920
+
921
+ -
922
+ """
923
+ _query = dict()
924
+ _query["action"] = "get_dead_band"
925
+ _query["parameters"] = dict()
926
+ _query["parameters"]["id"] = id
927
+ return self.__query(_query)
928
+
929
+ def get_display_unit_by_tag(self, tag:str)->str:
930
+ r"""Documentation here
931
+
932
+ # Parameters
933
+
934
+ -
935
+
936
+ # Returns
937
+
938
+ -
939
+ """
940
+ _query = dict()
941
+ _query["action"] = "get_display_unit_by_tag"
942
+ _query["parameters"] = dict()
943
+ _query["parameters"]["tag"] = tag
944
+ return self.__query(_query)
945
+
946
+ @logging_error_handler
947
+ def set_value(self, id:str, value, timestamp:datetime):
948
+ r"""Documentation here
949
+
950
+ # Parameters
951
+
952
+ -
953
+
954
+ # Returns
955
+
956
+ -
957
+ """
958
+ if not timestamp:
959
+ timestamp = datetime.now()
960
+ _query = dict()
961
+ _query["action"] = "set_value"
962
+ _query["parameters"] = dict()
963
+ _query["parameters"]["id"] = id
964
+ _query["parameters"]["value"] = value
965
+ _query["parameters"]["timestamp"] = timestamp
966
+ return self.__query(_query)
967
+
968
+ def set_data_type(self, data_type):
969
+ r"""Documentation here
970
+
971
+ # Parameters
972
+
973
+ -
974
+
975
+ # Returns
976
+
977
+ -
978
+ """
979
+ _query = dict()
980
+ _query["action"] = "set_data_type"
981
+ _query["parameters"] = dict()
982
+ _query["parameters"]["data_type"] = data_type
983
+ return self.__query(_query)
984
+
985
+ def is_tag_defined(self, name:str)->bool:
986
+ r"""Documentation here
987
+
988
+ # Parameters
989
+
990
+ -
991
+
992
+ # Returns
993
+
994
+ -
995
+ """
996
+ _query = dict()
997
+ _query["action"] = "is_tag_defined"
998
+ _query["parameters"] = dict()
999
+ _query["parameters"]["name"] = name
1000
+ return self.__query(_query)
1001
+
1002
+ def attach(self, name:str, observer):
1003
+ """
1004
+ Attaches an observer object to a Tag, observer gets notified when the Tag value changes.
1005
+
1006
+ **Parameters:**
1007
+
1008
+ * **name** (str): Tag name.
1009
+ * **observer** (str): TagObserver instance.
1010
+ """
1011
+ _query = dict()
1012
+ _query["action"] = "attach_observer"
1013
+ _query["parameters"] = dict()
1014
+ _query["parameters"]["name"] = name
1015
+ _query["parameters"]["observer"] = observer
1016
+ return self.__query(_query)
1017
+
1018
+ def detach(self, name:str, observer):
1019
+ """
1020
+ Detaches an observer object from a Tag, observer no longer gets notified when the Tag value changes.
1021
+
1022
+ **Parameters:**
1023
+
1024
+ * **name** (str): Tag name.
1025
+ * **observer** (str): TagObserver instance.
1026
+ """
1027
+
1028
+ _query = dict()
1029
+ _query["action"] = "detach_observer"
1030
+
1031
+ _query["parameters"] = dict()
1032
+ _query["parameters"]["name"] = name
1033
+ _query["parameters"]["observer"] = observer
1034
+
1035
+ self.request(_query)
1036
+ result = self.response()
1037
+
1038
+ if result["result"]:
1039
+ return result["response"]
1040
+
1041
+ def serialize(self, id:str)->dict:
1042
+ r"""Documentation here
1043
+
1044
+ # Parameters
1045
+
1046
+ -
1047
+
1048
+ # Returns
1049
+
1050
+ -
1051
+ """
1052
+ _query = dict()
1053
+ _query["action"] = "serialize"
1054
+ _query["parameters"] = dict()
1055
+ _query["parameters"]["id"] = id
1056
+ return self.__query(_query)
1057
+
1058
+ def serialize_by_tag_name(self, name:str)->dict|None:
1059
+ r"""Documentation here
1060
+
1061
+ # Parameters
1062
+
1063
+ -
1064
+
1065
+ # Returns
1066
+
1067
+ -
1068
+ """
1069
+ _query = dict()
1070
+ _query["action"] = "serialize_by_tag_name"
1071
+ _query["parameters"] = dict()
1072
+ _query["parameters"]["name"] = name
1073
+ return self.__query(_query)
1074
+
1075
+ def __query(self, query:dict)->dict:
1076
+
1077
+ self.request(query)
1078
+ result = self.response()
1079
+ if result["result"]:
1080
+ return result["response"]
1081
+
1082
+ def request(self, query:dict):
1083
+ r"""
1084
+ It does the request to the tags repository according query's structure, in a thread-safe mechanism
1085
+
1086
+ **Parameters**
1087
+
1088
+ * **query** (dict): Query to tags repository
1089
+
1090
+ ## Query Structure
1091
+
1092
+ ```python
1093
+ query = {
1094
+ "action": (str)
1095
+ "parameters": (dict)
1096
+ }
1097
+ ```
1098
+ ## Valid actions in query
1099
+
1100
+ * set_tag
1101
+ * get_tags
1102
+ * get_value
1103
+ * get_data_type
1104
+ * get_unit
1105
+ * get_description
1106
+ * get_display_name
1107
+ * get_min_value
1108
+ * get_max_value
1109
+ * get_attributes
1110
+ * set_value
1111
+ * attach
1112
+ * detach
1113
+
1114
+ ## Parameters strcuture in query
1115
+
1116
+ ```python
1117
+ parameters = {
1118
+ "name": (str) tag name to do request
1119
+ "unit": (str)[Optional] Unit to get value
1120
+ "value": (float)[Optional] If you use *set_value* function, you must pass this parameter
1121
+ "observer": (TagObserver)[Optional] If you use *attach* and *detach* function, you must pass this parameter
1122
+ }
1123
+ ```
1124
+
1125
+ """
1126
+ self._request_lock.acquire()
1127
+ action = query["action"]
1128
+ error_msg = f"Error in CVTEngine with action: {action}"
1129
+
1130
+ try:
1131
+
1132
+ if hasattr(self._cvt, action):
1133
+
1134
+ method = getattr(self._cvt, action)
1135
+
1136
+ if 'parameters' in query:
1137
+
1138
+ resp = method(**query["parameters"])
1139
+
1140
+ else:
1141
+
1142
+ resp = method()
1143
+
1144
+ self.__true_response(resp)
1145
+
1146
+ except Exception as e:
1147
+
1148
+ self.__log_error(e, error_msg)
1149
+
1150
+ self._response_lock.release()
1151
+
1152
+ def __log_error(self, e:Exception, msg:str):
1153
+ r"""
1154
+ Documentation here
1155
+ """
1156
+ logging.error(f"{e} Message: {msg}")
1157
+ self._response = {
1158
+ "result": False,
1159
+ "response": None
1160
+ }
1161
+
1162
+ def __true_response(self, resp):
1163
+ r"""
1164
+ Documentation here
1165
+ """
1166
+ self._response = {
1167
+ "result": True,
1168
+ "response": resp
1169
+ }
1170
+
1171
+ def response(self)->dict:
1172
+ r"""
1173
+ Handles the python GIL to emit the request's response in a thread-safe mechanism.
1174
+ """
1175
+ self._response_lock.acquire()
1176
+
1177
+ result = self._response
1178
+
1179
+ self._request_lock.release()
1180
+
1181
+ return result
1182
+
1183
+ def __getstate__(self):
1184
+
1185
+ self._response_lock.release()
1186
+ state = self.__dict__.copy()
1187
+ del state['_request_lock']
1188
+ del state['_response_lock']
1189
+ return state
1190
+
1191
+ def __setstate__(self, state):
1192
+
1193
+ self.__dict__.update(state)
1194
+ self._request_lock = threading.Lock()
1195
+ self._response_lock = threading.Lock()
1196
+ self._response_lock.acquire()
1197
+
1198
+