MTPPy2.0 0.1.5__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 (214) hide show
  1. examples/__init__.py +0 -0
  2. examples/minimal+pid.py +82 -0
  3. examples/minimal.py +32 -0
  4. examples/minimal_recipe.py +117 -0
  5. examples/pea_alarm_base.py +47 -0
  6. examples/pea_minimal.py +258 -0
  7. mtppy/__init__.py +1 -0
  8. mtppy/part1/__init__.py +4 -0
  9. mtppy/part1/empty_folder_test.py +7 -0
  10. mtppy/part3/__init__.py +4 -0
  11. mtppy/part3/data_assembly_set/__init__.py +4 -0
  12. mtppy/part3/data_assembly_set/active_elements/__init__.py +4 -0
  13. mtppy/part3/data_assembly_set/active_elements/active_element.py +22 -0
  14. mtppy/part3/data_assembly_set/active_elements/drives/__init__.py +4 -0
  15. mtppy/part3/data_assembly_set/active_elements/drives/ana_drv.py +446 -0
  16. mtppy/part3/data_assembly_set/active_elements/drives/ana_drv_test.py +567 -0
  17. mtppy/part3/data_assembly_set/active_elements/drives/bin_drv.py +365 -0
  18. mtppy/part3/data_assembly_set/active_elements/drives/bin_drv_test.py +503 -0
  19. mtppy/part3/data_assembly_set/active_elements/drives/mon_ana_drv.py +469 -0
  20. mtppy/part3/data_assembly_set/active_elements/drives/mon_ana_drv_test.py +418 -0
  21. mtppy/part3/data_assembly_set/active_elements/drives/mon_bin_drv.py +295 -0
  22. mtppy/part3/data_assembly_set/active_elements/drives/mon_bin_drv_test.py +265 -0
  23. mtppy/part3/data_assembly_set/active_elements/pid/__init__.py +4 -0
  24. mtppy/part3/data_assembly_set/active_elements/pid/pid_ctrl.py +333 -0
  25. mtppy/part3/data_assembly_set/active_elements/pid/pid_ctrl_test.py +156 -0
  26. mtppy/part3/data_assembly_set/active_elements/valves/__init__.py +4 -0
  27. mtppy/part3/data_assembly_set/active_elements/valves/ana_vlv.py +402 -0
  28. mtppy/part3/data_assembly_set/active_elements/valves/ana_vlv_test.py +732 -0
  29. mtppy/part3/data_assembly_set/active_elements/valves/bin_vlv.py +279 -0
  30. mtppy/part3/data_assembly_set/active_elements/valves/bin_vlv_test.py +585 -0
  31. mtppy/part3/data_assembly_set/active_elements/valves/mon_ana_vlv.py +402 -0
  32. mtppy/part3/data_assembly_set/active_elements/valves/mon_ana_vlv_test.py +270 -0
  33. mtppy/part3/data_assembly_set/active_elements/valves/mon_bin_vlv.py +286 -0
  34. mtppy/part3/data_assembly_set/active_elements/valves/mon_bin_vlv_test.py +224 -0
  35. mtppy/part3/data_assembly_set/base_functions/__init__.py +4 -0
  36. mtppy/part3/data_assembly_set/base_functions/operation_mode.py +252 -0
  37. mtppy/part3/data_assembly_set/base_functions/operation_mode_test.py +260 -0
  38. mtppy/part3/data_assembly_set/base_functions/oslevel.py +35 -0
  39. mtppy/part3/data_assembly_set/base_functions/source_mode.py +189 -0
  40. mtppy/part3/data_assembly_set/base_functions/source_mode_test.py +112 -0
  41. mtppy/part3/data_assembly_set/base_functions/wqc.py +75 -0
  42. mtppy/part3/data_assembly_set/base_functions/wqc_test.py +86 -0
  43. mtppy/part3/data_assembly_set/data_assembly.py +65 -0
  44. mtppy/part3/data_assembly_set/diagnostic_elements/__init__.py +4 -0
  45. mtppy/part3/data_assembly_set/diagnostic_elements/diagnostic_element.py +19 -0
  46. mtppy/part3/data_assembly_set/indicator_elements/__init__.py +4 -0
  47. mtppy/part3/data_assembly_set/indicator_elements/indicator_element.py +22 -0
  48. mtppy/part3/data_assembly_set/indicator_elements/views/__init__.py +4 -0
  49. mtppy/part3/data_assembly_set/indicator_elements/views/ana_view.py +68 -0
  50. mtppy/part3/data_assembly_set/indicator_elements/views/bin_view.py +73 -0
  51. mtppy/part3/data_assembly_set/indicator_elements/views/dint_view.py +76 -0
  52. mtppy/part3/data_assembly_set/indicator_elements/views/string_view.py +55 -0
  53. mtppy/part3/data_assembly_set/operation_elements/__init__.py +4 -0
  54. mtppy/part3/data_assembly_set/operation_elements/operation_element.py +19 -0
  55. mtppy/part3/data_assembly_set/pea_elements/__init__.py +4 -0
  56. mtppy/part3/data_assembly_set/pea_elements/pea_element.py +28 -0
  57. mtppy/part3/data_assembly_set/pea_elements/pea_information_label/__init__.py +4 -0
  58. mtppy/part3/data_assembly_set/pea_elements/pea_information_label/pea_information_label.py +231 -0
  59. mtppy/part3/data_assembly_set/pea_elements/web_server_url_info/__init__.py +4 -0
  60. mtppy/part3/data_assembly_set/pea_elements/web_server_url_info/web_server_url_info.py +42 -0
  61. mtppy/part3/definitions/__init__.py +4 -0
  62. mtppy/part3/definitions/mtp_access.py +15 -0
  63. mtppy/part4/__init__.py +4 -0
  64. mtppy/part4/automation_services/__init__.py +4 -0
  65. mtppy/part4/automation_services/base_functions/__init__.py +4 -0
  66. mtppy/part4/automation_services/base_functions/service_operation_mode.py +368 -0
  67. mtppy/part4/automation_services/base_functions/service_operation_mode_test.py +242 -0
  68. mtppy/part4/automation_services/base_functions/service_operator_interaction.py +62 -0
  69. mtppy/part4/automation_services/base_functions/service_source_mode.py +138 -0
  70. mtppy/part4/automation_services/base_functions/service_source_mode_test.py +179 -0
  71. mtppy/part4/automation_services/helpers/__init__.py +4 -0
  72. mtppy/part4/automation_services/helpers/command_en_control.py +330 -0
  73. mtppy/part4/automation_services/helpers/command_en_control_test.py +302 -0
  74. mtppy/part4/automation_services/helpers/state_machine.py +167 -0
  75. mtppy/part4/automation_services/helpers/state_machine_test.py +294 -0
  76. mtppy/part4/automation_services/helpers/thread_control.py +39 -0
  77. mtppy/part4/automation_services/helpers/thread_control_test.py +80 -0
  78. mtppy/part4/automation_services/procedure.py +111 -0
  79. mtppy/part4/automation_services/procedure_test.py +185 -0
  80. mtppy/part4/automation_services/service.py +463 -0
  81. mtppy/part4/automation_services/service_elements/__init__.py +4 -0
  82. mtppy/part4/automation_services/service_elements/parameter_elements/__init__.py +4 -0
  83. mtppy/part4/automation_services/service_elements/parameter_elements/ana_serv_param.py +108 -0
  84. mtppy/part4/automation_services/service_elements/parameter_elements/ana_serv_param_test.py +59 -0
  85. mtppy/part4/automation_services/service_elements/parameter_elements/bin_serv_param.py +70 -0
  86. mtppy/part4/automation_services/service_elements/parameter_elements/bin_serv_param_test.py +39 -0
  87. mtppy/part4/automation_services/service_elements/parameter_elements/common_serv_param.py +111 -0
  88. mtppy/part4/automation_services/service_elements/parameter_elements/dint_serv_param.py +77 -0
  89. mtppy/part4/automation_services/service_elements/parameter_elements/dint_serv_param_test.py +58 -0
  90. mtppy/part4/automation_services/service_elements/parameter_elements/parameter_element.py +175 -0
  91. mtppy/part4/automation_services/service_elements/parameter_elements/parameter_element_test.py +246 -0
  92. mtppy/part4/automation_services/service_elements/parameter_elements/string_serv_param.py +48 -0
  93. mtppy/part4/automation_services/service_elements/parameter_elements/string_serv_param_test.py +37 -0
  94. mtppy/part4/automation_services/service_elements/procedure_health_view.py +36 -0
  95. mtppy/part4/automation_services/service_elements/service_control.py +473 -0
  96. mtppy/part4/automation_services/service_elements/service_control_test.py +150 -0
  97. mtppy/part4/automation_services/service_elements/service_element.py +20 -0
  98. mtppy/part4/automation_services/service_test.py +407 -0
  99. mtppy/part4/definitions/__init__.py +4 -0
  100. mtppy/part4/definitions/command_codes.py +58 -0
  101. mtppy/part4/definitions/command_codes_test.py +44 -0
  102. mtppy/part4/definitions/state_codes.py +65 -0
  103. mtppy/part4/definitions/state_codes_test.py +44 -0
  104. mtppy/part4/process_values/__init__.py +4 -0
  105. mtppy/part4/process_values/base_functions/__init__.py +4 -0
  106. mtppy/part4/process_values/base_functions/vqc.py +48 -0
  107. mtppy/part4/process_values/input_elements/__init__.py +4 -0
  108. mtppy/part4/process_values/input_elements/ana_process_value_in.py +160 -0
  109. mtppy/part4/process_values/input_elements/bin_process_value_in.py +83 -0
  110. mtppy/part4/process_values/input_elements/dint_process_value_in.py +159 -0
  111. mtppy/part4/process_values/input_elements/input_element.py +40 -0
  112. mtppy/part4/process_values/input_elements/string_process_value_in.py +45 -0
  113. mtppy/part5/__init__.py +4 -0
  114. mtppy/part5/communication_object.py +99 -0
  115. mtppy/part5/communication_object_test.py +59 -0
  116. mtppy/part5/definitions/__init__.py +4 -0
  117. mtppy/part5/definitions/iec61131_data_types.py +31 -0
  118. mtppy/part5/definitions/iec61131_data_types_test.py +82 -0
  119. mtppy/part5/definitions/opcua_data_types.py +36 -0
  120. mtppy/part5/definitions/opcua_data_types_test.py +90 -0
  121. mtppy/part5/definitions/w3c_xml_data_types.py +177 -0
  122. mtppy/part5/definitions/w3c_xml_data_types_test.py +143 -0
  123. mtppy/part5/server_assembly_set/__init__.py +5 -0
  124. mtppy/part6/__init__.py +4 -0
  125. mtppy/part6/alarm_set/__init__.py +4 -0
  126. mtppy/part6/alarm_set/alarm.py +132 -0
  127. mtppy/part6/alarm_set/alarm_test.py +265 -0
  128. mtppy/pea/__init__.py +19 -0
  129. mtppy/pea/data_assembly_factory.py +124 -0
  130. mtppy/pea/data_assembly_factory_test.py +229 -0
  131. mtppy/pea/mtp_generator/__init__.py +4 -0
  132. mtppy/pea/mtp_generator/mtp_generator.py +1183 -0
  133. mtppy/pea/mtp_generator/mtp_generator_test.py +518 -0
  134. mtppy/pea/pea.py +139 -0
  135. mtppy/pea/pea_opcua_server/__init__.py +4 -0
  136. mtppy/pea/pea_opcua_server/marshalling.py +60 -0
  137. mtppy/pea/pea_opcua_server/opcua_options_provider/__init__.py +4 -0
  138. mtppy/pea/pea_opcua_server/opcua_options_provider/instances/basic_opcua_options_provider.py +50 -0
  139. mtppy/pea/pea_opcua_server/opcua_options_provider/opcua_options_provider.py +103 -0
  140. mtppy/pea/pea_opcua_server/pea_opcua_server.py +492 -0
  141. mtppy/pea/pea_opcua_server/subscription_list.py +55 -0
  142. mtppy/pea/pea_test.py +59 -0
  143. mtppy/pea/rmtp_pea.py +155 -0
  144. mtppy/pea/rmtp_pea_test.py +27 -0
  145. mtppy/pea/service_factory.py +178 -0
  146. mtppy/pea/service_factory_test.py +156 -0
  147. mtppy/pea/user_logic/__init__.py +4 -0
  148. mtppy/pea/user_logic/user_logic_pea.py +72 -0
  149. mtppy/pea/user_logic/user_logic_registry.py +106 -0
  150. mtppy/pea/user_logic/user_logic_registry_test.py +167 -0
  151. mtppy/pea/user_logic/user_logic_service.py +228 -0
  152. mtppy/pea/user_logic/user_logic_service_test.py +289 -0
  153. mtppy/utils/__init__.py +4 -0
  154. mtppy/utils/data_assembly_dict.py +61 -0
  155. mtppy/utils/data_assembly_dict_test.py +107 -0
  156. mtppy/utils/data_item.py +446 -0
  157. mtppy/utils/data_item_com_test.py +82 -0
  158. mtppy/utils/data_item_test.py +325 -0
  159. mtppy/utils/logging.py +23 -0
  160. mtppy/utils/logging_test.py +26 -0
  161. mtppy/utils/mtp_file_handling/__init__.py +4 -0
  162. mtppy/utils/mtp_file_handling/extract_manifest_from_mtp_file.py +22 -0
  163. mtppy/utils/mtp_file_handling/extract_mtp_file.py +22 -0
  164. mtppy/utils/mtp_file_handling/is_valid_mtp.py +22 -0
  165. mtppy/utils/mtp_file_handling/is_valid_path_for_manifest.py +15 -0
  166. mtppy/utils/mtp_file_handling/is_valid_path_to_manifest.py +17 -0
  167. mtppy/utils/mtp_file_handling/is_valid_path_to_mtp.py +17 -0
  168. mtppy/utils/mtp_file_handling/load_manifest_file.py +24 -0
  169. mtppy/utils/mtp_file_handling/mtp_reader/__init__.py +4 -0
  170. mtppy/utils/mtp_file_handling/mtp_reader/mtp_reader.py +103 -0
  171. mtppy/utils/mtp_file_handling/mtp_reader/mtp_reader_test.py +96 -0
  172. mtppy/utils/mtp_file_handling/mtp_reader/mtp_xml.py +226 -0
  173. mtppy/utils/mtp_file_handling/mtp_reader/mtp_xml_test.py +108 -0
  174. mtppy/utils/mtp_file_handling/mtp_writer/__init__.py +4 -0
  175. mtppy/utils/mtp_file_handling/mtp_writer/mtp_writer.py +77 -0
  176. mtppy/utils/mtp_file_handling/mtp_writer/ressources/get_content_types.py +10 -0
  177. mtppy/utils/mtp_file_handling/mtp_writer/ressources/get_rels.py +11 -0
  178. mtppy/utils/mtp_file_handling/mtp_writer/test_mtp_writer.py +25 -0
  179. mtppy/utils/mtp_file_handling/write_manifest_file.py +29 -0
  180. mtppy/utils/mtp_file_handling/write_manifest_file_test.py +22 -0
  181. mtppy/utils/mtp_file_handling/write_mtp_file.py +43 -0
  182. mtppy/utils/mtp_file_handling/write_mtp_file_test.py +22 -0
  183. mtppy/utils/testing.py +31 -0
  184. mtppy2_0-0.1.5.dist-info/METADATA +75 -0
  185. mtppy2_0-0.1.5.dist-info/RECORD +214 -0
  186. mtppy2_0-0.1.5.dist-info/WHEEL +5 -0
  187. mtppy2_0-0.1.5.dist-info/entry_points.txt +3 -0
  188. mtppy2_0-0.1.5.dist-info/licenses/LICENSE +21 -0
  189. mtppy2_0-0.1.5.dist-info/top_level.txt +4 -0
  190. tools/__init__.py +0 -0
  191. tools/create_bugreport.py +150 -0
  192. tools/inspect_mtp.py +110 -0
  193. utils/__init__.py +1 -0
  194. utils/file_handling/__init__.py +4 -0
  195. utils/file_handling/cleanup_folder.py +9 -0
  196. utils/file_handling/get_utc_foldername_with_ms.py +15 -0
  197. utils/file_handling/get_work_dir.py +9 -0
  198. utils/file_handling/is_valid_path_for_json.py +11 -0
  199. utils/file_handling/is_valid_path_to_json.py +17 -0
  200. utils/file_handling/load_json_file.py +18 -0
  201. utils/file_handling/read_file_from_file_archive.py +43 -0
  202. utils/file_handling/read_file_string.py +10 -0
  203. utils/file_handling/validate_json_data.py +28 -0
  204. utils/file_handling/write_json_file.py +23 -0
  205. utils/get_uuid.py +18 -0
  206. utils/get_uuid_test.py +17 -0
  207. utils/logging.py +100 -0
  208. utils/logging_test.py +67 -0
  209. utils/opcua/check_node.py +79 -0
  210. utils/resolve_unit/resolve_unit.py +11 -0
  211. utils/resolve_unit/unit_lib.py +2101 -0
  212. utils/resolve_unit/unit_lib_test.py +36 -0
  213. utils/type_conversion.py +236 -0
  214. utils/type_conversion_test.py +114 -0
