from datetime import datetime, timedelta from typing import * import attr from dlms_cosem import a_xdr, cosem, enumerations from dlms_cosem.cosem import CosemAttribute from dlms_cosem.cosem.association import ( AccessRight, AssociationObjectListItem, AttributeAccessRights, MethodAccessRights, ) from dlms_cosem.time import datetime_from_bytes @attr.s(auto_attribs=True) class ColumnValue: attribute: CosemAttribute value: Any @attr.s(auto_attribs=True) class ProfileGenericBufferParser: capture_objects: List[CosemAttribute] capture_period: int # minutes def parse_bytes(self, profile_bytes: bytes): """ Profile generic are sent as a sequence of A-XDR encoded DlmsData. """ data_decoder = a_xdr.AXdrDecoder( encoding_conf=a_xdr.EncodingConf( attributes=[a_xdr.Sequence(attribute_name="data")] ) ) entries: List[List[Any]] = data_decoder.decode(profile_bytes)["data"] return self.parse_entries(entries) def parse_entries( self, entries: List[List[Optional[Any]]] ) -> List[List[Optional[ColumnValue]]]: """ Returns a list of columns with the cosem attribut linked to the value with a ColumnValue. It also sets the timestamp on each column calculated from the prevoius entry if the data has been sent compressed using null values """ parsed_entries = list() last_entry_timestamp: Optional[datetime] = None for entry in entries: if len(entry) != len(self.capture_objects): raise ValueError( f"Unable to parse ProfileGeneric entry as the amount of columns " f"({len(entry)}) differ from the parsers set capture_object length " f"({len(self.capture_objects)}) " ) parsed_column = list() for index, column in enumerate(entry): cosem_attribute = self.capture_objects[index] if column is not None: if cosem_attribute.interface == enumerations.CosemInterface.CLOCK: # parse as time. value = datetime_from_bytes(column)[ 0 ] # TODO: do we need clock status? last_entry_timestamp = value parsed_column.append( ColumnValue(attribute=cosem_attribute, value=value) ) else: parsed_column.append( ColumnValue(attribute=cosem_attribute, value=column) ) else: if cosem_attribute.interface == enumerations.CosemInterface.CLOCK: if last_entry_timestamp: value = last_entry_timestamp + timedelta( minutes=self.capture_period ) last_entry_timestamp = value parsed_column.append( ColumnValue(attribute=cosem_attribute, value=value) ) else: parsed_column.append(None) parsed_entries.append(parsed_column) return parsed_entries class AssociationObjectListParser: @staticmethod def parse_bytes(profile_bytes: bytes): """ Profile generic are sent as a sequence of A-XDR encoded DlmsData. """ data_decoder = a_xdr.AXdrDecoder( encoding_conf=a_xdr.EncodingConf( attributes=[a_xdr.Sequence(attribute_name="data")] ) ) entries: List[List[Any]] = data_decoder.decode(profile_bytes)["data"] return AssociationObjectListParser.parse_entries(entries) @staticmethod def parse_access_right(access_right: int) -> List[AccessRight]: parsed_access_rights = list() if bool(access_right & 0b00000001): parsed_access_rights.append(AccessRight.READ_ACCESS.value) if bool(access_right & 0b00000010): parsed_access_rights.append(AccessRight.WRITE_ACCESS.value) if bool(access_right & 0b00000100): parsed_access_rights.append(AccessRight.AUTHENTICATED_REQUEST.value) if bool(access_right & 0b00001000): parsed_access_rights.append(AccessRight.ENCRYPTED_REQUEST.value) if bool(access_right & 0b00010000): parsed_access_rights.append(AccessRight.DIGITALLY_SIGNED_REQUEST.value) if bool(access_right & 0b00100000): parsed_access_rights.append(AccessRight.AUTHENTICATED_RESPONSE.value) if bool(access_right & 0b01000000): parsed_access_rights.append(AccessRight.ENCRYPTED_RESPONSE.value) if bool(access_right & 0b10000000): parsed_access_rights.append(AccessRight.DIGITALLY_SIGNED_RESPONSE.value) return parsed_access_rights @staticmethod def parse_attribute_access_rights( access_rights: List[List[Optional[Union[int, List[int]]]]] ): parsed_access_rights = list() for right in access_rights: parsed_access_rights.append( AttributeAccessRights( attribute=right[0], access_rights=AssociationObjectListParser.parse_access_right( right[1] ), access_selectors=right[2], ) ) return parsed_access_rights @staticmethod def parse_method_access_rights( access_rights: List[List[int]], ) -> List[MethodAccessRights]: parsed_access_rights = list() for right in access_rights: parsed_access_rights.append( MethodAccessRights( method=right[0], access_rights=AssociationObjectListParser.parse_access_right( right[1] ), ) ) return parsed_access_rights @staticmethod def parse_entries(object_list): parsed_objects = list() for obj in object_list: interface = enumerations.CosemInterface(obj[0]).value version = obj[1] logical_name = cosem.Obis.from_bytes(obj[2]) access_rights = obj[3] attribute_access_rights = ( AssociationObjectListParser.parse_attribute_access_rights( access_rights[0] ) ) attribute_access_dict = { access.attribute: access for access in attribute_access_rights } method_access_rights = ( AssociationObjectListParser.parse_method_access_rights(access_rights[1]) ) method_access_dict = { access.method: access for access in method_access_rights } parsed_objects.append( AssociationObjectListItem( interface=interface, version=version, logical_name=logical_name, attribute_access_rights=attribute_access_dict, method_access_rights=method_access_dict, ) ) return parsed_objects