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.
- examples/__init__.py +0 -0
- examples/minimal+pid.py +82 -0
- examples/minimal.py +32 -0
- examples/minimal_recipe.py +117 -0
- examples/pea_alarm_base.py +47 -0
- examples/pea_minimal.py +258 -0
- mtppy/__init__.py +1 -0
- mtppy/part1/__init__.py +4 -0
- mtppy/part1/empty_folder_test.py +7 -0
- mtppy/part3/__init__.py +4 -0
- mtppy/part3/data_assembly_set/__init__.py +4 -0
- mtppy/part3/data_assembly_set/active_elements/__init__.py +4 -0
- mtppy/part3/data_assembly_set/active_elements/active_element.py +22 -0
- mtppy/part3/data_assembly_set/active_elements/drives/__init__.py +4 -0
- mtppy/part3/data_assembly_set/active_elements/drives/ana_drv.py +446 -0
- mtppy/part3/data_assembly_set/active_elements/drives/ana_drv_test.py +567 -0
- mtppy/part3/data_assembly_set/active_elements/drives/bin_drv.py +365 -0
- mtppy/part3/data_assembly_set/active_elements/drives/bin_drv_test.py +503 -0
- mtppy/part3/data_assembly_set/active_elements/drives/mon_ana_drv.py +469 -0
- mtppy/part3/data_assembly_set/active_elements/drives/mon_ana_drv_test.py +418 -0
- mtppy/part3/data_assembly_set/active_elements/drives/mon_bin_drv.py +295 -0
- mtppy/part3/data_assembly_set/active_elements/drives/mon_bin_drv_test.py +265 -0
- mtppy/part3/data_assembly_set/active_elements/pid/__init__.py +4 -0
- mtppy/part3/data_assembly_set/active_elements/pid/pid_ctrl.py +333 -0
- mtppy/part3/data_assembly_set/active_elements/pid/pid_ctrl_test.py +156 -0
- mtppy/part3/data_assembly_set/active_elements/valves/__init__.py +4 -0
- mtppy/part3/data_assembly_set/active_elements/valves/ana_vlv.py +402 -0
- mtppy/part3/data_assembly_set/active_elements/valves/ana_vlv_test.py +732 -0
- mtppy/part3/data_assembly_set/active_elements/valves/bin_vlv.py +279 -0
- mtppy/part3/data_assembly_set/active_elements/valves/bin_vlv_test.py +585 -0
- mtppy/part3/data_assembly_set/active_elements/valves/mon_ana_vlv.py +402 -0
- mtppy/part3/data_assembly_set/active_elements/valves/mon_ana_vlv_test.py +270 -0
- mtppy/part3/data_assembly_set/active_elements/valves/mon_bin_vlv.py +286 -0
- mtppy/part3/data_assembly_set/active_elements/valves/mon_bin_vlv_test.py +224 -0
- mtppy/part3/data_assembly_set/base_functions/__init__.py +4 -0
- mtppy/part3/data_assembly_set/base_functions/operation_mode.py +252 -0
- mtppy/part3/data_assembly_set/base_functions/operation_mode_test.py +260 -0
- mtppy/part3/data_assembly_set/base_functions/oslevel.py +35 -0
- mtppy/part3/data_assembly_set/base_functions/source_mode.py +189 -0
- mtppy/part3/data_assembly_set/base_functions/source_mode_test.py +112 -0
- mtppy/part3/data_assembly_set/base_functions/wqc.py +75 -0
- mtppy/part3/data_assembly_set/base_functions/wqc_test.py +86 -0
- mtppy/part3/data_assembly_set/data_assembly.py +65 -0
- mtppy/part3/data_assembly_set/diagnostic_elements/__init__.py +4 -0
- mtppy/part3/data_assembly_set/diagnostic_elements/diagnostic_element.py +19 -0
- mtppy/part3/data_assembly_set/indicator_elements/__init__.py +4 -0
- mtppy/part3/data_assembly_set/indicator_elements/indicator_element.py +22 -0
- mtppy/part3/data_assembly_set/indicator_elements/views/__init__.py +4 -0
- mtppy/part3/data_assembly_set/indicator_elements/views/ana_view.py +68 -0
- mtppy/part3/data_assembly_set/indicator_elements/views/bin_view.py +73 -0
- mtppy/part3/data_assembly_set/indicator_elements/views/dint_view.py +76 -0
- mtppy/part3/data_assembly_set/indicator_elements/views/string_view.py +55 -0
- mtppy/part3/data_assembly_set/operation_elements/__init__.py +4 -0
- mtppy/part3/data_assembly_set/operation_elements/operation_element.py +19 -0
- mtppy/part3/data_assembly_set/pea_elements/__init__.py +4 -0
- mtppy/part3/data_assembly_set/pea_elements/pea_element.py +28 -0
- mtppy/part3/data_assembly_set/pea_elements/pea_information_label/__init__.py +4 -0
- mtppy/part3/data_assembly_set/pea_elements/pea_information_label/pea_information_label.py +231 -0
- mtppy/part3/data_assembly_set/pea_elements/web_server_url_info/__init__.py +4 -0
- mtppy/part3/data_assembly_set/pea_elements/web_server_url_info/web_server_url_info.py +42 -0
- mtppy/part3/definitions/__init__.py +4 -0
- mtppy/part3/definitions/mtp_access.py +15 -0
- mtppy/part4/__init__.py +4 -0
- mtppy/part4/automation_services/__init__.py +4 -0
- mtppy/part4/automation_services/base_functions/__init__.py +4 -0
- mtppy/part4/automation_services/base_functions/service_operation_mode.py +368 -0
- mtppy/part4/automation_services/base_functions/service_operation_mode_test.py +242 -0
- mtppy/part4/automation_services/base_functions/service_operator_interaction.py +62 -0
- mtppy/part4/automation_services/base_functions/service_source_mode.py +138 -0
- mtppy/part4/automation_services/base_functions/service_source_mode_test.py +179 -0
- mtppy/part4/automation_services/helpers/__init__.py +4 -0
- mtppy/part4/automation_services/helpers/command_en_control.py +330 -0
- mtppy/part4/automation_services/helpers/command_en_control_test.py +302 -0
- mtppy/part4/automation_services/helpers/state_machine.py +167 -0
- mtppy/part4/automation_services/helpers/state_machine_test.py +294 -0
- mtppy/part4/automation_services/helpers/thread_control.py +39 -0
- mtppy/part4/automation_services/helpers/thread_control_test.py +80 -0
- mtppy/part4/automation_services/procedure.py +111 -0
- mtppy/part4/automation_services/procedure_test.py +185 -0
- mtppy/part4/automation_services/service.py +463 -0
- mtppy/part4/automation_services/service_elements/__init__.py +4 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/__init__.py +4 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/ana_serv_param.py +108 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/ana_serv_param_test.py +59 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/bin_serv_param.py +70 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/bin_serv_param_test.py +39 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/common_serv_param.py +111 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/dint_serv_param.py +77 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/dint_serv_param_test.py +58 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/parameter_element.py +175 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/parameter_element_test.py +246 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/string_serv_param.py +48 -0
- mtppy/part4/automation_services/service_elements/parameter_elements/string_serv_param_test.py +37 -0
- mtppy/part4/automation_services/service_elements/procedure_health_view.py +36 -0
- mtppy/part4/automation_services/service_elements/service_control.py +473 -0
- mtppy/part4/automation_services/service_elements/service_control_test.py +150 -0
- mtppy/part4/automation_services/service_elements/service_element.py +20 -0
- mtppy/part4/automation_services/service_test.py +407 -0
- mtppy/part4/definitions/__init__.py +4 -0
- mtppy/part4/definitions/command_codes.py +58 -0
- mtppy/part4/definitions/command_codes_test.py +44 -0
- mtppy/part4/definitions/state_codes.py +65 -0
- mtppy/part4/definitions/state_codes_test.py +44 -0
- mtppy/part4/process_values/__init__.py +4 -0
- mtppy/part4/process_values/base_functions/__init__.py +4 -0
- mtppy/part4/process_values/base_functions/vqc.py +48 -0
- mtppy/part4/process_values/input_elements/__init__.py +4 -0
- mtppy/part4/process_values/input_elements/ana_process_value_in.py +160 -0
- mtppy/part4/process_values/input_elements/bin_process_value_in.py +83 -0
- mtppy/part4/process_values/input_elements/dint_process_value_in.py +159 -0
- mtppy/part4/process_values/input_elements/input_element.py +40 -0
- mtppy/part4/process_values/input_elements/string_process_value_in.py +45 -0
- mtppy/part5/__init__.py +4 -0
- mtppy/part5/communication_object.py +99 -0
- mtppy/part5/communication_object_test.py +59 -0
- mtppy/part5/definitions/__init__.py +4 -0
- mtppy/part5/definitions/iec61131_data_types.py +31 -0
- mtppy/part5/definitions/iec61131_data_types_test.py +82 -0
- mtppy/part5/definitions/opcua_data_types.py +36 -0
- mtppy/part5/definitions/opcua_data_types_test.py +90 -0
- mtppy/part5/definitions/w3c_xml_data_types.py +177 -0
- mtppy/part5/definitions/w3c_xml_data_types_test.py +143 -0
- mtppy/part5/server_assembly_set/__init__.py +5 -0
- mtppy/part6/__init__.py +4 -0
- mtppy/part6/alarm_set/__init__.py +4 -0
- mtppy/part6/alarm_set/alarm.py +132 -0
- mtppy/part6/alarm_set/alarm_test.py +265 -0
- mtppy/pea/__init__.py +19 -0
- mtppy/pea/data_assembly_factory.py +124 -0
- mtppy/pea/data_assembly_factory_test.py +229 -0
- mtppy/pea/mtp_generator/__init__.py +4 -0
- mtppy/pea/mtp_generator/mtp_generator.py +1183 -0
- mtppy/pea/mtp_generator/mtp_generator_test.py +518 -0
- mtppy/pea/pea.py +139 -0
- mtppy/pea/pea_opcua_server/__init__.py +4 -0
- mtppy/pea/pea_opcua_server/marshalling.py +60 -0
- mtppy/pea/pea_opcua_server/opcua_options_provider/__init__.py +4 -0
- mtppy/pea/pea_opcua_server/opcua_options_provider/instances/basic_opcua_options_provider.py +50 -0
- mtppy/pea/pea_opcua_server/opcua_options_provider/opcua_options_provider.py +103 -0
- mtppy/pea/pea_opcua_server/pea_opcua_server.py +492 -0
- mtppy/pea/pea_opcua_server/subscription_list.py +55 -0
- mtppy/pea/pea_test.py +59 -0
- mtppy/pea/rmtp_pea.py +155 -0
- mtppy/pea/rmtp_pea_test.py +27 -0
- mtppy/pea/service_factory.py +178 -0
- mtppy/pea/service_factory_test.py +156 -0
- mtppy/pea/user_logic/__init__.py +4 -0
- mtppy/pea/user_logic/user_logic_pea.py +72 -0
- mtppy/pea/user_logic/user_logic_registry.py +106 -0
- mtppy/pea/user_logic/user_logic_registry_test.py +167 -0
- mtppy/pea/user_logic/user_logic_service.py +228 -0
- mtppy/pea/user_logic/user_logic_service_test.py +289 -0
- mtppy/utils/__init__.py +4 -0
- mtppy/utils/data_assembly_dict.py +61 -0
- mtppy/utils/data_assembly_dict_test.py +107 -0
- mtppy/utils/data_item.py +446 -0
- mtppy/utils/data_item_com_test.py +82 -0
- mtppy/utils/data_item_test.py +325 -0
- mtppy/utils/logging.py +23 -0
- mtppy/utils/logging_test.py +26 -0
- mtppy/utils/mtp_file_handling/__init__.py +4 -0
- mtppy/utils/mtp_file_handling/extract_manifest_from_mtp_file.py +22 -0
- mtppy/utils/mtp_file_handling/extract_mtp_file.py +22 -0
- mtppy/utils/mtp_file_handling/is_valid_mtp.py +22 -0
- mtppy/utils/mtp_file_handling/is_valid_path_for_manifest.py +15 -0
- mtppy/utils/mtp_file_handling/is_valid_path_to_manifest.py +17 -0
- mtppy/utils/mtp_file_handling/is_valid_path_to_mtp.py +17 -0
- mtppy/utils/mtp_file_handling/load_manifest_file.py +24 -0
- mtppy/utils/mtp_file_handling/mtp_reader/__init__.py +4 -0
- mtppy/utils/mtp_file_handling/mtp_reader/mtp_reader.py +103 -0
- mtppy/utils/mtp_file_handling/mtp_reader/mtp_reader_test.py +96 -0
- mtppy/utils/mtp_file_handling/mtp_reader/mtp_xml.py +226 -0
- mtppy/utils/mtp_file_handling/mtp_reader/mtp_xml_test.py +108 -0
- mtppy/utils/mtp_file_handling/mtp_writer/__init__.py +4 -0
- mtppy/utils/mtp_file_handling/mtp_writer/mtp_writer.py +77 -0
- mtppy/utils/mtp_file_handling/mtp_writer/ressources/get_content_types.py +10 -0
- mtppy/utils/mtp_file_handling/mtp_writer/ressources/get_rels.py +11 -0
- mtppy/utils/mtp_file_handling/mtp_writer/test_mtp_writer.py +25 -0
- mtppy/utils/mtp_file_handling/write_manifest_file.py +29 -0
- mtppy/utils/mtp_file_handling/write_manifest_file_test.py +22 -0
- mtppy/utils/mtp_file_handling/write_mtp_file.py +43 -0
- mtppy/utils/mtp_file_handling/write_mtp_file_test.py +22 -0
- mtppy/utils/testing.py +31 -0
- mtppy2_0-0.1.5.dist-info/METADATA +75 -0
- mtppy2_0-0.1.5.dist-info/RECORD +214 -0
- mtppy2_0-0.1.5.dist-info/WHEEL +5 -0
- mtppy2_0-0.1.5.dist-info/entry_points.txt +3 -0
- mtppy2_0-0.1.5.dist-info/licenses/LICENSE +21 -0
- mtppy2_0-0.1.5.dist-info/top_level.txt +4 -0
- tools/__init__.py +0 -0
- tools/create_bugreport.py +150 -0
- tools/inspect_mtp.py +110 -0
- utils/__init__.py +1 -0
- utils/file_handling/__init__.py +4 -0
- utils/file_handling/cleanup_folder.py +9 -0
- utils/file_handling/get_utc_foldername_with_ms.py +15 -0
- utils/file_handling/get_work_dir.py +9 -0
- utils/file_handling/is_valid_path_for_json.py +11 -0
- utils/file_handling/is_valid_path_to_json.py +17 -0
- utils/file_handling/load_json_file.py +18 -0
- utils/file_handling/read_file_from_file_archive.py +43 -0
- utils/file_handling/read_file_string.py +10 -0
- utils/file_handling/validate_json_data.py +28 -0
- utils/file_handling/write_json_file.py +23 -0
- utils/get_uuid.py +18 -0
- utils/get_uuid_test.py +17 -0
- utils/logging.py +100 -0
- utils/logging_test.py +67 -0
- utils/opcua/check_node.py +79 -0
- utils/resolve_unit/resolve_unit.py +11 -0
- utils/resolve_unit/unit_lib.py +2101 -0
- utils/resolve_unit/unit_lib_test.py +36 -0
- utils/type_conversion.py +236 -0
- utils/type_conversion_test.py +114 -0
mtppy/utils/data_item.py
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
"""DataItem and DataItemCollection, core API for data management and notification."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__copyright__ = (
|
|
6
|
+
"Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
|
|
7
|
+
)
|
|
8
|
+
__license__ = "MIT"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
import copy
|
|
12
|
+
from collections.abc import (
|
|
13
|
+
Callable,
|
|
14
|
+
Iterator,
|
|
15
|
+
KeysView,
|
|
16
|
+
MutableMapping,
|
|
17
|
+
)
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
import utils.type_conversion as tc
|
|
22
|
+
from mtppy.part5.communication_object import CommunicationObject
|
|
23
|
+
|
|
24
|
+
from mtppy.part3.definitions.mtp_access import MTPAccess
|
|
25
|
+
from mtppy.part5.definitions.w3c_xml_data_types import W3cXmlDataTypes
|
|
26
|
+
from mtppy.utils.logging import logger
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class DataItem:
|
|
30
|
+
"""Represent a scalar typed value hosted by a data-item collection.
|
|
31
|
+
|
|
32
|
+
A DataItem validates and stores its value according to a W3C XML data
|
|
33
|
+
type. It can notify an internal callback and any number of external
|
|
34
|
+
communication observers when its value is written.
|
|
35
|
+
|
|
36
|
+
The class is hosted by a
|
|
37
|
+
[DataItemCollection][mtppy.utils.data_item.DataItemCollection],
|
|
38
|
+
the base class for
|
|
39
|
+
[DataAssembly][mtppy.part3.data_assembly_set.data_assembly.DataAssembly].
|
|
40
|
+
|
|
41
|
+
A DataItem is associated with an Attribute of a DataAssembly definition
|
|
42
|
+
within MTP 2.0.
|
|
43
|
+
|
|
44
|
+
Attributes:
|
|
45
|
+
name: DataItem name.
|
|
46
|
+
w3c_xml_data_type: DataItem value type.
|
|
47
|
+
access: DataItem access mode.
|
|
48
|
+
init_value: Initial, type-corrected value.
|
|
49
|
+
description: Human-readable description.
|
|
50
|
+
ext_wrt_cb: Callback invoked for external writes.
|
|
51
|
+
int_wrt_cb: Callback invoked for internal writes.
|
|
52
|
+
communication_objects: External communication observers notified on
|
|
53
|
+
value writes.
|
|
54
|
+
comm_obj: Backwards-compatible alias for the first communication
|
|
55
|
+
observer.
|
|
56
|
+
_value: Current type-corrected value.
|
|
57
|
+
_is_notifying: Whether an internal notification is in progress.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
name: str,
|
|
63
|
+
w3c_xml_data_type: W3cXmlDataTypes,
|
|
64
|
+
access: MTPAccess,
|
|
65
|
+
init_value: tc.ANY_TYPE | None = 0,
|
|
66
|
+
description: str = "",
|
|
67
|
+
external_write_cb: Callable[[tc.ANY_TYPE], None] | None = None,
|
|
68
|
+
internal_write_cb: Callable[[tc.ANY_TYPE], None] | None = None,
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Initialize a DataItem.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
name: Name of the data item.
|
|
74
|
+
w3c_xml_data_type: W3C XML type used to convert assigned values.
|
|
75
|
+
access: Access mode for the data item.
|
|
76
|
+
init_value: Initial value. ``None`` is treated as ``0``.
|
|
77
|
+
description: Human-readable description.
|
|
78
|
+
external_write_cb: Callback for external writes received through
|
|
79
|
+
the data item.
|
|
80
|
+
internal_write_cb: Callback for internal writes to the data item.
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
TypeError: If ``w3c_xml_data_type`` or ``access`` is not the
|
|
84
|
+
expected enumeration type.
|
|
85
|
+
"""
|
|
86
|
+
self.name: str = name
|
|
87
|
+
# Type checking
|
|
88
|
+
if not isinstance(
|
|
89
|
+
w3c_xml_data_type,
|
|
90
|
+
W3cXmlDataTypes,
|
|
91
|
+
): # pyright: ignore[reportUnnecessaryIsInstance]
|
|
92
|
+
exc_msg = f"data type {w3c_xml_data_type} not supported"
|
|
93
|
+
raise TypeError(exc_msg)
|
|
94
|
+
self.w3c_xml_data_type: W3cXmlDataTypes = w3c_xml_data_type
|
|
95
|
+
if not isinstance(
|
|
96
|
+
access,
|
|
97
|
+
MTPAccess,
|
|
98
|
+
): # pyright: ignore[reportUnnecessaryIsInstance]
|
|
99
|
+
exc_msg = f"access type {access} not supported"
|
|
100
|
+
raise TypeError(exc_msg)
|
|
101
|
+
self.access: MTPAccess = access
|
|
102
|
+
self._is_notifying: bool = False
|
|
103
|
+
|
|
104
|
+
if init_value is None:
|
|
105
|
+
init_value = 0
|
|
106
|
+
|
|
107
|
+
self.description: str = description
|
|
108
|
+
type_casted_value = self._correct_type(init_value)
|
|
109
|
+
self.init_value: tc.ANY_TYPE = type_casted_value
|
|
110
|
+
self._value: tc.ANY_TYPE = type_casted_value
|
|
111
|
+
self.communication_objects: list[CommunicationObject] = []
|
|
112
|
+
self.ext_wrt_cb: Callable[[tc.ANY_TYPE], None] | None = external_write_cb
|
|
113
|
+
self.int_wrt_cb: Callable[[tc.ANY_TYPE], None] | None = internal_write_cb
|
|
114
|
+
|
|
115
|
+
def get_value(self) -> tc.ANY_TYPE:
|
|
116
|
+
"""Return the current value of the DataItem.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
The current value, already converted to the DataItem's
|
|
120
|
+
pythonic ``w3c_xml_data_type``
|
|
121
|
+
"""
|
|
122
|
+
return self._value
|
|
123
|
+
|
|
124
|
+
def get_value_str(self) -> str:
|
|
125
|
+
"""Return a human-readable string representation of the DataItem.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
A string of the form ``"<name>=<value>"``, e.g. ``"WQC=128"``.
|
|
129
|
+
"""
|
|
130
|
+
return f"{self.name}={self._value}"
|
|
131
|
+
|
|
132
|
+
def _notify_local_subscribers(self, value: tc.ANY_TYPE) -> None:
|
|
133
|
+
"""Notify the internal subscriber and update the stored value.
|
|
134
|
+
|
|
135
|
+
Recursive writes made by the internal callback update the value
|
|
136
|
+
directly while the outer notification remains active.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
value: Type-corrected value to publish internally.
|
|
140
|
+
"""
|
|
141
|
+
if self._is_notifying: # Our callbacks did all the checks, now finally write
|
|
142
|
+
self._value = value
|
|
143
|
+
return
|
|
144
|
+
self._is_notifying = True
|
|
145
|
+
try:
|
|
146
|
+
if self.int_wrt_cb is not None:
|
|
147
|
+
logger.mtp(f"{self.name}:={value} => {self.int_wrt_cb.__name__}")
|
|
148
|
+
self.int_wrt_cb(value)
|
|
149
|
+
else:
|
|
150
|
+
self._value = value
|
|
151
|
+
finally:
|
|
152
|
+
self._is_notifying = False
|
|
153
|
+
|
|
154
|
+
def _notify_external_subscribers(self) -> None:
|
|
155
|
+
"""Notify each attached external communication observer."""
|
|
156
|
+
if not self.communication_objects:
|
|
157
|
+
logger.opcua(f"{self.name}:={self._value} => no communication object")
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
for communication_object in self.communication_objects:
|
|
161
|
+
communication_object.write_value_callback(self._value)
|
|
162
|
+
logger.opcua(f"{self.name}:={self._value} => notified")
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def comm_obj(self) -> CommunicationObject | None:
|
|
166
|
+
"""Return the first communication object, if one is attached.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
The first attached observer, or ``None`` when no observer exists.
|
|
170
|
+
"""
|
|
171
|
+
return self.communication_objects[0] if self.communication_objects else None
|
|
172
|
+
|
|
173
|
+
@comm_obj.setter
|
|
174
|
+
def comm_obj(self, communication_object: CommunicationObject | None) -> None:
|
|
175
|
+
"""Replace all observers with one communication object.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
communication_object: Observer to retain, or ``None`` to remove
|
|
179
|
+
all observers.
|
|
180
|
+
"""
|
|
181
|
+
self.communication_objects = (
|
|
182
|
+
[] if communication_object is None else [communication_object]
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def set_value(self, value: tc.ANY_TYPE) -> None:
|
|
186
|
+
"""Set the value and notify internal and external subscribers.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
value: Value to convert, store, and publish.
|
|
190
|
+
"""
|
|
191
|
+
corrected_value = self._correct_type(value)
|
|
192
|
+
if self.get_value() != corrected_value:
|
|
193
|
+
self._notify_local_subscribers(corrected_value)
|
|
194
|
+
# do we really always want to try to notify external subscribers? No,
|
|
195
|
+
# we shall wait until all the internal updates are finished
|
|
196
|
+
if not self._is_notifying:
|
|
197
|
+
self._notify_external_subscribers()
|
|
198
|
+
|
|
199
|
+
logger.debug(f"New value for {self.name} is {self._value}")
|
|
200
|
+
|
|
201
|
+
def _correct_type(self, value: tc.ANY_TYPE) -> tc.ANY_TYPE:
|
|
202
|
+
"""Convert a value to the DataItem's configured type.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
value: Value.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Converted value, or ``int`` when conversion fails.
|
|
209
|
+
"""
|
|
210
|
+
conversion_function = self.w3c_xml_data_type.converse
|
|
211
|
+
if conversion_function is not None:
|
|
212
|
+
try:
|
|
213
|
+
return conversion_function(value)
|
|
214
|
+
except (ValueError, TypeError) as e:
|
|
215
|
+
logger.warning(
|
|
216
|
+
f"{self.name} - converting '{value}' to "
|
|
217
|
+
f"type {self.w3c_xml_data_type}: {e}",
|
|
218
|
+
)
|
|
219
|
+
# tbd: understand this - it magically works - why?
|
|
220
|
+
return int
|
|
221
|
+
else:
|
|
222
|
+
exc_msg = f"data type {self.w3c_xml_data_type} not supported"
|
|
223
|
+
raise TypeError(exc_msg)
|
|
224
|
+
|
|
225
|
+
def attach_communication_object(
|
|
226
|
+
self,
|
|
227
|
+
communication_object: CommunicationObject,
|
|
228
|
+
) -> None:
|
|
229
|
+
"""Attach an external communication observer.
|
|
230
|
+
|
|
231
|
+
Duplicate observers are ignored. Observers are notified in attachment
|
|
232
|
+
order whenever the DataItem is written.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
communication_object: Communication object that exposes this
|
|
236
|
+
DataItem to an external client.
|
|
237
|
+
"""
|
|
238
|
+
if communication_object not in self.communication_objects:
|
|
239
|
+
self.communication_objects.append(communication_object)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
class DataItemCollection(MutableMapping[str, DataItem]):
|
|
243
|
+
"""Key-immutable, dict-like container of [DataItem][mtppy.utils.data_item.DataItem]
|
|
244
|
+
instances.
|
|
245
|
+
|
|
246
|
+
The collection is populated at construction time from the ClassVar
|
|
247
|
+
``CLASS_DATA_ITEMS`` mapping declared statically on the owning class
|
|
248
|
+
(and all of its bases in MRO order). Each entry is deep-copied so that two
|
|
249
|
+
collections derived from the same class do not share DataItems.
|
|
250
|
+
|
|
251
|
+
The collection supports both mapping-style (``da["WQC"]``) and
|
|
252
|
+
attribute-style (``da.WQC``) access to the current *value* of
|
|
253
|
+
a DataItem.
|
|
254
|
+
|
|
255
|
+
Both the dict-keys and the attributes of the class are immutable, that is
|
|
256
|
+
a lhs ``da["NEW"]`` does not add a new entry with key "NDW" to the
|
|
257
|
+
DataItemCollection. The same is true for lhs da.new or self.new - this
|
|
258
|
+
hardens the class against typos.
|
|
259
|
+
|
|
260
|
+
Note:
|
|
261
|
+
In cases where the SNAKE_CASE string holding constant is not distinguishable
|
|
262
|
+
from the CamelCase PNO MTP 2.0 name, our best practice is to have the constant
|
|
263
|
+
name end with an _, e.g. WQC_, P_, PV_
|
|
264
|
+
|
|
265
|
+
Attention:
|
|
266
|
+
The collection is key-immutable and attribute-immutable. Setting an unknown
|
|
267
|
+
key or attribute raises [KeyError][keyerror], while reading an unknown
|
|
268
|
+
attribute raises [AttributeError][attributeerror]. Consequently,
|
|
269
|
+
``hasattr(collection, name)`` returns ``False`` for missing attributes,
|
|
270
|
+
as it does for normal Python objects.
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
def __init__(self) -> None:
|
|
274
|
+
"""Initialize the collection from the class-level ``CLASS_DATA_ITEMS``.
|
|
275
|
+
|
|
276
|
+
Walks the class MRO in reverse order so that base-class entries are
|
|
277
|
+
inserted first and subclass entries take precedence on duplicate
|
|
278
|
+
keys. Each declared data item is deep-copied to avoid shared state
|
|
279
|
+
between instances.
|
|
280
|
+
"""
|
|
281
|
+
self._data_items: dict[str, DataItem] = {} # "SrcChannel" : ...
|
|
282
|
+
for cls in reversed(self.__class__.__mro__):
|
|
283
|
+
params = getattr(cls, "CLASS_DATA_ITEMS", None)
|
|
284
|
+
if params is None:
|
|
285
|
+
continue
|
|
286
|
+
for key, data_item in params.items():
|
|
287
|
+
self._data_items[key] = copy.deepcopy(data_item)
|
|
288
|
+
|
|
289
|
+
# whenever a public attribute is added to this class it
|
|
290
|
+
# needs to be added in __setattr__ to the currently
|
|
291
|
+
# empty list if name in ()
|
|
292
|
+
|
|
293
|
+
def __setitem__(self, key: str, value: tc.ANY_TYPE) -> None:
|
|
294
|
+
"""Set the value of an existing data item.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
key: The data item name. Must already exist in the collection.
|
|
298
|
+
value: The new value. It is converted to the data item's
|
|
299
|
+
declared type via its subscription/value setter.
|
|
300
|
+
|
|
301
|
+
Raises:
|
|
302
|
+
KeyError: If ``key`` is not a known data item of the collection
|
|
303
|
+
(the collection is key-immutable).
|
|
304
|
+
"""
|
|
305
|
+
if key in self._data_items:
|
|
306
|
+
self._data_items[key].set_value(value)
|
|
307
|
+
else:
|
|
308
|
+
exc_msg = f"Cannot add new key key='{key}', assembly is key-immutable."
|
|
309
|
+
raise KeyError(exc_msg)
|
|
310
|
+
|
|
311
|
+
def __delitem__(self, key: str) -> None:
|
|
312
|
+
"""Delete a data item from the collection.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
key: The data item name.
|
|
316
|
+
|
|
317
|
+
Raises:
|
|
318
|
+
TypeError: Always. The collection is key-immutable and does
|
|
319
|
+
not support deletion of items.
|
|
320
|
+
"""
|
|
321
|
+
exc_msg = f"delete attempt key='{key}': assembly is key-immutable."
|
|
322
|
+
raise TypeError(exc_msg)
|
|
323
|
+
|
|
324
|
+
# Standard MutableMapping interface
|
|
325
|
+
def __getitem__(self, key: str) -> tc.ANY_TYPE:
|
|
326
|
+
"""Return the current value of the data item identified by ``key``.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
key: The data item name.
|
|
330
|
+
|
|
331
|
+
Returns:
|
|
332
|
+
The current value of the data item (already type-cast by the
|
|
333
|
+
underlying [DataItem][mtppy.utils.data_item.DataItem]).
|
|
334
|
+
"""
|
|
335
|
+
return self._data_items[key].get_value()
|
|
336
|
+
|
|
337
|
+
def __iter__(self) -> Iterator[str]:
|
|
338
|
+
"""Return an iterator over the data item names.
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
An iterator yielding the registered data item keys.
|
|
342
|
+
"""
|
|
343
|
+
return iter(self._data_items)
|
|
344
|
+
|
|
345
|
+
def __len__(self) -> int:
|
|
346
|
+
"""Return the number of data items in the collection.
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
The number of registered data items.
|
|
350
|
+
"""
|
|
351
|
+
return len(self._data_items)
|
|
352
|
+
|
|
353
|
+
def keys(self) -> KeysView[str]:
|
|
354
|
+
"""Return a view of the data item names.
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
A ``KeysView`` of the registered data item keys.
|
|
358
|
+
"""
|
|
359
|
+
return self._data_items.keys()
|
|
360
|
+
|
|
361
|
+
def values(self) -> list[tc.ANY_TYPE]:
|
|
362
|
+
"""Return the current values of all data items.
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
A list with the current value of every data item, in the same
|
|
366
|
+
order as `keys`.
|
|
367
|
+
"""
|
|
368
|
+
return [item.get_value() for item in self._data_items.values()]
|
|
369
|
+
|
|
370
|
+
def items(self) -> list[tuple[str, tc.ANY_TYPE]]:
|
|
371
|
+
"""Return the current ``{name: value}`` dict of all data items.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
A list of ``(key, value)`` tuples for every data item, where
|
|
375
|
+
``value`` is the current (already type-cast) value.
|
|
376
|
+
"""
|
|
377
|
+
return [(k, v.get_value()) for k, v in self._data_items.items()]
|
|
378
|
+
|
|
379
|
+
def get_item(self, key: str) -> DataItem:
|
|
380
|
+
"""Access the underlying [DataItem][mtppy.utils.data_item.DataItem] for
|
|
381
|
+
introspection.
|
|
382
|
+
|
|
383
|
+
Unlike [``__getitem__``][], this returns the full DataItem object
|
|
384
|
+
instead of only its current value, allowing callers to inspect or
|
|
385
|
+
modify attributes such as the data type, access mode or
|
|
386
|
+
subscription callback.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
key: The data item name.
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
The [DataItem][mtppy.utils.data_item.DataItem] instance registered
|
|
393
|
+
under ``key``.
|
|
394
|
+
"""
|
|
395
|
+
return self._data_items[key]
|
|
396
|
+
|
|
397
|
+
def __getattr__(self, name: str) -> tc.ANY_TYPE:
|
|
398
|
+
"""Enable both dict style on the rhs, that is dict-style
|
|
399
|
+
(``da["OSLevel"]``) and attribute-style (``da.OSLevel``) read access.
|
|
400
|
+
|
|
401
|
+
Missing attributes raise ``AttributeError`` so that ``hasattr`` follows
|
|
402
|
+
normal Python behavior.
|
|
403
|
+
"""
|
|
404
|
+
try:
|
|
405
|
+
if name in self._data_items:
|
|
406
|
+
return self._data_items[name].get_value()
|
|
407
|
+
key = getattr(self.__class__, name)
|
|
408
|
+
except AttributeError as exc:
|
|
409
|
+
exc_msg = f"DataItemCollection has no attribute '{name}'"
|
|
410
|
+
raise AttributeError(exc_msg) from exc
|
|
411
|
+
if key in self._data_items:
|
|
412
|
+
return self._data_items[key].get_value()
|
|
413
|
+
exc_msg = f"DataItemCollection has no attribute '{name}'"
|
|
414
|
+
raise AttributeError(exc_msg)
|
|
415
|
+
|
|
416
|
+
def __setattr__(self, name: str, value: tc.ANY_TYPE) -> None:
|
|
417
|
+
"""Enable both lhs dict style (``da["OSLevel"]``) and
|
|
418
|
+
attribute-style (`da.OSLevel``) write access.
|
|
419
|
+
|
|
420
|
+
Internal attributes and private names are stored
|
|
421
|
+
normally. Everything else is mapped to the matching class
|
|
422
|
+
constant key, if one exists, and validated/cast through the
|
|
423
|
+
underlying ``DataItem``.
|
|
424
|
+
"""
|
|
425
|
+
if name.startswith("_"): # or name in (set of protected names)
|
|
426
|
+
super().__setattr__(name, value)
|
|
427
|
+
return
|
|
428
|
+
if name in self._data_items:
|
|
429
|
+
self._data_items[name].set_value(value)
|
|
430
|
+
elif any(name in cls.__dict__ for cls in type(self).__mro__) or any(
|
|
431
|
+
name in getattr(cls, "__annotations__", {}) for cls in type(self).__mro__
|
|
432
|
+
):
|
|
433
|
+
super().__setattr__(name, value)
|
|
434
|
+
else:
|
|
435
|
+
exc_msg = f"Cannot add unknown attribute '{name}' to data item collection."
|
|
436
|
+
raise KeyError(exc_msg)
|
|
437
|
+
|
|
438
|
+
def __repr__(self) -> str:
|
|
439
|
+
"""Return an unambiguous string representation of the collection.
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
A string of the form ``"data_items={...}"`` containing the
|
|
443
|
+
current ``name -> value`` mapping of every registered data item.
|
|
444
|
+
"""
|
|
445
|
+
params = {k: v.get_value() for k, v in self._data_items.items()}
|
|
446
|
+
return f"class={self.__class__.__name__},data_items={params})"
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Test Suite for DataItem and OPCUACommunicationObject."""
|
|
2
|
+
|
|
3
|
+
__copyright__ = (
|
|
4
|
+
"Copyright (c) 2026 Dresden University of Technology, Process-to-Order Group"
|
|
5
|
+
)
|
|
6
|
+
__license__ = "MIT"
|
|
7
|
+
|
|
8
|
+
from unittest.mock import Mock
|
|
9
|
+
|
|
10
|
+
from mtppy.part3.definitions.mtp_access import MTPAccess
|
|
11
|
+
from mtppy.part5.communication_object import (
|
|
12
|
+
MODBUSCommunicationObject,
|
|
13
|
+
OPCUACommunicationObject,
|
|
14
|
+
)
|
|
15
|
+
from mtppy.part5.definitions.w3c_xml_data_types import W3cXmlDataTypes
|
|
16
|
+
from mtppy.utils.data_item import DataItem
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_internal_write_triggers_only_subscriber_callbacks():
|
|
20
|
+
# Mock the subscription callback
|
|
21
|
+
mock_ext_wrt_cb = Mock()
|
|
22
|
+
|
|
23
|
+
# Mock the OPCUACommunicationObject
|
|
24
|
+
mock_comm_obj = Mock(spec=OPCUACommunicationObject)
|
|
25
|
+
mock_comm_obj.write_value_callback = Mock()
|
|
26
|
+
mock_comm_obj._node_id = "dummy"
|
|
27
|
+
|
|
28
|
+
# Create a DataItem instance with the mocked subscription callback
|
|
29
|
+
data_item = DataItem(
|
|
30
|
+
name="TestItem",
|
|
31
|
+
w3c_xml_data_type=W3cXmlDataTypes.INT,
|
|
32
|
+
access=MTPAccess.ReadWriteAccess,
|
|
33
|
+
init_value=0,
|
|
34
|
+
external_write_cb=mock_ext_wrt_cb,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Attach the mocked communication object
|
|
38
|
+
data_item.attach_communication_object(mock_comm_obj)
|
|
39
|
+
|
|
40
|
+
# Set a new value and ensure callbacks are triggered
|
|
41
|
+
new_value = 42
|
|
42
|
+
data_item.set_value(new_value)
|
|
43
|
+
|
|
44
|
+
# Assert that the subscription callback was never called
|
|
45
|
+
mock_ext_wrt_cb.assert_not_called
|
|
46
|
+
|
|
47
|
+
# Assert that the communication object's write_value_callback was called with the correct value
|
|
48
|
+
mock_comm_obj.write_value_callback.assert_called_once_with(new_value)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_modbus_communication_object_writes_data_item_value_to_register() -> None:
|
|
52
|
+
modbus_client = Mock()
|
|
53
|
+
communication_object = MODBUSCommunicationObject(modbus_client, "42")
|
|
54
|
+
data_item = DataItem(
|
|
55
|
+
name="TestItem",
|
|
56
|
+
w3c_xml_data_type=W3cXmlDataTypes.INT,
|
|
57
|
+
access=MTPAccess.ReadWriteAccess,
|
|
58
|
+
init_value=0,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
data_item.attach_communication_object(communication_object)
|
|
62
|
+
data_item.set_value(7)
|
|
63
|
+
|
|
64
|
+
modbus_client.write_register.assert_called_once_with(address=42, value=7)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_data_item_notifies_all_attached_communication_objects() -> None:
|
|
68
|
+
first_communication_object = Mock()
|
|
69
|
+
second_communication_object = Mock()
|
|
70
|
+
data_item = DataItem(
|
|
71
|
+
name="TestItem",
|
|
72
|
+
w3c_xml_data_type=W3cXmlDataTypes.INT,
|
|
73
|
+
access=MTPAccess.ReadWriteAccess,
|
|
74
|
+
init_value=0,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
data_item.attach_communication_object(first_communication_object)
|
|
78
|
+
data_item.attach_communication_object(second_communication_object)
|
|
79
|
+
data_item.set_value(7)
|
|
80
|
+
|
|
81
|
+
first_communication_object.write_value_callback.assert_called_once_with(7)
|
|
82
|
+
second_communication_object.write_value_callback.assert_called_once_with(7)
|