@@ -0,0 +1,518 @@
1
+ """test suite for the mtp_generator."""
2
+
3
+ # ruff: noqa: S101
4
+
5
+ __copyright__ = (
6
+ "Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
7
+ )
8
+ __license__ = "MIT"
9
+
10
+ import time
11
+ import random
12
+ import xml.etree.ElementTree as ET
13
+ from types import SimpleNamespace
14
+
15
+ import pytest
16
+
17
+ from mtppy.pea.pea import PEA
18
+ from mtppy.pea.mtp_generator.mtp_generator import MTPGenerator
19
+ from mtppy.part5.definitions.w3c_xml_data_types import W3cXmlDataTypes
20
+ from mtppy.part3.data_assembly_set.active_elements.pid.pid_ctrl import PIDCtrl
21
+ from mtppy.part3.data_assembly_set.indicator_elements.views.ana_view import AnaView
22
+ from mtppy.part3.data_assembly_set.indicator_elements.views.bin_view import BinView
23
+ from mtppy.part3.data_assembly_set.indicator_elements.views.dint_view import DIntView
24
+ from mtppy.part3.data_assembly_set.indicator_elements.views.string_view import (
25
+ StringView,
26
+ )
27
+ from mtppy.part4.automation_services.service import Service
28
+ from mtppy.part4.automation_services.procedure import Procedure
29
+ from mtppy.part4.automation_services.service_elements.service_control import (
30
+ ServiceControl,
31
+ )
32
+ from mtppy.part4.automation_services.service_elements.parameter_elements.dint_serv_param import (
33
+ DIntServParam,
34
+ )
35
+ from mtppy.part6.alarm_set.alarm import Alarm
36
+
37
+
38
+ class RandomNumberGenerator(Service):
39
+ def __init__(self, tag_name, tag_description):
40
+ super().__init__(tag_name, tag_description)
41
+
42
+ # Procedure definition
43
+ proc_0 = Procedure(1, "cont", is_self_completing=False)
44
+
45
+ # Adding two procedure parameters
46
+ proc_0.add_procedure_parameter(
47
+ DIntServParam(
48
+ "lower_bound", v_min=0, v_max=100, v_scl_min=0, v_scl_max=100, v_unit=23
49
+ )
50
+ )
51
+ proc_0.add_procedure_parameter(
52
+ DIntServParam(
53
+ "upper_bound", v_min=0, v_max=100, v_scl_min=0, v_scl_max=100, v_unit=23
54
+ )
55
+ )
56
+
57
+ # Adding procedure report value
58
+ proc_0.add_report_value(
59
+ AnaView("generated_value", v_scl_min=0, v_scl_max=100, v_unit=23),
60
+ )
61
+
62
+ # Allocating procedure to the service
63
+ self.add_procedure(proc_0)
64
+
65
+ def idle(self):
66
+ print("- Idle -")
67
+ cycle = 0
68
+ while self.is_state("idle"):
69
+ print(f"Idle cycle {cycle}")
70
+ print("Doing nothing...")
71
+ cycle += 1
72
+ time.sleep(1)
73
+
74
+ def starting(self):
75
+ print("- Starting -")
76
+ print("Applying procedure parameters...")
77
+ self.state_change()
78
+
79
+ def execute(self):
80
+ print("- Execute -")
81
+ cycle = 0
82
+ while self.is_state("execute"):
83
+ print(f"Execute cycle {cycle}")
84
+
85
+ # Read procedure parameters
86
+ lower_bound = (
87
+ self.service_control.procedures[0]
88
+ .procedure_parameters["lower_bound"]
89
+ .get_v_out()
90
+ )
91
+ upper_bound = (
92
+ self.service_control.procedures[0]
93
+ .procedure_parameters["upper_bound"]
94
+ .get_v_out()
95
+ )
96
+
97
+ # Execute random number generation
98
+ generated_number = random.randint(lower_bound, upper_bound)
99
+
100
+ # Return report value
101
+ self.service_control.procedures[0].report_values["generated_value"].set_v(
102
+ generated_number
103
+ )
104
+
105
+ cycle += 1
106
+ time.sleep(0.1)
107
+
108
+ def completing(self):
109
+ self.state_change()
110
+
111
+ def completed(self):
112
+ pass
113
+
114
+ def pausing(self):
115
+ pass
116
+
117
+ def paused(self):
118
+ pass
119
+
120
+ def resuming(self):
121
+ pass
122
+
123
+ def holding(self):
124
+ pass
125
+
126
+ def held(self):
127
+ pass
128
+
129
+ def unholding(self):
130
+ pass
131
+
132
+ def stopping(self):
133
+ pass
134
+
135
+ def stopped(self):
136
+ pass
137
+
138
+ def aborting(self):
139
+ pass
140
+
141
+ def aborted(self):
142
+ pass
143
+
144
+ def resetting(self):
145
+ print("- Resetting -")
146
+ self.state_change()
147
+
148
+
149
+ def init_empty_pea():
150
+ pea = PEA(endpoint="opc.tcp://localhost:4060/")
151
+
152
+ # Service definition
153
+ service_1 = RandomNumberGenerator(
154
+ "rand_num_gen", "This services generates random number"
155
+ )
156
+ pea.add_service(service_1)
157
+
158
+ # Active element
159
+ pid_ctrl = PIDCtrl("pid_ctrl")
160
+ pea.add_data_assembly(pid_ctrl)
161
+
162
+ return pea
163
+
164
+
165
+ def test_mtp_generator():
166
+ pea = init_empty_pea()
167
+ try:
168
+ pea.start()
169
+ mtp_generator = MTPGenerator()
170
+ mtp_generator.generate(pea)
171
+ finally:
172
+ pea.pea_opcua_server.opcua_server.stop()
173
+ time.sleep(1)
174
+
175
+
176
+ def _capture_manifest(pea: PEA) -> ET.Element:
177
+ """Generate and parse the manifest without writing an MTP archive."""
178
+ mtp_generator = MTPGenerator()
179
+ captured: list[bytes] = []
180
+
181
+ def capture(content: bytes, **_kwargs: object) -> None:
182
+ captured.append(content)
183
+
184
+ mtp_generator._mtp_writer.write_mtp = capture # noqa: SLF001
185
+ mtp_generator.generate(pea)
186
+
187
+ assert len(captured) == 1
188
+ return ET.fromstring(captured[0]) # noqa: S314
189
+
190
+
191
+ def _required_element(parent: ET.Element, path: str) -> ET.Element:
192
+ """Return a required XML element with a useful assertion failure."""
193
+ element = parent.find(path)
194
+ assert element is not None, f"Missing generated element: {path}"
195
+ return element
196
+
197
+
198
+ def test_generate_serializes_alarm_set_base_with_resolved_links() -> None:
199
+ """Generated Base alarms link their group, trigger and message targets."""
200
+ pea = PEA()
201
+ trigger_assembly = BinView("LevelHigh")
202
+ trigger_item = trigger_assembly.get_item(BinView.V_)
203
+ # Model the existing node hosted by the PEA OPC UA server. This is not a
204
+ # connection to an external OPC UA device.
205
+ trigger_item.comm_obj = SimpleNamespace(
206
+ _node_id=SimpleNamespace(
207
+ nodeid=SimpleNamespace(Identifier="LevelHigh.V"),
208
+ ),
209
+ )
210
+ pea.add_data_assembly(trigger_assembly)
211
+ pea.add_alarm(
212
+ Alarm(
213
+ tag_name="LevelHigh.Alarm",
214
+ data_assembly=trigger_assembly,
215
+ trigger=trigger_item,
216
+ trigger_value=True,
217
+ message="Level is too high.",
218
+ ack_required=True,
219
+ severity=500,
220
+ ),
221
+ )
222
+
223
+ root = _capture_manifest(pea)
224
+ data_assembly = _required_element(
225
+ root,
226
+ ".//{*}InstanceHierarchy[@Name='DataAssemblies']/"
227
+ "{*}InternalElement[@Name='LevelHigh']",
228
+ )
229
+ trigger = _required_element(
230
+ data_assembly,
231
+ "{*}Attribute[@Name='V']",
232
+ )
233
+ alarm_group = _required_element(
234
+ root,
235
+ ".//{*}InstanceHierarchy[@Name='Alarms']/"
236
+ "{*}InternalElement[@RefBaseSystemUnitPath='MTPAlarmSUCLib/AlarmGroup']",
237
+ )
238
+ alarm = _required_element(
239
+ alarm_group,
240
+ "{*}InternalElement[@RefBaseSystemUnitPath='MTPAlarmSUCLib/Alarm']",
241
+ )
242
+ message = _required_element(
243
+ root,
244
+ ".//{*}InstanceHierarchy[@Name='Texts']/"
245
+ "{*}InternalElement["
246
+ "@RefBaseSystemUnitPath='MTPTextSUCLib/TextDefinition/AlarmMessage']",
247
+ )
248
+ alarm_text = _required_element(
249
+ message,
250
+ "{*}InternalElement["
251
+ "@RefBaseSystemUnitPath="
252
+ "'MTPTextSUCLib/TextDefinition/AlarmMessage/AlarmText']",
253
+ )
254
+
255
+ assert alarm_group.get("Name") == "LevelHigh_AlarmGroup"
256
+ assert alarm_group.findtext("{*}Attribute[@Name='RefID']/{*}Value") == (
257
+ data_assembly.findtext("{*}Attribute[@Name='RefID']/{*}Value")
258
+ )
259
+ assert alarm.get("Name") == "LevelHigh.Alarm"
260
+ assert alarm.findtext("{*}Attribute[@Name='AckRequired']/{*}Value") == "true"
261
+ assert alarm.findtext("{*}Attribute[@Name='Severity']/{*}Value") == "500"
262
+ assert alarm.findtext("{*}Attribute[@Name='Trigger']/{*}Value") == trigger.get(
263
+ "ID",
264
+ )
265
+ assert trigger.get("AttributeDataType") == "xs:string"
266
+ assert alarm.findtext("{*}Attribute[@Name='TriggerValue']/{*}Value") == ("true")
267
+ assert alarm.findtext("{*}Attribute[@Name='MessageRef']/{*}Value") == (
268
+ message.get("ID")
269
+ )
270
+ assert alarm_text.findtext("{*}Attribute[@Name='Text']/{*}Value") == (
271
+ "Level is too high."
272
+ )
273
+
274
+ trigger_reference = _required_element(
275
+ alarm,
276
+ "{*}Attribute[@Name='Trigger']",
277
+ )
278
+ message_reference = _required_element(
279
+ alarm,
280
+ "{*}Attribute[@Name='MessageRef']",
281
+ )
282
+ trigger_value = _required_element(
283
+ alarm,
284
+ "{*}Attribute[@Name='TriggerValue']",
285
+ )
286
+ classification = _required_element(
287
+ alarm,
288
+ "{*}Attribute[@Name='Classification']",
289
+ )
290
+ assert trigger_reference.get("RefAttributeType") == (
291
+ "MTPATLib/IDReferenceType/IDLinkAttributeType"
292
+ )
293
+ assert message_reference.get("RefAttributeType") == (
294
+ "MTPTextATLib/TextRefAttributeType"
295
+ )
296
+ assert trigger_value.get("AttributeDataType") == "xs:boolean"
297
+ assert classification.get("RefAttributeType") == (
298
+ "AutomationMLBaseAttributeTypeLib/OrderedListType"
299
+ )
300
+
301
+ object_ids = {element.get("ID") for element in root.iter() if element.get("ID")}
302
+ assert trigger_reference.findtext("{*}Value") in object_ids
303
+ assert message_reference.findtext("{*}Value") in object_ids
304
+ pea_server_reference = trigger.findtext("{*}Value")
305
+ pea_server_item = _required_element(
306
+ root,
307
+ ".//{*}InstanceHierarchy[@Name='ServerAssemblies']/"
308
+ "{*}InternalElement/"
309
+ f"{{*}}ExternalInterface[@ID='{pea_server_reference}']",
310
+ )
311
+ assert pea_server_item.findtext(
312
+ "{*}Attribute[@Name='Identifier']/{*}Value",
313
+ ) == ("LevelHigh.V")
314
+ assert alarm.find("{*}Attribute[@Name='AlarmMgmtRef']") is None
315
+ assert (
316
+ alarm.find(
317
+ "{*}SupportedRoleClass[@RefRoleClassPath='MTPAlarmRCLib/IsManaged']",
318
+ )
319
+ is None
320
+ )
321
+ assert (
322
+ root.find(
323
+ ".//{*}InternalElement["
324
+ "@RefBaseSystemUnitPath="
325
+ "'MTPDataAssemblySUCLib/DataAssembly/AlarmElement/AlarmMgmt']",
326
+ )
327
+ is None
328
+ )
329
+
330
+
331
+ def test_generate_rejects_alarm_with_unregistered_data_assembly() -> None:
332
+ """Generator fails instead of emitting a dangling AlarmGroup RefID."""
333
+ pea = PEA()
334
+ trigger_assembly = BinView("NotRegistered")
335
+ pea.add_alarm(
336
+ Alarm(
337
+ tag_name="NotRegistered.Alarm",
338
+ data_assembly=trigger_assembly,
339
+ trigger=trigger_assembly.get_item(BinView.V_),
340
+ trigger_value=True,
341
+ message="Unregistered trigger.",
342
+ ack_required=False,
343
+ severity=100,
344
+ ),
345
+ )
346
+ mtp_generator = MTPGenerator()
347
+ mtp_generator._mtp_writer.write_mtp = lambda *_args, **_kwargs: None # noqa: SLF001
348
+
349
+ with pytest.raises(ValueError, match="registered DataAssembly"):
350
+ mtp_generator.generate(pea)
351
+
352
+
353
+ def test_generate_revalidates_mutated_alarm_trigger_type() -> None:
354
+ """Generator rejects an Alarm whose mutable DataItem changed after creation."""
355
+ pea = PEA()
356
+ trigger_assembly = BinView("Mutated")
357
+ alarm = Alarm(
358
+ tag_name="Mutated.Alarm",
359
+ data_assembly=trigger_assembly,
360
+ trigger=trigger_assembly.get_item(BinView.V_),
361
+ trigger_value=True,
362
+ message="Mutated trigger.",
363
+ ack_required=False,
364
+ severity=100,
365
+ )
366
+ pea.add_data_assembly(trigger_assembly)
367
+ pea.add_alarm(alarm)
368
+ alarm.trigger.w3c_xml_data_type = W3cXmlDataTypes.FLOAT
369
+ mtp_generator = MTPGenerator()
370
+ mtp_generator._mtp_writer.write_mtp = lambda *_args, **_kwargs: None # noqa: SLF001
371
+
372
+ with pytest.raises(TypeError, match="trigger type"):
373
+ mtp_generator.generate(pea)
374
+
375
+
376
+ def test_alarm_links_stay_with_multiply_registered_data_assembly() -> None:
377
+ """Alarm group and trigger links resolve within the same DataAssembly."""
378
+ pea = PEA()
379
+ service = RandomNumberGenerator(
380
+ "DuplicatedService",
381
+ "Service control registered through two public paths",
382
+ )
383
+ target = service.service_control
384
+ trigger = target.get_item(ServiceControl.PROC_PARAM_APPLY_EN)
385
+ pea.add_data_assembly(target)
386
+ pea.add_service(service)
387
+ pea.add_alarm(
388
+ Alarm(
389
+ tag_name="DuplicatedService.Alarm",
390
+ data_assembly=target,
391
+ trigger=trigger,
392
+ trigger_value=False,
393
+ message="Duplicate target.",
394
+ ack_required=False,
395
+ severity=100,
396
+ ),
397
+ )
398
+
399
+ root = _capture_manifest(pea)
400
+ data_assemblies = root.findall(
401
+ ".//{*}InstanceHierarchy[@Name='DataAssemblies']/"
402
+ "{*}InternalElement[@Name='DuplicatedService']",
403
+ )
404
+ alarm_group = _required_element(
405
+ root,
406
+ ".//{*}InstanceHierarchy[@Name='Alarms']/"
407
+ "{*}InternalElement[@RefBaseSystemUnitPath='MTPAlarmSUCLib/AlarmGroup']",
408
+ )
409
+ alarm = _required_element(
410
+ alarm_group,
411
+ "{*}InternalElement[@RefBaseSystemUnitPath='MTPAlarmSUCLib/Alarm']",
412
+ )
413
+ group_ref = alarm_group.findtext("{*}Attribute[@Name='RefID']/{*}Value")
414
+ trigger_id = alarm.findtext("{*}Attribute[@Name='Trigger']/{*}Value")
415
+ referenced_assemblies = [
416
+ assembly
417
+ for assembly in data_assemblies
418
+ if assembly.findtext("{*}Attribute[@Name='RefID']/{*}Value") == group_ref
419
+ ]
420
+
421
+ assert len(referenced_assemblies) == 1
422
+ assert (
423
+ referenced_assemblies[0].find(
424
+ f"{{*}}Attribute[@ID='{trigger_id}']",
425
+ )
426
+ is not None
427
+ )
428
+
429
+
430
+ def test_generate_groups_alarms_by_referenced_data_assembly() -> None:
431
+ """Alarms sharing a DataAssembly share one generated AlarmGroup."""
432
+ pea = PEA()
433
+ first_assembly = BinView("First")
434
+ second_assembly = BinView("Second")
435
+ pea.add_data_assembly(first_assembly)
436
+ pea.add_data_assembly(second_assembly)
437
+ for tag_name, assembly, trigger_value in (
438
+ ("First.High", first_assembly, True),
439
+ ("First.Normal", first_assembly, False),
440
+ ("Second.High", second_assembly, True),
441
+ ):
442
+ pea.add_alarm(
443
+ Alarm(
444
+ tag_name=tag_name,
445
+ data_assembly=assembly,
446
+ trigger=assembly.get_item(BinView.V_),
447
+ trigger_value=trigger_value,
448
+ message=f"{tag_name} message.",
449
+ ack_required=False,
450
+ severity=100,
451
+ ),
452
+ )
453
+
454
+ root = _capture_manifest(pea)
455
+ alarm_hierarchy = _required_element(
456
+ root,
457
+ ".//{*}InstanceHierarchy[@Name='Alarms']",
458
+ )
459
+ groups = alarm_hierarchy.findall(
460
+ "{*}InternalElement[@RefBaseSystemUnitPath='MTPAlarmSUCLib/AlarmGroup']",
461
+ )
462
+ assert {group.get("Name") for group in groups} == {
463
+ "First_AlarmGroup",
464
+ "Second_AlarmGroup",
465
+ }
466
+ alarm_counts = {
467
+ group.get("Name"): len(
468
+ group.findall(
469
+ "{*}InternalElement[" "@RefBaseSystemUnitPath='MTPAlarmSUCLib/Alarm']",
470
+ ),
471
+ )
472
+ for group in groups
473
+ }
474
+ assert alarm_counts == {"First_AlarmGroup": 2, "Second_AlarmGroup": 1}
475
+
476
+ first_data_assembly = _required_element(
477
+ root,
478
+ ".//{*}InstanceHierarchy[@Name='DataAssemblies']/"
479
+ "{*}InternalElement[@Name='First']",
480
+ )
481
+ assert len(first_data_assembly.findall("{*}Attribute[@Name='V']")) == 1
482
+
483
+
484
+ @pytest.mark.parametrize(
485
+ ("assembly_type", "trigger_value", "xml_type"),
486
+ [(DIntView, 12, "xs:short"), (StringView, "ALARM", "xs:string")],
487
+ )
488
+ def test_generate_preserves_trigger_value_xml_type(
489
+ assembly_type: type[DIntView] | type[StringView],
490
+ trigger_value: int | str,
491
+ xml_type: str,
492
+ ) -> None:
493
+ """TriggerValue carries the concrete W3C type of integer and string data."""
494
+ pea = PEA()
495
+ trigger_assembly = assembly_type("TypedTrigger")
496
+ pea.add_data_assembly(trigger_assembly)
497
+ pea.add_alarm(
498
+ Alarm(
499
+ tag_name="TypedTrigger.Alarm",
500
+ data_assembly=trigger_assembly,
501
+ trigger=trigger_assembly.get_item(trigger_assembly.V_),
502
+ trigger_value=trigger_value,
503
+ message="Typed trigger.",
504
+ ack_required=False,
505
+ severity=100,
506
+ ),
507
+ )
508
+
509
+ root = _capture_manifest(pea)
510
+ trigger_value_attribute = _required_element(
511
+ root,
512
+ ".//{*}InstanceHierarchy[@Name='Alarms']/"
513
+ "{*}InternalElement[@RefBaseSystemUnitPath='MTPAlarmSUCLib/AlarmGroup']/"
514
+ "{*}InternalElement[@RefBaseSystemUnitPath='MTPAlarmSUCLib/Alarm']/"
515
+ "{*}Attribute[@Name='TriggerValue']",
516
+ )
517
+ assert trigger_value_attribute.get("AttributeDataType") == xml_type
518
+ assert trigger_value_attribute.findtext("{*}Value") == str(trigger_value)
mtppy/pea/pea.py ADDED
@@ -0,0 +1,139 @@
1
+ """The PEA (Process Equipment Assembly)."""
2
+
3
+ __copyright__ = (
4
+ "Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
5
+ )
6
+ __license__ = "MIT"
7
+
8
+ from mtppy.pea.pea_opcua_server.opcua_options_provider.instances.basic_opcua_options_provider import (
9
+ BasicOPCUAOptionsProvider,
10
+ )
11
+ from mtppy.pea.pea_opcua_server.pea_opcua_server import PeaOPCUAServer
12
+ from mtppy.pea.pea_opcua_server.subscription_list import SubscriptionList
13
+
14
+ from mtppy.part3.data_assembly_set.data_assembly import DataAssembly
15
+ from mtppy.part3.data_assembly_set.indicator_elements.indicator_element import (
16
+ IndicatorElement,
17
+ )
18
+ from mtppy.part3.data_assembly_set.pea_elements.pea_information_label.pea_information_label import (
19
+ PeaInformationLabel,
20
+ )
21
+ from mtppy.part4.process_values.input_elements.input_element import InputElement
22
+ from mtppy.part4.automation_services.service import Service
23
+ from mtppy.part6.alarm_set.alarm import Alarm
24
+
25
+ from mtppy.utils.data_assembly_dict import DataAssemblyDict
26
+
27
+
28
+ class PEA:
29
+ """Main Process Equipment Assembly (PEA) class.
30
+
31
+ The PEA class serves as the central coordinator for managing all aspects of a
32
+ Process Element Assembly, including data assemblies, process values, services,
33
+ and OPC UA server functionality. It provides methods to add components and control
34
+ the overall operation of the PEA system.
35
+
36
+ Attributes:
37
+ service_set: Dictionary of service instances managed by this PEA
38
+ data_assemblies: Dictionary of data assemblies managed by this PEA
39
+ process_value_ins: Dictionary of input process values managed by this PEA
40
+ process_value_outs: Dictionary of output process values managed by this PEA
41
+ alarms: Dictionary of alarm instances managed by this PEA
42
+ endpoint: OPC UA server endpoint address
43
+ opcua_options_provider: Provider for OPC UA server options
44
+ pea_opcua_server: OPC UA server instance for this PEA
45
+ subscription_list: List of active subscriptions
46
+ web_server_url: URL for web server access
47
+ pea_information_label: Information label containing PEA metadata
48
+ """
49
+
50
+ def __init__(self, endpoint: str = "opc.tcp://localhost:4840/") -> None:
51
+ """Initializes the PEA with default or specified OPC UA endpoint.
52
+
53
+ Args:
54
+ endpoint: Endpoint of the OPC UA server. If not specified, opc.tcp://localhost:4840/ is used.
55
+ """
56
+ self.service_set = DataAssemblyDict(Service)
57
+ self.data_assemblies = DataAssemblyDict(DataAssembly)
58
+ self.process_value_ins = DataAssemblyDict(InputElement)
59
+ self.process_value_outs = DataAssemblyDict(IndicatorElement)
60
+ self.alarms = DataAssemblyDict(Alarm)
61
+ self.endpoint = endpoint
62
+ self.opcua_options_provider = BasicOPCUAOptionsProvider()
63
+ self.pea_opcua_server = PeaOPCUAServer(
64
+ self.opcua_options_provider, self.endpoint
65
+ )
66
+ self.subscription_list = SubscriptionList()
67
+ self.web_server_url = None
68
+
69
+ self.pea_information_label = PeaInformationLabel("PeaInformationLabel", "")
70
+
71
+ def add_data_assembly(
72
+ self,
73
+ data_assembly: DataAssembly | list[DataAssembly] | dict[str, DataAssembly],
74
+ ) -> None:
75
+ """Add a DataAssembly to the PEA.
76
+
77
+ Args:
78
+ data_assembly: The DataAssembly instance to add
79
+ """
80
+ self.data_assemblies.add(data_assembly)
81
+
82
+ def add_alarm(self, alarm: Alarm) -> None:
83
+ """Add a static AlarmSet.Base alarm to the PEA.
84
+
85
+ Args:
86
+ alarm: Alarm description to register.
87
+
88
+ Raises:
89
+ TypeError: If alarm is not an Alarm instance.
90
+ ValueError: If an alarm with the same tag name is already registered.
91
+ """
92
+ if not isinstance(alarm, Alarm):
93
+ msg = f"{getattr(alarm, 'tag_name', repr(alarm))} is not an Alarm"
94
+ raise TypeError(msg)
95
+ if alarm.tag_name in self.alarms:
96
+ msg = f"Alarm {alarm.tag_name!r} is already registered"
97
+ raise ValueError(msg)
98
+ self.alarms.add(alarm)
99
+
100
+ def add_process_value_in(self, process_value_in: InputElement):
101
+ """Add an input ProcessValue to the PEA.
102
+
103
+ Args:
104
+ process_value_in: The InputElement instance to add as input process value
105
+ """
106
+ self.process_value_ins.add(process_value_in)
107
+
108
+ def add_process_value_out(self, process_value_out: IndicatorElement):
109
+ """Add an output ProcessValue to the PEA.
110
+
111
+ Args:
112
+ process_value_out: The output process value to add.
113
+ """
114
+ self.process_value_outs.add(process_value_out)
115
+
116
+ def add_service(self, service: Service):
117
+ """Add a Service to the PEA.
118
+
119
+ Args:
120
+ service: The Service instance to add
121
+ """
122
+ self.service_set.add(service)
123
+
124
+ def start(self):
125
+ """Start the OPC UA server instance and begin PEA operation.
126
+
127
+ Initializes and starts the OPC UA server with the configured data assemblies
128
+ and services, making the PEA available for communication and operation.
129
+ """
130
+ self.pea_opcua_server.init_opcua_server()
131
+ self.pea_opcua_server.run_opcua_server(self.data_assemblies, self.service_set)
132
+
133
+ def stop(self):
134
+ """Stop the OPC UA server instance and shut down PEA operation.
135
+
136
+ Stops the OPC UA server and performs cleanup operations to properly
137
+ terminate the PEA's operation.
138
+ """
139
+ self.pea_opcua_server.opcua_server.stop()
@@ -0,0 +1,4 @@
1
+ __copyright__ = (
2
+ "Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
3
+ )
4
+ __license__ = "MIT"