aas-standard-parser 0.1.1__py3-none-any.whl → 0.1.3__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.
- aas_standard_parser/__init__.py +18 -0
- aas_standard_parser/aid_parser.py +305 -0
- aas_standard_parser/aimc_parser.py +244 -0
- {aas_standard_parser-0.1.1.dist-info → aas_standard_parser-0.1.3.dist-info}/METADATA +1 -1
- aas_standard_parser-0.1.3.dist-info/RECORD +8 -0
- aas_standard_parser-0.1.3.dist-info/top_level.txt +1 -0
- aas_standard_parser-0.1.1.dist-info/RECORD +0 -5
- aas_standard_parser-0.1.1.dist-info/top_level.txt +0 -1
- {aas_standard_parser-0.1.1.dist-info → aas_standard_parser-0.1.3.dist-info}/WHEEL +0 -0
- {aas_standard_parser-0.1.1.dist-info → aas_standard_parser-0.1.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
import importlib.metadata
|
|
3
|
+
|
|
4
|
+
__copyright__ = f"Copyright (C) {datetime.now().year} :em engineering methods AG. All rights reserved."
|
|
5
|
+
__author__ = "Daniel Klein"
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
__version__ = importlib.metadata.version(__name__)
|
|
9
|
+
except importlib.metadata.PackageNotFoundError:
|
|
10
|
+
__version__ = "0.0.0-dev"
|
|
11
|
+
|
|
12
|
+
__project__ = "aas-standard-parser"
|
|
13
|
+
__package__ = "aas-standard-parser"
|
|
14
|
+
|
|
15
|
+
from aas_standard_parser import aid_parser, aimc_parser
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
__all__ = ["aid_parser", "aimc_parser"]
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""This module provides functions to parse AID Submodels and extract MQTT interface descriptions."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterator
|
|
4
|
+
from typing import NamedTuple
|
|
5
|
+
|
|
6
|
+
from basyx.aas.model import (
|
|
7
|
+
ExternalReference,
|
|
8
|
+
Key,
|
|
9
|
+
KeyTypes,
|
|
10
|
+
NamespaceSet,
|
|
11
|
+
Property,
|
|
12
|
+
Reference,
|
|
13
|
+
Submodel,
|
|
14
|
+
SubmodelElement,
|
|
15
|
+
SubmodelElementCollection,
|
|
16
|
+
)
|
|
17
|
+
from basyx.aas.util import traversal
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MQTTInterfaceDescription(NamedTuple):
|
|
21
|
+
"""Represents an MQTT interface configuration for a specific asset.
|
|
22
|
+
|
|
23
|
+
:param interface_smc: The SubmodelElementCollection representing the MQTT interface.
|
|
24
|
+
:param base_url: The base URL for the MQTT interface.
|
|
25
|
+
:param websocket_connection: Whether this interface is using a WebSocket connection or not (default).
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
interface_smc: SubmodelElementCollection
|
|
29
|
+
base_url: str
|
|
30
|
+
websocket_connection: bool = False
|
|
31
|
+
|
|
32
|
+
class AIDParser:
|
|
33
|
+
"""A class to handle parsing of AID Submodels and connecting to MQTT topics.
|
|
34
|
+
|
|
35
|
+
It extracts the MQTT topic information from the AID Submodel as well as the base url of the MQTT broker.
|
|
36
|
+
All MQTT interface configurations are stored in a list of MQTTInterfaceDescriptions.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
_mqtt_interface_descriptions: list[MQTTInterfaceDescription]
|
|
40
|
+
_default_mqtt_interface: MQTTInterfaceDescription = None
|
|
41
|
+
_fallback_mqtt_interface: MQTTInterfaceDescription = None
|
|
42
|
+
_topic_map: dict[str, str] = {}
|
|
43
|
+
|
|
44
|
+
def __init__(self, aid_sm: Submodel):
|
|
45
|
+
"""Initialize the AIDParser with a JSON representation of an AID Submodel.
|
|
46
|
+
|
|
47
|
+
Extract all MQTT interface collections and find the contained MQTT topics using the default interface.
|
|
48
|
+
"""
|
|
49
|
+
mqtt_interfaces: list[SubmodelElementCollection] = self._find_all_mqtt_interfaces(aid_sm)
|
|
50
|
+
if mqtt_interfaces == []:
|
|
51
|
+
print("No MQTT interface description found in AID Submodel.")
|
|
52
|
+
|
|
53
|
+
self._mqtt_interface_descriptions = [
|
|
54
|
+
MQTTInterfaceDescription(
|
|
55
|
+
interface_smc=smc,
|
|
56
|
+
base_url=self._get_base_url_from_interface(smc),
|
|
57
|
+
websocket_connection=self._uses_websocket(smc)
|
|
58
|
+
)
|
|
59
|
+
for smc in mqtt_interfaces
|
|
60
|
+
]
|
|
61
|
+
print(f"Found {len(self._mqtt_interface_descriptions)} MQTT interfaces in AID Submodel.")
|
|
62
|
+
self._default_mqtt_interface = self._get_default_mqtt_interface_description()
|
|
63
|
+
self._fallback_mqtt_interface = self._get_fallback_mqtt_interface_description()
|
|
64
|
+
self._create_topic_map(self._default_mqtt_interface.interface_smc)
|
|
65
|
+
|
|
66
|
+
def _find_all_mqtt_interfaces(self, aid_sm: Submodel) -> list[SubmodelElementCollection]:
|
|
67
|
+
"""Find all MQTT interface collections in the AID Submodel by semantic_id and supplemental_semantic_id.
|
|
68
|
+
|
|
69
|
+
:return: A list of MQTT interface SubmodelElementCollections or an empty list if none are found.
|
|
70
|
+
"""
|
|
71
|
+
interfaces: list[SubmodelElement] = find_all_by_semantic_id(
|
|
72
|
+
aid_sm.submodel_element, "https://admin-shell.io/idta/AssetInterfacesDescription/1/0/Interface"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return [interface for interface in interfaces if isinstance(interface, SubmodelElementCollection) and
|
|
76
|
+
contains_supplemental_semantic_id(interface, "http://www.w3.org/2011/mqtt")] if interfaces else []
|
|
77
|
+
|
|
78
|
+
def _get_base_url_from_interface(self, mqtt_interface: SubmodelElementCollection) -> str:
|
|
79
|
+
"""Set the base URL for the MQTT interface from the EndpointMetadata SMC."""
|
|
80
|
+
endpoint_metadata: SubmodelElementCollection = find_by_semantic_id(
|
|
81
|
+
mqtt_interface.value, "https://admin-shell.io/idta/AssetInterfacesDescription/1/0/EndpointMetadata"
|
|
82
|
+
)
|
|
83
|
+
if endpoint_metadata is None:
|
|
84
|
+
raise ValueError("EndpointMetadata SMC not found in AID Submodel.")
|
|
85
|
+
base: Property = find_by_semantic_id(
|
|
86
|
+
endpoint_metadata.value, "https://www.w3.org/2019/wot/td#base"
|
|
87
|
+
)
|
|
88
|
+
if base is None:
|
|
89
|
+
raise ValueError("BaseUrl Property not found in EndpointMetadata SMC.")
|
|
90
|
+
return base.value
|
|
91
|
+
|
|
92
|
+
def _create_topic_map(self, mqtt_interface: SubmodelElementCollection):
|
|
93
|
+
"""Find all MQTT topics by their property definitions in the MQTT interface SMC and create a new topic Map.
|
|
94
|
+
|
|
95
|
+
The topic Map is a dictionary of the Topic Name (IdShort of the Property Definition) and the MQTT topic link.
|
|
96
|
+
|
|
97
|
+
:param mqtt_interface: The MQTT interface SubmodelElementCollection to use.
|
|
98
|
+
"""
|
|
99
|
+
print(f"Creating topic map for MQTT interface {mqtt_interface.id_short}.")
|
|
100
|
+
mqtt_property_collection: SubmodelElementCollection = self._get_mqtt_properties(mqtt_interface)
|
|
101
|
+
if not mqtt_property_collection:
|
|
102
|
+
print(f"No MQTT properties found in InteractionMetadata of MQTT Interface {mqtt_interface.id_short}.")
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
property_definitions: list[SubmodelElementCollection] = [
|
|
106
|
+
prop_def for prop_def in find_all_by_semantic_id(
|
|
107
|
+
traversal.walk_submodel(mqtt_property_collection),
|
|
108
|
+
"https://admin-shell.io/idta/AssetInterfaceDescription/1/0/PropertyDefinition"
|
|
109
|
+
)
|
|
110
|
+
if isinstance(prop_def, SubmodelElementCollection) and
|
|
111
|
+
find_by_semantic_id(prop_def.value, "https://www.w3.org/2019/wot/td#hasForm") is not None
|
|
112
|
+
]
|
|
113
|
+
self._topic_map = self._get_topics_from_property_definitions(property_definitions)
|
|
114
|
+
|
|
115
|
+
def _get_topics_from_property_definitions(self, property_definitions: list[SubmodelElementCollection]) -> dict[str, str]:
|
|
116
|
+
"""Create a mapping of MQTT topics from the property definitions.
|
|
117
|
+
|
|
118
|
+
:param property_definitions: The list of property definitions from the AID SM.
|
|
119
|
+
:return: A dictionary mapping IdShort of MQTT topic definitions to their MQTT topic links.
|
|
120
|
+
"""
|
|
121
|
+
topic_map: dict[str, str] = {}
|
|
122
|
+
for prop_def in property_definitions:
|
|
123
|
+
forms = find_by_semantic_id(
|
|
124
|
+
prop_def.value, "https://www.w3.org/2019/wot/td#hasForm"
|
|
125
|
+
)
|
|
126
|
+
if forms is None:
|
|
127
|
+
print(f"Form SMC not found in PropertyDefinition {prop_def.id_short}.")
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
target_href = find_by_semantic_id(
|
|
131
|
+
forms.value, "https://www.w3.org/2019/wot/hypermedia#hasTarget"
|
|
132
|
+
)
|
|
133
|
+
if target_href is None:
|
|
134
|
+
print(f"Target property not found in Form SMC of PropertyDefinition {prop_def.id_short}.")
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
topic_map[prop_def.id_short] = target_href.value
|
|
138
|
+
return topic_map
|
|
139
|
+
|
|
140
|
+
def _get_default_mqtt_interface_description(self) -> MQTTInterfaceDescription:
|
|
141
|
+
"""Get the default MQTT interface description from the list of MQTT interfaces.
|
|
142
|
+
|
|
143
|
+
Default MQTT interface does not use Websocket. If no such interface is found, simply return the first one.
|
|
144
|
+
|
|
145
|
+
:return: The default MQTT interface or None if no interface is found.
|
|
146
|
+
"""
|
|
147
|
+
for interface in self._mqtt_interface_descriptions:
|
|
148
|
+
if not interface.websocket_connection:
|
|
149
|
+
print(f"Using default MQTT interface: {interface.interface_smc.id_short}")
|
|
150
|
+
return interface
|
|
151
|
+
|
|
152
|
+
if len(self._mqtt_interface_descriptions) > 0:
|
|
153
|
+
print(f"Using default MQTT interface: {self._mqtt_interface_descriptions[0].interface_smc.id_short}")
|
|
154
|
+
return self._mqtt_interface_descriptions[0]
|
|
155
|
+
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
def _get_fallback_mqtt_interface_description(self) -> MQTTInterfaceDescription:
|
|
159
|
+
"""Get the fallback MQTT interface description from the list of MQTT interfaces.
|
|
160
|
+
|
|
161
|
+
:return: The fallback MQTT interface or None if no second interface is provided in the AID SM.
|
|
162
|
+
"""
|
|
163
|
+
if len(self._mqtt_interface_descriptions) > 1:
|
|
164
|
+
for interface in self._mqtt_interface_descriptions:
|
|
165
|
+
if interface.websocket_connection:
|
|
166
|
+
print(f"Using fallback MQTT interface: {interface.interface_smc.id_short}")
|
|
167
|
+
return interface
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
def _get_mqtt_properties(self, default_mqtt_interface: SubmodelElementCollection) -> SubmodelElementCollection | None:
|
|
171
|
+
"""Get the MQTT properties from the InteractionMetadata SMC.
|
|
172
|
+
|
|
173
|
+
:return: The SubmodelElementCollection containing MQTT properties on None if not found.
|
|
174
|
+
"""
|
|
175
|
+
interaction_metadata: SubmodelElementCollection = find_by_semantic_id(
|
|
176
|
+
default_mqtt_interface.value, "https://admin-shell.io/idta/AssetInterfacesDescription/1/0/InteractionMetadata"
|
|
177
|
+
)
|
|
178
|
+
if interaction_metadata is None:
|
|
179
|
+
print("InteractionMetadata SMC not found in MQTT interface description.")
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
mqtt_property_collection: SubmodelElementCollection = find_by_semantic_id(
|
|
183
|
+
interaction_metadata.value, "https://www.w3.org/2019/wot/td#PropertyAffordance"
|
|
184
|
+
)
|
|
185
|
+
if mqtt_property_collection is None:
|
|
186
|
+
print("PropertyAffordance SMC not found in InteractionMetadata SMC.")
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
return mqtt_property_collection
|
|
190
|
+
|
|
191
|
+
def _uses_websocket(self, mqtt_interface: SubmodelElementCollection) -> bool:
|
|
192
|
+
"""Check if the given MQTT interface uses a WebSocket connection by searching for the appropriate semantic ID.
|
|
193
|
+
|
|
194
|
+
:param mqtt_interface: The MQTT interface to check.
|
|
195
|
+
:return: True if the interface uses WebSocket, False otherwise.
|
|
196
|
+
"""
|
|
197
|
+
return contains_supplemental_semantic_id(mqtt_interface, "https://www.rfc-editor.org/rfc/rfc6455")
|
|
198
|
+
|
|
199
|
+
def get_mqtt_topic_map(self, fallback: bool = False) -> dict[str, str]:
|
|
200
|
+
"""Get the MQTT topic map.
|
|
201
|
+
|
|
202
|
+
If the fallback value needs to be used, regenerate the topic map from the fallback MQTT interface.
|
|
203
|
+
|
|
204
|
+
:param fallback: Whether to use the fallback MQTT interface, defaults to False
|
|
205
|
+
:return: The MQTT topic map.
|
|
206
|
+
"""
|
|
207
|
+
if fallback:
|
|
208
|
+
self._create_topic_map(self._get_fallback().interface_smc)
|
|
209
|
+
return self._topic_map
|
|
210
|
+
|
|
211
|
+
def get_mqtt_base_url(self, fallback: bool = False) -> str:
|
|
212
|
+
"""Get the base URL for the MQTT connection.
|
|
213
|
+
|
|
214
|
+
:param fallback: Whether to use the fallback MQTT interface, defaults to False
|
|
215
|
+
:return: The base URL of the MQTT interface.
|
|
216
|
+
"""
|
|
217
|
+
if fallback:
|
|
218
|
+
return self._get_fallback().base_url
|
|
219
|
+
|
|
220
|
+
return self._default_mqtt_interface.base_url
|
|
221
|
+
|
|
222
|
+
def _get_fallback(self):
|
|
223
|
+
"""Get the fallback MQTT interface description if it exists.
|
|
224
|
+
|
|
225
|
+
:raises ConnectionError: If no fallback MQTT interface is available.
|
|
226
|
+
:return: The fallback MQTT interface description.
|
|
227
|
+
"""
|
|
228
|
+
if not self._fallback_mqtt_interface:
|
|
229
|
+
raise ConnectionError("No fallback MQTT interface available.")
|
|
230
|
+
return self._fallback_mqtt_interface
|
|
231
|
+
|
|
232
|
+
def uses_websocket_interface(self, fallback: bool = False) -> bool:
|
|
233
|
+
"""Check if the MQTT connection will be initialized using Websocket.
|
|
234
|
+
|
|
235
|
+
:param fallback: Whether to use the fallback MQTT interface, defaults to False
|
|
236
|
+
:return: True if the MQTT interface uses WebSocket, False otherwise.
|
|
237
|
+
"""
|
|
238
|
+
if fallback:
|
|
239
|
+
return self._get_fallback().websocket_connection
|
|
240
|
+
|
|
241
|
+
return self._default_mqtt_interface.websocket_connection
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def find_all_by_semantic_id(parent: Iterator[SubmodelElement], semantic_id_value: str) -> list[SubmodelElement]:
|
|
245
|
+
"""Find all SubmodelElements having a specific Semantic ID.
|
|
246
|
+
|
|
247
|
+
:param parent: The NamespaceSet to search within.
|
|
248
|
+
:param semantic_id_value: The semantic ID value to search for.
|
|
249
|
+
:return: The found SubmodelElement(s) or an empty list if not found.
|
|
250
|
+
"""
|
|
251
|
+
reference: Reference = ExternalReference(
|
|
252
|
+
[Key(
|
|
253
|
+
type_= KeyTypes.GLOBAL_REFERENCE,
|
|
254
|
+
value=semantic_id_value
|
|
255
|
+
)]
|
|
256
|
+
)
|
|
257
|
+
found_elements: list[SubmodelElement] = [
|
|
258
|
+
element for element in parent if element.semantic_id.__eq__(reference)
|
|
259
|
+
]
|
|
260
|
+
return found_elements
|
|
261
|
+
|
|
262
|
+
def find_by_semantic_id(parent: NamespaceSet[SubmodelElement], semantic_id_value: str) -> SubmodelElement:
|
|
263
|
+
"""Find a SubmodelElement by its semantic ID.
|
|
264
|
+
|
|
265
|
+
:param parent: The NamespaceSet to search within.
|
|
266
|
+
:param semantic_id_value: The semantic ID value to search for.
|
|
267
|
+
:return: The first found SubmodelElement, or None if not found.
|
|
268
|
+
"""
|
|
269
|
+
reference: Reference = ExternalReference(
|
|
270
|
+
[Key(
|
|
271
|
+
type_= KeyTypes.GLOBAL_REFERENCE,
|
|
272
|
+
value=semantic_id_value
|
|
273
|
+
)]
|
|
274
|
+
)
|
|
275
|
+
for element in parent:
|
|
276
|
+
if element.semantic_id.__eq__(reference):
|
|
277
|
+
return element
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
def find_by_supplemental_semantic_id(parent: NamespaceSet[SubmodelElement], semantic_id_value: str) -> SubmodelElement:
|
|
281
|
+
"""Find a SubmodelElement by its supplemental semantic ID.
|
|
282
|
+
|
|
283
|
+
:param parent: The NamespaceSet to search within.
|
|
284
|
+
:param semantic_id_value: The supplemental semantic ID value to search for.
|
|
285
|
+
:return: The first found SubmodelElement, or None if not found.
|
|
286
|
+
"""
|
|
287
|
+
for element in parent:
|
|
288
|
+
if contains_supplemental_semantic_id(element, semantic_id_value):
|
|
289
|
+
return element
|
|
290
|
+
return None
|
|
291
|
+
|
|
292
|
+
def contains_supplemental_semantic_id(element: SubmodelElement, semantic_id_value: str) -> bool:
|
|
293
|
+
"""Check if the element contains a specific supplemental semantic ID.
|
|
294
|
+
|
|
295
|
+
:param element: The SubmodelElement to check.
|
|
296
|
+
:param semantic_id_value: The supplemental semantic ID value to search for.
|
|
297
|
+
:return: True if the element contains the supplemental semantic ID, False otherwise.
|
|
298
|
+
"""
|
|
299
|
+
reference: Reference = ExternalReference(
|
|
300
|
+
[Key(
|
|
301
|
+
type_= KeyTypes.GLOBAL_REFERENCE,
|
|
302
|
+
value=semantic_id_value
|
|
303
|
+
)]
|
|
304
|
+
)
|
|
305
|
+
return element.supplemental_semantic_id.__contains__(reference)
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
import basyx.aas.adapter.json
|
|
5
|
+
from basyx.aas import model
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SourceSinkRelation:
|
|
11
|
+
"""Class representing a source-sink relation in the mapping configuration."""
|
|
12
|
+
|
|
13
|
+
aid_submodel_id: str
|
|
14
|
+
source: model.ExternalReference
|
|
15
|
+
sink: model.ExternalReference
|
|
16
|
+
property_name: str
|
|
17
|
+
|
|
18
|
+
def source_as_dict(self) -> dict:
|
|
19
|
+
"""Convert the source reference to a dictionary.
|
|
20
|
+
|
|
21
|
+
:return: The source reference as a dictionary.
|
|
22
|
+
"""
|
|
23
|
+
dict_string = json.dumps(self.source, cls=basyx.aas.adapter.json.AASToJsonEncoder)
|
|
24
|
+
dict_string = dict_string.replace("GlobalReference", "Submodel").replace("FragmentReference", "SubmodelElementCollection")
|
|
25
|
+
return json.loads(dict_string)
|
|
26
|
+
|
|
27
|
+
def sink_as_dict(self) -> dict:
|
|
28
|
+
"""Convert the sink reference to a dictionary.
|
|
29
|
+
|
|
30
|
+
:return: The sink reference as a dictionary.
|
|
31
|
+
"""
|
|
32
|
+
return json.loads(json.dumps(self.sink, cls=basyx.aas.adapter.json.AASToJsonEncoder))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class MappingConfiguration:
|
|
36
|
+
"""Class representing a mapping configuration."""
|
|
37
|
+
|
|
38
|
+
interface_reference: model.ReferenceElement
|
|
39
|
+
aid_submodel_id: str
|
|
40
|
+
source_sink_relations: list[SourceSinkRelation]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class MappingConfigurations:
|
|
44
|
+
"""Class representing mapping configurations from AIMC submodel."""
|
|
45
|
+
|
|
46
|
+
configurations: list[MappingConfiguration]
|
|
47
|
+
aid_submodel_ids: list[str]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class AIMCParser:
|
|
51
|
+
"""Parser for the AIMC submodel.
|
|
52
|
+
|
|
53
|
+
:return: The parsed AIMC submodel.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
aimc_submodel: model.Submodel | None = None
|
|
57
|
+
mapping_configuration_element: model.SubmodelElementCollection | None = None
|
|
58
|
+
|
|
59
|
+
def __init__(self, aimc_submodel: model.Submodel):
|
|
60
|
+
"""Initialize the AIMC parser.
|
|
61
|
+
|
|
62
|
+
:param aimc_submodel: The AIMC submodel to parse.
|
|
63
|
+
"""
|
|
64
|
+
if aimc_submodel is None:
|
|
65
|
+
raise ValueError("AIMC submodel cannot be None.")
|
|
66
|
+
|
|
67
|
+
self.aimc_submodel = aimc_submodel
|
|
68
|
+
|
|
69
|
+
def get_mapping_configuration_root_element(self) -> model.SubmodelElementCollection | None:
|
|
70
|
+
"""Get the mapping configuration root submodel element collection from the AIMC submodel.
|
|
71
|
+
|
|
72
|
+
:return: The mapping configuration root submodel element collection or None if not found.
|
|
73
|
+
"""
|
|
74
|
+
self.mapping_configuration_element = next(
|
|
75
|
+
(elem for elem in self.aimc_submodel.submodel_element if elem.id_short == "MappingConfigurations"), None
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if self.mapping_configuration_element is None:
|
|
79
|
+
logger.error("'MappingConfigurations' element list not found in AIMC submodel.")
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
return self.mapping_configuration_element
|
|
83
|
+
|
|
84
|
+
def get_mapping_configuration_elements(self) -> list[model.SubmodelElementCollection] | None:
|
|
85
|
+
"""Get all mapping configurations elements from the AIMC submodel.
|
|
86
|
+
|
|
87
|
+
:return: A dictionary containing all mapping configurations elements.
|
|
88
|
+
"""
|
|
89
|
+
if self.mapping_configuration_element is None:
|
|
90
|
+
self.mapping_configuration_element = self.get_mapping_configuration_root_element()
|
|
91
|
+
|
|
92
|
+
if self.mapping_configuration_element is None:
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
mapping_configurations: list[model.SubmodelElementCollection] = [
|
|
96
|
+
element for element in self.mapping_configuration_element.value if isinstance(element, model.SubmodelElementCollection)
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
logger.debug(f"Found {len(mapping_configurations)} mapping configuration elements in AIMC submodel.")
|
|
100
|
+
|
|
101
|
+
return mapping_configurations
|
|
102
|
+
|
|
103
|
+
def parse_mapping_configurations(self) -> MappingConfigurations:
|
|
104
|
+
"""Parse all mapping configurations in the AIMC submodel.
|
|
105
|
+
|
|
106
|
+
:return: A list of parsed mapping configurations.
|
|
107
|
+
"""
|
|
108
|
+
logger.info("Parse mapping configurations from AIMC submodel.")
|
|
109
|
+
|
|
110
|
+
mapping_configurations: list[MappingConfiguration] = []
|
|
111
|
+
|
|
112
|
+
mc_elements = self.get_mapping_configuration_elements()
|
|
113
|
+
|
|
114
|
+
if mc_elements is None:
|
|
115
|
+
logger.error("No mapping configuration elements found in AIMC submodel.")
|
|
116
|
+
return mapping_configurations
|
|
117
|
+
|
|
118
|
+
for mc_element in mc_elements:
|
|
119
|
+
mc = self.parse_mapping_configuration(mc_element)
|
|
120
|
+
if mc is not None:
|
|
121
|
+
mapping_configurations.append(mc)
|
|
122
|
+
|
|
123
|
+
logger.debug(f"Parsed {len(mapping_configurations)} mapping configurations.")
|
|
124
|
+
|
|
125
|
+
mcs = MappingConfigurations()
|
|
126
|
+
mcs.configurations = mapping_configurations
|
|
127
|
+
# add all unique AID submodel IDs from all mapping configurations
|
|
128
|
+
mcs.aid_submodel_ids = list({mc.aid_submodel_id for mc in mapping_configurations})
|
|
129
|
+
|
|
130
|
+
logger.debug(f"Found {len(mcs.aid_submodel_ids)} unique AID submodel IDs in mapping configurations.")
|
|
131
|
+
logger.debug(f"Found {len(mcs.configurations)} mapping configurations in AIMC submodel.")
|
|
132
|
+
|
|
133
|
+
return mcs
|
|
134
|
+
|
|
135
|
+
def parse_mapping_configuration(self, mapping_configuration_element: model.SubmodelElementCollection) -> MappingConfiguration | None:
|
|
136
|
+
"""Parse a mapping configuration element.
|
|
137
|
+
|
|
138
|
+
:param mapping_configuration_element: The mapping configuration element to parse.
|
|
139
|
+
:return: The parsed mapping configuration or None if parsing failed.
|
|
140
|
+
"""
|
|
141
|
+
if mapping_configuration_element is None:
|
|
142
|
+
logger.error("Mapping configuration element is None.")
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
logger.debug(f"Parse mapping configuration '{mapping_configuration_element}'")
|
|
146
|
+
|
|
147
|
+
interface_reference = self._get_interface_reference(mapping_configuration_element)
|
|
148
|
+
|
|
149
|
+
if interface_reference is None:
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
source_sink_relations = self._generate_source_sink_relations(mapping_configuration_element)
|
|
153
|
+
|
|
154
|
+
if len(source_sink_relations) == 0:
|
|
155
|
+
logger.error(f"No source-sink relations found in mapping configuration '{mapping_configuration_element.id_short}'.")
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
# check if all relations have the same AID submodel
|
|
159
|
+
aid_submodel_ids = list({source_sink_relation.aid_submodel_id for source_sink_relation in source_sink_relations})
|
|
160
|
+
|
|
161
|
+
if len(aid_submodel_ids) != 1:
|
|
162
|
+
logger.error(
|
|
163
|
+
f"Multiple AID submodel IDs found in mapping configuration '{mapping_configuration_element.id_short}': {aid_submodel_ids}. Expected exactly one AID submodel ID."
|
|
164
|
+
)
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
mc = MappingConfiguration()
|
|
168
|
+
mc.interface_reference = interface_reference
|
|
169
|
+
mc.source_sink_relations = source_sink_relations
|
|
170
|
+
# add all unique AID submodel IDs from source-sink relations
|
|
171
|
+
mc.aid_submodel_id = aid_submodel_ids[0]
|
|
172
|
+
return mc
|
|
173
|
+
|
|
174
|
+
def _get_interface_reference(self, mapping_configuration_element: model.SubmodelElementCollection) -> model.ReferenceElement | None:
|
|
175
|
+
"""Get the interface reference ID from the mapping configuration element.
|
|
176
|
+
|
|
177
|
+
:param mapping_configuration_element: The mapping configuration element to extract the interface reference ID from.
|
|
178
|
+
:return: The interface reference ID or None if not found.
|
|
179
|
+
"""
|
|
180
|
+
logger.debug(f"Get 'InterfaceReference' from mapping configuration '{mapping_configuration_element}'.")
|
|
181
|
+
|
|
182
|
+
interface_ref: model.ReferenceElement = next(
|
|
183
|
+
(elem for elem in mapping_configuration_element.value if elem.id_short == "InterfaceReference"), None
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
if interface_ref is None or not isinstance(interface_ref, model.ReferenceElement):
|
|
187
|
+
logger.error(f"'InterfaceReference' not found in mapping configuration '{mapping_configuration_element.id_short}'.")
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
if interface_ref.value is None or len(interface_ref.value.key) == 0:
|
|
191
|
+
logger.error(f"'InterfaceReference' has no value in mapping configuration '{mapping_configuration_element.id_short}'.")
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
return interface_ref
|
|
195
|
+
|
|
196
|
+
def _generate_source_sink_relations(self, mapping_configuration_element: model.SubmodelElementCollection) -> list[SourceSinkRelation]:
|
|
197
|
+
source_sink_relations: list[SourceSinkRelation] = []
|
|
198
|
+
|
|
199
|
+
logger.debug(f"Get 'MappingSourceSinkRelations' from mapping configuration '{mapping_configuration_element}'.")
|
|
200
|
+
|
|
201
|
+
relations_list: model.SubmodelElementList = next(
|
|
202
|
+
(elem for elem in mapping_configuration_element.value if elem.id_short == "MappingSourceSinkRelations"), None
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
if relations_list is None or not isinstance(relations_list, model.SubmodelElementList):
|
|
206
|
+
logger.error(f"'MappingSourceSinkRelations' not found in mapping configuration '{mapping_configuration_element.id_short}'.")
|
|
207
|
+
return source_sink_relations
|
|
208
|
+
|
|
209
|
+
for source_sink_relation in relations_list.value:
|
|
210
|
+
logger.debug(f"Parse source-sink relation '{source_sink_relation}'.")
|
|
211
|
+
|
|
212
|
+
if not isinstance(source_sink_relation, model.RelationshipElement):
|
|
213
|
+
logger.warning(f"'{source_sink_relation.id_short}' is not a RelationshipElement")
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
if source_sink_relation.first is None or len(source_sink_relation.first.key) == 0:
|
|
217
|
+
logger.warning(f"'first' reference is missing in RelationshipElement '{source_sink_relation.id_short}'")
|
|
218
|
+
continue
|
|
219
|
+
|
|
220
|
+
if source_sink_relation.second is None or len(source_sink_relation.second.key) == 0:
|
|
221
|
+
logger.warning(f"'second' reference is missing in RelationshipElement '{source_sink_relation.id_short}'")
|
|
222
|
+
continue
|
|
223
|
+
|
|
224
|
+
global_ref = next((key for key in source_sink_relation.first.key if key.type == model.KeyTypes.GLOBAL_REFERENCE), None)
|
|
225
|
+
|
|
226
|
+
if global_ref is None:
|
|
227
|
+
logger.warning(f"No GLOBAL_REFERENCE key found in 'first' reference of RelationshipElement '{source_sink_relation.id_short}'")
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
last_fragment_ref = next((key for key in reversed(source_sink_relation.first.key) if key.type == model.KeyTypes.FRAGMENT_REFERENCE), None)
|
|
231
|
+
|
|
232
|
+
if last_fragment_ref is None:
|
|
233
|
+
logger.warning(f"No FRAGMENT_REFERENCE key found in 'first' reference of RelationshipElement '{source_sink_relation.id_short}'")
|
|
234
|
+
continue
|
|
235
|
+
|
|
236
|
+
relation = SourceSinkRelation()
|
|
237
|
+
relation.source = source_sink_relation.first
|
|
238
|
+
relation.sink = source_sink_relation.second
|
|
239
|
+
relation.aid_submodel_id = global_ref.value
|
|
240
|
+
relation.property_name = last_fragment_ref.value
|
|
241
|
+
|
|
242
|
+
source_sink_relations.append(relation)
|
|
243
|
+
|
|
244
|
+
return source_sink_relations
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
aas_standard_parser/__init__.py,sha256=JEyzB-q1WkxIMyO6JMBh4I4VqwpjZGPNFQqPshO5nUU,503
|
|
2
|
+
aas_standard_parser/aid_parser.py,sha256=0U3J9WOkt5t_xh9SZZ_7h_uQ-bR6Q7NaZltbyq69Qsc,13758
|
|
3
|
+
aas_standard_parser/aimc_parser.py,sha256=GR70DhhkcsBYSKOlD6bJ9jm0GF5u4uYijY7S79jgJLE,10571
|
|
4
|
+
aas_standard_parser-0.1.3.dist-info/licenses/LICENSE,sha256=simqYMD2P9Ikm0Kh9n7VGNpaVcm2TMVVQmECYZ_xVZ8,1065
|
|
5
|
+
aas_standard_parser-0.1.3.dist-info/METADATA,sha256=7zNapXytrtQoP717BRMz25LwzkJ93PuuwYAafRsIahI,1718
|
|
6
|
+
aas_standard_parser-0.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
+
aas_standard_parser-0.1.3.dist-info/top_level.txt,sha256=OQaK6cwYttR1-eKTz5u4M0jbwSfp4HqJ56chaf0nHnw,20
|
|
8
|
+
aas_standard_parser-0.1.3.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
aas_standard_parser
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
aas_standard_parser-0.1.1.dist-info/licenses/LICENSE,sha256=simqYMD2P9Ikm0Kh9n7VGNpaVcm2TMVVQmECYZ_xVZ8,1065
|
|
2
|
-
aas_standard_parser-0.1.1.dist-info/METADATA,sha256=RObxSGDYUd_Pqt3LMW5xhagvyvNojdzc83VyjIz2j4Y,1718
|
|
3
|
-
aas_standard_parser-0.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
4
|
-
aas_standard_parser-0.1.1.dist-info/top_level.txt,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
5
|
-
aas_standard_parser-0.1.1.dist-info/RECORD,,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
File without changes
|
{aas_standard_parser-0.1.1.dist-info → aas_standard_parser-0.1.3.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|