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,265 @@
1
+ """Tests for static AlarmSet.Base alarm descriptions."""
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
+ from typing import ClassVar
11
+
12
+ import pytest
13
+ from mtppy.utils.data_item import DataItem
14
+ from mtppy.part3.data_assembly_set.data_assembly import DataAssembly
15
+ from mtppy.part3.definitions.mtp_access import MTPAccess
16
+ from mtppy.part5.definitions.w3c_xml_data_types import W3cXmlDataTypes
17
+ from mtppy.part6.alarm_set.alarm import Alarm
18
+
19
+ DEFAULT_SEVERITY = 500
20
+ INTEGER_RANGES = [
21
+ (W3cXmlDataTypes.BYTE, -(2**7), (2**7) - 1),
22
+ (W3cXmlDataTypes.UNSIGNED_BYTE, 0, (2**8) - 1),
23
+ (W3cXmlDataTypes.SHORT, -(2**15), (2**15) - 1),
24
+ (W3cXmlDataTypes.UNSIGNED_SHORT, 0, (2**16) - 1),
25
+ (W3cXmlDataTypes.INT, -(2**31), (2**31) - 1),
26
+ (W3cXmlDataTypes.UNSIGNED_INT, 0, (2**32) - 1),
27
+ (W3cXmlDataTypes.LONG, -(2**63), (2**63) - 1),
28
+ (W3cXmlDataTypes.UNSIGNED_LONG, 0, (2**64) - 1),
29
+ ]
30
+
31
+
32
+ class TriggerAssembly(DataAssembly):
33
+ """DataAssembly exposing all trigger types relevant to the tests."""
34
+
35
+ BOOL_TRIGGER = "BoolTrigger"
36
+ INT_TRIGGER = "IntTrigger"
37
+ TEXT_TRIGGER = "TextTrigger"
38
+ FLOAT_TRIGGER = "FloatTrigger"
39
+
40
+ CLASS_DATA_ITEMS: ClassVar = {
41
+ BOOL_TRIGGER: DataItem(
42
+ BOOL_TRIGGER,
43
+ W3cXmlDataTypes.BOOLEAN,
44
+ MTPAccess.ReadAccess,
45
+ init_value=False,
46
+ ),
47
+ INT_TRIGGER: DataItem(
48
+ INT_TRIGGER,
49
+ W3cXmlDataTypes.INT,
50
+ MTPAccess.ReadAccess,
51
+ 0,
52
+ ),
53
+ TEXT_TRIGGER: DataItem(
54
+ TEXT_TRIGGER,
55
+ W3cXmlDataTypes.STRING,
56
+ MTPAccess.ReadAccess,
57
+ "",
58
+ ),
59
+ FLOAT_TRIGGER: DataItem(
60
+ FLOAT_TRIGGER,
61
+ W3cXmlDataTypes.FLOAT,
62
+ MTPAccess.ReadAccess,
63
+ 0.0,
64
+ ),
65
+ }
66
+
67
+
68
+ def build_alarm(
69
+ *,
70
+ assembly: TriggerAssembly | None = None,
71
+ trigger_name: str = TriggerAssembly.BOOL_TRIGGER,
72
+ trigger_value: object = True,
73
+ severity: int = DEFAULT_SEVERITY,
74
+ ack_required: bool = True,
75
+ tag_name: str = "HighLevel.Alarm",
76
+ message: str = "Level is too high.",
77
+ ) -> Alarm:
78
+ """Build a valid alarm while allowing one property to be varied."""
79
+ target = assembly or TriggerAssembly("Triggers")
80
+ return Alarm(
81
+ tag_name=tag_name,
82
+ data_assembly=target,
83
+ trigger=target.get_item(trigger_name),
84
+ trigger_value=trigger_value,
85
+ severity=severity,
86
+ ack_required=ack_required,
87
+ message=message,
88
+ )
89
+
90
+
91
+ def build_integer_alarm(
92
+ data_type: W3cXmlDataTypes,
93
+ trigger_value: int,
94
+ ) -> Alarm:
95
+ """Build an alarm with one concrete W3C integer trigger type."""
96
+
97
+ class IntegerTriggerAssembly(DataAssembly):
98
+ """DataAssembly containing the parameterized integer trigger."""
99
+
100
+ TRIGGER = "Trigger"
101
+ CLASS_DATA_ITEMS: ClassVar = {
102
+ TRIGGER: DataItem(
103
+ TRIGGER,
104
+ data_type,
105
+ MTPAccess.ReadAccess,
106
+ init_value=0,
107
+ ),
108
+ }
109
+
110
+ assembly = IntegerTriggerAssembly("IntegerTrigger")
111
+ return Alarm(
112
+ tag_name="Integer.Alarm",
113
+ data_assembly=assembly,
114
+ trigger=assembly.get_item(IntegerTriggerAssembly.TRIGGER),
115
+ trigger_value=trigger_value,
116
+ severity=DEFAULT_SEVERITY,
117
+ ack_required=False,
118
+ message="Integer alarm.",
119
+ )
120
+
121
+
122
+ def test_alarm_preserves_static_base_metadata() -> None:
123
+ """A valid AlarmSet.Base description retains its public metadata."""
124
+ assembly = TriggerAssembly("Triggers")
125
+ trigger = assembly.get_item(TriggerAssembly.BOOL_TRIGGER)
126
+
127
+ alarm = build_alarm(assembly=assembly)
128
+
129
+ assert alarm.tag_name == "HighLevel.Alarm"
130
+ assert alarm.data_assembly is assembly
131
+ assert alarm.trigger is trigger
132
+ assert alarm.trigger_value is True
133
+ assert alarm.severity == DEFAULT_SEVERITY
134
+ assert alarm.ack_required is True
135
+ assert alarm.message == "Level is too high."
136
+
137
+
138
+ @pytest.mark.parametrize("severity", [1, 500, 1000])
139
+ def test_alarm_accepts_valid_severity_boundaries(severity: int) -> None:
140
+ """Severity accepts the complete specified inclusive range."""
141
+ assert build_alarm(severity=severity).severity == severity
142
+
143
+
144
+ @pytest.mark.parametrize("severity", [0, 1001, -1, True, 1.0, "500", None])
145
+ def test_alarm_rejects_invalid_severity(severity: object) -> None:
146
+ """Severity rejects out-of-range values and non-integer types."""
147
+ with pytest.raises((TypeError, ValueError)):
148
+ build_alarm(severity=severity) # type: ignore[arg-type]
149
+
150
+
151
+ @pytest.mark.parametrize("ack_required", [0, 1, "true", None])
152
+ def test_alarm_rejects_non_boolean_ack_required(ack_required: object) -> None:
153
+ """AckRequired must be an actual bool rather than a truthy value."""
154
+ with pytest.raises(TypeError):
155
+ build_alarm(ack_required=ack_required) # type: ignore[arg-type]
156
+
157
+
158
+ @pytest.mark.parametrize(
159
+ ("tag_name", "message"),
160
+ [("", "message"), (" ", "message"), ("alarm", ""), ("alarm", " ")],
161
+ )
162
+ def test_alarm_rejects_empty_names_and_messages(tag_name: str, message: str) -> None:
163
+ """Alarm identity and operator message must contain visible text."""
164
+ with pytest.raises(ValueError, match="must not be empty"):
165
+ build_alarm(tag_name=tag_name, message=message)
166
+
167
+
168
+ @pytest.mark.parametrize(
169
+ ("trigger_name", "trigger_value"),
170
+ [
171
+ (TriggerAssembly.BOOL_TRIGGER, True),
172
+ (TriggerAssembly.INT_TRIGGER, 12),
173
+ (TriggerAssembly.TEXT_TRIGGER, "ALARM"),
174
+ ],
175
+ )
176
+ def test_alarm_accepts_supported_trigger_types(
177
+ trigger_name: str,
178
+ trigger_value: object,
179
+ ) -> None:
180
+ """Boolean, integer and string equality triggers are supported."""
181
+ alarm = build_alarm(trigger_name=trigger_name, trigger_value=trigger_value)
182
+
183
+ assert alarm.trigger_value == trigger_value
184
+
185
+
186
+ @pytest.mark.parametrize(("data_type", "lower", "upper"), INTEGER_RANGES)
187
+ def test_alarm_accepts_integer_type_boundaries(
188
+ data_type: W3cXmlDataTypes,
189
+ lower: int,
190
+ upper: int,
191
+ ) -> None:
192
+ """Every supported W3C integer type accepts both inclusive boundaries."""
193
+ assert build_integer_alarm(data_type, lower).trigger_value == lower
194
+ assert build_integer_alarm(data_type, upper).trigger_value == upper
195
+
196
+
197
+ @pytest.mark.parametrize(("data_type", "lower", "upper"), INTEGER_RANGES)
198
+ def test_alarm_rejects_values_outside_integer_type_range(
199
+ data_type: W3cXmlDataTypes,
200
+ lower: int,
201
+ upper: int,
202
+ ) -> None:
203
+ """W3C integer triggers reject values that cannot be represented."""
204
+ for invalid_value in (lower - 1, upper + 1):
205
+ with pytest.raises(ValueError, match="range"):
206
+ build_integer_alarm(data_type, invalid_value)
207
+
208
+
209
+ @pytest.mark.parametrize(
210
+ ("trigger_name", "trigger_value"),
211
+ [
212
+ (TriggerAssembly.BOOL_TRIGGER, 1),
213
+ (TriggerAssembly.INT_TRIGGER, True),
214
+ (TriggerAssembly.INT_TRIGGER, 1.0),
215
+ (TriggerAssembly.TEXT_TRIGGER, b"ALARM"),
216
+ ],
217
+ )
218
+ def test_alarm_requires_exact_trigger_value_type(
219
+ trigger_name: str,
220
+ trigger_value: object,
221
+ ) -> None:
222
+ """TriggerValue must use the exact Python type declared by the DataItem."""
223
+ with pytest.raises(TypeError, match="TriggerValue"):
224
+ build_alarm(trigger_name=trigger_name, trigger_value=trigger_value)
225
+
226
+
227
+ def test_alarm_rejects_float_trigger() -> None:
228
+ """AlarmSet.Base does not support floating-point equality triggers."""
229
+ with pytest.raises(TypeError, match="trigger type"):
230
+ build_alarm(
231
+ trigger_name=TriggerAssembly.FLOAT_TRIGGER,
232
+ trigger_value=1.5,
233
+ )
234
+
235
+
236
+ def test_alarm_trigger_must_belong_to_data_assembly() -> None:
237
+ """A trigger cannot be linked through an unrelated DataAssembly."""
238
+ assembly = TriggerAssembly("Triggers")
239
+ foreign = TriggerAssembly("Foreign")
240
+
241
+ with pytest.raises(ValueError, match="DataAssembly"):
242
+ Alarm(
243
+ tag_name="Foreign.Alarm",
244
+ data_assembly=assembly,
245
+ trigger=foreign.get_item(TriggerAssembly.BOOL_TRIGGER),
246
+ trigger_value=True,
247
+ severity=500,
248
+ ack_required=True,
249
+ message="Foreign trigger.",
250
+ )
251
+
252
+
253
+ def test_base_alarm_does_not_install_runtime_callbacks() -> None:
254
+ """Describing a base alarm must not alter DataItem runtime behavior."""
255
+ assembly = TriggerAssembly("Triggers")
256
+ trigger = assembly.get_item(TriggerAssembly.BOOL_TRIGGER)
257
+ internal_callback = trigger.int_wrt_cb
258
+ external_callback = trigger.ext_wrt_cb
259
+
260
+ alarm = build_alarm(assembly=assembly)
261
+
262
+ assert trigger.int_wrt_cb is internal_callback
263
+ assert trigger.ext_wrt_cb is external_callback
264
+ assert not hasattr(alarm, "acknowledge")
265
+ assert not hasattr(alarm, "shelve")
mtppy/pea/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ __copyright__ = (
2
+ "Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
3
+ )
4
+ __license__ = "MIT"
5
+
6
+ from typing import TYPE_CHECKING
7
+
8
+ if TYPE_CHECKING:
9
+ from mtppy.pea.rmtp_pea import RMTPPea
10
+
11
+ __all__ = ["RMTPPea"]
12
+
13
+
14
+ def __getattr__(name: str):
15
+ if name == "RMTPPea":
16
+ from mtppy.pea.rmtp_pea import RMTPPea
17
+
18
+ return RMTPPea
19
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,124 @@
1
+ """Factory for creating data assemblies from a DataAssemblyConfig dictionary.
2
+
3
+ This module provides a factory class for creating DataAssembly instances
4
+ based on a configuration dictionaries of type DataAssemblyConfig.
5
+
6
+ """
7
+
8
+ __copyright__ = (
9
+ "Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
10
+ )
11
+ __license__ = "MIT"
12
+
13
+ from typing import ClassVar
14
+
15
+ from mtppy.part3.data_assembly_set.data_assembly import DataAssembly
16
+ from mtppy.part3.data_assembly_set.pea_elements.pea_information_label.pea_information_label import (
17
+ PeaInformationLabel,
18
+ )
19
+ from mtppy.part3.data_assembly_set.active_elements.pid.pid_ctrl import PIDCtrl
20
+ from mtppy.part3.data_assembly_set.indicator_elements.views.ana_view import AnaView
21
+ from mtppy.part3.data_assembly_set.indicator_elements.views.bin_view import BinView
22
+ from mtppy.part3.data_assembly_set.indicator_elements.views.dint_view import DIntView
23
+ from mtppy.part3.data_assembly_set.indicator_elements.views.string_view import (
24
+ StringView,
25
+ )
26
+ from mtppy.part4.automation_services.service_elements.parameter_elements.ana_serv_param import (
27
+ AnaServParam,
28
+ )
29
+ from mtppy.part4.automation_services.service_elements.parameter_elements.bin_serv_param import (
30
+ BinServParam,
31
+ )
32
+ from mtppy.part4.automation_services.service_elements.parameter_elements.dint_serv_param import (
33
+ DIntServParam,
34
+ )
35
+ from mtppy.part4.automation_services.service_elements.parameter_elements.string_serv_param import (
36
+ StringServParam,
37
+ )
38
+ from mtppy.part4.automation_services.service_elements.procedure_health_view import (
39
+ ProcedureHealthView,
40
+ )
41
+ from mtppy.part4.automation_services.service_elements.service_control import (
42
+ ServiceControl,
43
+ )
44
+
45
+ DataAssemblyConfig = dict[str, str | dict[str, str | None]]
46
+
47
+
48
+ class DataAssemblyFactory:
49
+ """Factory class for creating DataAssembly instances."""
50
+
51
+ _ROOT: str = "MTPDataAssemblySUCLib/DataAssembly"
52
+
53
+ _FACTORY: ClassVar[dict[str, type[DataAssembly]]] = {
54
+ f"{_ROOT}/ActiveElement/PIDCtrl": PIDCtrl,
55
+ f"{_ROOT}/IndicatorElement/AnaView": AnaView,
56
+ f"{_ROOT}/IndicatorElement/BinView": BinView,
57
+ f"{_ROOT}/IndicatorElement/DIntView": DIntView,
58
+ f"{_ROOT}/IndicatorElement/StringView": StringView,
59
+ f"{_ROOT}/ServiceElement/ParameterElement/DIntServParam": DIntServParam,
60
+ f"{_ROOT}/ServiceElement/ParameterElement/AnaServParam": AnaServParam,
61
+ f"{_ROOT}/ServiceElement/ParameterElement/BinServParam": BinServParam,
62
+ f"{_ROOT}/ServiceElement/ParameterElement/StringServParam": StringServParam,
63
+ f"{_ROOT}/PeaElement/PeaInformationLabel": PeaInformationLabel,
64
+ f"{_ROOT}/ServiceElement/ProcedureHealthView": ProcedureHealthView,
65
+ # TODO: reintroduce after fixing namespace handling
66
+ # f"{_ROOT}/ServiceElement/ServiceControl": ServiceControl,
67
+ }
68
+
69
+ @staticmethod
70
+ def create_data_assemblies(
71
+ da_dict: dict[str, DataAssemblyConfig],
72
+ ) -> dict[str, DataAssembly]:
73
+ """Create multiple DataAssembly instances from a dictionary of configurations.
74
+
75
+ Args:
76
+ da_dict: Dictionary mapping names to DataAssemblyConfig dictionaries.
77
+
78
+ Returns:
79
+ Dictionary mapping names to created DataAssembly instances.
80
+ """
81
+ data_assemblies: dict[str, DataAssembly] = {}
82
+ for key, value in da_dict.items():
83
+ data_assembly = DataAssemblyFactory.create_data_assembly(value)
84
+ if data_assembly is not None:
85
+ data_assemblies[key] = data_assembly
86
+ return data_assemblies
87
+
88
+ @staticmethod
89
+ def create_data_assembly(config: DataAssemblyConfig) -> DataAssembly | None:
90
+ """Create a single DataAssembly instance from a configuration entry.
91
+ If a default value is given in the config this value is set as init_value of
92
+ the underlying DataItem and the actual value of the DataAssembly is
93
+ initialized accordingly.
94
+
95
+ Args:
96
+ config: Configuration dictionary for the data assembly.
97
+
98
+ Returns:
99
+ Created DataAssembly instance.
100
+ """
101
+
102
+ da_type: str = config["RefBaseSystemUnitPath"]
103
+ tag_name: str = config["TagName"]["value"]
104
+ tag_description: str = (
105
+ config["TagDescription"]["value"] if "TagDescription" in config else ""
106
+ )
107
+
108
+ if da_type in DataAssemblyFactory._FACTORY:
109
+ instance = DataAssemblyFactory._FACTORY[da_type](
110
+ tag_name=tag_name,
111
+ tag_description=tag_description,
112
+ )
113
+ for alias, v in config.items():
114
+ if (
115
+ "default" in v
116
+ and v["default"] is not None
117
+ and len(v["default"]) > 0
118
+ ):
119
+ if alias in instance.keys():
120
+ da = instance.get_item(alias)
121
+ da.init_value = da.w3c_xml_data_type.converse(v["default"])
122
+ instance[alias] = da.w3c_xml_data_type.converse(v["default"])
123
+ return instance
124
+ return None
@@ -0,0 +1,229 @@
1
+ """
2
+ Unit tests for the DataAssemblyFactory class.
3
+
4
+ This module contains tests for the DataAssemblyFactory which creates
5
+ DataAssembly instances from configuration dictionaries.
6
+ """
7
+
8
+ import pytest
9
+
10
+ from mtppy.pea.data_assembly_factory import DataAssemblyFactory, DataAssemblyConfig
11
+ from mtppy.part3.data_assembly_set.data_assembly import DataAssembly
12
+ from mtppy.part3.data_assembly_set.active_elements.pid.pid_ctrl import PIDCtrl
13
+ from mtppy.part3.data_assembly_set.indicator_elements.views.ana_view import AnaView
14
+ from mtppy.part3.data_assembly_set.pea_elements.pea_information_label.pea_information_label import (
15
+ PeaInformationLabel,
16
+ )
17
+ from mtppy.part4.automation_services.service_elements.procedure_health_view import (
18
+ ProcedureHealthView,
19
+ )
20
+ from mtppy.part4.automation_services.service_elements.service_control import (
21
+ ServiceControl,
22
+ )
23
+
24
+
25
+ @pytest.fixture
26
+ def basic_config() -> DataAssemblyConfig:
27
+ """Create a basic configuration for testing."""
28
+ return {
29
+ "RefBaseSystemUnitPath": "MTPDataAssemblySUCLib/DataAssembly/IndicatorElement/AnaView",
30
+ "TagName": {"value": "TestAnaView"},
31
+ "TagDescription": {"value": "Test Description"},
32
+ "VSclMin": {"default": "10"},
33
+ "VSclMax": {"default": "100"},
34
+ }
35
+
36
+
37
+ @pytest.fixture
38
+ def pid_config() -> DataAssemblyConfig:
39
+ """Create a PID controller configuration for testing."""
40
+ return {
41
+ "RefBaseSystemUnitPath": "MTPDataAssemblySUCLib/DataAssembly/ActiveElement/PIDCtrl",
42
+ "TagName": {"value": "TestPID"},
43
+ "TagDescription": {"value": "Test PID Controller"},
44
+ "PVSclMin": {"default": "-100.0"},
45
+ "PVSclMax": {"default": "100.0"},
46
+ }
47
+
48
+
49
+ @pytest.fixture
50
+ def ana_serv_config() -> DataAssemblyConfig:
51
+ """Create an ana serv parameter configuration for testing."""
52
+ return {
53
+ "RefBaseSystemUnitPath": "MTPDataAssemblySUCLib/DataAssembly/ServiceElement/ParameterElement/AnaServParam",
54
+ "TagName": {"value": "Input Setpoint"},
55
+ "VMin": {"default": "10.0"},
56
+ "VMax": {"default": "90.0"},
57
+ "VSclMin": {"default": "0.0"},
58
+ "VSclMax": {"default": "100.0"},
59
+ }
60
+
61
+
62
+ def test_create_data_assembly_success(basic_config: DataAssemblyConfig):
63
+ """Test successful creation of a DataAssembly instance."""
64
+ # Test with basic configuration
65
+ data_assembly = DataAssemblyFactory.create_data_assembly(basic_config)
66
+
67
+ assert data_assembly is not None
68
+ assert isinstance(data_assembly, AnaView)
69
+ assert hasattr(data_assembly, "tag_name")
70
+ assert hasattr(data_assembly, "tag_description")
71
+ assert data_assembly.tag_name == "TestAnaView"
72
+ assert data_assembly.tag_description == "Test Description"
73
+ assert data_assembly.VSclMin == 10.0
74
+ assert data_assembly.VSclMax == 100.0
75
+
76
+
77
+ def test_create_data_assembly_with_default_value(basic_config: DataAssemblyConfig):
78
+ """Test DataAssembly creation with default value."""
79
+ # Modify config to include another default value
80
+ basic_config["VSclMin"] = {"default": "20.0"}
81
+
82
+ data_assembly = DataAssemblyFactory.create_data_assembly(basic_config)
83
+
84
+ # Check that default value was set correctly
85
+
86
+ assert data_assembly is not None
87
+ assert data_assembly["VSclMin"] == 20.0
88
+
89
+
90
+ def test_created_data_assembly_inner_logic(ana_serv_config: DataAssemblyConfig):
91
+ data_assembly = DataAssemblyFactory.create_data_assembly(ana_serv_config)
92
+
93
+ # setup channels for internal access
94
+ assert data_assembly is not None
95
+ data_assembly.service_op_mode.StateChannel = True
96
+ data_assembly.service_src_mode.SrcChannel = True
97
+ # put in automatic state (from off most obviously)
98
+ data_assembly.service_op_mode.StateAutAut = True
99
+ assert data_assembly.service_op_mode.StateAutAct == True
100
+ # put in internal state
101
+ data_assembly.service_src_mode.SrcIntAut = True
102
+ assert data_assembly.service_src_mode.SrcIntAct == True
103
+
104
+ # set a value within the bounds
105
+ data_assembly.VInt = 30
106
+ data_assembly.ApplyInt = True
107
+ assert data_assembly.VOut == 30
108
+
109
+ # set a value outside the bounds
110
+ data_assembly.VInt = -10
111
+ data_assembly.ApplyInt = True
112
+ assert data_assembly.VOut == 30
113
+
114
+
115
+ def test_create_data_assembly_pid_controller(pid_config: DataAssemblyConfig):
116
+ """Test creation of PID controller DataAssembly."""
117
+ data_assembly = DataAssemblyFactory.create_data_assembly(pid_config)
118
+
119
+ assert data_assembly is not None
120
+ assert isinstance(data_assembly, PIDCtrl)
121
+ assert data_assembly.tag_name == "TestPID"
122
+ assert data_assembly.tag_description == "Test PID Controller"
123
+ assert data_assembly.PVSclMin == -100.0
124
+ assert data_assembly.PVSclMax == 100.0
125
+
126
+
127
+ @pytest.mark.parametrize(
128
+ ("assembly_type", "path"),
129
+ [
130
+ (
131
+ PeaInformationLabel,
132
+ "MTPDataAssemblySUCLib/DataAssembly/PeaElement/PeaInformationLabel",
133
+ ),
134
+ (
135
+ ProcedureHealthView,
136
+ "MTPDataAssemblySUCLib/DataAssembly/ServiceElement/ProcedureHealthView",
137
+ ),
138
+ # TODO: reintroduce after fixing namespace handling
139
+ # (
140
+ # ServiceControl,
141
+ # "MTPDataAssemblySUCLib/DataAssembly/ServiceElement/ServiceControl",
142
+ # ),
143
+ ],
144
+ )
145
+ def test_create_newly_supported_data_assemblies(
146
+ assembly_type: type[DataAssembly], path: str
147
+ ):
148
+ """Create each data assembly type registered by the factory."""
149
+ config: DataAssemblyConfig = {
150
+ "RefBaseSystemUnitPath": path,
151
+ "TagName": {"value": "TestAssembly"},
152
+ "TagDescription": {"value": "Test description"},
153
+ }
154
+
155
+ data_assembly = DataAssemblyFactory.create_data_assembly(config)
156
+
157
+ assert data_assembly is not None
158
+ assert isinstance(data_assembly, assembly_type)
159
+ assert data_assembly.tag_name == "TestAssembly"
160
+ assert data_assembly.tag_description == "Test description"
161
+
162
+
163
+ def test_create_data_assembly_returns_none_for_unknown_type():
164
+ """Unsupported AML paths are ignored by the factory."""
165
+ config: DataAssemblyConfig = {
166
+ "RefBaseSystemUnitPath": "Unknown/DataAssembly",
167
+ "TagName": {"value": "Unsupported"},
168
+ }
169
+
170
+ assert DataAssemblyFactory.create_data_assembly(config) is None
171
+
172
+
173
+ def test_create_data_assembly_ignores_empty_default(basic_config: DataAssemblyConfig):
174
+ """Empty defaults do not overwrite a class default value."""
175
+ basic_config["VSclMin"] = {"default": ""}
176
+
177
+ data_assembly = DataAssemblyFactory.create_data_assembly(basic_config)
178
+
179
+ assert data_assembly is not None
180
+ assert data_assembly.VSclMin == 0.0
181
+
182
+
183
+ def test_create_data_assemblies_multiple():
184
+ """Test creating multiple DataAssembly instances."""
185
+ config_dict: dict[str, DataAssemblyConfig] = {
186
+ "assembly1": {
187
+ "RefBaseSystemUnitPath": "MTPDataAssemblySUCLib/DataAssembly/IndicatorElement/AnaView",
188
+ "TagName": {"value": "TestAnaView1"},
189
+ "TagDescription": {"value": "Test Description 1"},
190
+ },
191
+ "assembly2": {
192
+ "RefBaseSystemUnitPath": "MTPDataAssemblySUCLib/DataAssembly/IndicatorElement/BinView",
193
+ "TagName": {"value": "TestBinView"},
194
+ "TagDescription": {"value": "Test Description 2"},
195
+ },
196
+ }
197
+
198
+ assemblies = DataAssemblyFactory.create_data_assemblies(config_dict)
199
+
200
+ assert len(assemblies) == 2
201
+ assert "assembly1" in assemblies
202
+ assert "assembly2" in assemblies
203
+ assert assemblies["assembly1"].tag_name == "TestAnaView1"
204
+ assert assemblies["assembly2"].tag_name == "TestBinView"
205
+
206
+
207
+ def test_factory_contains_expected_types():
208
+ """Test that the factory contains expected types."""
209
+ expected_types = [
210
+ "MTPDataAssemblySUCLib/DataAssembly/ActiveElement/PIDCtrl",
211
+ "MTPDataAssemblySUCLib/DataAssembly/IndicatorElement/AnaView",
212
+ "MTPDataAssemblySUCLib/DataAssembly/IndicatorElement/BinView",
213
+ "MTPDataAssemblySUCLib/DataAssembly/IndicatorElement/DIntView",
214
+ "MTPDataAssemblySUCLib/DataAssembly/ServiceElement/ParameterElement/DIntServParam",
215
+ "MTPDataAssemblySUCLib/DataAssembly/ServiceElement/ParameterElement/AnaServParam",
216
+ "MTPDataAssemblySUCLib/DataAssembly/ServiceElement/ParameterElement/BinServParam",
217
+ "MTPDataAssemblySUCLib/DataAssembly/ServiceElement/ParameterElement/StringServParam",
218
+ "MTPDataAssemblySUCLib/DataAssembly/PeaElement/PeaInformationLabel",
219
+ "MTPDataAssemblySUCLib/DataAssembly/ServiceElement/ProcedureHealthView",
220
+ # TODO: reintroduce after fixing namespace handling
221
+ # "MTPDataAssemblySUCLib/DataAssembly/ServiceElement/ServiceControl",
222
+ ]
223
+
224
+ for expected_type in expected_types:
225
+ assert expected_type in DataAssemblyFactory._FACTORY
226
+
227
+
228
+ if __name__ == "__main__":
229
+ pytest.main([__file__])
@@ -0,0 +1,4 @@
1
+ __copyright__ = (
2
+ "Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
3
+ )
4
+ __license__ = "MIT"