Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix the implementation of DataSpecificationIEC61360 and its De-/Serialization #141

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 21 additions & 14 deletions basyx/aas/adapter/_generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,19 +70,26 @@
model.EntityType.CO_MANAGED_ENTITY: 'CoManagedEntity',
model.EntityType.SELF_MANAGED_ENTITY: 'SelfManagedEntity'}

IEC61360_DATA_TYPES: Dict[model.base.IEC61360DataType, str] = {
model.base.IEC61360DataType.DATE: 'DATE',
model.base.IEC61360DataType.STRING: 'STRING',
model.base.IEC61360DataType.STRING_TRANSLATABLE: 'STRING_TRANSLATABLE',
model.base.IEC61360DataType.REAL_MEASURE: 'REAL_MEASURE',
model.base.IEC61360DataType.REAL_COUNT: 'REAL_COUNT',
model.base.IEC61360DataType.REAL_CURRENCY: 'REAL_CURRENCY',
model.base.IEC61360DataType.BOOLEAN: 'BOOLEAN',
model.base.IEC61360DataType.URL: 'URL',
model.base.IEC61360DataType.RATIONAL: 'RATIONAL',
model.base.IEC61360DataType.RATIONAL_MEASURE: 'RATIONAL_MEASURE',
model.base.IEC61360DataType.TIME: 'TIME',
model.base.IEC61360DataType.TIMESTAMP: 'TIMESTAMP',
IEC61360_DATA_TYPES: Dict[model.base.DataTypeIEC61360, str] = {
model.base.DataTypeIEC61360.DATE: 'DATE',
model.base.DataTypeIEC61360.STRING: 'STRING',
model.base.DataTypeIEC61360.STRING_TRANSLATABLE: 'STRING_TRANSLATABLE',
model.base.DataTypeIEC61360.INTEGER_MEASURE: 'INTEGER_MEASURE',
model.base.DataTypeIEC61360.INTEGER_COUNT: 'INTEGER_COUNT',
model.base.DataTypeIEC61360.INTEGER_CURRENCY: 'INTEGER_CURRENCY',
model.base.DataTypeIEC61360.REAL_MEASURE: 'REAL_MEASURE',
model.base.DataTypeIEC61360.REAL_COUNT: 'REAL_COUNT',
model.base.DataTypeIEC61360.REAL_CURRENCY: 'REAL_CURRENCY',
model.base.DataTypeIEC61360.BOOLEAN: 'BOOLEAN',
model.base.DataTypeIEC61360.IRI: 'IRI',
model.base.DataTypeIEC61360.IRDI: 'IRDI',
model.base.DataTypeIEC61360.RATIONAL: 'RATIONAL',
model.base.DataTypeIEC61360.RATIONAL_MEASURE: 'RATIONAL_MEASURE',
model.base.DataTypeIEC61360.TIME: 'TIME',
model.base.DataTypeIEC61360.TIMESTAMP: 'TIMESTAMP',
model.base.DataTypeIEC61360.HTML: 'HTML',
model.base.DataTypeIEC61360.BLOB: 'BLOB',
model.base.DataTypeIEC61360.FILE: 'FILE',
}

IEC61360_LEVEL_TYPES: Dict[model.base.IEC61360LevelType, str] = {
Expand All @@ -100,7 +107,7 @@
REFERENCE_TYPES_INVERSE: Dict[str, Type[model.Reference]] = {v: k for k, v in REFERENCE_TYPES.items()}
KEY_TYPES_INVERSE: Dict[str, model.KeyTypes] = {v: k for k, v in KEY_TYPES.items()}
ENTITY_TYPES_INVERSE: Dict[str, model.EntityType] = {v: k for k, v in ENTITY_TYPES.items()}
IEC61360_DATA_TYPES_INVERSE: Dict[str, model.base.IEC61360DataType] = {v: k for k, v in IEC61360_DATA_TYPES.items()}
IEC61360_DATA_TYPES_INVERSE: Dict[str, model.base.DataTypeIEC61360] = {v: k for k, v in IEC61360_DATA_TYPES.items()}
IEC61360_LEVEL_TYPES_INVERSE: Dict[str, model.base.IEC61360LevelType] = \
{v: k for k, v in IEC61360_LEVEL_TYPES.items()}

Expand Down
4 changes: 2 additions & 2 deletions basyx/aas/adapter/json/aasJSONSchema.json
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@
"modelType"
]
},
"DataSpecificationIEC61360": {
"DataSpecificationIec61360": {
"allOf": [
{
"$ref": "#/definitions/DataSpecificationContent"
Expand Down Expand Up @@ -797,7 +797,7 @@
"Blob",
"Capability",
"ConceptDescription",
"DataSpecificationIEC61360",
"DataSpecificationIec61360",
"Entity",
"File",
"MultiLanguageProperty",
Expand Down
25 changes: 11 additions & 14 deletions basyx/aas/adapter/json/json_deserialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ def object_hook(cls, dct: Dict[str, object]) -> object:
'Property': cls._construct_property,
'Range': cls._construct_range,
'ReferenceElement': cls._construct_reference_element,
'DataSpecificationIEC61360': cls._construct_data_specification_iec61360,
'DataSpecificationIec61360': cls._construct_data_specification_iec61360,
}

# Get modelType and constructor function
Expand Down Expand Up @@ -265,8 +265,7 @@ def _amend_abstract_attributes(cls, obj: object, dct: Dict[str, object]) -> None
# TODO: remove the following type: ignore comment when mypy supports abstract types for Type[T]
# see https://github.com/python/mypy/issues/5374
model.EmbeddedDataSpecification(
data_specification=cls._construct_external_reference(_get_ts(dspec, 'dataSpecification',
dict)),
data_specification=cls._construct_reference(_get_ts(dspec, 'dataSpecification', dict)),
data_specification_content=_get_ts(dspec, 'dataSpecificationContent',
model.DataSpecificationContent) # type: ignore
)
Expand Down Expand Up @@ -388,11 +387,11 @@ def _construct_lang_string_set(cls, lst: List[Dict[str, object]], object_class:
return object_class(ret)

@classmethod
def _construct_value_list(cls, dct: Dict[str, object], value_format: model.DataTypeDefXsd) -> model.ValueList:
def _construct_value_list(cls, dct: Dict[str, object]) -> model.ValueList:
ret: model.ValueList = set()
for element in _get_ts(dct, 'valueReferencePairs', list):
try:
ret.add(cls._construct_value_reference_pair(element, value_format=value_format))
ret.add(cls._construct_value_reference_pair(element))
except (KeyError, TypeError) as e:
error_message = "Error while trying to convert JSON object into ValueReferencePair: {} >>> {}".format(
e, pprint.pformat(element, depth=2, width=2 ** 14, compact=True))
Expand All @@ -403,11 +402,10 @@ def _construct_value_list(cls, dct: Dict[str, object], value_format: model.DataT
return ret

@classmethod
def _construct_value_reference_pair(cls, dct: Dict[str, object], value_format: model.DataTypeDefXsd,
def _construct_value_reference_pair(cls, dct: Dict[str, object],
object_class=model.ValueReferencePair) -> model.ValueReferencePair:
return object_class(value=model.datatypes.from_xsd(_get_ts(dct, 'value', str), value_format),
value_id=cls._construct_reference(_get_ts(dct, 'valueId', dict)),
value_type=value_format)
return object_class(value=_get_ts(dct, 'value', str),
value_id=cls._construct_reference(_get_ts(dct, 'valueId', dict)))

# #############################################################################
# Direct Constructor Methods (for classes with `modelType`) starting from here
Expand All @@ -425,8 +423,7 @@ def _construct_asset_information(cls, dct: Dict[str, object], object_class=model
ret.global_asset_id = _get_ts(dct, 'globalAssetId', str)
if 'specificAssetIds' in dct:
for desc_data in _get_ts(dct, "specificAssetIds", list):
ret.specific_asset_id.add(cls._construct_specific_asset_id(desc_data,
model.SpecificAssetId))
ret.specific_asset_id.add(cls._construct_specific_asset_id(desc_data, model.SpecificAssetId))
if 'assetType' in dct:
ret.asset_type = _get_ts(dct, 'assetType', str)
if 'defaultThumbnail' in dct:
Expand Down Expand Up @@ -482,11 +479,11 @@ def _construct_data_specification_iec61360(cls, dct: Dict[str, object],
if 'symbol' in dct:
ret.symbol = _get_ts(dct, 'symbol', str)
if 'valueFormat' in dct:
ret.value_format = model.datatypes.XSD_TYPE_CLASSES[_get_ts(dct, 'valueFormat', str)]
ret.value_format = _get_ts(dct, 'valueFormat', str)
if 'valueList' in dct:
ret.value_list = cls._construct_value_list(_get_ts(dct, 'valueList', dict), value_format=ret.value_format)
ret.value_list = cls._construct_value_list(_get_ts(dct, 'valueList', dict))
if 'value' in dct:
ret.value = model.datatypes.from_xsd(_get_ts(dct, 'value', str), ret.value_format)
ret.value = _get_ts(dct, 'value', str)
if 'valueId' in dct:
ret.value_id = cls._construct_reference(_get_ts(dct, 'valueId', dict))
if 'levelType' in dct:
Expand Down
9 changes: 5 additions & 4 deletions basyx/aas/adapter/json/json_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def _abstract_classes_to_json(cls, obj: object) -> Dict[str, object]:
]

if isinstance(obj, model.Referable):
if obj.id_short:
if obj.id_short and not isinstance(obj.parent, model.SubmodelElementList):
data['idShort'] = obj.id_short
if obj.display_name:
data['displayName'] = obj.display_name
Expand Down Expand Up @@ -340,7 +340,7 @@ def _data_specification_iec61360_to_json(
:return: dict with the serialized attributes of this object
"""
data_spec: Dict[str, object] = {
'modelType': 'DataSpecificationIEC61360',
'modelType': 'DataSpecificationIec61360',
'preferredName': obj.preferred_name
}
if obj.data_type is not None:
Expand All @@ -357,11 +357,12 @@ def _data_specification_iec61360_to_json(
data_spec['sourceOfDefinition'] = obj.source_of_definition
if obj.symbol is not None:
data_spec['symbol'] = obj.symbol
data_spec['valueFormat'] = model.datatypes.XSD_TYPE_NAMES[obj.value_format]
if obj.value_format is not None:
data_spec['valueFormat'] = obj.value_format
if obj.value_list is not None:
data_spec['valueList'] = cls._value_list_to_json(obj.value_list)
if obj.value is not None:
data_spec['value'] = model.datatypes.xsd_repr(obj.value) if obj.value is not None else None
data_spec['value'] = obj.value
if obj.level_types:
data_spec['levelType'] = {v: k in obj.level_types for k, v in _generic.IEC61360_LEVEL_TYPES.items()}
return data_spec
Expand Down
24 changes: 9 additions & 15 deletions basyx/aas/adapter/xml/xml_deserialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -1033,26 +1033,21 @@ def construct_submodel(cls, element: etree.Element, object_class=model.Submodel,
return submodel

@classmethod
def construct_value_reference_pair(cls, element: etree.Element, value_format: model.DataTypeDefXsd,
object_class=model.ValueReferencePair, **_kwargs: Any) \
-> model.ValueReferencePair:
return object_class(
model.datatypes.from_xsd(_child_text_mandatory(element, NS_AAS + "value"), value_format),
_child_construct_mandatory(element, NS_AAS + "valueId", cls.construct_reference),
value_format
)
def construct_value_reference_pair(cls, element: etree.Element, object_class=model.ValueReferencePair,
**_kwargs: Any) -> model.ValueReferencePair:
return object_class(_child_text_mandatory(element, NS_AAS + "value"),
_child_construct_mandatory(element, NS_AAS + "valueId", cls.construct_reference))

@classmethod
def construct_value_list(cls, element: etree.Element, value_format: model.DataTypeDefXsd, **_kwargs: Any) \
-> model.ValueList:
def construct_value_list(cls, element: etree.Element, **_kwargs: Any) -> model.ValueList:
"""
This function doesn't support the object_class parameter, because ValueList is just a generic type alias.
"""

return set(
_child_construct_multiple(_get_child_mandatory(element, NS_AAS + "valueReferencePairs"),
NS_AAS + "valueReferencePair", cls.construct_value_reference_pair,
cls.failsafe, value_format=value_format)
cls.failsafe)
)

@classmethod
Expand Down Expand Up @@ -1126,16 +1121,15 @@ def construct_data_specification_iec61360(cls, element: etree.Element, object_cl
cls.failsafe)
if definition is not None:
ds_iec.definition = definition
value_format = _get_text_mapped_or_none(element.find(NS_AAS + "valueFormat"), model.datatypes.XSD_TYPE_CLASSES)
value_format = _get_text_or_none(element.find(NS_AAS + "valueFormat"))
if value_format is not None:
ds_iec.value_format = value_format
value_list = _failsafe_construct(element.find(NS_AAS + "valueList"), cls.construct_value_list, cls.failsafe,
value_format=ds_iec.value_format)
value_list = _failsafe_construct(element.find(NS_AAS + "valueList"), cls.construct_value_list, cls.failsafe)
if value_list is not None:
ds_iec.value_list = value_list
value = _get_text_or_none(element.find(NS_AAS + "value"))
if value is not None and value_format is not None:
ds_iec.value = model.datatypes.from_xsd(value, value_format)
ds_iec.value = value
level_type = element.find(NS_AAS + "levelType")
if level_type is not None:
for child in level_type:
Expand Down
12 changes: 6 additions & 6 deletions basyx/aas/adapter/xml/xml_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def abstract_classes_to_xml(tag: str, obj: object) -> etree.Element:
if isinstance(obj, model.Referable):
if obj.category:
elm.append(_generate_element(name=NS_AAS + "category", text=obj.category))
if obj.id_short:
if obj.id_short and not isinstance(obj.parent, model.SubmodelElementList):
elm.append(_generate_element(name=NS_AAS + "idShort", text=obj.id_short))
if obj.display_name:
elm.append(lang_string_set_to_xml(obj.display_name, tag=NS_AAS + "displayName"))
Expand Down Expand Up @@ -297,7 +297,7 @@ def value_reference_pair_to_xml(obj: model.ValueReferencePair,
"""
et_vrp = _generate_element(tag)
# TODO: value_type isn't used at all by _value_to_xml(), thus we can ignore the type here for now
et_vrp.append(_value_to_xml(obj.value, obj.value_type)) # type: ignore
et_vrp.append(_generate_element(NS_AAS+"value", text=obj.value)) # type: ignore
et_vrp.append(reference_to_xml(obj.value_id, NS_AAS+"valueId"))
return et_vrp

Expand Down Expand Up @@ -445,15 +445,15 @@ def data_specification_iec61360_to_xml(obj: model.DataSpecificationIEC61360,
text=_generic.IEC61360_DATA_TYPES[obj.data_type]))
if obj.definition is not None:
et_data_specification_iec61360.append(lang_string_set_to_xml(obj.definition, NS_AAS + "definition"))
et_data_specification_iec61360.append(_generate_element(NS_AAS + "valueFormat",
text=model.datatypes.XSD_TYPE_NAMES[obj.value_format]))

if obj.value_format is not None:
et_data_specification_iec61360.append(_generate_element(NS_AAS + "valueFormat", text=obj.value_format))
# this can be either None or an empty set, both of which are equivalent to the bool false
# thus we don't check 'is not None' for this property
if obj.value_list:
et_data_specification_iec61360.append(value_list_to_xml(obj.value_list))
if obj.value is not None:
et_data_specification_iec61360.append(_generate_element(NS_AAS + "value",
text=model.datatypes.xsd_repr(obj.value)))
et_data_specification_iec61360.append(_generate_element(NS_AAS + "value", text=obj.value))
if obj.level_types:
et_level_types = _generate_element(NS_AAS + "levelType")
for k, v in _generic.IEC61360_LEVEL_TYPES.items():
Expand Down
30 changes: 16 additions & 14 deletions basyx/aas/examples/data/_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,10 @@ def _check_referable_equal(self, object_: model.Referable, expected_object: mode
:param expected_object: The expected referable object
:return: The value of expression to be used in control statements
"""
self.check_attribute_equal(object_, "id_short", expected_object.id_short)
# For SubmodelElementLists, the id_shorts of children are randomly generated.
# Thus, this check would always fail if enabled.
if not isinstance(object_.parent, model.SubmodelElementList):
self.check_attribute_equal(object_, "id_short", expected_object.id_short)
self.check_attribute_equal(object_, "category", expected_object.category)
self.check_attribute_equal(object_, "description", expected_object.description)
self.check_attribute_equal(object_, "display_name", expected_object.display_name)
Expand Down Expand Up @@ -383,13 +386,14 @@ def check_submodel_element_list_equal(self, object_: model.SubmodelElementList,
self.check_attribute_equal(object_, 'type_value_list_element', expected_value.type_value_list_element)
self.check_contained_element_length(object_, 'value', object_.type_value_list_element,
len(expected_value.value))
if object_.order_relevant:
# compare ordered
for se1, se2 in zip(object_.value, expected_value.value):
self._check_submodel_element(se1, se2)
else:
# compare unordered
self._check_submodel_elements_equal_unordered(object_, expected_value)
if not object_.order_relevant or not expected_value.order_relevant:
# It is impossible to compare SubmodelElementLists with order_relevant=False, since it is impossible
# to know which element should be compared against which other element.
raise NotImplementedError("A SubmodelElementList with order_relevant=False cannot be compared!")

# compare ordered
for se1, se2 in zip(object_.value, expected_value.value):
self._check_submodel_element(se1, se2)

def check_relationship_element_equal(self, object_: model.RelationshipElement,
expected_value: model.RelationshipElement):
Expand Down Expand Up @@ -846,13 +850,11 @@ def _check_value_list_equal(self, object_: model.ValueList, expected_value: mode
:return:
"""
for expected_pair in expected_value:
pair = self._find_element_by_attribute(expected_pair, object_, 'value', 'value_id', 'value_type')
self.check(pair is not None, 'ValueReferencePair[value={}, value_id={}, value_type={}] '
'must exist'.format(expected_pair.value, expected_pair.value_id,
expected_pair.value_type))
pair = self._find_element_by_attribute(expected_pair, object_, 'value', 'value_id')
self.check(pair is not None, 'ValueReferencePair[value={}, value_id={}] '
'must exist'.format(expected_pair.value, expected_pair.value_id))

found_elements = self._find_extra_elements_by_attribute(object_, expected_value,
'value', 'value_id', 'value_type')
found_elements = self._find_extra_elements_by_attribute(object_, expected_value, 'value', 'value_id')
self.check(found_elements == set(), 'ValueList must not have extra ValueReferencePairs',
value=found_elements)

Expand Down
Loading
Loading