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,492 @@
1
+ """Implementation of the PEA opcua server."""
2
+
3
+ __copyright__ = (
4
+ "Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
5
+ )
6
+ __license__ = "MIT"
7
+
8
+ from typing import Any
9
+ from opcua import Server, ua
10
+
11
+ from mtppy.utils.logging import logger
12
+ from mtppy.pea.pea_opcua_server.opcua_options_provider.opcua_options_provider import (
13
+ OPCUAOptionsProvider,
14
+ )
15
+ from mtppy.pea.pea_opcua_server.marshalling import Marshalling
16
+ from mtppy.part5.communication_object import OPCUACommunicationObject
17
+ from mtppy.part3.data_assembly_set.data_assembly import DataAssembly
18
+ from mtppy.pea.pea_opcua_server.subscription_list import SubscriptionList
19
+ from mtppy.part3.definitions.mtp_access import MTPAccess
20
+ from mtppy.part5.definitions.w3c_xml_data_types import W3cXmlDataTypes
21
+ from mtppy.part4.automation_services.procedure import Procedure
22
+ from mtppy.part4.automation_services.service import Service
23
+
24
+
25
+ class PeaOPCUAServer:
26
+ def __init__(
27
+ self,
28
+ opcua_options_provider: OPCUAOptionsProvider,
29
+ endpoint: str = "opc.tcp://localhost:4840/",
30
+ ) -> None:
31
+ """
32
+ Initializes an OPC UA server for PEA.
33
+
34
+ Args:
35
+ opcua_options_provider: Instance of an OPCUAOptionsProvider.
36
+ endpoint: Endpoint of the OPC UA server. If not specified, opc.tcp://localhost:4840/ is used.
37
+ """
38
+ self.endpoint = endpoint
39
+ self.opcua_server = None
40
+ self.opcua_ns = None
41
+ self.opcua_ns_index = None
42
+ self.opcua_options_provider = opcua_options_provider
43
+ self.subscription_list = SubscriptionList()
44
+
45
+ def init_opcua_server(self) -> None:
46
+ """Initializes an OPC UA server."""
47
+ logger.opcua(f"Initializing PEA OPC UA server: {self.endpoint}")
48
+ self.opcua_server = Server()
49
+ self.opcua_server.set_endpoint(self.endpoint)
50
+
51
+ # set all possible endpoint policies for clients to connect through
52
+ self.opcua_server.set_security_policy(
53
+ [
54
+ ua.SecurityPolicyType.NoSecurity,
55
+ # ua.SecurityPolicyType.Basic256Sha256_SignAndEncrypt,
56
+ # ua.SecurityPolicyType.Basic256Sha256_Sign,
57
+ ]
58
+ )
59
+
60
+ # init our own namespace
61
+ namespace_uri = self.opcua_options_provider.get_default_namespace_uri()
62
+ self.opcua_ns_index = self.opcua_server.register_namespace(namespace_uri)
63
+
64
+ def _get_opcua_server(self):
65
+ """
66
+ Gets OPC UA server instance object.
67
+
68
+ Returns:
69
+ The OPC UA server instance.
70
+ """
71
+ return self.opcua_server
72
+
73
+ def _get_opcua_ns_index(self) -> int:
74
+ """
75
+ Gets OPC UA server namespace index of default namespace uri.
76
+
77
+ Returns:
78
+ The OPC UA server namespace index.
79
+ """
80
+ return self.opcua_ns_index
81
+
82
+ def get_node_part_ns_index(self) -> str:
83
+ """Get OPC UA server namespace index of default namespace uri.
84
+
85
+ Returns:
86
+ The OPC UA server namespace index as a string.
87
+ """
88
+ return f"ns={self.opcua_ns_index};"
89
+
90
+ def _get_opcua_ns_uri(self):
91
+ """Get OPC UA server default namespace.
92
+
93
+ Returns:
94
+ The OPC UA server default namespace.
95
+ """
96
+ return self.opcua_options_provider.get_default_namespace_uri()
97
+
98
+ def run_opcua_server(self, data_assemblies, service_set):
99
+ """Start the OPC UA server instance."""
100
+ self.opcua_server.start()
101
+ self._build_opcua_server(data_assemblies, service_set)
102
+ self._start_subscription()
103
+
104
+ def _build_opcua_server(self, data_assemblies, service_set):
105
+ """Create an OPC UA server instance adding required nodes according to
106
+ defined DataAssemblies."""
107
+ logger.opcua(f"Adding OPC UA nodes")
108
+ server = self.opcua_server.get_objects_node()
109
+
110
+ # Iterate over DataAssemblies
111
+ root_node_data_assemblies_folder = server.add_folder(
112
+ f"{self.get_node_part_ns_index()}s=data_assemblies", "data_assemblies"
113
+ )
114
+ for data_assembly_instance in data_assemblies.values():
115
+ logger.opcua(f"- DataAssembly {data_assembly_instance.tag_name}")
116
+ self._create_opcua_objects_for_data_assembly(
117
+ data_assembly_instance, root_node_data_assemblies_folder
118
+ )
119
+
120
+ # Iterate over services
121
+ root_node_services_folder = server.add_folder(
122
+ f"{self.get_node_part_ns_index()}s=services", "services"
123
+ )
124
+ for service_instance in service_set.values():
125
+ logger.opcua(f"- Service {service_instance.name}")
126
+ self._create_opcua_objects_for_service(
127
+ service_instance, root_node_services_folder
128
+ )
129
+
130
+ def _create_opcua_objects_for_service(self, service_instance: Service, root_node):
131
+
132
+ root_node_service_folder = root_node.add_folder(
133
+ f"{self.get_node_part_ns_index()}s=service.{service_instance.name}",
134
+ service_instance.name,
135
+ )
136
+
137
+ logger.opcua(f"- Service Control {service_instance.service_control.tag_name}")
138
+ self._create_opcua_objects_for_data_assembly(
139
+ service_instance.service_control, root_node_service_folder
140
+ )
141
+
142
+ if service_instance.configuration_parameters:
143
+ root_node_service_config_param_folder = root_node_service_folder.add_folder(
144
+ f"{self.get_node_part_ns_index()}s=service.{service_instance.name}.configuration_parameters",
145
+ "configuration_parameters",
146
+ )
147
+ for (
148
+ configuration_parameter
149
+ ) in service_instance.configuration_parameters.values():
150
+ logger.opcua(
151
+ f"- Service Configuration Parameter {configuration_parameter.tag_name}"
152
+ )
153
+ self._create_opcua_objects_for_data_assembly(
154
+ configuration_parameter, root_node_service_config_param_folder
155
+ )
156
+
157
+ root_node_service_procedures_folder = root_node_service_folder.add_folder(
158
+ f"{self.get_node_part_ns_index()}s=service.{service_instance.name}.procedures",
159
+ "procedures",
160
+ )
161
+ for procedure in service_instance._get_procedures().values():
162
+ logger.opcua(f"- Service Procedure {procedure.name}")
163
+ self._create_opcua_objects_for_procedure(
164
+ procedure, root_node_service_procedures_folder, service_instance.name
165
+ )
166
+
167
+ def _create_opcua_objects_for_procedure(
168
+ self, procedure: Procedure, root_node, service_name: str
169
+ ):
170
+
171
+ root_node_procedure_folder = root_node.add_folder(
172
+ f"{self.get_node_part_ns_index()}s=service.{service_name}.procedures.{procedure.name}",
173
+ procedure.name,
174
+ )
175
+
176
+ logger.opcua(f"- Procedure {procedure.name}")
177
+ self._create_opcua_objects_for_data_assembly(
178
+ procedure.procedure_health_view, root_node_procedure_folder
179
+ )
180
+
181
+ if procedure.procedure_parameters:
182
+ root_node_service_proc_param_folder = root_node_procedure_folder.add_folder(
183
+ f"{self.get_node_part_ns_index()}s=service.{service_name}.procedures.{procedure.name}.procedure_parameters",
184
+ "procedure_parameters",
185
+ )
186
+ for procedure_parameter in procedure.procedure_parameters.values():
187
+ logger.opcua(
188
+ f"- Procedure ProcedureParameter {procedure_parameter.tag_name}"
189
+ )
190
+ self._create_opcua_objects_for_data_assembly(
191
+ procedure_parameter, root_node_service_proc_param_folder
192
+ )
193
+
194
+ if procedure.process_value_ins:
195
+ root_node_service_process_value_ins_folder = root_node_procedure_folder.add_folder(
196
+ f"{self.get_node_part_ns_index()}s=service.{service_name}.procedures.{procedure.name}.process_value_ins",
197
+ "process_value_ins",
198
+ )
199
+ for process_value_in in procedure.process_value_ins.values():
200
+ logger.opcua(f"- Procedure ProcessValueIn {process_value_in.tag_name}")
201
+ self._create_opcua_objects_for_data_assembly(
202
+ process_value_in, root_node_service_process_value_ins_folder
203
+ )
204
+
205
+ if procedure.process_value_outs:
206
+ root_node_service_process_value_outs_folder = root_node_procedure_folder.add_folder(
207
+ f"{self.get_node_part_ns_index()}s=service.{service_name}.procedures.{procedure.name}.process_value_outs",
208
+ "process_value_outs",
209
+ )
210
+ for process_value_out in procedure.process_value_outs.values():
211
+ logger.opcua(
212
+ f"- Procedure ProcessValueOut {process_value_out.tag_name}"
213
+ )
214
+ self._create_opcua_objects_for_data_assembly(
215
+ process_value_out, root_node_service_process_value_outs_folder
216
+ )
217
+ if procedure.report_values:
218
+ root_node_service_report_values_folder = root_node_procedure_folder.add_folder(
219
+ f"{self.get_node_part_ns_index()}s=service.{service_name}.procedures.{procedure.name}.report_values",
220
+ "report_values",
221
+ )
222
+ for report_value in procedure.report_values.values():
223
+ logger.opcua(f"- Procedure ReportValue {report_value.tag_name}")
224
+ self._create_opcua_objects_for_data_assembly(
225
+ report_value, root_node_service_report_values_folder
226
+ )
227
+
228
+ def node_exists(self, nodeid):
229
+ try:
230
+ node = self.opcua_server.get_node(nodeid)
231
+ _ = node.get_browse_name() # Triggers error if node doesn't exist
232
+ return True
233
+ except ua.uaerrors._auto.BadNodeIdUnknown:
234
+ return False
235
+
236
+ def is_folder(self, nodeid):
237
+ try:
238
+ # Get the NodeClass of the node
239
+ node = self.opcua_server.get_node(nodeid)
240
+ node_class = node.get_node_class()
241
+ if node_class == ua.NodeClass.Object:
242
+ # Check if the ObjectType is FolderType
243
+ object_type = node.get_type_definition()
244
+ if object_type.Identifier == ua.ObjectIds.FolderType:
245
+ return True
246
+ return False
247
+ except Exception as e:
248
+ print(f"Error checking if node is a folder: {e}")
249
+ return False
250
+
251
+ # root_node_data_assembly = root_node.add_folder( f"{self.get_node_part_ns_index()}s={tag_name}", tag_name)
252
+ def add_folder_if_not_exists(self, root_node, tag_name):
253
+ # Construct the NodeId for the folder
254
+ parent_identifier = root_node.nodeid.Identifier
255
+ folder_identifier = (
256
+ f"{parent_identifier}.{tag_name}"
257
+ if isinstance(parent_identifier, str)
258
+ else tag_name
259
+ )
260
+ node_id = f"{self.get_node_part_ns_index()}s={folder_identifier}"
261
+ children = root_node.get_children()
262
+
263
+ # Check if a child with the desired browse name already exists
264
+ for child in children:
265
+ if (
266
+ child.get_browse_name().Name == tag_name
267
+ and child.nodeid.to_string() == node_id
268
+ ):
269
+ logger.opcua(f"Folder '{tag_name}' exists with NodeId: {child.nodeid}")
270
+ return child # Return the existing node
271
+
272
+ # If the folder does not exist, create it
273
+ if self.node_exists(node_id) and self.is_folder(node_id):
274
+ node = self.opcua_server.get_node(node_id)
275
+ root_node.add_reference(node.nodeid, ua.ObjectIds.Organizes)
276
+ logger.opcua(
277
+ f"Folder '{node.get_browse_name().Name}' exists with NodeId: {node.nodeid}"
278
+ )
279
+ return node
280
+ else:
281
+ new_folder = root_node.add_folder(node_id, tag_name)
282
+ logger.opcua(
283
+ f"Folder '{tag_name}' created with NodeId: {new_folder.nodeid}"
284
+ )
285
+ return new_folder
286
+
287
+ def _create_opcua_objects_for_data_assembly(
288
+ self, data_assembly: DataAssembly, root_node
289
+ ):
290
+ """Iterate over DataAssemblies to create OPC UA objects.
291
+
292
+ Args:
293
+ data_assembly: DataAssembly.
294
+ root_node: Parent OPC UA node where the DataAssembly shall be attached.
295
+ """
296
+ # create new root node for DataAssembly
297
+ if hasattr(data_assembly, "tag_name"):
298
+ tag_name = data_assembly.tag_name
299
+ elif hasattr(data_assembly, "CLASS_DATA_ITEMS"):
300
+ tag_name = data_assembly["TagName"]
301
+
302
+ try:
303
+ root_node_data_assembly = self.add_folder_if_not_exists(root_node, tag_name)
304
+
305
+ base_functions = [
306
+ "op_mode",
307
+ "src_mode",
308
+ "os_level",
309
+ "wqc",
310
+ "vqc",
311
+ "service_op_mode",
312
+ "service_src_mode",
313
+ "service_operator_interaction",
314
+ ]
315
+
316
+ if hasattr(data_assembly, "_data_items"):
317
+ self._create_opcua_nodes_for_data_items(
318
+ data_assembly._data_items,
319
+ tag_name,
320
+ root_node_data_assembly,
321
+ )
322
+
323
+ for base_function_name in base_functions:
324
+ if not hasattr(data_assembly, base_function_name):
325
+ continue
326
+ base_function_root_node = self.add_folder_if_not_exists(
327
+ root_node_data_assembly, base_function_name
328
+ )
329
+ base_function = eval(f"data_assembly.{base_function_name}")
330
+ logger.info(
331
+ f"Creating opcua nodes for {base_function_name}, type{type(base_function)}"
332
+ )
333
+ self._create_opcua_nodes_for_data_items(
334
+ base_function._data_items, # TBD: brutal, mitten rein greifen, am offenen Herzen
335
+ tag_name,
336
+ base_function_root_node,
337
+ )
338
+ except ua.UaStatusCodeError as e:
339
+ msg = f"Error creating DataAssembly {tag_name}: {e}"
340
+ logger.warning(msg)
341
+
342
+ def add_node_or_reference(
343
+ self,
344
+ data_item: DataAssembly,
345
+ parent_node: ua.NodeId,
346
+ node_id: str,
347
+ browse_name: str,
348
+ init_value: Any,
349
+ variant_type: ua.VariantType,
350
+ reference_type: int = ua.ObjectIds.Organizes,
351
+ ):
352
+ """
353
+ Adds a node to the namespace if it does not exist. If the node already exists,
354
+ it creates a reference to the existing node.
355
+
356
+ Arguments:
357
+ parent_node: The parent node under which the new node or reference will be added.
358
+ node_id: The NodeId of the node to add or reference.
359
+ browse_name: The BrowseName of the node to add or reference.
360
+ variant_type: The type of the variable node
361
+ reference_type: The type of reference to create if the node already exists (default is Organizes).
362
+
363
+ Retuns
364
+ The created or referenced node.
365
+ """
366
+ # Check if the node already exists in the namespace
367
+ existing_node = next(
368
+ (
369
+ child
370
+ for child in parent_node.get_children()
371
+ if child.nodeid.to_string() == node_id
372
+ ),
373
+ None,
374
+ )
375
+
376
+ if existing_node is None and self.node_exists(node_id):
377
+ existing_node = self.opcua_server.get_node(node_id)
378
+
379
+ if existing_node:
380
+ # Node already exists, create a reference to it
381
+ parent_node.add_reference(existing_node.nodeid, reference_type)
382
+ logger.opcua(f"Reference added to existing node: {browse_name}")
383
+ return existing_node
384
+ else:
385
+ # Node does not exist, create it
386
+ opcua_node_obj = parent_node.add_variable(
387
+ node_id,
388
+ browse_name,
389
+ init_value,
390
+ varianttype=variant_type,
391
+ )
392
+ opcua_comm_obj = OPCUACommunicationObject(
393
+ opcua_node_obj, node_id=opcua_node_obj
394
+ )
395
+ data_item.attach_communication_object(opcua_comm_obj)
396
+ if (data_item.access is MTPAccess.ReadWriteAccess) or (
397
+ data_item.access is MTPAccess.WriteAccess
398
+ ):
399
+ opcua_node_obj.set_writable(True)
400
+ self.subscription_list.append(opcua_node_obj, data_item.ext_wrt_cb)
401
+ else:
402
+ opcua_node_obj.set_writable(False)
403
+ logger.opcua(f"Node created: {browse_name}")
404
+ return opcua_node_obj
405
+
406
+ def _create_opcua_nodes_for_data_items(
407
+ self, data_items, parent_name: str, root_node
408
+ ):
409
+ """
410
+ Iterate over DataItems of DataAssembly to create corresponding OPC UA nodes.
411
+
412
+ Args:
413
+ data_items: Elements of DataItem that an OPC UA node is to created for.
414
+ parent_name: Name of the DataItem parent, should be TagName of DataAssembly.
415
+ root_node: Parent OPC UA node where the Data is to add to.
416
+
417
+ """
418
+ logger.debug(f"Creating OPC UA nodes for: {parent_name}")
419
+
420
+ for data_item in data_items.values():
421
+ data_item_node_id = (
422
+ self.get_node_part_ns_index()
423
+ + self.opcua_options_provider.generate_node_id(
424
+ data_assembly_name=parent_name, data_item_name=data_item.name
425
+ )
426
+ )
427
+
428
+ # We attach communication objects to be able to write values on opcua server on DataItems change
429
+ opcua_type = self._map_data_type(data_item.w3c_xml_data_type)
430
+
431
+ _ = self.add_node_or_reference(
432
+ data_item=data_item,
433
+ parent_node=root_node,
434
+ node_id=data_item_node_id,
435
+ browse_name=data_item.name,
436
+ init_value=data_item.init_value,
437
+ variant_type=opcua_type,
438
+ )
439
+ # opcua_node_obj = root_node.add_variable(
440
+ # data_item_node_id,
441
+ # data_item.name,
442
+ # data_item.init_value,
443
+ # varianttype=opcua_type,
444
+ # )
445
+ # opcua_node_obj.set_attribute(
446
+ # ua.AttributeIds.DisplayName,
447
+ # ua.DataValue(ua.LocalizedText(data_item.name)),
448
+ # )
449
+ # opcua_node_obj.set_attribute(
450
+ # ua.AttributeIds.Description,
451
+ # ua.DataValue(ua.LocalizedText(data_item.description)),
452
+ # )
453
+
454
+ logger.opcua(
455
+ f"OPCUA Node: {data_item_node_id}, Name: {data_item.name}, Value: {data_item.init_value}"
456
+ )
457
+
458
+ @staticmethod
459
+ def _map_data_type(w3c_xml_data_type):
460
+ """
461
+ Translate a python data type to a suitable OPC UA data type.
462
+
463
+ Args:
464
+ w3c_xml_data_type: W3cXmlDataType.
465
+
466
+ Returns:
467
+ OPC UA data type
468
+ """
469
+ if not isinstance(w3c_xml_data_type, W3cXmlDataTypes):
470
+ raise TypeError(
471
+ f"Invalid argument: {w3c_xml_data_type} is not a valid W3cXmlDataType"
472
+ )
473
+
474
+ opcua_variant = w3c_xml_data_type.opcua_variant
475
+ if opcua_variant is None:
476
+ raise TypeError(
477
+ f"Data type {w3c_xml_data_type} is not supported for OPC UA mapping"
478
+ )
479
+ return opcua_variant
480
+
481
+ def _start_subscription(self):
482
+ """
483
+ Subscribes to defined OPC UA nodes.
484
+ :return:
485
+ """
486
+ handler = Marshalling()
487
+ handler.import_subscription_list(self.subscription_list)
488
+ node_ids = self.subscription_list.get_nodeid_list()
489
+ if not node_ids:
490
+ return
491
+ sub = self.opcua_server.create_subscription(500, handler)
492
+ sub.subscribe_data_change(node_ids)
@@ -0,0 +1,55 @@
1
+ """Subscription list that contains all OPC UA nodes that a PEA must subscribe to."""
2
+
3
+ __copyright__ = (
4
+ "Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
5
+ )
6
+ __license__ = "MIT"
7
+
8
+
9
+ class SubscriptionList:
10
+ """A class to manage subscriptions to OPC UA nodes.
11
+
12
+ This class maintains a list of OPC UA nodes that a PEA (Process Execution Agent)
13
+ must subscribe to. It provides methods to add subscriptions, retrieve node IDs,
14
+ and obtain callback functions for value changes.
15
+ """
16
+
17
+ def __init__(self):
18
+ """Initialize instance of Subscription List with empty list."""
19
+ self.sub_list = {}
20
+
21
+ def append(self, node_id, cb_value_change):
22
+ """Add a subscription entity.
23
+
24
+ Args:
25
+ node_id: OPC UA node.
26
+ cb_value_change: Callback function for a value change.
27
+ """
28
+ identifier = node_id.nodeid.Identifier
29
+ self.sub_list[identifier] = {"node_id": node_id, "callback": cb_value_change}
30
+
31
+ def get_nodeid_list(self):
32
+ """Extract a list of node ids in the subscription list.
33
+
34
+ Returns:
35
+ List of node ids.
36
+ """
37
+ if len(self.sub_list) == 0:
38
+ return None
39
+ else:
40
+ node_id_list = []
41
+ for node_id in self.sub_list.values():
42
+ node_id_list.append(node_id["node_id"])
43
+ return node_id_list
44
+
45
+ def get_callback(self, node_id):
46
+ """Get a callback function for a specific OPC UA node.
47
+
48
+ Args:
49
+ node_id: OPC UA node id
50
+ """
51
+ identifier = node_id.nodeid.Identifier
52
+ if identifier in self.sub_list.keys():
53
+ return self.sub_list[identifier]["callback"]
54
+ else:
55
+ return None
mtppy/pea/pea_test.py ADDED
@@ -0,0 +1,59 @@
1
+ """Tests for PEA component registration."""
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 pytest
11
+
12
+ from mtppy.part3.data_assembly_set.indicator_elements.views.bin_view import BinView
13
+ from mtppy.part6.alarm_set.alarm import Alarm
14
+ from mtppy.pea.pea import PEA
15
+
16
+
17
+ def build_alarm(tag_name: str = "LevelHigh.Alarm") -> Alarm:
18
+ """Build an alarm suitable for PEA registration tests."""
19
+ trigger_assembly = BinView("LevelHigh")
20
+ return Alarm(
21
+ tag_name=tag_name,
22
+ data_assembly=trigger_assembly,
23
+ trigger=trigger_assembly.get_item(BinView.V_),
24
+ trigger_value=True,
25
+ message="Level is too high.",
26
+ ack_required=True,
27
+ severity=500,
28
+ )
29
+
30
+
31
+ def test_add_alarm_registers_alarm_by_tag_name() -> None:
32
+ """PEA exposes registered alarms through its alarm dictionary."""
33
+ pea = PEA()
34
+ alarm = build_alarm()
35
+
36
+ pea.add_alarm(alarm)
37
+
38
+ assert pea.alarms == {alarm.tag_name: alarm}
39
+
40
+
41
+ def test_add_alarm_rejects_wrong_type() -> None:
42
+ """PEA rejects arbitrary objects in the alarm registry."""
43
+ pea = PEA()
44
+
45
+ with pytest.raises(TypeError, match="Alarm"):
46
+ pea.add_alarm(object()) # type: ignore[arg-type]
47
+
48
+
49
+ def test_add_alarm_rejects_duplicate_tag_name() -> None:
50
+ """A later alarm must not silently replace an existing alarm."""
51
+ pea = PEA()
52
+ first = build_alarm()
53
+ duplicate = build_alarm()
54
+ pea.add_alarm(first)
55
+
56
+ with pytest.raises(ValueError, match="already registered"):
57
+ pea.add_alarm(duplicate)
58
+
59
+ assert pea.alarms[first.tag_name] is first