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/core.py ADDED
@@ -0,0 +1,1775 @@
1
+ import sys, logging, json, os, jwt, requests, urllib3, secrets
2
+ from logging.handlers import RotatingFileHandler
3
+ from math import ceil
4
+ from datetime import datetime, timezone
5
+ # DRIVERS IMPORTATION
6
+ from peewee import SqliteDatabase, MySQLDatabase, PostgresqlDatabase
7
+ # from peewee_migrations import Router
8
+ from .dbmodels.users import Roles, Users
9
+ from .dbmodels.machines import Machines
10
+ # PYAUTOMATION MODULES IMPORTATION
11
+ from .singleton import Singleton
12
+ from .workers import LoggerWorker
13
+ from .managers import DBManager, OPCUAClientManager, AlarmManager
14
+ from .opcua.models import Client
15
+ from .tags import CVTEngine, Tag
16
+ from .logger.datalogger import DataLoggerEngine
17
+ from .logger.events import EventsLoggerEngine
18
+ from .logger.alarms import AlarmsLoggerEngine
19
+ from .logger.logs import LogsLoggerEngine
20
+ from .logger.machines import MachinesLoggerEngine
21
+ from .logger.opcua_server import OPCUAServerLoggerEngine
22
+ from .alarms import Alarm
23
+ from .state_machine import Machine, DAQ, AutomationStateMachine, StateMachine
24
+ from .opcua.subscription import DAS
25
+ from .buffer import Buffer
26
+ from .models import StringType, FloatType
27
+ from .modules.users.users import users, User
28
+ from .modules.users.roles import roles, Role
29
+ from .dbmodels.core import BaseModel
30
+ from .utils.decorators import validate_types, logging_error_handler
31
+ from flask_socketio import SocketIO
32
+ from geventwebsocket.handler import WebSocketHandler
33
+ from .variables import VARIABLES
34
+ # DASH APP CONFIGURATION PAGES IMPORTATION
35
+ from .pages.main import ConfigView
36
+ from .pages.callbacks import init_callbacks
37
+ import dash_bootstrap_components as dbc
38
+
39
+
40
+ class PyAutomation(Singleton):
41
+ r"""
42
+ Automation is a `singleton <https://en.wikipedia.org/wiki/Singleton_pattern>`_ class designed to develop multi-threaded web applications for Industrial Applications.
43
+
44
+ You can initialize and run PyAutomation Framework in different ways depending on your requirements.
45
+
46
+ **Example 1**: Using only PyAutomation Framework
47
+
48
+ ```python
49
+ from automation import PyAutomation, server
50
+ app = PyAutomation()
51
+ app.define_dash_app(server=server) # This is the configuration page
52
+ app.run(debug=True, create_tables=True)
53
+
54
+ ```
55
+
56
+ **Example 2**: Extending PyAutomation Framework with Flask Application
57
+
58
+ ```python
59
+ from automation import PyAutomation
60
+ from app import CreateApp
61
+ application = CreateApp()
62
+ server = application() # Flask App
63
+ app = PyAutomation()
64
+ app.define_dash_app(server=server) # This is the configuration page
65
+ app.run(create_tables=True)
66
+
67
+ ```
68
+
69
+ """
70
+
71
+ PORTS = 65535
72
+ def __init__(self):
73
+
74
+ self.machine = Machine()
75
+ self.machine_manager = self.machine.get_state_machine_manager()
76
+ self.is_starting = True
77
+ self.cvt = CVTEngine()
78
+ self.logger_engine = DataLoggerEngine()
79
+ self.events_engine = EventsLoggerEngine()
80
+ self.alarms_engine = AlarmsLoggerEngine()
81
+ self.logs_engine = LogsLoggerEngine()
82
+ self.machines_engine = MachinesLoggerEngine()
83
+ self.opcua_server_engine = OPCUAServerLoggerEngine()
84
+ self.db_manager = DBManager()
85
+ self.opcua_client_manager = OPCUAClientManager()
86
+ self.alarm_manager = AlarmManager()
87
+ self.workers = list()
88
+ self.das = DAS()
89
+ self.sio = None
90
+ folder_path = os.path.join(".", "logs")
91
+
92
+ if not os.path.exists(folder_path):
93
+
94
+ os.makedirs(folder_path)
95
+
96
+ folder_db = os.path.join(".", "db")
97
+
98
+ if not os.path.exists(folder_db):
99
+
100
+ os.makedirs(folder_db)
101
+
102
+ folder_db_backups = os.path.join(".", "db", "backups")
103
+
104
+ if not os.path.exists(folder_db_backups):
105
+
106
+ os.makedirs(folder_db_backups)
107
+
108
+ folder_ssl = os.path.join(".", "ssl")
109
+
110
+ if not os.path.exists(folder_ssl):
111
+
112
+ os.makedirs(folder_ssl)
113
+
114
+ self.set_log(file=os.path.join(folder_path, "app.log") ,level=logging.WARNING)
115
+ self.__log_histories = False
116
+
117
+ @logging_error_handler
118
+ def define_dash_app(self, certfile:str=None, keyfile:str=None, **kwargs)->None:
119
+ r"""
120
+ Documentation here
121
+ """
122
+ self.dash_app = ConfigView(use_pages=True, external_stylesheets=[dbc.themes.BOOTSTRAP], prevent_initial_callbacks=True, pages_folder=".", **kwargs)
123
+ self.dash_app.set_automation_app(self)
124
+ init_callbacks(app=self.dash_app)
125
+ if certfile and keyfile:
126
+
127
+ self.sio = SocketIO(
128
+ self.dash_app.server,
129
+ cors_allowed_origins='*',
130
+ ping_timeout=10,
131
+ ping_interval=10,
132
+ async_mode='gevent',
133
+ ssl_context=(certfile, keyfile),
134
+ handler_class=WebSocketHandler
135
+ )
136
+
137
+ else:
138
+ self.sio = SocketIO(self.dash_app.server, cors_allowed_origins='*', ping_timeout=10, ping_interval=10, async_mode='gevent', handler_class=WebSocketHandler)
139
+
140
+ self.cvt._cvt.set_socketio(sio=self.sio)
141
+
142
+ @self.sio.on('connect')
143
+ def handle_connect(auth=None):
144
+
145
+ payload= {
146
+ "tags": self.get_tags() or list(),
147
+ "alarms": self.serialize_alarms() or list(),
148
+ "machines": self.serialize_machines() or list(),
149
+ "last_alarms": self.get_lasts_alarms(lasts=10) or list(),
150
+ "last_active_alarms": self.get_lasts_active_alarms(lasts=3) or list(),
151
+ "last_events": self.get_lasts_events(lasts=10) or list(),
152
+ "last_logs": self.get_lasts_logs(lasts=10) or list()
153
+ }
154
+ self.sio.emit("on_connection", data=payload)
155
+
156
+ @logging_error_handler
157
+ @validate_types(name=StringType, output=StateMachine|None)
158
+ def get_machine(self, name:StringType)->StateMachine:
159
+ r"""
160
+ Documentation here
161
+ """
162
+ return self.machine_manager.get_machine(name=name)
163
+
164
+ @logging_error_handler
165
+ def get_machines(self)->list[tuple[Machine, int, str]]:
166
+ r"""
167
+ Documentation here
168
+ """
169
+ return self.machine_manager.get_machines()
170
+
171
+ @logging_error_handler
172
+ @validate_types(output=list)
173
+ def serialize_machines(self)->list[dict]:
174
+ r"""
175
+ Documentation here
176
+ """
177
+ return self.machine_manager.serialize_machines()
178
+
179
+ @logging_error_handler
180
+ @validate_types(machine=AutomationStateMachine, tag=Tag, output=dict)
181
+ def subscribe_tag_into_automation_machine(self, machine:AutomationStateMachine, tag:Tag)->dict:
182
+ r"""
183
+ Documentation here
184
+ """
185
+ machine.subscribe_to(tag)
186
+
187
+ # TAGS METHODS
188
+ @logging_error_handler
189
+ @validate_types(
190
+ name=str,
191
+ unit=str,
192
+ display_unit=str,
193
+ variable=str,
194
+ data_type=str,
195
+ description=str|type(None),
196
+ display_name=str|type(None),
197
+ opcua_address=str|type(None),
198
+ node_namespace=str|type(None),
199
+ scan_time=int|float|type(None),
200
+ dead_band=int|float|type(None),
201
+ process_filter=bool,
202
+ gaussian_filter=bool,
203
+ gaussian_filter_threshold=float|int,
204
+ gaussian_filter_r_value=float|int,
205
+ outlier_detection=bool,
206
+ out_of_range_detection=bool,
207
+ frozen_data_detection=bool,
208
+ manufacturer=str|type(None),
209
+ segment=str|type(None),
210
+ id=str|type(None),
211
+ user=User|type(None),
212
+ reload=bool,
213
+ output=(Tag|None, str)
214
+ )
215
+ def create_tag(self,
216
+ name:str,
217
+ unit:str,
218
+ variable:str,
219
+ display_unit:str="",
220
+ data_type:str='float',
221
+ description:str=None,
222
+ display_name:str=None,
223
+ opcua_address:str=None,
224
+ node_namespace:str=None,
225
+ scan_time:int=None,
226
+ dead_band:float=None,
227
+ process_filter:bool=False,
228
+ gaussian_filter:bool=False,
229
+ gaussian_filter_threshold:float=1.0,
230
+ gaussian_filter_r_value:float=0.0,
231
+ outlier_detection:bool=False,
232
+ out_of_range_detection:bool=False,
233
+ frozen_data_detection:bool=False,
234
+ segment:str|None="",
235
+ manufacturer:str|None="",
236
+ id:str=None,
237
+ user:User|None=None,
238
+ reload:bool=False,
239
+ )->tuple[Tag,str]:
240
+ r"""
241
+ Create tag to automation app.
242
+
243
+ Addding tag from this way, you get the following features.
244
+
245
+ - Add tag to CVT.
246
+ -
247
+
248
+ ```python
249
+ >>> from automation import PyAutomation
250
+ >>> app = PyAutomation()
251
+ >>> tag_name = "tag1"
252
+ >>> unit = "Pa"
253
+ >>> variable = "Pressure"
254
+ >>> app.create_tag(name=tag_name, unit=unit, variable=variable)
255
+ tag, message
256
+
257
+ ```
258
+
259
+ """
260
+ if not display_name:
261
+
262
+ display_name = name
263
+
264
+ tag, message = self.cvt.set_tag(
265
+ name=name,
266
+ unit=unit,
267
+ display_unit=display_unit,
268
+ variable=variable,
269
+ data_type=data_type,
270
+ description=description,
271
+ display_name=display_name,
272
+ opcua_address=opcua_address,
273
+ node_namespace=node_namespace,
274
+ scan_time=scan_time,
275
+ dead_band=dead_band,
276
+ process_filter=process_filter,
277
+ gaussian_filter=gaussian_filter,
278
+ gaussian_filter_threshold=gaussian_filter_threshold,
279
+ gaussian_filter_r_value=gaussian_filter_r_value,
280
+ outlier_detection=outlier_detection,
281
+ out_of_range_detection=out_of_range_detection,
282
+ frozen_data_detection=frozen_data_detection,
283
+ segment=segment,
284
+ manufacturer=manufacturer,
285
+ id=id,
286
+ user=user
287
+ )
288
+
289
+ # CREATE OPCUA SUBSCRIPTION
290
+ if tag:
291
+
292
+ if self.is_db_connected():
293
+ self.logger_engine.set_tag(tag=tag)
294
+ self.db_manager.attach(tag_name=name)
295
+
296
+ if scan_time:
297
+
298
+ self.das.buffer[name] = {
299
+ "timestamp": Buffer(size=ceil(10 / ceil(scan_time / 1000))),
300
+ "values": Buffer(size=ceil(10 / ceil(scan_time / 1000))),
301
+ "unit": display_unit
302
+ }
303
+
304
+ else:
305
+
306
+ self.das.buffer[name] = {
307
+ "timestamp": Buffer(),
308
+ "values": Buffer(),
309
+ "unit": display_unit
310
+ }
311
+
312
+ self.subscribe_opcua(tag=self.cvt.get_tag_by_name(name=name), opcua_address=opcua_address, node_namespace=node_namespace, scan_time=scan_time, reload=reload)
313
+
314
+ return tag, message
315
+
316
+ else:
317
+
318
+ return None, message
319
+
320
+ @logging_error_handler
321
+ @validate_types(output=list)
322
+ def get_tags(self)->list:
323
+ r"""Documentation here
324
+
325
+ # Parameters
326
+
327
+ -
328
+
329
+ # Returns
330
+
331
+ -
332
+ """
333
+
334
+ return self.cvt.get_tags()
335
+
336
+ @logging_error_handler
337
+ @validate_types(names=list, output=list)
338
+ def get_tags_by_names(self, names:list)->list[Tag|None]:
339
+ r"""
340
+ # Parameters
341
+
342
+ - names: list of tag names
343
+
344
+ # Returns
345
+
346
+ - list of tags
347
+ """
348
+ return self.cvt.get_tags_by_names(names=names)
349
+
350
+ @logging_error_handler
351
+ @validate_types(name=str, output=Tag|None)
352
+ def get_tag_by_name(self, name:str)->Tag:
353
+
354
+ return self.cvt.get_tag_by_name(name=name)
355
+
356
+ @logging_error_handler
357
+ @validate_types(namespace=str, output=Tag|None)
358
+ def get_tag_by_node_namespace(self, namespace:str)->Tag:
359
+
360
+ return self.cvt.get_tag_by_node_namespace(node_namespace=namespace)
361
+
362
+ @logging_error_handler
363
+ def get_trends(self, start:str, stop:str, timezone:str, *tags):
364
+ r"""
365
+ Documentation here
366
+ """
367
+ return self.logger_engine.read_trends(start, stop, timezone, *tags)
368
+
369
+ @logging_error_handler
370
+ def get_tags_tables(self, start:str, stop:str, timezone:str, tags:list, page:int=1, limit:int=20):
371
+ r"""
372
+ Documentation here
373
+ """
374
+ return self.logger_engine.read_table(start, stop, timezone, tags, page, limit)
375
+
376
+ @logging_error_handler
377
+ def get_segments(self):
378
+ r"""
379
+ Documentation here
380
+ """
381
+ return self.logger_engine.read_segments()
382
+
383
+ @logging_error_handler
384
+ @validate_types(id=str, output=None|str)
385
+ def delete_tag(self, id:str, user:User|None=None)->None|str:
386
+ r"""
387
+ Documentation here
388
+ """
389
+ tag = self.cvt.get_tag(id=id)
390
+ tag_name = tag.get_name()
391
+ alarm = self.alarm_manager.get_alarm_by_tag(tag=tag_name)
392
+ if alarm:
393
+
394
+ return f"Tag {tag_name} has an alarm associated"
395
+
396
+ self.unsubscribe_opcua(tag=tag)
397
+ self.cvt.delete_tag(id=id, user=user)
398
+ self.das.buffer.pop(tag_name)
399
+ # Persist Tag on Database
400
+ if self.is_db_connected():
401
+
402
+ self.logger_engine.delete_tag(id=id)
403
+
404
+ @logging_error_handler
405
+ def update_tag(
406
+ self,
407
+ id:str,
408
+ user:User|None=None,
409
+ **kwargs
410
+ )->tuple[Tag|None, str]:
411
+ r"""
412
+ Documentation here
413
+ """
414
+ tag = self.cvt.get_tag(id=id)
415
+ if "name" in kwargs:
416
+ tag_name = tag.get_name()
417
+ machines_with_tags_subscribed = list()
418
+ for _machine, _, _ in self.get_machines():
419
+
420
+ if tag_name in _machine.get_subscribed_tags():
421
+
422
+ machines_with_tags_subscribed.append(_machine.name.value)
423
+
424
+ if machines_with_tags_subscribed:
425
+
426
+ return None, f"{tag_name} is subscribed into {machines_with_tags_subscribed}"
427
+
428
+ keys_to_check = ["gaussian_filter", "threshold", "R-value"]
429
+
430
+ if not any(key in kwargs for key in keys_to_check):
431
+
432
+ self.unsubscribe_opcua(tag)
433
+
434
+ # Persist Tag on Database
435
+ if "variable" in kwargs:
436
+
437
+ kwargs["unit"] = list(VARIABLES[kwargs["variable"]].values())[0]
438
+ kwargs["display_unit"] = list(VARIABLES[kwargs["variable"]].values())[0]
439
+
440
+ if "R-value" in kwargs:
441
+
442
+ try:
443
+ r_value = float(kwargs.pop("R-value"))
444
+ if r_value < 0.0 or r_value > 100.0:
445
+
446
+ r_value = tag.gaussian_filter_r_value * 100.0
447
+
448
+ except Exception as err:
449
+
450
+ r_value = tag.gaussian_filter_r_value
451
+
452
+ kwargs['gaussian_filter_r_value'] = r_value / 100.0
453
+
454
+ if "threshold" in kwargs:
455
+
456
+ try:
457
+
458
+ threshold = float(kwargs.pop("threshold"))
459
+ if threshold < 0.0:
460
+
461
+ threshold = tag.gaussian_filter_threshold
462
+
463
+ except Exception as err:
464
+
465
+ threshold = tag.gaussian_filter_threshold
466
+
467
+ kwargs['gaussian_filter_threshold'] = threshold
468
+
469
+
470
+ result = self.cvt.update_tag(
471
+ id=id,
472
+ user=user,
473
+ **kwargs
474
+ )
475
+ if self.is_db_connected():
476
+
477
+ if 'variable' in kwargs:
478
+
479
+ kwargs.pop("variable")
480
+
481
+ if kwargs:
482
+
483
+ self.logger_engine.update_tag(
484
+ id=id,
485
+ **kwargs
486
+ )
487
+
488
+ if "name" in kwargs:
489
+
490
+ self.das.buffer.pop(tag_name)
491
+
492
+ keys_to_check = ["gaussian_filter", "gaussian_filter_threshold", "gaussian_filter_r_value"]
493
+
494
+ if kwargs:
495
+
496
+ if not any(key in kwargs for key in keys_to_check):
497
+
498
+ self.__update_buffer(tag=tag)
499
+
500
+ if "scan_time" in kwargs:
501
+ scan_time = kwargs["scan_time"]
502
+ if isinstance(scan_time, int):
503
+ self.subscribe_opcua(tag, opcua_address=tag.get_opcua_address(), node_namespace=tag.get_node_namespace(), scan_time=scan_time)
504
+ else:
505
+ self.subscribe_opcua(tag, opcua_address=tag.get_opcua_address(), node_namespace=tag.get_node_namespace(), scan_time=tag.get_scan_time())
506
+ else:
507
+
508
+ self.subscribe_opcua(tag, opcua_address=tag.get_opcua_address(), node_namespace=tag.get_node_namespace(), scan_time=tag.get_scan_time())
509
+
510
+ return result
511
+
512
+ @logging_error_handler
513
+ @validate_types(name=str, output=None|str)
514
+ def delete_tag_by_name(self, name:str, user:User|None=None):
515
+ r"""
516
+ Documentation here
517
+ """
518
+ tag = self.cvt.get_tag_by_name(name=name)
519
+ alarm = self.alarm_manager.get_alarm_by_tag(tag=name)
520
+ if alarm:
521
+
522
+ return f"Tag {name} has an alarm associated"
523
+
524
+ self.unsubscribe_opcua(tag=tag)
525
+ # Persist Tag on Database
526
+ if self.is_db_connected():
527
+
528
+ self.logger_engine.delete_tag(id=tag.id)
529
+
530
+ self.cvt.delete_tag(id=tag.id, user=user)
531
+
532
+ # USERS METHODS
533
+ @logging_error_handler
534
+ @validate_types(
535
+ username=str|type(None),
536
+ email=str|type(None),
537
+ password=str,
538
+ name=str|type(None),
539
+ output=tuple
540
+ )
541
+ def login(
542
+ self,
543
+ password:str,
544
+ username:str="",
545
+ email:str=""
546
+ )->tuple[User|None, str]:
547
+ # Check Token on Database
548
+ if self.is_db_connected():
549
+
550
+ return self.db_manager.login(password=password, username=username, email=email)
551
+
552
+ return users.login(password=password, username=username, email=email)
553
+
554
+ @logging_error_handler
555
+ @validate_types(
556
+ username=str,
557
+ role_name=str,
558
+ email=str,
559
+ password=str,
560
+ name=str|type(None),
561
+ lastname=str|type(None),
562
+ output=(User|None, str)
563
+ )
564
+ def signup(
565
+ self,
566
+ username:str,
567
+ role_name:str,
568
+ email:str,
569
+ password:str,
570
+ name:str=None,
571
+ lastname:str=None
572
+ )->tuple[User|None, str]:
573
+ r"""
574
+ Documentation here
575
+ """
576
+ user, message = users.signup(
577
+ username=username,
578
+ role_name=role_name,
579
+ email=email,
580
+ password=password,
581
+ name=name,
582
+ lastname=lastname
583
+ )
584
+ if user:
585
+
586
+ # Persist Tag on Database
587
+ if self.is_db_connected():
588
+
589
+ _, message = self.db_manager.set_user(user=user)
590
+
591
+ return user, message
592
+
593
+ return None, message
594
+
595
+ @logging_error_handler
596
+ @validate_types(role_name=str, output=str)
597
+ def create_token(self, role_name:str)->str:
598
+ r"""
599
+ Documentation here
600
+ """
601
+ from . import server
602
+ payload = {
603
+ "created_on": datetime.now(timezone.utc).strftime(self.cvt.DATETIME_FORMAT),
604
+ "role": role_name
605
+ }
606
+ return jwt.encode(payload, server.config['TPT_TOKEN'], algorithm="HS256")
607
+
608
+ @logging_error_handler
609
+ @validate_types(name=str, level=int, output=(Role|None, str))
610
+ def set_role(self, name:str, level:int)->Role|None:
611
+ r"""
612
+ Documentation here
613
+ """
614
+ role = Role(name=name, level=level)
615
+ if roles.check_role_name(name=name):
616
+
617
+ return None, f"Role {name} exists"
618
+
619
+ role_id, message = roles.add(role=role)
620
+ if role_id:
621
+
622
+ # Persist Tag on Database
623
+ if self.is_db_connected():
624
+
625
+ _, message = self.db_manager.set_role(name=name, level=level, identifier=role.identifier)
626
+
627
+ return role, message
628
+
629
+ return None, message
630
+
631
+ # OPCUA METHODS
632
+ @logging_error_handler
633
+ @validate_types(host=str|type(None), port=int|type(None), output=dict)
634
+ def find_opcua_servers(self, host:str='127.0.0.1', port:int=4840)->dict:
635
+ r"""
636
+ Documentation here
637
+ """
638
+ result = {
639
+ "message": f"Connection refused to opc.tcp://{host}:{port}"
640
+ }
641
+ try:
642
+
643
+ server = self.opcua_client_manager.discovery(host=host, port=port)
644
+ result["message"] = f"Successfully connection to {server[0]['DiscoveryUrls'][0]}"
645
+ result["data"] = server
646
+
647
+ except Exception as err:
648
+
649
+ result["data"] = list()
650
+
651
+ return result
652
+
653
+ @logging_error_handler
654
+ @validate_types(output=dict)
655
+ def get_opcua_clients(self):
656
+ r"""
657
+ Documentation here
658
+ """
659
+ return self.opcua_client_manager.serialize()
660
+
661
+ @logging_error_handler
662
+ @validate_types(client_name=str, output=Client)
663
+ def get_opcua_client(self, client_name:str):
664
+ r"""
665
+ Documentation here
666
+ """
667
+ return self.opcua_client_manager.get(client_name=client_name)
668
+
669
+ @logging_error_handler
670
+ @validate_types(opcua_address=str, output=Client|None)
671
+ def get_opcua_client_by_address(self, opcua_address:str)->Client|None:
672
+ r"""
673
+ Obtiene el cliente OPC UA correspondiente a una dirección
674
+
675
+ Args:
676
+ opcua_address: Dirección del servidor OPC UA (ej: "opc.tcp://localhost:4840")
677
+
678
+ Returns:
679
+ Client: Cliente OPC UA si existe y está conectado, None en caso contrario
680
+ """
681
+ return self.opcua_client_manager.get_client_by_address(opcua_address=opcua_address)
682
+
683
+ @logging_error_handler
684
+ @validate_types(opcua_address=str, node_namespace=str, value=float|int|bool|str, output=tuple)
685
+ def write_opcua_value(self, opcua_address:str, node_namespace:str, value:float|int|bool|str)->tuple[dict, int]:
686
+ r"""
687
+ Escribe un valor a un nodo OPC UA
688
+
689
+ Args:
690
+ opcua_address: Dirección del servidor OPC UA
691
+ node_namespace: Namespace del nodo (ej: "ns=2;i=1234")
692
+ value: Valor a escribir (float, int, bool, str)
693
+
694
+ Returns:
695
+ tuple: (dict con resultado, status_code)
696
+ """
697
+ opcua_client = self.get_opcua_client_by_address(opcua_address=opcua_address)
698
+
699
+ if not opcua_client:
700
+ return {
701
+ 'message': f'Cliente OPC UA no encontrado o no conectado para {opcua_address}',
702
+ 'opcua_address': opcua_address,
703
+ 'node_namespace': node_namespace,
704
+ 'success': False
705
+ }, 404
706
+
707
+ return opcua_client.write_value(node_namespace=node_namespace, value=value)
708
+
709
+ @logging_error_handler
710
+ def create_opcua_server_record(self, name:str, namespace:str, access_type:str="Read"):
711
+ r"""
712
+ Documentation here
713
+ """
714
+ return self.opcua_server_engine.create(name=name, namespace=namespace, access_type=access_type)
715
+
716
+ @logging_error_handler
717
+ def update_opcua_server_access_type(self, namespace:str, access_type:str):
718
+ r"""
719
+ Documentation here
720
+ """
721
+ return self.opcua_server_engine.put(namespace=namespace, access_type=access_type)
722
+
723
+ @logging_error_handler
724
+ def get_opcua_server_record_by_namespace(self, namespace:str):
725
+ r"""
726
+ Documentation here
727
+ """
728
+ return self.opcua_server_engine.read_by_namespace(namespace=namespace)
729
+
730
+ @logging_error_handler
731
+ @validate_types(client_name=str, namespaces=list, output=list)
732
+ def get_node_values(self, client_name:str, namespaces:list)->list:
733
+ r"""
734
+ Documentation here
735
+ """
736
+
737
+ return self.opcua_client_manager.get_node_values(client_name=client_name, namespaces=namespaces)
738
+
739
+ @logging_error_handler
740
+ @validate_types(client_name=str, namespaces=list, output=list|None)
741
+ def get_node_attributes(self, client_name:str, namespaces:list)->list[dict]:
742
+ r"""
743
+ Documentation here
744
+ """
745
+
746
+ return self.opcua_client_manager.get_node_attributes(client_name=client_name, namespaces=namespaces)
747
+
748
+ @logging_error_handler
749
+ def get_opcua_tree(self, client_name:str):
750
+ r"""
751
+ Documentation here
752
+ """
753
+ return self.opcua_client_manager.get_opcua_tree(client_name=client_name)
754
+
755
+ @logging_error_handler
756
+ @validate_types(client_name=str, host=str|type(None), port=int|type(None), output=(bool, str|dict))
757
+ def add_opcua_client(self, client_name:str, host:str="127.0.0.1", port:int=4840):
758
+ r"""
759
+ Documentation here
760
+ """
761
+ servers = self.find_opcua_servers(host=host, port=port)
762
+
763
+ if servers:
764
+
765
+ return self.opcua_client_manager.add(client_name=client_name, host=host, port=port)
766
+
767
+ @logging_error_handler
768
+ @validate_types(client_name=str, host=str|type(None), port=int|type(None), output=bool)
769
+ def remove_opcua_client(self, client_name:str):
770
+ r"""
771
+ Documentation here
772
+ """
773
+ return self.opcua_client_manager.remove(client_name=client_name)
774
+
775
+ @logging_error_handler
776
+ @validate_types(tag=Tag, opcua_address=str|type(None), node_namespace=str|type(None), scan_time=float|int|type(None), reload=bool, output=None)
777
+ def subscribe_opcua(self, tag:Tag, opcua_address:str, node_namespace:str, scan_time:float, reload:bool=False):
778
+ r"""
779
+ Documentation here
780
+ """
781
+ if opcua_address and node_namespace:
782
+
783
+ if not scan_time or scan_time<=100: # SUBSCRIBE BY DAS
784
+
785
+ for client_name, info in self.get_opcua_clients().items():
786
+
787
+ if opcua_address==info["server_url"]:
788
+
789
+ opcua_client = self.get_opcua_client(client_name=client_name)
790
+ subscription = opcua_client.create_subscription(1000, self.das)
791
+ node_id = opcua_client.get_node_id_by_namespace(node_namespace)
792
+ self.das.subscribe(subscription=subscription, client_name=client_name, node_id=node_id)
793
+ break
794
+
795
+ else: # SUBSCRIBE BY DAQ
796
+
797
+ self.subscribe_tag(tag_name=tag.get_name(), scan_time=scan_time, reload=reload)
798
+
799
+ self.das.buffer[tag.get_name()].update({
800
+ "unit": tag.get_display_unit()
801
+ })
802
+
803
+ @logging_error_handler
804
+ @validate_types(tag_name=str, scan_time=float|int, reload=bool, output=None)
805
+ def subscribe_tag(self, tag_name:str, scan_time:float|int, reload:bool=False):
806
+ r"""
807
+ Documentatio here
808
+ """
809
+ scan_time = float(scan_time)
810
+ daq_name = StringType(f"DAQ-{int(scan_time)}")
811
+ daq = self.machine_manager.get_machine(name=daq_name)
812
+ tag = self.cvt.get_tag_by_name(name=tag_name)
813
+ if not daq:
814
+
815
+ daq = DAQ(name=daq_name)
816
+ interval = FloatType(scan_time / 1000)
817
+ daq.set_opcua_client_manager(manager=self.opcua_client_manager)
818
+ self.machine.append_machine(machine=daq, interval=interval, mode="async")
819
+
820
+ if not reload:
821
+
822
+ if self.machine.state_worker:
823
+ self.machine.join(machine=daq)
824
+ else:
825
+ self.machine.start()
826
+
827
+ daq.subscribe_to(tag=tag)
828
+
829
+ @logging_error_handler
830
+ @validate_types(tag=Tag, output=None)
831
+ def unsubscribe_opcua(self, tag:Tag):
832
+ r"""
833
+ Documentation here
834
+ """
835
+
836
+ if tag.get_node_namespace():
837
+
838
+ for client_name, info in self.get_opcua_clients().items():
839
+
840
+ if tag.get_opcua_address()==info["server_url"]:
841
+
842
+ opcua_client = self.get_opcua_client(client_name=client_name)
843
+ node_id = opcua_client.get_node_id_by_namespace(tag.get_node_namespace())
844
+ self.das.unsubscribe(client_name=client_name, node_id=node_id)
845
+ break
846
+
847
+ drop_machine_from_worker, _, _ = self.machine_manager.unsubscribe_tag(tag=tag)
848
+ if drop_machine_from_worker:
849
+
850
+ self.machine.drop(machine=drop_machine_from_worker)
851
+
852
+ # CLEAR BUFFER
853
+ scan_time = tag.get_scan_time()
854
+ if scan_time:
855
+
856
+ self.das.buffer[tag.get_name()].update({
857
+ "timestamp": Buffer(size=ceil(10 / ceil(scan_time / 1000))),
858
+ "values": Buffer(size=ceil(10 / ceil(scan_time / 1000)))
859
+ })
860
+ else:
861
+ self.das.buffer[tag.get_name()].update({
862
+ "timestamp": Buffer(),
863
+ "values": Buffer()
864
+ })
865
+
866
+ @logging_error_handler
867
+ def __update_buffer(self, tag:Tag):
868
+ r"""
869
+ Documentation here
870
+ """
871
+ tag_name = tag.get_name()
872
+ scan_time = tag.get_scan_time()
873
+ unit = tag.get_display_unit()
874
+
875
+ if scan_time:
876
+
877
+ self.das.buffer[tag_name] = {
878
+ "timestamp": Buffer(size=ceil(10 / ceil(scan_time / 1000))),
879
+ "values": Buffer(size=ceil(10 / ceil(scan_time / 1000))),
880
+ "unit": unit
881
+ }
882
+
883
+ else:
884
+
885
+ self.das.buffer[tag_name] = {
886
+ "timestamp": Buffer(),
887
+ "values": Buffer(),
888
+ "unit": unit
889
+ }
890
+
891
+ # ERROR LOGS
892
+ @logging_error_handler
893
+ @validate_types(level=int, file=str, output=None)
894
+ def set_log(self, level:int=logging.INFO, file:str="logs/app.log"):
895
+ r"""
896
+ Sets the log file and level.
897
+
898
+ **Parameters:**
899
+
900
+ * **level** (str): `logging.LEVEL` (default: logging.INFO).
901
+ * **file** (str): log filename (default: 'app.log').
902
+
903
+ **Returns:** `None`
904
+
905
+ Usage:
906
+
907
+ ```python
908
+ >>> app.set_log(file="app.log")
909
+ ```
910
+ """
911
+
912
+ self._logging_level = level
913
+ self._log_file = file
914
+
915
+ # DATABASES
916
+ @validate_types(
917
+ dbtype=str,
918
+ drop_table=bool,
919
+ clear_default_tables=bool,
920
+ dbfile=str|type(None),
921
+ user=str|type(None),
922
+ password=str|type(None),
923
+ host=str|type(None),
924
+ port=int|type(None),
925
+ name=str|type(None),
926
+ output=None)
927
+ def set_db(
928
+ self,
929
+ dbtype:str='sqlite',
930
+ drop_table:bool=False,
931
+ clear_default_tables:bool=False,
932
+ **kwargs):
933
+ r"""
934
+ Sets the database, it supports SQLite and Postgres,
935
+ in case of SQLite, the filename must be provided.
936
+
937
+ if app mode is "Development" you must use SQLite Databse
938
+
939
+ **Parameters:**
940
+
941
+ * **dbfile** (str): a path to database file.
942
+ * *drop_table** (bool): If you want to drop table.
943
+ * **cascade** (bool): if there are some table dependency, drop it as well
944
+ * **kwargs**: Same attributes to a postgres connection.
945
+
946
+ **Returns:** `None`
947
+
948
+ Usage:
949
+
950
+ ```python
951
+ >>> app.set_db(dbfile="app.db")
952
+ ```
953
+ """
954
+
955
+ from .dbmodels import proxy
956
+
957
+ if clear_default_tables:
958
+
959
+ self.db_manager.clear_default_tables()
960
+
961
+ if dbtype.lower()=='sqlite':
962
+
963
+ dbfile = kwargs.get("dbfile", ":memory:")
964
+ if not dbfile.endswith(".db"):
965
+ dbfile = f"{dbfile}.db"
966
+
967
+ self._db = SqliteDatabase(os.path.join(".", "db", dbfile), pragmas={
968
+ 'journal_mode': 'wal',
969
+ 'wal_checkpoint': 1,
970
+ 'cache_size': -1024 * 10, # 10MB
971
+ 'foreign_keys': 1,
972
+ 'ignore_check_constraints': 0,
973
+ 'synchronous': 1
974
+ }
975
+ )
976
+
977
+ elif dbtype.lower()=='mysql':
978
+
979
+ db_name = kwargs['name']
980
+ del kwargs['name']
981
+ self._db = MySQLDatabase(db_name, **kwargs)
982
+
983
+ elif dbtype.lower()=='postgresql':
984
+
985
+ db_name = kwargs['name']
986
+ del kwargs['name']
987
+ self._db = PostgresqlDatabase(db_name, **kwargs)
988
+
989
+ proxy.initialize(self._db)
990
+ self._db.connect()
991
+ self.db_manager.set_db(self._db, is_history_logged=self.__log_histories)
992
+ self.db_manager.set_dropped(drop_table)
993
+
994
+ @logging_error_handler
995
+ @validate_types(
996
+ dbtype=str,
997
+ dbfile=str,
998
+ user=str|type(None),
999
+ password=str|type(None),
1000
+ host=str|type(None),
1001
+ port=int|str|type(None),
1002
+ name=str|type(None),
1003
+ output=None)
1004
+ def set_db_config(
1005
+ self,
1006
+ dbtype:str="sqlite",
1007
+ dbfile:str="app.db",
1008
+ user:str|None="admin",
1009
+ password:str|None="admin",
1010
+ host:str|None="127.0.0.1",
1011
+ port:int|str|None=5432,
1012
+ name:str|None="app_db"
1013
+ ):
1014
+ r"""
1015
+ Documentation here
1016
+ """
1017
+ if dbtype.lower()=="sqlite":
1018
+
1019
+ db_config = {
1020
+ "dbtype": dbtype,
1021
+ "dbfile": dbfile
1022
+ }
1023
+
1024
+ else:
1025
+
1026
+ db_config = {
1027
+ "dbtype": dbtype,
1028
+ 'user': user,
1029
+ 'password': password,
1030
+ 'host': host,
1031
+ 'port': port,
1032
+ 'name': name,
1033
+ }
1034
+
1035
+ with open('./db/db_config.json', 'w') as json_file:
1036
+
1037
+ json.dump(db_config, json_file)
1038
+
1039
+ @logging_error_handler
1040
+ @validate_types(output=dict|None)
1041
+ def get_db_config(self):
1042
+ r"""
1043
+ Documentation here
1044
+ """
1045
+ try:
1046
+
1047
+ with open('./db/db_config.json', 'r') as json_file:
1048
+
1049
+ db_config = json.load(json_file)
1050
+
1051
+ return db_config
1052
+
1053
+ except Exception as e:
1054
+ _, _, e_traceback = sys.exc_info()
1055
+ e_filename = os.path.split(e_traceback.tb_frame.f_code.co_filename)[1]
1056
+ e_message = str(e)
1057
+ e_line_number = e_traceback.tb_lineno
1058
+ message = f"Database is not configured: {e_line_number} - {e_filename} - {e_message}"
1059
+ logging.warning(message)
1060
+ return None
1061
+
1062
+ @logging_error_handler
1063
+ @validate_types(output=bool)
1064
+ def is_db_connected(self):
1065
+ r"""
1066
+ Documentation here
1067
+ """
1068
+ if self.db_manager.get_db():
1069
+
1070
+ return True
1071
+
1072
+ return False
1073
+
1074
+ @validate_types(test=bool|type(None), reload=bool|type(None), output=None|bool)
1075
+ def connect_to_db(self, test:bool=False, reload:bool=False):
1076
+ r"""
1077
+ Documentation here
1078
+ """
1079
+ try:
1080
+ db_config = self.get_db_config()
1081
+
1082
+ if test:
1083
+
1084
+ db_config = {"dbtype": "sqlite", "dbfile": "test.db"}
1085
+
1086
+ if db_config:
1087
+
1088
+ dbtype = db_config.pop("dbtype")
1089
+ self.__log_histories = True
1090
+ self.set_db(dbtype=dbtype, **db_config)
1091
+ self.db_manager.init_database()
1092
+ self.load_opcua_clients_from_db()
1093
+ self.load_db_to_cvt()
1094
+ self.load_db_to_alarm_manager()
1095
+ self.load_db_to_roles()
1096
+ self.load_db_to_users()
1097
+ if reload:
1098
+
1099
+ self.load_db_tags_to_machine()
1100
+
1101
+ return True
1102
+
1103
+ except Exception as err:
1104
+ logging.critical(f"CONNECTING DATABASE ERROR: {err}")
1105
+ return False
1106
+
1107
+ @validate_types(test=bool|type(None), reload=bool|type(None), output=None|bool)
1108
+ def reconnect_to_db(self, test:bool=False):
1109
+ r"""
1110
+ Documentation here
1111
+ """
1112
+ try:
1113
+ db_config = self.get_db_config()
1114
+
1115
+ if test:
1116
+
1117
+ db_config = {"dbtype": "sqlite", "dbfile": "test.db"}
1118
+
1119
+ if db_config:
1120
+
1121
+ dbtype = db_config.pop("dbtype")
1122
+ self.__log_histories = True
1123
+ self.set_db(dbtype=dbtype, **db_config)
1124
+ self.db_manager.init_database()
1125
+ self.load_opcua_clients_from_db()
1126
+ self.load_db_to_cvt()
1127
+ self.load_db_to_alarm_manager()
1128
+ self.load_db_to_roles()
1129
+ self.load_db_to_users()
1130
+ self.load_db_tags_to_machine()
1131
+
1132
+ return True
1133
+ else:
1134
+ return False
1135
+
1136
+ except Exception as err:
1137
+ self.db_manager._logger.logger._db = None
1138
+ return False
1139
+
1140
+ @logging_error_handler
1141
+ @validate_types(output=None)
1142
+ def disconnect_to_db(self):
1143
+ r"""
1144
+ Documentation here
1145
+ """
1146
+ self.__log_histories = False
1147
+ self.db_manager._logger.logger.stop_db()
1148
+
1149
+ @logging_error_handler
1150
+ @validate_types(output=None)
1151
+ def load_db_to_cvt(self):
1152
+ r"""
1153
+ Documentation here
1154
+ """
1155
+ if self.is_db_connected():
1156
+
1157
+ tags = self.db_manager.get_tags()
1158
+
1159
+
1160
+ for tag in tags:
1161
+
1162
+ active = tag.pop("active")
1163
+
1164
+ if active:
1165
+
1166
+ self.create_tag(reload=True, **tag)
1167
+
1168
+ @logging_error_handler
1169
+ @validate_types(output=None)
1170
+ def load_db_to_alarm_manager(self):
1171
+ r"""
1172
+ Documentation here
1173
+ """
1174
+ if self.is_db_connected():
1175
+
1176
+ alarms = self.db_manager.get_alarms()
1177
+ if alarms:
1178
+ for alarm in alarms:
1179
+
1180
+ self.create_alarm(reload=True, **alarm)
1181
+
1182
+ @logging_error_handler
1183
+ @validate_types(output=None)
1184
+ def load_db_to_roles(self):
1185
+ r"""
1186
+ Documentation here
1187
+ """
1188
+ if self.is_db_connected():
1189
+
1190
+ Roles.fill_cvt_roles()
1191
+
1192
+ @logging_error_handler
1193
+ @validate_types(output=None)
1194
+ def load_db_to_users(self):
1195
+ r"""
1196
+ Documentation here
1197
+ """
1198
+ if self.is_db_connected():
1199
+
1200
+ Users.fill_cvt_users()
1201
+
1202
+ @logging_error_handler
1203
+ @validate_types(output=None)
1204
+ def load_opcua_clients_from_db(self):
1205
+ r"""
1206
+ Documentation here
1207
+ """
1208
+
1209
+ if self.is_db_connected():
1210
+
1211
+ clients = self.db_manager.get_opcua_clients()
1212
+
1213
+ for client in clients:
1214
+
1215
+ self.add_opcua_client(**client)
1216
+
1217
+ @logging_error_handler
1218
+ def load_db_tags_to_machine(self):
1219
+
1220
+ machines = self.machine_manager.get_machines()
1221
+
1222
+ for machine, _, _ in machines:
1223
+
1224
+ if machine.classification.value.lower()!="data acquisition system":
1225
+
1226
+ machine_name = machine.name.value
1227
+ machine_db = Machines.get_or_none(name=machine_name)
1228
+
1229
+ if not machine_db:
1230
+
1231
+ return f"{machine_name} not found into DB", 404
1232
+
1233
+ machine.identifier.value = machine_db.identifier
1234
+ tags_machine = machine_db.get_tags()
1235
+
1236
+ for tag_machine in tags_machine:
1237
+
1238
+ _tag = tag_machine.serialize()
1239
+ tag_name = _tag["tag"]["name"]
1240
+ tag = self.cvt.get_tag_by_name(name=tag_name)
1241
+ machine.subscribe_to(tag=tag, default_tag_name=_tag["default_tag_name"])
1242
+
1243
+ else:
1244
+
1245
+ machine_name = machine.name.value
1246
+ machine_db = Machines.get_or_none(name=machine_name)
1247
+ machine.identifier.value = machine_db.identifier
1248
+
1249
+ @logging_error_handler
1250
+ def add_db_table(self, table:BaseModel):
1251
+ r"""
1252
+ Documentation here
1253
+ """
1254
+ self.db_manager.register_table(table)
1255
+
1256
+ @logging_error_handler
1257
+ def get_db_table(self, tablename:str):
1258
+ r"""
1259
+ Documentation here
1260
+ """
1261
+ return self.db_manager.get_db_table(tablename=tablename)
1262
+
1263
+ # ALARMS METHODS
1264
+ @logging_error_handler
1265
+ @validate_types(output=AlarmManager)
1266
+ def get_alarm_manager(self)->AlarmManager:
1267
+ r"""
1268
+ Documentation here
1269
+ """
1270
+ return self.alarm_manager
1271
+
1272
+ @logging_error_handler
1273
+ @validate_types(
1274
+ name=str,
1275
+ tag=str,
1276
+ alarm_type=str,
1277
+ trigger_value=bool|float|int,
1278
+ description=str|type(None),
1279
+ identifier=str|type(None),
1280
+ state=str,
1281
+ timestamp=str|type(None),
1282
+ ack_timestamp=str|type(None),
1283
+ user=User|type(None),
1284
+ reload=bool,
1285
+ output=(Alarm, str)
1286
+ )
1287
+ def create_alarm(
1288
+ self,
1289
+ name:str,
1290
+ tag:str,
1291
+ alarm_type:str="BOOL",
1292
+ trigger_value:bool|float|int=True,
1293
+ description:str="",
1294
+ identifier:str=None,
1295
+ state:str="Normal",
1296
+ timestamp:str=None,
1297
+ ack_timestamp:str=None,
1298
+ user:User=None,
1299
+ reload:bool=False
1300
+ )->tuple[Alarm, str]:
1301
+ r"""
1302
+ Append alarm to the Alarm Manager
1303
+
1304
+ **Paramters**
1305
+
1306
+ * **alarm**: (Alarm Object)
1307
+
1308
+ **Returns**
1309
+
1310
+ * **None**
1311
+ """
1312
+ alarm, message = self.alarm_manager.append_alarm(
1313
+ name=name,
1314
+ tag=tag,
1315
+ type=alarm_type,
1316
+ trigger_value=trigger_value,
1317
+ description=description,
1318
+ identifier=identifier,
1319
+ state=state,
1320
+ timestamp=timestamp,
1321
+ ack_timestamp=ack_timestamp,
1322
+ user=user,
1323
+ reload=reload,
1324
+ sio=self.sio
1325
+ )
1326
+
1327
+ if alarm:
1328
+
1329
+ # Persist Tag on Database
1330
+ if not reload:
1331
+ if self.is_db_connected():
1332
+
1333
+ alarm = self.alarm_manager.get_alarm_by_name(name=name)
1334
+
1335
+ self.alarms_engine.create(
1336
+ id=alarm.identifier,
1337
+ name=name,
1338
+ tag=tag,
1339
+ trigger_type=alarm_type,
1340
+ trigger_value=trigger_value,
1341
+ description=description
1342
+ )
1343
+
1344
+ return alarm, message
1345
+
1346
+ return None, message
1347
+
1348
+ @logging_error_handler
1349
+ @validate_types(lasts=int, output=list)
1350
+ def get_lasts_alarms(self, lasts:int=10)->list:
1351
+ r"""
1352
+ Documentation here
1353
+ """
1354
+ if self.is_db_connected():
1355
+
1356
+ return self.alarms_engine.get_lasts(lasts=lasts)
1357
+
1358
+ return list()
1359
+
1360
+ @logging_error_handler
1361
+ def filter_alarms_by(self, **fields):
1362
+ r"""
1363
+ Documentation here
1364
+ """
1365
+ if self.is_db_connected():
1366
+
1367
+ return self.alarms_engine.filter_alarm_summary_by(**fields)
1368
+
1369
+ @logging_error_handler
1370
+ @validate_types(id=str, name=str|None, description=str|None, alarm_type=str|None, trigger_value=int|float|None, output=None)
1371
+ def update_alarm(
1372
+ self,
1373
+ id:str,
1374
+ name:str=None,
1375
+ tag:str=None,
1376
+ description:str=None,
1377
+ alarm_type:str=None,
1378
+ trigger_value:int|float=None)->None:
1379
+ r"""
1380
+ Updates alarm attributes
1381
+
1382
+ **Parameters**
1383
+
1384
+ * **id** (int).
1385
+ * **name** (str)[Optional]:
1386
+ * **tag** (str)[Optional]:
1387
+ * **description** (str)[Optional]:
1388
+ * **alarm_type** (str)[Optional]:
1389
+ * **trigger** (float)[Optional]:
1390
+
1391
+ **Returns**
1392
+
1393
+ * **alarm** (dict) Alarm Object jsonable
1394
+ """
1395
+ self.alarm_manager.put(
1396
+ id=id,
1397
+ name=name,
1398
+ tag=tag,
1399
+ description=description,
1400
+ alarm_type=alarm_type,
1401
+ trigger_value=trigger_value
1402
+ )
1403
+ # Persist Tag on Database
1404
+ if self.is_db_connected():
1405
+
1406
+ self.alarms_engine.put(
1407
+ id=id,
1408
+ name=name,
1409
+ tag=tag,
1410
+ description=description,
1411
+ alarm_type=alarm_type,
1412
+ trigger_value=trigger_value)
1413
+
1414
+ @logging_error_handler
1415
+ @validate_types(id=str, output=Alarm)
1416
+ def get_alarm(self, id:str)->Alarm:
1417
+ r"""
1418
+ Gets alarm from the Alarm Manager by id
1419
+
1420
+ **Paramters**
1421
+
1422
+ * **id**: (int) Alarm ID
1423
+
1424
+ **Returns**
1425
+
1426
+ * **alarm** (Alarm Object)
1427
+ """
1428
+ return self.alarm_manager.get_alarm(id=id)
1429
+
1430
+ @logging_error_handler
1431
+ @validate_types(output=dict)
1432
+ def get_alarms(self)->dict:
1433
+ r"""
1434
+ Gets all alarms
1435
+
1436
+ **Returns**
1437
+
1438
+ * **alarms**: (dict) Alarm objects
1439
+ """
1440
+ return self.alarm_manager.get_alarms()
1441
+
1442
+ @logging_error_handler
1443
+ @validate_types(output=list)
1444
+ def serialize_alarms(self)->list:
1445
+ r"""
1446
+ Gets all alarms
1447
+
1448
+ **Returns**
1449
+
1450
+ * **alarms**: (dict) Alarm objects
1451
+ """
1452
+ result = list()
1453
+ for _, alarm in self.alarm_manager.get_alarms().items():
1454
+
1455
+ result.append(alarm.serialize())
1456
+
1457
+ return result
1458
+
1459
+ @logging_error_handler
1460
+ @validate_types(lasts=int|None, output=list)
1461
+ def get_lasts_active_alarms(self, lasts:int=None)->list:
1462
+ r"""
1463
+ Documentation here
1464
+ """
1465
+ return self.alarm_manager.get_lasts_active_alarms(lasts=lasts) or list()
1466
+
1467
+ @logging_error_handler
1468
+ @validate_types(name=str, output=Alarm)
1469
+ def get_alarm_by_name(self, name:str)->Alarm:
1470
+ r"""
1471
+ Gets alarm from the Alarm Manager by name
1472
+
1473
+ **Paramters**
1474
+
1475
+ * **name**: (str) Alarm name
1476
+
1477
+ **Returns**
1478
+
1479
+ * **alarm** (Alarm Object)
1480
+ """
1481
+ return self.alarm_manager.get_alarm_by_name(name=name)
1482
+
1483
+ @logging_error_handler
1484
+ @validate_types(tag=str, output=list)
1485
+ def get_alarms_by_tag(self, tag:str)->list:
1486
+ r"""
1487
+ Gets all alarms associated to some tag
1488
+
1489
+ **Parameters**
1490
+
1491
+ * **tag**: (str) tag name binded to alarm
1492
+
1493
+ **Returns**
1494
+
1495
+ * **alarm** (dict) of alarm objects
1496
+ """
1497
+ return self.alarm_manager.get_alarms_by_tag(tag=tag)
1498
+
1499
+ @logging_error_handler
1500
+ @validate_types(id=str, user=User|type(None), output=None)
1501
+ def delete_alarm(self, id:str, user:User=None):
1502
+ r"""
1503
+ Removes alarm
1504
+
1505
+ **Paramters**
1506
+
1507
+ * **id** (int): Alarm ID
1508
+ """
1509
+ self.alarm_manager.delete_alarm(id=id, user=user)
1510
+ if self.is_db_connected():
1511
+
1512
+ self.alarms_engine.delete(id=id)
1513
+
1514
+ # EVENTS METHODS
1515
+ @logging_error_handler
1516
+ @validate_types(lasts=int, output=list)
1517
+ def get_lasts_events(self, lasts:int=10)->list:
1518
+ r"""
1519
+ Documentation here
1520
+ """
1521
+ if self.is_db_connected():
1522
+
1523
+ return self.events_engine.get_lasts(lasts=lasts)
1524
+
1525
+ return list()
1526
+
1527
+ @logging_error_handler
1528
+ def filter_events_by(
1529
+ self,
1530
+ usernames:list[str]=None,
1531
+ priorities:list[int]=None,
1532
+ criticities:list[int]=None,
1533
+ message:str="",
1534
+ classification:str="",
1535
+ description:str="",
1536
+ greater_than_timestamp:datetime=None,
1537
+ less_than_timestamp:datetime=None,
1538
+ timezone:str="UTC")->list:
1539
+ r"""
1540
+ Documentation here
1541
+ """
1542
+ if self.is_db_connected():
1543
+
1544
+ return self.events_engine.filter_by(
1545
+ usernames=usernames,
1546
+ priorities=priorities,
1547
+ criticities=criticities,
1548
+ message=message,
1549
+ description=description,
1550
+ classification=classification,
1551
+ greater_than_timestamp=greater_than_timestamp,
1552
+ less_than_timestamp=less_than_timestamp,
1553
+ timezone=timezone
1554
+ )
1555
+
1556
+ return list()
1557
+
1558
+ # LOGS METHODS
1559
+ @logging_error_handler
1560
+ def create_log(
1561
+ self,
1562
+ message:str,
1563
+ user:User,
1564
+ description:str=None,
1565
+ classification:str=None,
1566
+ alarm_summary_id:int=None,
1567
+ event_id:int=None,
1568
+ timestamp:datetime=None
1569
+ )->tuple:
1570
+ r"""
1571
+ Documentation here
1572
+ """
1573
+ if self.is_db_connected():
1574
+
1575
+ log, message = self.logs_engine.create(
1576
+ message=message,
1577
+ user=user,
1578
+ description=description,
1579
+ classification=classification,
1580
+ alarm_summary_id=alarm_summary_id,
1581
+ event_id=event_id,
1582
+ timestamp=timestamp
1583
+ )
1584
+
1585
+ if self.sio:
1586
+
1587
+ self.sio.emit("on.log", data=log.serialize())
1588
+
1589
+ return log, message
1590
+
1591
+ return None, "Logs DB is not up"
1592
+
1593
+ @logging_error_handler
1594
+ def filter_logs_by(
1595
+ self,
1596
+ usernames:list[str]=None,
1597
+ alarm_names:list[str]=None,
1598
+ event_ids:list[int]=None,
1599
+ classification:str="",
1600
+ message:str="",
1601
+ description:str="",
1602
+ greater_than_timestamp:datetime=None,
1603
+ less_than_timestamp:datetime=None,
1604
+ timezone:str="UTC"
1605
+ )->list:
1606
+ r"""
1607
+ Documentation here
1608
+ """
1609
+ if self.is_db_connected():
1610
+
1611
+ return self.logs_engine.filter_by(
1612
+ usernames=usernames,
1613
+ alarm_names=alarm_names,
1614
+ event_ids=event_ids,
1615
+ classification=classification,
1616
+ message=message,
1617
+ description=description,
1618
+ greater_than_timestamp=greater_than_timestamp,
1619
+ less_than_timestamp=less_than_timestamp,
1620
+ timezone=timezone
1621
+ )
1622
+
1623
+ @logging_error_handler
1624
+ @validate_types(lasts=int, output=list)
1625
+ def get_lasts_logs(self, lasts:int=10)->list:
1626
+ r"""
1627
+ Documentation here
1628
+ """
1629
+ if self.is_db_connected():
1630
+
1631
+ return self.logs_engine.get_lasts(lasts=lasts) or list()
1632
+
1633
+ return list()
1634
+
1635
+ # INIT APP
1636
+ @logging_error_handler
1637
+ def run(self, debug:bool=False, test:bool=False, create_tables:bool=False, machines:tuple=None)->None:
1638
+ r"""
1639
+ Runs main app thread and all defined threads by decorators and State Machines besides this method starts app logger
1640
+
1641
+ **Returns:** `None`
1642
+ """
1643
+ self.safe_start(test=test, create_tables=create_tables, machines=machines)
1644
+ self.create_system_user()
1645
+
1646
+ if not test:
1647
+
1648
+ if debug:
1649
+
1650
+ self.dash_app.run(debug=debug, use_reloader=False)
1651
+
1652
+ @logging_error_handler
1653
+ def create_system_user(self):
1654
+ # Create system user
1655
+ users = Users()
1656
+ roles = Roles()
1657
+
1658
+ # Verificar si el usuario system existe
1659
+ if not users.read_by_username(username="system"):
1660
+ # Obtener el rol de administrador
1661
+ admin_role = roles.read_by_name(name="sudo")
1662
+ if admin_role:
1663
+ # Generar password e identificador dinámicamente
1664
+ system_password = secrets.token_urlsafe(32)
1665
+ self.signup(
1666
+ username="system",
1667
+ role_name="sudo",
1668
+ email="system@intelcon.com",
1669
+ password=system_password,
1670
+ name="System",
1671
+ lastname="Intelcon"
1672
+ )
1673
+
1674
+ @logging_error_handler
1675
+ def safe_start(self, test:bool=False, create_tables:bool=True, machines:tuple=None):
1676
+ r"""
1677
+ Run the app without a main thread, only run the app with the threads and state machines define
1678
+ """
1679
+ self._create_tables = create_tables
1680
+ self.__start_logger()
1681
+ self.__start_workers(test=test, machines=machines)
1682
+
1683
+ @logging_error_handler
1684
+ @validate_types(output=None)
1685
+ def safe_stop(self)->None:
1686
+ r"""
1687
+ Stops the app in safe way with the threads
1688
+ """
1689
+ self.__stop_workers()
1690
+
1691
+ @logging_error_handler
1692
+ def state_machine_diagrams(self, folder_path:str):
1693
+ r"""
1694
+ Documentation here"""
1695
+ for machine, _, _ in self._manager.get_machines():
1696
+ # SAVE STATE DIAGRAM
1697
+ img_path = f"{folder_path}{machine.name.value}.png"
1698
+ machine._graph().write_png(img_path)
1699
+
1700
+ # WORKERS
1701
+ @logging_error_handler
1702
+ def __start_workers(self, test:bool=False, machines:tuple=None)->None:
1703
+ r"""
1704
+ Starts all workers.
1705
+
1706
+ * LoggerWorker
1707
+ * StateMachineWorker
1708
+ * DASWorker
1709
+ """
1710
+ if self._create_tables:
1711
+
1712
+ self.db_worker = LoggerWorker(self.db_manager)
1713
+ self.connect_to_db(test=test)
1714
+ self.db_worker.start()
1715
+
1716
+ if machines:
1717
+
1718
+ for machine in machines:
1719
+
1720
+ machine.set_socketio(sio=self.sio)
1721
+
1722
+ self.machine.start(machines=machines)
1723
+
1724
+ if self.is_db_connected():
1725
+
1726
+ self.load_db_tags_to_machine()
1727
+
1728
+ self.is_starting = False
1729
+
1730
+ @logging_error_handler
1731
+ @validate_types(output=None)
1732
+ def __stop_workers(self)->None:
1733
+ r"""
1734
+ Safe stop workers execution
1735
+ """
1736
+ self.machine.stop()
1737
+ self.db_worker.stop()
1738
+ if hasattr(self, 'subscription_monitor'):
1739
+ self.subscription_monitor.stop()
1740
+
1741
+ @logging_error_handler
1742
+ @validate_types(output=None)
1743
+ def __start_logger(self)->None:
1744
+ r"""
1745
+ Starts logger in log file
1746
+ """
1747
+
1748
+ requests.urllib3.disable_warnings()
1749
+ urllib3.disable_warnings()
1750
+ logging.getLogger("urllib3").setLevel(logging.WARNING)
1751
+ logging.getLogger("requests").setLevel(logging.WARNING)
1752
+ logging.getLogger('peewee').setLevel(logging.WARNING)
1753
+ logging.getLogger('opcua').setLevel(logging.CRITICAL)
1754
+ # Configure root logger with rotating file handler (size-based)
1755
+ root_logger = logging.getLogger()
1756
+ root_logger.setLevel(self._logging_level)
1757
+ # Clear existing handlers to avoid duplicates
1758
+ for _h in list(root_logger.handlers):
1759
+ root_logger.removeHandler(_h)
1760
+
1761
+ handler = RotatingFileHandler(
1762
+ filename=self._log_file,
1763
+ maxBytes=10 * 1024 * 1024,
1764
+ backupCount=10,
1765
+ encoding="utf-8",
1766
+ )
1767
+ log_format = "%(asctime)s:%(levelname)s:%(message)s"
1768
+ formatter = logging.Formatter(log_format)
1769
+ handler.setFormatter(formatter)
1770
+ root_logger.addHandler(handler)
1771
+
1772
+ # Ensure named logger propagates to root (no extra handler to avoid duplicates)
1773
+ app_logger = logging.getLogger("pyautomation")
1774
+ app_logger.setLevel(self._logging_level)
1775
+ app_logger.propagate = True