diff --git a/test/resources/module-definition-test.knxproj b/test/resources/module-definition-test.knxproj index be37f78..c06db96 100644 Binary files a/test/resources/module-definition-test.knxproj and b/test/resources/module-definition-test.knxproj differ diff --git a/test/resources/stubs/module-definition-test.json b/test/resources/stubs/module-definition-test.json index 70b90f9..a25c11a 100644 --- a/test/resources/stubs/module-definition-test.json +++ b/test/resources/stubs/module-definition-test.json @@ -2,7 +2,7 @@ "info": { "project_id": "P-0810", "name": "ModuleDefinitionTest", - "last_modified": "2024-02-11T11:28:14.4900833Z", + "last_modified": "2024-04-06T19:56:10.5912809Z", "group_address_style": "ThreeLevel", "guid": "26520acd-6231-4fe4-a409-4d1610e7a251", "created_by": "ETS5", @@ -549,6 +549,227 @@ "group_address_links": [ "0/6/6" ] + }, + "1.1.4/MD-4_M-15_MI-1_SM-1_M-1_MI-1-1-1_SM-1_O-3-0_R-1": { + "name": "oEnMon.devicePage[{{EnMonPageNum}}][{{EnMonDevNameNum}}].energy", + "number": 263, + "text": "[EM][IC1][Dev 1.1] Verbrauchte Energie", + "function_text": "W·h", + "description": "", + "device_address": "1.1.4", + "device_application": "M-0071_A-5531-37-FDF4", + "module_def": { + "definition": "MD-4_SM-1", + "root_number": 0 + }, + "channel": null, + "dpts": [ + { + "main": 13, + "sub": 10 + } + ], + "object_size": "4 Bytes", + "flags": { + "read": false, + "write": true, + "communication": true, + "update": true, + "read_on_init": false, + "transmit": true + }, + "group_address_links": [ + "0/6/12" + ] + }, + "1.1.4/MD-4_M-15_MI-1_SM-1_M-1_MI-1-1-1_SM-1_O-3-1_R-2": { + "name": "oEnMon.devicePage[{{EnMonPageNum}}][{{EnMonDevNameNum}}].power", + "number": 264, + "text": "[EM][IC1][Dev 1.1] Verbrauchte Leistung", + "function_text": "W", + "description": "", + "device_address": "1.1.4", + "device_application": "M-0071_A-5531-37-FDF4", + "module_def": { + "definition": "MD-4_SM-1", + "root_number": 1 + }, + "channel": null, + "dpts": [ + { + "main": 14, + "sub": 56 + } + ], + "object_size": "4 Bytes", + "flags": { + "read": false, + "write": true, + "communication": true, + "update": false, + "read_on_init": false, + "transmit": false + }, + "group_address_links": [ + "0/6/13" + ] + }, + "1.1.4/O-256_R-36": { + "name": "oEnMon.consumedEnergy", + "number": 256, + "text": "[EM] Verbrauchte Energie", + "function_text": "W·h", + "description": "", + "device_address": "1.1.4", + "device_application": "M-0071_A-5531-37-FDF4", + "module_def": null, + "channel": "CH-1", + "dpts": [ + { + "main": 13, + "sub": 10 + } + ], + "object_size": "4 Bytes", + "flags": { + "read": false, + "write": true, + "communication": true, + "update": true, + "read_on_init": false, + "transmit": true + }, + "group_address_links": [ + "0/6/11" + ] + }, + "1.1.4/MD-4_M-15_MI-2_SM-1_M-1_MI-2-1-1_SM-1_O-3-0_R-1": { + "name": "oEnMon.devicePage[{{EnMonPageNum}}][{{EnMonDevNameNum}}].energy", + "number": 277, + "text": "[EM][IC2][Dev 2.1] Verbrauchte Energie", + "function_text": "W·h", + "description": "", + "device_address": "1.1.4", + "device_application": "M-0071_A-5531-37-FDF4", + "module_def": { + "definition": "MD-4_SM-1", + "root_number": 0 + }, + "channel": null, + "dpts": [ + { + "main": 13, + "sub": 10 + } + ], + "object_size": "4 Bytes", + "flags": { + "read": false, + "write": true, + "communication": true, + "update": true, + "read_on_init": false, + "transmit": true + }, + "group_address_links": [ + "0/6/15" + ] + }, + "1.1.4/MD-4_M-15_MI-1_SM-1_M-1_MI-1-1-2_SM-1_O-3-0_R-1": { + "name": "oEnMon.devicePage[{{EnMonPageNum}}][{{EnMonDevNameNum}}].energy", + "number": 263, + "text": "[EM][IC1][Dev 1.2] Verbrauchte Energie", + "function_text": "W·h", + "description": "", + "device_address": "1.1.4", + "device_application": "M-0071_A-5531-37-FDF4", + "module_def": { + "definition": "MD-4_SM-1", + "root_number": 0 + }, + "channel": null, + "dpts": [ + { + "main": 13, + "sub": 10 + } + ], + "object_size": "4 Bytes", + "flags": { + "read": false, + "write": true, + "communication": true, + "update": true, + "read_on_init": false, + "transmit": true + }, + "group_address_links": [ + "0/6/14" + ] + }, + "1.1.4/MD-4_M-15_MI-2_SM-1_M-1_MI-2-1-2_SM-1_O-3-0_R-1": { + "name": "oEnMon.devicePage[{{EnMonPageNum}}][{{EnMonDevNameNum}}].energy", + "number": 277, + "text": "[EM][IC2][Dev 2.2] Verbrauchte Energie", + "function_text": "W·h", + "description": "", + "device_address": "1.1.4", + "device_application": "M-0071_A-5531-37-FDF4", + "module_def": { + "definition": "MD-4_SM-1", + "root_number": 0 + }, + "channel": null, + "dpts": [ + { + "main": 13, + "sub": 10 + } + ], + "object_size": "4 Bytes", + "flags": { + "read": false, + "write": true, + "communication": true, + "update": true, + "read_on_init": false, + "transmit": true + }, + "group_address_links": [ + "0/6/16" + ] + }, + "1.1.4/MD-4_M-15_MI-2_SM-1_M-1_MI-2-1-2_SM-1_O-3-1_R-2": { + "name": "oEnMon.devicePage[{{EnMonPageNum}}][{{EnMonDevNameNum}}].power", + "number": 278, + "text": "[EM][IC2][Dev 2.2] Verbrauchte Leistung", + "function_text": "W", + "description": "", + "device_address": "1.1.4", + "device_application": "M-0071_A-5531-37-FDF4", + "module_def": { + "definition": "MD-4_SM-1", + "root_number": 1 + }, + "channel": null, + "dpts": [ + { + "main": 14, + "sub": 56 + } + ], + "object_size": "4 Bytes", + "flags": { + "read": false, + "write": true, + "communication": true, + "update": false, + "read_on_init": false, + "transmit": false + }, + "group_address_links": [ + "0/6/17" + ] } }, "topology": { @@ -580,7 +801,8 @@ "devices": [ "1.1.1", "1.1.2", - "1.1.3" + "1.1.3", + "1.1.4" ], "medium_type": "Twisted Pair (TP)" } @@ -750,6 +972,35 @@ "name": "Bewegungsmelder" } } + }, + "1.1.4": { + "name": "Z70 v2", + "hardware_name": "Z70 v2", + "order_number": "ZVIZ70V2", + "description": "SubModules with Allocators", + "manufacturer_name": "Zennio", + "individual_address": "1.1.4", + "application": "M-0071_A-5531-37-FDF4", + "project_uid": 40, + "communication_object_ids": [ + "1.1.4/MD-4_M-15_MI-1_SM-1_M-1_MI-1-1-1_SM-1_O-3-0_R-1", + "1.1.4/MD-4_M-15_MI-1_SM-1_M-1_MI-1-1-1_SM-1_O-3-1_R-2", + "1.1.4/O-256_R-36", + "1.1.4/MD-4_M-15_MI-2_SM-1_M-1_MI-2-1-1_SM-1_O-3-0_R-1", + "1.1.4/MD-4_M-15_MI-1_SM-1_M-1_MI-1-1-2_SM-1_O-3-0_R-1", + "1.1.4/MD-4_M-15_MI-2_SM-1_M-1_MI-2-1-2_SM-1_O-3-0_R-1", + "1.1.4/MD-4_M-15_MI-2_SM-1_M-1_MI-2-1-2_SM-1_O-3-1_R-2" + ], + "channels": { + "CH-1": { + "identifier": "CH-1", + "name": "Allgemein" + }, + "CH-2": { + "identifier": "CH-2", + "name": "Visualisierung" + } + } } }, "group_addresses": { @@ -1042,6 +1293,125 @@ "description": "", "comment": "" }, + "0/6/11": { + "name": "SM Object number 256", + "identifier": "GA-20", + "raw_address": 1547, + "address": "0/6/11", + "project_uid": 42, + "dpt": { + "main": 13, + "sub": 10 + }, + "data_secure": false, + "communication_object_ids": [ + "1.1.4/O-256_R-36" + ], + "description": "", + "comment": "" + }, + "0/6/12": { + "name": "SM Object number 263", + "identifier": "GA-21", + "raw_address": 1548, + "address": "0/6/12", + "project_uid": 43, + "dpt": { + "main": 13, + "sub": 10 + }, + "data_secure": false, + "communication_object_ids": [ + "1.1.4/MD-4_M-15_MI-1_SM-1_M-1_MI-1-1-1_SM-1_O-3-0_R-1" + ], + "description": "", + "comment": "" + }, + "0/6/13": { + "name": "SM Object number 264", + "identifier": "GA-22", + "raw_address": 1549, + "address": "0/6/13", + "project_uid": 44, + "dpt": { + "main": 14, + "sub": 56 + }, + "data_secure": false, + "communication_object_ids": [ + "1.1.4/MD-4_M-15_MI-1_SM-1_M-1_MI-1-1-1_SM-1_O-3-1_R-2" + ], + "description": "", + "comment": "" + }, + "0/6/14": { + "name": "SM Object number 265", + "identifier": "GA-23", + "raw_address": 1550, + "address": "0/6/14", + "project_uid": 45, + "dpt": { + "main": 13, + "sub": 10 + }, + "data_secure": false, + "communication_object_ids": [ + "1.1.4/MD-4_M-15_MI-1_SM-1_M-1_MI-1-1-2_SM-1_O-3-0_R-1" + ], + "description": "", + "comment": "" + }, + "0/6/15": { + "name": "SM Object number 275", + "identifier": "GA-24", + "raw_address": 1551, + "address": "0/6/15", + "project_uid": 46, + "dpt": { + "main": 13, + "sub": 10 + }, + "data_secure": false, + "communication_object_ids": [ + "1.1.4/MD-4_M-15_MI-2_SM-1_M-1_MI-2-1-1_SM-1_O-3-0_R-1" + ], + "description": "", + "comment": "" + }, + "0/6/16": { + "name": "SM Object number 277", + "identifier": "GA-25", + "raw_address": 1552, + "address": "0/6/16", + "project_uid": 47, + "dpt": { + "main": 13, + "sub": 10 + }, + "data_secure": false, + "communication_object_ids": [ + "1.1.4/MD-4_M-15_MI-2_SM-1_M-1_MI-2-1-2_SM-1_O-3-0_R-1" + ], + "description": "", + "comment": "" + }, + "0/6/17": { + "name": "SM Object number 278", + "identifier": "GA-26", + "raw_address": 1553, + "address": "0/6/17", + "project_uid": 48, + "dpt": { + "main": 14, + "sub": 56 + }, + "data_secure": false, + "communication_object_ids": [ + "1.1.4/MD-4_M-15_MI-2_SM-1_M-1_MI-2-1-2_SM-1_O-3-1_R-2" + ], + "description": "", + "comment": "" + }, "0/7/2": { "name": "Temperatur Soll", "identifier": "GA-6", @@ -1118,7 +1488,14 @@ "0/6/7", "0/6/8", "0/6/9", - "0/6/10" + "0/6/10", + "0/6/11", + "0/6/12", + "0/6/13", + "0/6/14", + "0/6/15", + "0/6/16", + "0/6/17" ], "comment": "", "group_ranges": {} diff --git a/test/xml/test_parser.py b/test/xml/test_parser.py index 8e37d73..cf13ee5 100644 --- a/test/xml/test_parser.py +++ b/test/xml/test_parser.py @@ -109,13 +109,13 @@ def test_parse_project_with_module_defs(): parser = XMLParser(knx_project_contents) parser.parse() - assert len(parser.group_addresses) == 18 + assert len(parser.group_addresses) == 25 assert parser.group_addresses[0].address == "0/0/1" assert parser.group_addresses[1].address == "0/0/2" assert parser.group_addresses[2].address == "0/0/3" assert len(parser.areas) == 2 assert len(parser.areas[1].lines) == 2 - assert len(parser.areas[1].lines[1].devices) == 3 + assert len(parser.areas[1].lines[1].devices) == 4 - assert len(parser.devices) == 3 + assert len(parser.devices) == 4 diff --git a/xknxproject/loader/application_program_loader.py b/xknxproject/loader/application_program_loader.py index 7ef2a64..a53a8c0 100644 --- a/xknxproject/loader/application_program_loader.py +++ b/xknxproject/loader/application_program_loader.py @@ -15,6 +15,7 @@ ComObjectRef, DeviceInstance, ModuleDefinitionArgumentInfo, + ModuleDefinitionNumericArg, ) from xknxproject.util import parse_dpt_types, parse_xml_flag @@ -49,6 +50,7 @@ def load( for device in devices for attribute in device.module_instance_arguments() } + numeric_args: dict[str, ModuleDefinitionNumericArg] = {} allocators: dict[str, Allocator] = {} with application_program_path.open(mode="rb") as application_xml: @@ -73,7 +75,9 @@ def load( start=int(elem.attrib.get("Start")), end=int(elem.attrib.get("maxInclusive")), ) - elif elem.tag.endswith("Argument"): # ModuleDefs/ModuleDef/Arguments/ + elif elem.tag.endswith("Argument"): + # ModuleDefs/ModuleDef/Arguments/ + # or ModuleDefs/ModuleDef/SubModuleDefs/ModuleDef/Arguments/ if (_id := elem.attrib.get("Id")) in used_module_arguments: allocates = elem.attrib.get("Allocates") used_module_arguments[_id] = ModuleDefinitionArgumentInfo( @@ -81,6 +85,16 @@ def load( allocates=int(allocates) if allocates is not None else None, ) elem.clear() + elif elem.tag.endswith("NumericArg"): + # in dynamic section of Modules + if (_id := elem.attrib.get("RefId")) in used_module_arguments: + value = elem.attrib.get("Value") + numeric_args[_id] = ModuleDefinitionNumericArg( + allocator_ref_id=elem.attrib.get("AllocatorRefId"), + base_value=elem.attrib.get("BaseValue"), + value=int(value) if value is not None else None, + ) + elem.clear() elif elem.tag.endswith("Languages"): elem.clear() # hold iterator for optional translation parsing @@ -101,6 +115,7 @@ def load( com_object_refs=com_object_refs, allocators=allocators, module_def_arguments=used_module_arguments, + numeric_args=numeric_args, ) @staticmethod diff --git a/xknxproject/models/__init__.py b/xknxproject/models/__init__.py index 4ce3c16..53ad706 100644 --- a/xknxproject/models/__init__.py +++ b/xknxproject/models/__init__.py @@ -30,6 +30,7 @@ ModuleInstance, ModuleInstanceArgument, ModuleDefinitionArgumentInfo, + ModuleDefinitionNumericArg, Product, TranslationsType, XMLArea, diff --git a/xknxproject/models/models.py b/xknxproject/models/models.py index e93654b..f75d3fd 100644 --- a/xknxproject/models/models.py +++ b/xknxproject/models/models.py @@ -3,7 +3,7 @@ from __future__ import annotations from collections.abc import Iterator -from dataclasses import dataclass +from dataclasses import dataclass, field import logging import re @@ -184,17 +184,17 @@ def module_instance_arguments(self) -> Iterator[ModuleInstanceArgument]: def merge_application_program_info(self, application: ApplicationProgram) -> None: """Merge items with their parent objects from the application program.""" - for attribute in self.module_instance_arguments(): - attribute.name = application.module_def_arguments[attribute.ref_id].name - attribute.allocates = application.module_def_arguments[ - attribute.ref_id + for argument in self.module_instance_arguments(): + argument.name = application.module_def_arguments[argument.ref_id].name + argument.allocates = application.module_def_arguments[ + argument.ref_id ].allocates for com_instance in self.com_object_instance_refs: com_instance.merge_application_program_info(application) com_instance.apply_module_base_number_argument( module_instances=self.module_instances, - application_allocators=application.allocators, + application=application, ) self._complete_channel_placeholders() @@ -232,10 +232,32 @@ class ChannelNode: class ModuleInstance: """Class that represents a ModuleInstance.""" - identifier: str - ref_id: str + identifier: str # MD-4_M-15_MI-2 / MD-4_M-15_MI-2_SM-1_M-1_MI-2-1-2 + ref_id: str # MD-_M- arguments: list[ModuleInstanceArgument] + module_def_id: str = field(init=False) # MD- + # MD-_M-_MI- if SubModule - reference may contain base values for arguments + base_module: str | None = field(init=False) + # ModuleDefUniqueNumber with SubModule id if present (used in result json) + definition_id: str = field(init=False) # MD-[_SM-] + + def __post_init__(self) -> None: + """Set is_submodule based on the identifier.""" + self.module_def_id = self.ref_id.split("_")[0] + _submodule_match = re.search(r"(_SM-[^_]+)", self.identifier) + if _submodule_match is not None: + self.base_module = f"{self.identifier.split('_SM-')[0]}" + self.definition_id = f"{self.module_def_id}{_submodule_match.group()}" + else: + self.base_module = None + self.definition_id = self.module_def_id + + def complete_arguments_ref_id(self, application_program_ref: str) -> None: + """Prepend the ref_id with the application program ref.""" + for arg in self.arguments: + arg.complete_ref_id(application_program_ref, self.module_def_id) + @dataclass class ModuleInstanceArgument: @@ -247,9 +269,12 @@ class ModuleInstanceArgument: name: str = "" # "Name" type="knx:Identifier50_t" use="required" allocates: int | None = None # "Allocates" type="xs:unsignedLong" use="optional" - def complete_ref_id(self, application_program_ref: str) -> None: + def complete_ref_id(self, application_program_ref: str, module_def_id: str) -> None: """Prepend the ref_id with the application program ref.""" - self.ref_id = f"{application_program_ref}_{self.ref_id}" + if self.ref_id.startswith("SM-"): # SubModule + self.ref_id = f"{application_program_ref}_{module_def_id}_{self.ref_id}" + else: + self.ref_id = f"{application_program_ref}_{self.ref_id}" @dataclass @@ -292,13 +317,29 @@ def resolve_com_object_ref_id( self, application_program_ref: str, knx_proj_contents: KNXProjContents ) -> None: """Prepend the ref_id with the application program ref.""" - # Remove module and ModuleInstance occurrence as they will not be in the application program directly - ref_id = re.sub(r"(M-\d+?_MI-\d+?_)", "", self.ref_id) - if knx_proj_contents.is_ets4_project(): - self.com_object_ref_id = ref_id + if knx_proj_contents.is_ets4_project() and self.ref_id.startswith( + application_program_ref + ): + # ETS4 doesn't use shortened ref_id and I think it doesn't support modules at all + self.com_object_ref_id = self.ref_id + return + + if self.ref_id.startswith("O-"): + ref_id = self.ref_id + elif self.ref_id.startswith("MD-"): + # Remove module and ModuleInstance occurrence as they will not be in the application program directly + module_definition = self.ref_id.split("_")[0] + object_reference = self.ref_id[self.ref_id.index("_O-") :] + _submodule_match = re.search(r"(_SM-[^_]+)", self.ref_id) + submodule = _submodule_match.group() if _submodule_match is not None else "" + ref_id = f"{module_definition}{submodule}{object_reference}" else: - self.application_program_id_prefix = f"{application_program_ref}_" - self.com_object_ref_id = f"{application_program_ref}_{ref_id}" + raise ValueError( + f"Unknown ref_id format: {self.ref_id} in application: {application_program_ref}" + ) + + self.application_program_id_prefix = f"{application_program_ref}_" + self.com_object_ref_id = f"{application_program_ref}_{ref_id}" def merge_application_program_info(self, application: ApplicationProgram) -> None: """Fill missing information with information parsed from the application program.""" @@ -344,7 +385,7 @@ def _merge_from_parent_object(self, com_object: ComObject | ComObjectRef) -> Non def apply_module_base_number_argument( self, module_instances: list[ModuleInstance], - application_allocators: dict[str, Allocator], + application: ApplicationProgram, ) -> None: """Apply module argument of base number.""" if ( @@ -353,30 +394,63 @@ def apply_module_base_number_argument( or self.number is None # only for type safety ): return - # there are 2 ways to get the base number - # 1. from the module instance arguments value "ObjNumberBase" directly - # 2. from the module defs allocator - adding the "Start" value to - # (the modules index - 1) * allocator size - # in this case the module instance argument value is the reference part - # of the allocator id ("L-1") - at least in the application tested (MDT Dali 64) + + def _parse_base_number_argument( + module_instance: ModuleInstance, + base_number_argument_ref: str, + ) -> int: + """Parse the argument value.""" + # there are 2 ways to get the base number + # 1. from the module instance arguments value "ObjNumberBase" directly + # 2. from the module defs allocator - adding the "Start" value to + # (the modules index - 1) * allocator size + # in this case the module instance argument value is the reference part + # of the allocator id ("L-1") - at least in the application tested (MDT Dali 64) + # + # for SubModules the NumericArg item may use a BaseValue reference to an + # Argument of the base ModuleDef containing the base value for all its SubModules + result = 0 + base_number_argument = next( + arg + for arg in module_instance.arguments + if arg.ref_id == base_number_argument_ref + ) + + try: + # path (1) if value is a number, we are done + # base module value should already be included + return int(base_number_argument.value) + except ValueError: + # path (2) value is a reference to an Allocator + if module_instance.base_module: + # recurse to get the base number from the base module (for SubModule value) + num_arg = application.numeric_args.get(base_number_argument.ref_id) + if ( + num_arg is not None + and (base_value_ref := num_arg.base_value) is not None + ): + base_module = next( + mi + for mi in module_instances + if mi.identifier == module_instance.base_module + ) + result += _parse_base_number_argument( + module_instance=base_module, + base_number_argument_ref=base_value_ref, + ) + return result + self._base_number_from_allocator( + base_number_argument, application.allocators + ) + _module_instance = next( mi for mi in module_instances if self.ref_id.startswith(f"{mi.identifier}_") ) com_object_number = self.number - base_number_argument = next( - arg - for arg in _module_instance.arguments - if arg.ref_id == self.base_number_argument_ref + self.number += _parse_base_number_argument( + _module_instance, self.base_number_argument_ref ) - try: - self.number += int(base_number_argument.value) - except ValueError: - self.number += self._base_number_from_allocator( - base_number_argument, application_allocators - ) - self.module = ModuleInstanceInfos( - definition=self.ref_id.split("_")[0], + definition=_module_instance.definition_id, root_number=com_object_number, ) @@ -413,6 +487,7 @@ class ApplicationProgram: com_object_refs: dict[str, ComObjectRef] # {Id: ComObjectRef} allocators: dict[str, Allocator] # {Id: Allocator} module_def_arguments: dict[str, ModuleDefinitionArgumentInfo] # {Id: ...} + numeric_args: dict[str, ModuleDefinitionNumericArg] # {RefId: ...} @dataclass @@ -433,6 +508,18 @@ class ModuleDefinitionArgumentInfo: allocates: int | None = None +@dataclass +class ModuleDefinitionNumericArg: + """Module Definition Numeric Argument.""" + + # shortened version (MD-_L-) should be Value in 0.xml ModuleInstanceArgument (at least with ETS 5) + allocator_ref_id: str | None + # if allocator_ref_id is not used, this is 0.xml ModuleInstanceArgument Value + value: int | None + # RefId to Argument (_MD-_A-) - Base value for arguments used in SubModules + base_value: str | None + + @dataclass class ComObject: """Class that represents a ComObject instance.""" diff --git a/xknxproject/xml/parser.py b/xknxproject/xml/parser.py index 50637ee..813acde 100644 --- a/xknxproject/xml/parser.py +++ b/xknxproject/xml/parser.py @@ -190,8 +190,9 @@ def _load(self, language: str | None) -> None: product = products_dict[device.product_ref] except KeyError: _LOGGER.warning( - "Could not find hardware product for device %s with product_ref %s", + "Could not find hardware product for device %s from %s with product_ref %s", device.individual_address, + device.manufacturer_name, device.product_ref, ) continue @@ -205,19 +206,22 @@ def _load(self, language: str | None) -> None: ] except KeyError: _LOGGER.warning( - "Could not find application_program_ref for device %s with hardware_program_ref %s", + "Could not find application_program_ref for device %s - %s - %s with hardware_program_ref %s", device.individual_address, + device.manufacturer_name, + device.product_name, device.hardware_program_ref, ) continue device.application_program_ref = application_program_ref for com_object in device.com_object_instance_refs: + # TODO: try and except here com_object.resolve_com_object_ref_id( application_program_ref, self.knx_proj_contents ) - for module_instance_argument in device.module_instance_arguments(): + for module_instance in device.module_instances: # need to complete ref_id before parsing application program - module_instance_argument.complete_ref_id(application_program_ref) + module_instance.complete_arguments_ref_id(application_program_ref) # only parse each application program file once and only extract used infos application_programs = (