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.
- automation/__init__.py +46 -0
- automation/alarms/__init__.py +563 -0
- automation/alarms/states.py +192 -0
- automation/alarms/trigger.py +64 -0
- automation/buffer.py +132 -0
- automation/core.py +1775 -0
- automation/dbmodels/__init__.py +23 -0
- automation/dbmodels/alarms.py +524 -0
- automation/dbmodels/core.py +86 -0
- automation/dbmodels/events.py +153 -0
- automation/dbmodels/logs.py +155 -0
- automation/dbmodels/machines.py +181 -0
- automation/dbmodels/opcua.py +81 -0
- automation/dbmodels/opcua_server.py +174 -0
- automation/dbmodels/tags.py +921 -0
- automation/dbmodels/users.py +259 -0
- automation/extensions/__init__.py +15 -0
- automation/extensions/api.py +149 -0
- automation/extensions/cors.py +18 -0
- automation/filter/__init__.py +19 -0
- automation/iad/__init__.py +3 -0
- automation/iad/frozen_data.py +54 -0
- automation/iad/out_of_range.py +51 -0
- automation/iad/outliers.py +51 -0
- automation/logger/__init__.py +0 -0
- automation/logger/alarms.py +426 -0
- automation/logger/core.py +265 -0
- automation/logger/datalogger.py +646 -0
- automation/logger/events.py +194 -0
- automation/logger/logdict.py +53 -0
- automation/logger/logs.py +203 -0
- automation/logger/machines.py +248 -0
- automation/logger/opcua_server.py +130 -0
- automation/logger/users.py +96 -0
- automation/managers/__init__.py +4 -0
- automation/managers/alarms.py +455 -0
- automation/managers/db.py +328 -0
- automation/managers/opcua_client.py +186 -0
- automation/managers/state_machine.py +183 -0
- automation/models.py +174 -0
- automation/modules/__init__.py +14 -0
- automation/modules/alarms/__init__.py +0 -0
- automation/modules/alarms/resources/__init__.py +10 -0
- automation/modules/alarms/resources/alarms.py +280 -0
- automation/modules/alarms/resources/summary.py +79 -0
- automation/modules/events/__init__.py +0 -0
- automation/modules/events/resources/__init__.py +10 -0
- automation/modules/events/resources/events.py +83 -0
- automation/modules/events/resources/logs.py +109 -0
- automation/modules/tags/__init__.py +0 -0
- automation/modules/tags/resources/__init__.py +8 -0
- automation/modules/tags/resources/tags.py +201 -0
- automation/modules/users/__init__.py +2 -0
- automation/modules/users/resources/__init__.py +10 -0
- automation/modules/users/resources/models/__init__.py +2 -0
- automation/modules/users/resources/models/roles.py +5 -0
- automation/modules/users/resources/models/users.py +14 -0
- automation/modules/users/resources/roles.py +38 -0
- automation/modules/users/resources/users.py +113 -0
- automation/modules/users/roles.py +121 -0
- automation/modules/users/users.py +335 -0
- automation/opcua/__init__.py +1 -0
- automation/opcua/models.py +541 -0
- automation/opcua/subscription.py +259 -0
- automation/pages/__init__.py +0 -0
- automation/pages/alarms.py +34 -0
- automation/pages/alarms_history.py +21 -0
- automation/pages/assets/styles.css +7 -0
- automation/pages/callbacks/__init__.py +28 -0
- automation/pages/callbacks/alarms.py +218 -0
- automation/pages/callbacks/alarms_summary.py +20 -0
- automation/pages/callbacks/db.py +222 -0
- automation/pages/callbacks/filter.py +238 -0
- automation/pages/callbacks/machines.py +29 -0
- automation/pages/callbacks/machines_detailed.py +581 -0
- automation/pages/callbacks/opcua.py +266 -0
- automation/pages/callbacks/opcua_server.py +244 -0
- automation/pages/callbacks/tags.py +495 -0
- automation/pages/callbacks/trends.py +119 -0
- automation/pages/communications.py +129 -0
- automation/pages/components/__init__.py +123 -0
- automation/pages/components/alarms.py +151 -0
- automation/pages/components/alarms_summary.py +45 -0
- automation/pages/components/database.py +128 -0
- automation/pages/components/gaussian_filter.py +69 -0
- automation/pages/components/machines.py +396 -0
- automation/pages/components/opcua.py +384 -0
- automation/pages/components/opcua_server.py +53 -0
- automation/pages/components/tags.py +253 -0
- automation/pages/components/trends.py +66 -0
- automation/pages/database.py +26 -0
- automation/pages/filter.py +55 -0
- automation/pages/machines.py +20 -0
- automation/pages/machines_detailed.py +41 -0
- automation/pages/main.py +63 -0
- automation/pages/opcua_server.py +28 -0
- automation/pages/tags.py +40 -0
- automation/pages/trends.py +35 -0
- automation/singleton.py +30 -0
- automation/state_machine.py +1672 -0
- automation/tags/__init__.py +2 -0
- automation/tags/cvt.py +1198 -0
- automation/tags/filter.py +55 -0
- automation/tags/tag.py +418 -0
- automation/tests/__init__.py +10 -0
- automation/tests/test_alarms.py +110 -0
- automation/tests/test_core.py +257 -0
- automation/tests/test_unit.py +21 -0
- automation/tests/test_user.py +155 -0
- automation/utils/__init__.py +164 -0
- automation/utils/decorators.py +222 -0
- automation/utils/npw.py +294 -0
- automation/utils/observer.py +21 -0
- automation/utils/units.py +118 -0
- automation/variables/__init__.py +55 -0
- automation/variables/adimentional.py +30 -0
- automation/variables/current.py +71 -0
- automation/variables/density.py +115 -0
- automation/variables/eng_time.py +68 -0
- automation/variables/force.py +90 -0
- automation/variables/length.py +104 -0
- automation/variables/mass.py +80 -0
- automation/variables/mass_flow.py +101 -0
- automation/variables/percentage.py +30 -0
- automation/variables/power.py +113 -0
- automation/variables/pressure.py +93 -0
- automation/variables/temperature.py +168 -0
- automation/variables/volume.py +70 -0
- automation/variables/volumetric_flow.py +100 -0
- automation/workers/__init__.py +2 -0
- automation/workers/logger.py +164 -0
- automation/workers/state_machine.py +207 -0
- automation/workers/worker.py +36 -0
- pyautomationio-0.0.0.dist-info/METADATA +198 -0
- pyautomationio-0.0.0.dist-info/RECORD +138 -0
- pyautomationio-0.0.0.dist-info/WHEEL +5 -0
- pyautomationio-0.0.0.dist-info/licenses/LICENSE +21 -0
- pyautomationio-0.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from ..utils.decorators import decorator
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
class KalmanFilter:
|
|
5
|
+
def __init__(
|
|
6
|
+
self,
|
|
7
|
+
x,
|
|
8
|
+
A:np.array=np.array([[1]]),
|
|
9
|
+
B:np.array=np.array([[0]]),
|
|
10
|
+
H:np.array=np.array([[1]]),
|
|
11
|
+
P:np.array=np.array([[1]]),
|
|
12
|
+
Q:np.array=np.array([[1e-5]]),
|
|
13
|
+
R:np.array=np.array([[0.5]])
|
|
14
|
+
):
|
|
15
|
+
self.A = A
|
|
16
|
+
self.B = B
|
|
17
|
+
self.H = H
|
|
18
|
+
self.Q = Q
|
|
19
|
+
self.R = R
|
|
20
|
+
self.P = P
|
|
21
|
+
self.x = x
|
|
22
|
+
self.previous_innov = None # Para guardar la innovación anterior
|
|
23
|
+
|
|
24
|
+
def predict(self, u=0):
|
|
25
|
+
self.x = np.dot(self.A, self.x) + np.dot(self.B, u)
|
|
26
|
+
self.P = np.dot(np.dot(self.A, self.P), self.A.T) + self.Q
|
|
27
|
+
|
|
28
|
+
def update(self, z, threshold:float=100, r_value:float=0.5):
|
|
29
|
+
|
|
30
|
+
innov = z - np.dot(self.H, self.x) # Innovación
|
|
31
|
+
if self.previous_innov is not None:
|
|
32
|
+
innov_var = np.std([self.previous_innov, innov]) # Varianza de las innovaciones
|
|
33
|
+
self.R = 0.0 if innov_var > threshold else r_value # Ajustar R
|
|
34
|
+
K = np.dot(np.dot(self.P, self.H.T), np.linalg.inv(np.dot(np.dot(self.H, self.P), self.H.T) + self.R))
|
|
35
|
+
self.x = self.x + np.dot(K, innov)
|
|
36
|
+
self.P = self.P - np.dot(np.dot(K, self.H), self.P)
|
|
37
|
+
self.previous_innov = innov
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class GaussianFilter:
|
|
41
|
+
|
|
42
|
+
def __init__(self):
|
|
43
|
+
|
|
44
|
+
self.kf = None
|
|
45
|
+
|
|
46
|
+
def __call__(self, value:float, threshold:float=100, r_value:float=0.5):
|
|
47
|
+
|
|
48
|
+
if self.kf is None:
|
|
49
|
+
|
|
50
|
+
self.kf = KalmanFilter(value)
|
|
51
|
+
|
|
52
|
+
self.kf.predict()
|
|
53
|
+
self.kf.update(value, threshold=threshold, r_value=r_value)
|
|
54
|
+
filtered_value = self.kf.x[0][0]
|
|
55
|
+
return filtered_value
|
automation/tags/tag.py
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import secrets, logging
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from ..utils import Observer
|
|
4
|
+
from ..utils.decorators import logging_error_handler
|
|
5
|
+
from ..buffer import Buffer
|
|
6
|
+
from ..variables import (
|
|
7
|
+
Temperature,
|
|
8
|
+
Length,
|
|
9
|
+
Current,
|
|
10
|
+
Time,
|
|
11
|
+
Pressure,
|
|
12
|
+
Mass,
|
|
13
|
+
Force,
|
|
14
|
+
Power,
|
|
15
|
+
VolumetricFlow,
|
|
16
|
+
MassFlow,
|
|
17
|
+
Density,
|
|
18
|
+
Percentage,
|
|
19
|
+
Adimentional,
|
|
20
|
+
Volume
|
|
21
|
+
)
|
|
22
|
+
from .filter import GaussianFilter
|
|
23
|
+
|
|
24
|
+
DATETIME_FORMAT = "%m/%d/%Y, %H:%M:%S.%f"
|
|
25
|
+
|
|
26
|
+
class Tag:
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
name:str,
|
|
31
|
+
unit:str,
|
|
32
|
+
variable:str,
|
|
33
|
+
data_type:str,
|
|
34
|
+
display_name:str=None,
|
|
35
|
+
display_unit:str=None,
|
|
36
|
+
description:str="",
|
|
37
|
+
opcua_address:str=None,
|
|
38
|
+
node_namespace:str=None,
|
|
39
|
+
scan_time:int=None,
|
|
40
|
+
dead_band:float=None,
|
|
41
|
+
timestamp:datetime=None,
|
|
42
|
+
process_filter:bool=False,
|
|
43
|
+
gaussian_filter:bool=False,
|
|
44
|
+
gaussian_filter_threshold:float=1.0,
|
|
45
|
+
gaussian_filter_r_value:float=0.0,
|
|
46
|
+
outlier_detection:bool=False,
|
|
47
|
+
out_of_range_detection:bool=False,
|
|
48
|
+
frozen_data_detection:bool=False,
|
|
49
|
+
manufacturer:str="",
|
|
50
|
+
segment:str="",
|
|
51
|
+
id:str=None
|
|
52
|
+
):
|
|
53
|
+
self.id = secrets.token_hex(4)
|
|
54
|
+
if id:
|
|
55
|
+
self.id = id
|
|
56
|
+
self.name = name
|
|
57
|
+
self.data_type = data_type
|
|
58
|
+
self.description = description
|
|
59
|
+
self.variable = variable
|
|
60
|
+
self.display_name = name
|
|
61
|
+
if display_name:
|
|
62
|
+
self.display_name = display_name
|
|
63
|
+
self.display_unit = unit
|
|
64
|
+
if display_unit:
|
|
65
|
+
self.display_unit = display_unit
|
|
66
|
+
self.unit=unit
|
|
67
|
+
if variable.lower()=="temperature":
|
|
68
|
+
self.value = Temperature(value=0.0, unit=self.unit)
|
|
69
|
+
elif variable.lower()=="length":
|
|
70
|
+
self.value = Length(value=0.0, unit=self.unit)
|
|
71
|
+
elif variable.lower()=="time":
|
|
72
|
+
self.value = Time(value=0.0, unit=self.unit)
|
|
73
|
+
elif variable.lower()=="pressure":
|
|
74
|
+
self.value = Pressure(value=0.0, unit=self.unit)
|
|
75
|
+
elif variable.lower()=="mass":
|
|
76
|
+
self.value = Mass(value=0.0, unit=self.unit)
|
|
77
|
+
elif variable.lower()=="force":
|
|
78
|
+
self.value = Force(value=0.0, unit=self.unit)
|
|
79
|
+
elif variable.lower()=="power":
|
|
80
|
+
self.value = Power(value=0.0, unit=self.unit)
|
|
81
|
+
elif variable.lower()=="current":
|
|
82
|
+
self.value = Current(value=0.0, unit=self.unit)
|
|
83
|
+
elif variable.lower()=="volumetricflow":
|
|
84
|
+
self.value = VolumetricFlow(value=0.0, unit=self.unit)
|
|
85
|
+
elif variable.lower()=="massflow":
|
|
86
|
+
self.value = MassFlow(value=0.0, unit=self.unit)
|
|
87
|
+
elif variable.lower()=="density":
|
|
88
|
+
self.value = Density(value=0.0, unit=self.unit)
|
|
89
|
+
elif variable.lower()=="percentage":
|
|
90
|
+
self.value = Percentage(value=0.0, unit=self.unit)
|
|
91
|
+
elif variable.lower()=="adimentional":
|
|
92
|
+
self.value = Adimentional(value=0.0, unit=self.unit)
|
|
93
|
+
elif variable.lower()=="volume":
|
|
94
|
+
self.value = Volume(value=0.0, unit=self.unit)
|
|
95
|
+
|
|
96
|
+
self.values = Buffer()
|
|
97
|
+
self.timestamps = Buffer()
|
|
98
|
+
self.opcua_address = opcua_address
|
|
99
|
+
self.node_namespace = node_namespace
|
|
100
|
+
self.scan_time = scan_time
|
|
101
|
+
self.dead_band = dead_band
|
|
102
|
+
self.timestamp = timestamp
|
|
103
|
+
self.process_filter = process_filter
|
|
104
|
+
self.gaussian_filter = gaussian_filter
|
|
105
|
+
self.gaussian_filter_threshold = gaussian_filter_threshold
|
|
106
|
+
self.gaussian_filter_r_value = gaussian_filter_r_value
|
|
107
|
+
self.outlier_detection = outlier_detection
|
|
108
|
+
self.out_of_range_detection = out_of_range_detection
|
|
109
|
+
self.frozen_data_detection = frozen_data_detection
|
|
110
|
+
self.manufacturer = manufacturer
|
|
111
|
+
self.segment = segment
|
|
112
|
+
self.filter = GaussianFilter()
|
|
113
|
+
self._observers = set()
|
|
114
|
+
|
|
115
|
+
def set_name(self, name:str):
|
|
116
|
+
r"""
|
|
117
|
+
Documentation here
|
|
118
|
+
"""
|
|
119
|
+
self.name = name
|
|
120
|
+
|
|
121
|
+
@logging_error_handler
|
|
122
|
+
def set_value(self, value:float|str|int|bool, timestamp:datetime=None):
|
|
123
|
+
r"""
|
|
124
|
+
Documentation here
|
|
125
|
+
"""
|
|
126
|
+
if self.dead_band and isinstance(value, (int, float)):
|
|
127
|
+
try:
|
|
128
|
+
current_value = self.value.value
|
|
129
|
+
if abs(value - current_value) < self.dead_band:
|
|
130
|
+
|
|
131
|
+
return
|
|
132
|
+
except Exception as e:
|
|
133
|
+
logging.error(f"Error in deadband logic: {e}")
|
|
134
|
+
|
|
135
|
+
if not timestamp:
|
|
136
|
+
timestamp = datetime.now()
|
|
137
|
+
self.value.set_value(value=value, unit=self.display_unit)
|
|
138
|
+
self.timestamp = timestamp
|
|
139
|
+
self.values(self.get_value())
|
|
140
|
+
self.timestamps(timestamp.strftime(DATETIME_FORMAT))
|
|
141
|
+
self.notify()
|
|
142
|
+
|
|
143
|
+
def set_display_name(self, name:str):
|
|
144
|
+
r"""
|
|
145
|
+
Documentation here
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
self.display_name = name
|
|
149
|
+
|
|
150
|
+
def set_data_type(self, data_type:str):
|
|
151
|
+
r"""
|
|
152
|
+
Documentation here
|
|
153
|
+
"""
|
|
154
|
+
self.data_type = data_type
|
|
155
|
+
|
|
156
|
+
def set_variable(self, variable:str):
|
|
157
|
+
r"""
|
|
158
|
+
Documentation here
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
self.variable = variable
|
|
162
|
+
if variable.lower()=="temperature":
|
|
163
|
+
self.value = Temperature(value=0.0, unit=self.unit)
|
|
164
|
+
elif variable.lower()=="length":
|
|
165
|
+
self.value = Length(value=0.0, unit=self.unit)
|
|
166
|
+
elif variable.lower()=="time":
|
|
167
|
+
self.value = Time(value=0.0, unit=self.unit)
|
|
168
|
+
elif variable.lower()=="pressure":
|
|
169
|
+
self.value = Pressure(value=0.0, unit=self.unit)
|
|
170
|
+
elif variable.lower()=="mass":
|
|
171
|
+
self.value = Mass(value=0.0, unit=self.unit)
|
|
172
|
+
elif variable.lower()=="force":
|
|
173
|
+
self.value = Force(value=0.0, unit=self.unit)
|
|
174
|
+
elif variable.lower()=="power":
|
|
175
|
+
self.value = Power(value=0.0, unit=self.unit)
|
|
176
|
+
elif variable.lower()=="current":
|
|
177
|
+
self.value = Current(value=0.0, unit=self.unit)
|
|
178
|
+
elif variable.lower()=="volumetricflow":
|
|
179
|
+
self.value = VolumetricFlow(value=0.0, unit=self.unit)
|
|
180
|
+
elif variable.lower()=="massflow":
|
|
181
|
+
self.value = MassFlow(value=0.0, unit=self.unit)
|
|
182
|
+
elif variable.lower()=="density":
|
|
183
|
+
self.value = Density(value=0.0, unit=self.unit)
|
|
184
|
+
elif variable.lower()=="percentage":
|
|
185
|
+
self.value = Percentage(value=0.0, unit=self.unit)
|
|
186
|
+
elif variable.lower()=="adimentional":
|
|
187
|
+
self.value = Adimentional(value=0.0, unit=self.unit)
|
|
188
|
+
elif variable.lower()=="volume":
|
|
189
|
+
self.value = Volume(value=0.0, unit=self.unit)
|
|
190
|
+
|
|
191
|
+
def set_opcua_address(self, opcua_address:str):
|
|
192
|
+
r"""
|
|
193
|
+
Documentation here
|
|
194
|
+
"""
|
|
195
|
+
self.opcua_address = opcua_address
|
|
196
|
+
|
|
197
|
+
def set_unit(self, unit:str):
|
|
198
|
+
r"""
|
|
199
|
+
Documentation here
|
|
200
|
+
"""
|
|
201
|
+
self.unit = unit
|
|
202
|
+
|
|
203
|
+
def set_display_unit(self, unit:str):
|
|
204
|
+
r"""
|
|
205
|
+
Documentation here
|
|
206
|
+
"""
|
|
207
|
+
self.display_unit = unit
|
|
208
|
+
|
|
209
|
+
def set_node_namespace(self, node_namespace:str):
|
|
210
|
+
r"""
|
|
211
|
+
Documentation here
|
|
212
|
+
"""
|
|
213
|
+
self.node_namespace = node_namespace
|
|
214
|
+
|
|
215
|
+
def get_value(self):
|
|
216
|
+
r"""
|
|
217
|
+
Documentation here
|
|
218
|
+
"""
|
|
219
|
+
return round(self.value.convert(to_unit=self.display_unit), 3)
|
|
220
|
+
|
|
221
|
+
def set_description(self, description:str):
|
|
222
|
+
r"""
|
|
223
|
+
Documentation here
|
|
224
|
+
"""
|
|
225
|
+
self.description = description
|
|
226
|
+
|
|
227
|
+
def set_scan_time(self, scan_time:int):
|
|
228
|
+
r"""
|
|
229
|
+
Documentation here
|
|
230
|
+
"""
|
|
231
|
+
self.scan_time = scan_time
|
|
232
|
+
|
|
233
|
+
def set_dead_band(self, dead_band:float):
|
|
234
|
+
r"""
|
|
235
|
+
Documentation here
|
|
236
|
+
"""
|
|
237
|
+
self.dead_band = dead_band
|
|
238
|
+
|
|
239
|
+
def get_timestamp(self):
|
|
240
|
+
r"""
|
|
241
|
+
Documentation here
|
|
242
|
+
"""
|
|
243
|
+
return self.timestamp
|
|
244
|
+
|
|
245
|
+
def get_scan_time(self):
|
|
246
|
+
r"""
|
|
247
|
+
Documentation here
|
|
248
|
+
"""
|
|
249
|
+
return self.scan_time
|
|
250
|
+
|
|
251
|
+
def get_dead_band(self):
|
|
252
|
+
r"""
|
|
253
|
+
Documentation here
|
|
254
|
+
"""
|
|
255
|
+
return self.dead_band
|
|
256
|
+
|
|
257
|
+
def get_data_type(self):
|
|
258
|
+
r"""
|
|
259
|
+
Documentation here
|
|
260
|
+
"""
|
|
261
|
+
return self.data_type
|
|
262
|
+
|
|
263
|
+
def get_unit(self):
|
|
264
|
+
r"""
|
|
265
|
+
Documentation here
|
|
266
|
+
"""
|
|
267
|
+
return self.unit
|
|
268
|
+
|
|
269
|
+
def get_display_unit(self):
|
|
270
|
+
r"""
|
|
271
|
+
Documentation here
|
|
272
|
+
"""
|
|
273
|
+
return self.display_unit
|
|
274
|
+
|
|
275
|
+
def get_description(self):
|
|
276
|
+
r"""
|
|
277
|
+
Documentation here
|
|
278
|
+
"""
|
|
279
|
+
return self.description
|
|
280
|
+
|
|
281
|
+
def get_display_name(self)->str:
|
|
282
|
+
r"""
|
|
283
|
+
Documentation here
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
return self.display_name
|
|
287
|
+
|
|
288
|
+
def get_variable(self)->str:
|
|
289
|
+
r"""
|
|
290
|
+
Documentation here
|
|
291
|
+
"""
|
|
292
|
+
|
|
293
|
+
return self.variable
|
|
294
|
+
|
|
295
|
+
def get_id(self)->str:
|
|
296
|
+
r"""
|
|
297
|
+
Documentation here
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
return self.id
|
|
301
|
+
|
|
302
|
+
def get_name(self)->str:
|
|
303
|
+
r"""
|
|
304
|
+
Documentation here
|
|
305
|
+
"""
|
|
306
|
+
|
|
307
|
+
return self.name
|
|
308
|
+
|
|
309
|
+
def get_opcua_address(self):
|
|
310
|
+
r"""
|
|
311
|
+
Documentation here
|
|
312
|
+
"""
|
|
313
|
+
return self.opcua_address
|
|
314
|
+
|
|
315
|
+
def get_node_namespace(self):
|
|
316
|
+
r"""
|
|
317
|
+
Documentation here
|
|
318
|
+
"""
|
|
319
|
+
return self.node_namespace
|
|
320
|
+
|
|
321
|
+
def attach(self, observer:Observer):
|
|
322
|
+
r"""
|
|
323
|
+
Documentation here
|
|
324
|
+
"""
|
|
325
|
+
observer._subject = self
|
|
326
|
+
self._observers.add(observer)
|
|
327
|
+
|
|
328
|
+
def detach(self, observer:Observer):
|
|
329
|
+
r"""
|
|
330
|
+
Documentation here
|
|
331
|
+
"""
|
|
332
|
+
observer._subject = None
|
|
333
|
+
self._observers.discard(observer)
|
|
334
|
+
|
|
335
|
+
def notify(self):
|
|
336
|
+
r"""
|
|
337
|
+
Documentation here
|
|
338
|
+
"""
|
|
339
|
+
for observer in self._observers:
|
|
340
|
+
|
|
341
|
+
observer.update()
|
|
342
|
+
|
|
343
|
+
def serialize(self):
|
|
344
|
+
|
|
345
|
+
timestamp = self.get_timestamp()
|
|
346
|
+
if timestamp:
|
|
347
|
+
|
|
348
|
+
timestamp = timestamp.strftime(DATETIME_FORMAT)
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
"id": self.get_id(),
|
|
352
|
+
"value": self.get_value(),
|
|
353
|
+
"timestamp": timestamp,
|
|
354
|
+
"values": list(self.values),
|
|
355
|
+
"timestamps": list(self.timestamps),
|
|
356
|
+
"name": self.name,
|
|
357
|
+
"unit": self.get_unit(),
|
|
358
|
+
"display_unit": self.get_display_unit(),
|
|
359
|
+
"data_type": self.get_data_type(),
|
|
360
|
+
"variable": self.get_variable(),
|
|
361
|
+
"description": self.get_description(),
|
|
362
|
+
"display_name": self.get_display_name(),
|
|
363
|
+
"opcua_address": self.get_opcua_address(),
|
|
364
|
+
"node_namespace": self.get_node_namespace(),
|
|
365
|
+
"scan_time": self.get_scan_time(),
|
|
366
|
+
"dead_band": self.get_dead_band(),
|
|
367
|
+
"segment": self.segment,
|
|
368
|
+
"manufacturer": self.manufacturer,
|
|
369
|
+
"process_filter": self.process_filter,
|
|
370
|
+
"gaussian_filter": self.gaussian_filter,
|
|
371
|
+
"out_of_range_detection": self.out_of_range_detection,
|
|
372
|
+
"frozen_data_detection": self.frozen_data_detection,
|
|
373
|
+
"outlier_detection": self.outlier_detection
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
class TagObserver(Observer):
|
|
378
|
+
"""
|
|
379
|
+
Implement the Observer updating interface to keep its state
|
|
380
|
+
consistent with the subject's.
|
|
381
|
+
Store state that should stay consistent with the subject's.
|
|
382
|
+
"""
|
|
383
|
+
def __init__(self, tag_queue):
|
|
384
|
+
|
|
385
|
+
super(TagObserver, self).__init__()
|
|
386
|
+
self._tag_queue = tag_queue
|
|
387
|
+
|
|
388
|
+
def update(self):
|
|
389
|
+
|
|
390
|
+
"""
|
|
391
|
+
This methods inserts the changing Tag into a
|
|
392
|
+
Producer-Consumer Queue Design Pattern
|
|
393
|
+
"""
|
|
394
|
+
result = dict()
|
|
395
|
+
result["tag"] = self._subject.name
|
|
396
|
+
result["value"] = self._subject.value.convert(self._subject.get_display_unit())
|
|
397
|
+
result["timestamp"] = self._subject.timestamp
|
|
398
|
+
self._tag_queue.put(result, block=False)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
class MachineObserver(Observer):
|
|
402
|
+
"""
|
|
403
|
+
Implement the Observer updating interface to keep its state
|
|
404
|
+
consistent with the subject's.
|
|
405
|
+
Store state that should stay consistent with the subject's.
|
|
406
|
+
"""
|
|
407
|
+
def __init__(self, machine):
|
|
408
|
+
|
|
409
|
+
super(MachineObserver, self).__init__()
|
|
410
|
+
self.machine = machine
|
|
411
|
+
|
|
412
|
+
def update(self):
|
|
413
|
+
|
|
414
|
+
"""
|
|
415
|
+
This methods inserts the changing Tag into a
|
|
416
|
+
Producer-Consumer Queue Design Pattern
|
|
417
|
+
"""
|
|
418
|
+
self.machine.notify(tag=self._subject.name, value=self._subject.value, timestamp=self._subject.timestamp)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
|
|
2
|
+
def assert_dict_contains_subset(subset, superset, msg=None):
|
|
3
|
+
"""
|
|
4
|
+
Custom assertion to check if `subset` is a subset of `superset`.
|
|
5
|
+
"""
|
|
6
|
+
missing_keys = {key for key in subset if key not in superset}
|
|
7
|
+
assert not missing_keys, f"{msg or 'Dictionary subset check failed'}: Missing keys {missing_keys}"
|
|
8
|
+
|
|
9
|
+
for key, value in subset.items():
|
|
10
|
+
assert superset[key] == value, f"{msg or 'Dictionary subset check failed'}: Value mismatch for key '{key}'"
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from automation.alarms import Alarm
|
|
3
|
+
from automation.tags.tag import Tag
|
|
4
|
+
from automation.tags.cvt import CVTEngine
|
|
5
|
+
from automation.models import StringType, FloatType
|
|
6
|
+
|
|
7
|
+
cvt = CVTEngine()
|
|
8
|
+
|
|
9
|
+
class TestAlarms(unittest.TestCase):
|
|
10
|
+
|
|
11
|
+
def setUp(self) -> None:
|
|
12
|
+
|
|
13
|
+
return super().setUp()
|
|
14
|
+
|
|
15
|
+
def tearDown(self) -> None:
|
|
16
|
+
|
|
17
|
+
return super().tearDown()
|
|
18
|
+
|
|
19
|
+
def test_create_alarm(self):
|
|
20
|
+
r"""
|
|
21
|
+
Documentation here
|
|
22
|
+
"""
|
|
23
|
+
name = "alarm1"
|
|
24
|
+
cvt.set_tag(
|
|
25
|
+
name="tag1",
|
|
26
|
+
variable="Temperature",
|
|
27
|
+
unit="C",
|
|
28
|
+
data_type="FLOAT",
|
|
29
|
+
description="tag1"
|
|
30
|
+
)
|
|
31
|
+
tag = cvt.get_tag_by_name(name="tag1")
|
|
32
|
+
alarm = Alarm(
|
|
33
|
+
name=name,
|
|
34
|
+
tag=tag,
|
|
35
|
+
alarm_type=StringType("HIGH"),
|
|
36
|
+
alarm_setpoint=FloatType(50.0)
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
self.assertEqual(alarm.state.state.lower(), "normal")
|
|
40
|
+
|
|
41
|
+
def test_alarm_state_attribute(self):
|
|
42
|
+
r"""
|
|
43
|
+
Documentation here
|
|
44
|
+
"""
|
|
45
|
+
name = "alarm1"
|
|
46
|
+
cvt.set_tag(
|
|
47
|
+
name="tag2",
|
|
48
|
+
variable="Temperature",
|
|
49
|
+
unit="C",
|
|
50
|
+
data_type="FLOAT",
|
|
51
|
+
description="tag2"
|
|
52
|
+
)
|
|
53
|
+
tag = cvt.get_tag_by_name(name="tag2")
|
|
54
|
+
alarm = Alarm(
|
|
55
|
+
name=name,
|
|
56
|
+
tag=tag,
|
|
57
|
+
alarm_type=StringType("HIGH"),
|
|
58
|
+
alarm_setpoint=FloatType(50.0)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
with self.subTest("Test alarm Unack status"):
|
|
62
|
+
tag.set_value(value=55)
|
|
63
|
+
self.assertEqual(alarm.state.state.lower(), "unacknowledged")
|
|
64
|
+
|
|
65
|
+
with self.subTest("Test alarm Ack status"):
|
|
66
|
+
alarm.acknowledge()
|
|
67
|
+
self.assertEqual(alarm.state.state.lower(), "acknowledged")
|
|
68
|
+
|
|
69
|
+
with self.subTest("Test alarm Normal status"):
|
|
70
|
+
tag.set_value(value=45)
|
|
71
|
+
self.assertEqual(alarm.state.state.lower(), "normal")
|
|
72
|
+
|
|
73
|
+
def test_alarm_state_machine(self):
|
|
74
|
+
r"""
|
|
75
|
+
Documentation here
|
|
76
|
+
"""
|
|
77
|
+
name = "alarm1"
|
|
78
|
+
cvt.set_tag(
|
|
79
|
+
name="tag3",
|
|
80
|
+
variable="Temperature",
|
|
81
|
+
unit="C",
|
|
82
|
+
data_type="FLOAT",
|
|
83
|
+
description="tag3"
|
|
84
|
+
)
|
|
85
|
+
tag = cvt.get_tag_by_name(name="tag3")
|
|
86
|
+
alarm = Alarm(
|
|
87
|
+
name=name,
|
|
88
|
+
tag=tag,
|
|
89
|
+
alarm_type=StringType("HIGH"),
|
|
90
|
+
alarm_setpoint=FloatType(50.0)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
tag.set_value(value=55)
|
|
94
|
+
with self.subTest("Test alarm Unack status"):
|
|
95
|
+
self.assertEqual(alarm.current_state.value.lower(), "unack_alarm")
|
|
96
|
+
|
|
97
|
+
with self.subTest("Test alarm Ack status"):
|
|
98
|
+
alarm.acknowledge()
|
|
99
|
+
self.assertEqual(alarm.current_state.value.lower(), "ack_alarm")
|
|
100
|
+
|
|
101
|
+
with self.subTest("Test alarm Normal status"):
|
|
102
|
+
tag.set_value(value=45)
|
|
103
|
+
self.assertEqual(alarm.current_state.value.lower(), "normal")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|