aas-standard-parser 0.1.3__tar.gz → 0.1.5__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aas-standard-parser
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: Some auxiliary functions for parsing standard submodels
5
5
  Author-email: Daniel Klein <daniel.klein@em.ag>
6
6
  License: MIT License
@@ -1,8 +1,9 @@
1
1
  from datetime import datetime
2
2
  import importlib.metadata
3
3
 
4
+ # TODO: introduce MIT license
4
5
  __copyright__ = f"Copyright (C) {datetime.now().year} :em engineering methods AG. All rights reserved."
5
- __author__ = "Daniel Klein"
6
+ __author__ = "Daniel Klein, Celina Adelhardt, Tom Gneuß"
6
7
 
7
8
  try:
8
9
  __version__ = importlib.metadata.version(__name__)
@@ -12,7 +13,8 @@ except importlib.metadata.PackageNotFoundError:
12
13
  __project__ = "aas-standard-parser"
13
14
  __package__ = "aas-standard-parser"
14
15
 
15
- from aas_standard_parser import aid_parser, aimc_parser
16
+ from aas_standard_parser.aimc_parser import AIMCParser
17
+ from aas_standard_parser.aid_parser import AIDParser
16
18
 
17
19
 
18
- __all__ = ["aid_parser", "aimc_parser"]
20
+ __all__ = ["AIMCParser", "AIDParser"]
@@ -0,0 +1,259 @@
1
+ """This module provides functions to parse AID Submodels and extract MQTT interface descriptions."""
2
+ import base64
3
+ from typing import Dict, List
4
+
5
+ from basyx.aas.model import (
6
+ Property,
7
+ SubmodelElement,
8
+ SubmodelElementCollection, SubmodelElementList, Submodel,
9
+ )
10
+
11
+ from aas_standard_parser.collection_helpers import find_by_semantic_id, find_all_by_semantic_id, find_by_id_short
12
+
13
+
14
+ class PropertyDetails:
15
+
16
+ def __init__(self, href: str, keys: List[str]):
17
+ self.href = href
18
+ self.keys = keys
19
+
20
+
21
+ class IAuthenticationDetails:
22
+
23
+ def __init__(self):
24
+ # TODO: different implementations for different AID versions
25
+ pass
26
+
27
+
28
+ class BasicAuthenticationDetails(IAuthenticationDetails):
29
+
30
+ def __init__(self, user: str, password: str):
31
+ self.user = user
32
+ self.password = password
33
+ super().__init__()
34
+
35
+
36
+ class NoAuthenticationDetails(IAuthenticationDetails):
37
+
38
+ def __init__(self):
39
+ super().__init__()
40
+
41
+
42
+ class AIDParser():
43
+
44
+ def __init__(self):
45
+ pass
46
+
47
+ def get_base_url_from_interface(self, aid_interface: SubmodelElementCollection) -> str:
48
+ """Get the base address (EndpointMetadata.base) from a SMC describing an interface in the AID."""
49
+
50
+ endpoint_metadata: SubmodelElementCollection | None = find_by_semantic_id(
51
+ aid_interface.value, "https://admin-shell.io/idta/AssetInterfacesDescription/1/0/EndpointMetadata"
52
+ )
53
+ if endpoint_metadata is None:
54
+ raise ValueError(f"'EndpointMetadata' SMC not found in the provided '{aid_interface.id_short}' SMC.")
55
+
56
+ base: Property | None = find_by_semantic_id(
57
+ endpoint_metadata.value, "https://www.w3.org/2019/wot/td#baseURI"
58
+ )
59
+ if base is None:
60
+ raise ValueError("'base' Property not found in 'EndpointMetadata' SMC.")
61
+
62
+ return base.value
63
+
64
+
65
+ def create_property_to_href_map(self, aid_interface: SubmodelElementCollection) -> Dict[str, PropertyDetails]:
66
+ """Find all first-level and nested properties in a provided SMC describing one interface in the AID.
67
+ Map each property (either top-level or nested) to the according 'href' attribute.
68
+ Nested properties are further mapped to the hierarchical list of keys
69
+ that are necessary to extract their value from the payload of the interface.
70
+
71
+ :param aid_interface: An SMC describing an interface in the AID.
72
+ :return: A dictionary mapping each property (represented by its idShort-path) to PropertyDetails.
73
+ """
74
+ mapping: Dict[str, PropertyDetails] = {}
75
+
76
+ interaction_metadata: SubmodelElementCollection | None = find_by_semantic_id(
77
+ aid_interface.value, "https://admin-shell.io/idta/AssetInterfacesDescription/1/0/InteractionMetadata"
78
+ )
79
+ if interaction_metadata is None:
80
+ raise ValueError(f"'InteractionMetadata' SMC not found in the provided '{aid_interface.id_short}' SMC.")
81
+
82
+ properties: SubmodelElementCollection | None = find_by_semantic_id(
83
+ interaction_metadata.value, "https://www.w3.org/2019/wot/td#PropertyAffordance"
84
+ )
85
+ if properties is None:
86
+ raise ValueError("'properties' SMC not found in 'InteractionMetadata' SMC.")
87
+
88
+ fl_properties: List[SubmodelElement] = find_all_by_semantic_id(
89
+ properties.value, "https://admin-shell.io/idta/AssetInterfacesDescription/1/0/PropertyDefinition"
90
+ )
91
+ # TODO: some AIDs have typos in that semanticId but we only support the official ones
92
+ #fl_properties_alternative: List[SubmodelElement] = find_all_by_semantic_id(
93
+ # properties.value, "https://admin-shell.io/idta/AssetInterfaceDescription/1/0/PropertyDefinition"
94
+ #)
95
+ #fl_properties.extend(fl_properties_alternative)
96
+ if fl_properties is None:
97
+ #raise ValueError(f"No first-level 'property' SMC not found in 'properties' SMC.")
98
+ return {}
99
+
100
+ def traverse_property(smc: SubmodelElementCollection, parent_path: str, href: str, key_path: List[str | int],
101
+ is_items=False, idx=None, is_top_level=False):
102
+ # determine local key only if not top-level
103
+ if not is_top_level:
104
+ if is_items and idx is not None:
105
+ local_key = idx # integer index
106
+ else:
107
+ key_prop = find_by_semantic_id(
108
+ smc.value, "https://admin-shell.io/idta/AssetInterfacesDescription/1/0/key"
109
+ )
110
+ local_key = key_prop.value if key_prop else smc.id_short # string
111
+ new_key_path = key_path + [local_key]
112
+ else:
113
+ new_key_path = key_path # top-level: no key added
114
+
115
+ # register this property
116
+ full_path = f"{parent_path}.{smc.id_short}"
117
+ mapping[full_path] = PropertyDetails(href, new_key_path)
118
+
119
+ # traverse nested "properties" or "items"
120
+ # (nested properties = object members, nested items = array elements)
121
+ # TODO: some apparently use the wrong semanticId:
122
+ # "https://www.w3.org/2019/wot/td#PropertyAffordance"
123
+ for nested_sem_id in [
124
+ "https://www.w3.org/2019/wot/json-schema#properties",
125
+ "https://www.w3.org/2019/wot/json-schema#items",
126
+ ]:
127
+ nested_group: SubmodelElementCollection | None = find_by_semantic_id(smc.value, nested_sem_id)
128
+ if nested_group:
129
+ # attach the name of that SMC ("items" or "properties" or similar) to the key_path
130
+ full_path += "." + nested_group.id_short
131
+
132
+ # find all nested properties/items by semantic-ID
133
+ nested_properties: List[SubmodelElement] = find_all_by_semantic_id(
134
+ nested_group.value, "https://www.w3.org/2019/wot/json-schema#propertyName"
135
+ )
136
+ # TODO: some AIDs have typos or use wrong semanticIds but we only support the official ones
137
+ #nested_properties_alternative1: List[SubmodelElement] = find_all_by_semantic_id(
138
+ # nested_group.value, "https://admin-shell.io/idta/AssetInterfaceDescription/1/0/PropertyDefinition"
139
+ #)
140
+ # nested_properties_alternative2: List[SubmodelElement] = find_all_by_semantic_id(
141
+ # nested_group.value, "https://admin-shell.io/idta/AssetInterfacesDescription/1/0/PropertyDefinition"
142
+ # )
143
+ #nested_properties.extend(nested_properties_alternative1)
144
+ #nested_properties.extend(nested_properties_alternative2)
145
+
146
+ # traverse all nested properties/items recursively
147
+ for idx, nested in enumerate(nested_properties):
148
+ if nested_sem_id.endswith("#items"):
149
+ # for arrays: append index instead of property key
150
+ traverse_property(nested, full_path, href, new_key_path, is_items=True, idx=idx)
151
+ else:
152
+ traverse_property(nested, full_path, href, new_key_path)
153
+
154
+ # process all first-level properties
155
+ for fl_property in fl_properties:
156
+ forms: SubmodelElementCollection | None = find_by_semantic_id(
157
+ fl_property.value, "https://www.w3.org/2019/wot/td#hasForm"
158
+ )
159
+ if forms is None:
160
+ raise ValueError(f"'forms' SMC not found in '{fl_property.id_short}' SMC.")
161
+
162
+ href: Property | None = find_by_semantic_id(
163
+ forms.value, "https://www.w3.org/2019/wot/hypermedia#hasTarget"
164
+ )
165
+ if href is None:
166
+ raise ValueError("'href' Property not found in 'forms' SMC.")
167
+
168
+ href_value = href.value
169
+ idshort_path_prefix = f"{aid_interface.id_short}.{interaction_metadata.id_short}.{properties.id_short}"
170
+
171
+ traverse_property(
172
+ fl_property,
173
+ idshort_path_prefix,
174
+ href_value,
175
+ [],
176
+ is_top_level=True
177
+ )
178
+
179
+ return mapping
180
+
181
+
182
+ def parse_security(self, aid_interface: SubmodelElementCollection) -> IAuthenticationDetails:
183
+ """Extract the authentication details (EndpointMetadata.security) from the provided interface in the AID.
184
+
185
+ :param aid_interface: An SMC describing an interface in the AID.
186
+ :return: A subtype of IAuthenticationDetails with details depending on the specified authentication method for the interface.
187
+ """
188
+ endpoint_metadata: SubmodelElementCollection | None = find_by_semantic_id(
189
+ aid_interface.value, "https://admin-shell.io/idta/AssetInterfacesDescription/1/0/EndpointMetadata"
190
+ )
191
+ if endpoint_metadata is None:
192
+ raise ValueError(f"'EndpointMetadata' SMC not found in the provided '{aid_interface.id_short}' SMC.")
193
+
194
+ security: SubmodelElementList | None = find_by_semantic_id(
195
+ endpoint_metadata.value, "https://www.w3.org/2019/wot/td#hasSecurityConfiguration"
196
+ )
197
+ if security is None:
198
+ raise ValueError("'security' SML not found in 'EndpointMetadata' SMC.")
199
+
200
+ # TODO: resolve the full reference(s)
201
+ # for now, assume there is only one reference to the security in use
202
+ # -> access SML[0]
203
+ # assume that this ReferenceElement points to a security scheme in this very AID SM
204
+ # -> can just use the last key to determine the type of security
205
+ sc_idshort = security.value[0].value.key[-1].value
206
+
207
+ # get the securityDefinitions SMC
208
+ security_definitions: SubmodelElementCollection | None = find_by_semantic_id(
209
+ endpoint_metadata.value, "https://www.w3.org/2019/wot/td#definesSecurityScheme"
210
+ )
211
+ if security_definitions is None:
212
+ raise ValueError("'securityDefinitions' SMC not found in 'EndpointMetadata' SMC.")
213
+
214
+ # find the security scheme SMC with the same idShort as mentioned in the reference "sc"
215
+ referenced_security: SubmodelElementCollection | None = find_by_id_short(
216
+ security_definitions.value, sc_idshort
217
+ )
218
+ if referenced_security is None:
219
+ raise ValueError(f"Referenced security scheme '{sc_idshort}' SMC not found in 'securityDefinitions' SMC")
220
+
221
+ # get the name of the security scheme
222
+ scheme: Property | None = find_by_semantic_id(
223
+ referenced_security.value, "https://www.w3.org/2019/wot/security#SecurityScheme"
224
+ )
225
+ if scheme is None:
226
+ raise ValueError(f"'scheme' Property not found in referenced security scheme '{sc_idshort}' SMC.")
227
+
228
+ auth_details: IAuthenticationDetails = None
229
+
230
+ match scheme.value:
231
+ case "nosec":
232
+ auth_details = NoAuthenticationDetails()
233
+
234
+ case "basic":
235
+ basic_sc_name: Property | None = find_by_semantic_id(
236
+ referenced_security.value, "https://www.w3.org/2019/wot/security#name"
237
+ )
238
+ if basic_sc_name is None:
239
+ raise ValueError("'name' Property not found in 'basic_sc' SMC")
240
+
241
+ auth_base64 = basic_sc_name.value
242
+ auth_plain = base64.b64decode(auth_base64).decode("utf-8")
243
+ auth_details = BasicAuthenticationDetails(auth_plain.split(":")[0], auth_plain.split(":")[1])
244
+
245
+ # TODO: remaining cases
246
+ case "digest":
247
+ pass
248
+ case "bearer":
249
+ pass
250
+ case "psk":
251
+ pass
252
+ case "oauth2":
253
+ pass
254
+ case "apikey":
255
+ pass
256
+ case "auto":
257
+ pass
258
+
259
+ return auth_details
@@ -0,0 +1,88 @@
1
+ from basyx.aas.model import NamespaceSet, SubmodelElement, ExternalReference, Reference, Key, KeyTypes
2
+ from typing import List
3
+
4
+
5
+ def find_all_by_semantic_id(parent: NamespaceSet[SubmodelElement], semantic_id_value: str) -> List[SubmodelElement]:
6
+ """Find all SubmodelElements having a specific Semantic ID.
7
+
8
+ :param parent: The NamespaceSet to search within.
9
+ :param semantic_id_value: The semantic ID value to search for.
10
+ :return: The found SubmodelElement(s) or an empty list if not found.
11
+ """
12
+ reference: Reference = ExternalReference(
13
+ (Key(
14
+ type_=KeyTypes.GLOBAL_REFERENCE,
15
+ value=semantic_id_value
16
+ ),)
17
+ )
18
+ found_elements: list[SubmodelElement] = [
19
+ element for element in parent if element.semantic_id.__eq__(reference)
20
+ ]
21
+ return found_elements
22
+
23
+
24
+ def find_by_semantic_id(parent: NamespaceSet[SubmodelElement], semantic_id_value: str) -> SubmodelElement | None:
25
+ """Find a SubmodelElement by its semantic ID.
26
+
27
+ :param parent: The NamespaceSet to search within.
28
+ :param semantic_id_value: The semantic ID value to search for.
29
+ :return: The first found SubmodelElement, or None if not found.
30
+ @rtype: object
31
+ """
32
+
33
+ # create a Reference that acts like the to-be-matched semanticId
34
+ reference: Reference = ExternalReference(
35
+ (Key(
36
+ type_=KeyTypes.GLOBAL_REFERENCE,
37
+ value=semantic_id_value
38
+ ),)
39
+ )
40
+
41
+ # check if the constructed Reference appears as semanticId of the child elements
42
+ for element in parent:
43
+ if element.semantic_id.__eq__(reference):
44
+ return element
45
+ return None
46
+
47
+
48
+ def find_by_id_short(parent: NamespaceSet[SubmodelElement], id_short_value: str) -> SubmodelElement | None:
49
+ """Find a SubmodelElement by its idShort.
50
+
51
+ :param parent: The NamespaceSet to search within.
52
+ :param id_short_value: The idShort value to search for.
53
+ :return: The first found SubmodelElement, or None if not found.
54
+ """
55
+ for element in parent:
56
+ if element.id_short == id_short_value:
57
+ return element
58
+
59
+ return None
60
+
61
+
62
+ def find_by_supplemental_semantic_id(parent: NamespaceSet[SubmodelElement], semantic_id_value: str) -> SubmodelElement | None:
63
+ """Find a SubmodelElement by its supplemental semantic ID.
64
+
65
+ :param parent: The NamespaceSet to search within.
66
+ :param semantic_id_value: The supplemental semantic ID value to search for.
67
+ :return: The first found SubmodelElement, or None if not found.
68
+ """
69
+ for element in parent:
70
+ if contains_supplemental_semantic_id(element, semantic_id_value):
71
+ return element
72
+ return None
73
+
74
+
75
+ def contains_supplemental_semantic_id(element: SubmodelElement, semantic_id_value: str) -> bool:
76
+ """Check if the element contains a specific supplemental semantic ID.
77
+
78
+ :param element: The SubmodelElement to check.
79
+ :param semantic_id_value: The supplemental semantic ID value to search for.
80
+ :return: True if the element contains the supplemental semantic ID, False otherwise.
81
+ """
82
+ reference: Reference = ExternalReference(
83
+ (Key(
84
+ type_=KeyTypes.GLOBAL_REFERENCE,
85
+ value=semantic_id_value
86
+ ),)
87
+ )
88
+ return element.supplemental_semantic_id.__contains__(reference)
@@ -0,0 +1,12 @@
1
+ from basyx.aas.model import ModelReference
2
+
3
+
4
+ def construct_idshort_path_from_reference(reference: ModelReference) -> str:
5
+ idshort_path: str = ""
6
+
7
+ # start from the second Key and omit the Identifiable at the beginning of the list
8
+ for key in reference.key[1:]:
9
+ idshort_path += (key.value + ".")
10
+
11
+ # get rid of the trailing dot
12
+ return idshort_path[:-1]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aas-standard-parser
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: Some auxiliary functions for parsing standard submodels
5
5
  Author-email: Daniel Klein <daniel.klein@em.ag>
6
6
  License: MIT License
@@ -4,6 +4,8 @@ pyproject.toml
4
4
  aas_standard_parser/__init__.py
5
5
  aas_standard_parser/aid_parser.py
6
6
  aas_standard_parser/aimc_parser.py
7
+ aas_standard_parser/collection_helpers.py
8
+ aas_standard_parser/reference_helpers.py
7
9
  aas_standard_parser.egg-info/PKG-INFO
8
10
  aas_standard_parser.egg-info/SOURCES.txt
9
11
  aas_standard_parser.egg-info/dependency_links.txt
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "aas-standard-parser"
7
- version = "0.1.3"
7
+ version = "0.1.5"
8
8
  description = "Some auxiliary functions for parsing standard submodels"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -1,305 +0,0 @@
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)