def test_can_send_action_request_in_ready(action_request: xdlms.ActionRequestNormal):
    c = DlmsConnection(
        state=state.DlmsConnectionState(current_state=state.READY),
        client_system_title=b"12345678",
    )

    c.send(action_request)
    assert c.state.current_state == state.AWAITING_ACTION_RESPONSE
def test_can_release_in_ready_state(rlrq: acse.ReleaseRequest):
    c = DlmsConnection(
        state=state.DlmsConnectionState(current_state=state.READY),
        client_system_title=b"12345678",
    )

    c.send(rlrq)
    assert c.state.current_state == state.AWAITING_RELEASE_RESPONSE
def test_cannot_re_associate(aarq: acse.ApplicationAssociationRequest):
    c = DlmsConnection(
        state=state.DlmsConnectionState(current_state=state.READY),
        client_system_title=b"12345678",
    )

    with pytest.raises(LocalDlmsProtocolError):
        c.send(aarq)
def test_can_send_get_when_ready(get_request: xdlms.GetRequestNormal):
    c = DlmsConnection(
        state=state.DlmsConnectionState(current_state=state.READY),
        client_system_title=b"12345678",
    )

    c.send(get_request)
    assert c.state.current_state == state.AWAITING_GET_RESPONSE
def test_rejection_resets_connection_state(
    connection_with_hls: DlmsConnection,
    ciphered_hls_aare: acse.ApplicationAssociationResponse,
):
    connection_with_hls.state.current_state = state.AWAITING_ASSOCIATION_RESPONSE
    ciphered_hls_aare.result = enumerations.AssociationResult.REJECTED_PERMANENT
    connection_with_hls.receive_data(ciphered_hls_aare.to_bytes())
    connection_with_hls.next_event()
    assert connection_with_hls.state.current_state == state.NO_ASSOCIATION
Exemple #6
0
def test_set_request_sets_state_in_waiting_for_set_response(
    set_request: xdlms.SetRequestNormal, ):
    c = DlmsConnection(
        state=state.DlmsConnectionState(current_state=state.READY),
        client_system_title=b"12345678",
    )

    c.send(set_request)
    assert c.state.current_state == state.AWAITING_SET_RESPONSE
Exemple #7
0
def test_hls_is_started_automatically(
    connection_with_hls: DlmsConnection,
    ciphered_hls_aare: acse.ApplicationAssociationResponseApdu,
):
    # Force state into awaiting response
    connection_with_hls.state.current_state = state.AWAITING_ASSOCIATION_RESPONSE
    connection_with_hls.receive_data(ciphered_hls_aare.to_bytes())
    connection_with_hls.next_event()
    assert (connection_with_hls.state.current_state ==
            state.SHOULD_SEND_HLS_SEVER_CHALLENGE_RESULT)
Exemple #8
0
    def test_not_able_to_send_aarq(
            self, aarq: acse.ApplicationAssociationRequestApdu):
        c = DlmsConnection.with_pre_established_association(
            conformance=Conformance())

        with pytest.raises(LocalDlmsProtocolError):
            c.send(aarq)
Exemple #9
0
def connection_with_hls(system_title, global_encryption_key,
                        global_authentication_key) -> DlmsConnection:

    return DlmsConnection(
        client_system_title=system_title,
        authentication_method=enumerations.AuthenticationMechanism.HLS_GMAC,
        global_encryption_key=global_encryption_key,
        global_authentication_key=global_authentication_key,
        security_suite=0,
    )
def test_hls_fails(connection_with_hls: DlmsConnection):
    # Force state into awaiting response
    connection_with_hls.state.current_state = state.AWAITING_HLS_CLIENT_CHALLENGE_RESULT
    connection_with_hls.meter_system_title = b"12345678"
    connection_with_hls.meter_invocation_counter = 1
    failing_action_response = xdlms.ActionResponseNormal(
        status=enumerations.ActionResultStatus.OTHER_REASON
    )
    ciphered = security.encrypt(
        security_control=connection_with_hls.security_control,
        system_title=connection_with_hls.meter_system_title,
        auth_key=connection_with_hls.global_authentication_key,
        key=connection_with_hls.global_encryption_key,
        invocation_counter=2,
        plain_text=failing_action_response.to_bytes(),
    )
    ciphered_action_response = xdlms.GeneralGlobalCipher(
        security_control=connection_with_hls.security_control,
        system_title=connection_with_hls.meter_system_title,
        invocation_counter=2,
        ciphered_text=ciphered,
    )
    connection_with_hls.receive_data(ciphered_action_response.to_bytes())
    connection_with_hls.next_event()
    assert connection_with_hls.state.current_state == state.NO_ASSOCIATION
def test_receive_get_response_sets_state_to_ready():
    c = DlmsConnection(
        state=state.DlmsConnectionState(current_state=state.AWAITING_GET_RESPONSE),
        client_system_title=b"12345678",
    )
    c.receive_data(b"\xc4\x01\xc1\x00\x06\x00\x00\x13\x91")
    c.next_event()
    assert c.state.current_state == state.READY
def test_receive_rlre_terminates_association(rlre: acse.ReleaseResponse):
    c = DlmsConnection(
        state=state.DlmsConnectionState(current_state=state.AWAITING_RELEASE_RESPONSE),
        client_system_title=b"12345678",
    )
    c.receive_data(rlre.to_bytes())
    c.next_event()
    assert c.state.current_state == state.NO_ASSOCIATION
def test_set_response_sets_state_in_ready(set_response: xdlms.SetResponseNormal):
    c = DlmsConnection(
        state=state.DlmsConnectionState(current_state=state.AWAITING_SET_RESPONSE),
        client_system_title=b"12345678",
    )

    c.receive_data(set_response.to_bytes())
    c.next_event()
    assert c.state.current_state == state.READY
Exemple #14
0
def test_negotiated_conformance_is_updated():
    c = DlmsConnection(client_system_title=b"12345678")
    c.send(c.get_aarq())
    c.receive_data(
        acse.ApplicationAssociationResponseApdu(
            result=enumerations.AssociationResult.ACCEPTED,
            result_source_diagnostics=enumerations.AcseServiceUserDiagnostics.
            NULL,
            user_information=acse.UserInformation(
                content=xdlms.InitiateResponseApdu(
                    negotiated_conformance=Conformance(
                        general_protection=True, general_block_transfer=True),
                    server_max_receive_pdu_size=500,
                )),
        ).to_bytes())
    c.next_event()
    assert c.conformance.general_protection
    assert c.conformance.general_block_transfer
    assert c.max_pdu_size == 500
    assert c.state.current_state == state.READY
def test_receive_exception_response_sets_state_to_ready(
    exception_response: xdlms.ExceptionResponse,
):
    c = DlmsConnection(
        state=state.DlmsConnectionState(current_state=state.AWAITING_GET_RESPONSE),
        client_system_title=b"12345678",
    )
    c.receive_data(exception_response.to_bytes())
    c.next_event()
    assert c.state.current_state == state.READY
Exemple #16
0
    def test_state_is_ready_in_init(self):
        c = DlmsConnection.with_pre_established_association(
            conformance=Conformance(
                priority_management_supported=True,
                attribute_0_supported_with_get=True,
                block_transfer_with_action=True,
                block_transfer_with_get_or_read=True,
                block_transfer_with_set_or_write=True,
                multiple_references=True,
                get=True,
                set=True,
                selective_access=True,
                event_notification=True,
                action=True,
            ))

        assert c.state.current_state == state.READY
Exemple #17
0
def test_action_response_normal_sets_ready_when_awaiting_action_resoponse():

    c = DlmsConnection(
        state=state.DlmsConnectionState(
            current_state=state.AWAITING_ACTION_RESPONSE),
        client_system_title=b"12345678",
    )

    c.receive_data(
        xdlms.ActionResponseNormal(
            status=enumerations.ActionResultStatus.SUCCESS,
            invoke_id_and_priority=xdlms.InvokeIdAndPriority(
                invoke_id=1, confirmed=True, high_priority=True),
        ).to_bytes())
    c.next_event()
    assert c.state.current_state == state.READY
    def test_not_able_to_send_rlrq(self, rlrq: acse.ReleaseRequest):
        c = DlmsConnection.with_pre_established_association(conformance=Conformance())

        with pytest.raises(exceptions.PreEstablishedAssociationError):
            c.send(rlrq)
def test_conformance_exists_on_simple_init():
    c = DlmsConnection(client_system_title=b"12345678")
    assert c.conformance is not None
    assert not c.conformance.general_protection
    assert c.state.current_state == state.NO_ASSOCIATION
class DlmsClient:
    client_logical_address: int
    server_logical_address: int
    io_interface: DlmsIOInterface
    authentication_method: Optional[enumerations.AuthenticationMechanism] = attr.ib(
        default=None
    )
    password: Optional[bytes] = attr.ib(default=None)
    encryption_key: Optional[bytes] = attr.ib(default=None)
    authentication_key: Optional[bytes] = attr.ib(default=None)
    security_suite: Optional[int] = attr.ib(default=0)
    dedicated_ciphering: bool = attr.ib(default=False)
    block_transfer: bool = attr.ib(default=False)
    max_pdu_size: int = attr.ib(default=65535)
    client_system_title: Optional[bytes] = attr.ib(default=None)
    client_initial_invocation_counter: int = attr.ib(default=0)
    meter_initial_invocation_counter: int = attr.ib(default=0)

    dlms_connection: DlmsConnection = attr.ib(
        default=attr.Factory(
            lambda self: DlmsConnection(
                client_system_title=self.client_system_title,
                authentication_method=self.authentication_method,
                password=self.password,
                global_encryption_key=self.encryption_key,
                global_authentication_key=self.authentication_key,
                use_dedicated_ciphering=self.dedicated_ciphering,
                use_block_transfer=self.block_transfer,
                security_suite=self.security_suite,
                max_pdu_size=self.max_pdu_size,
                client_invocation_counter=self.client_initial_invocation_counter,
                meter_invocation_counter=self.meter_initial_invocation_counter,
            ),
            takes_self=True,
        )
    )

    @classmethod
    def with_serial_hdlc_transport(
        cls,
        serial_port: str,
        client_logical_address: int,
        server_logical_address: int,
        server_physical_address: Optional[int],
        client_physical_address: Optional[int] = None,
        baud_rate: int = 9600,
        authentication_method: Optional[enumerations.AuthenticationMechanism] = None,
        password: Optional[bytes] = None,
        encryption_key: Optional[bytes] = None,
        authentication_key: Optional[bytes] = None,
        security_suite: Optional[int] = 0,
        dedicated_ciphering: bool = False,
        block_transfer: bool = False,
        max_pdu_size: int = 65535,
        client_system_title: Optional[bytes] = None,
        client_initial_invocation_counter: int = 0,
        meter_initial_invocation_counter: int = 0,
    ):
        serial_client = SerialHdlcTransport(
            client_logical_address=client_logical_address,
            client_physical_address=client_physical_address,
            server_logical_address=server_logical_address,
            server_physical_address=server_physical_address,
            serial_port=serial_port,
            serial_baud_rate=baud_rate,
        )
        return cls(
            client_logical_address=client_logical_address,
            server_logical_address=server_logical_address,
            authentication_method=authentication_method,
            password=password,
            encryption_key=encryption_key,
            authentication_key=authentication_key,
            security_suite=security_suite,
            dedicated_ciphering=dedicated_ciphering,
            block_transfer=block_transfer,
            max_pdu_size=max_pdu_size,
            client_system_title=client_system_title,
            client_initial_invocation_counter=client_initial_invocation_counter,
            meter_initial_invocation_counter=meter_initial_invocation_counter,
            io_interface=serial_client,
        )

    @classmethod
    def with_tcp_transport(
        cls,
        host: str,
        port: int,
        client_logical_address: int,
        server_logical_address: int,
        authentication_method: Optional[enumerations.AuthenticationMechanism] = None,
        password: Optional[bytes] = None,
        encryption_key: Optional[bytes] = None,
        authentication_key: Optional[bytes] = None,
        security_suite: Optional[int] = 0,
        dedicated_ciphering: bool = False,
        block_transfer: bool = False,
        max_pdu_size: int = 65535,
        client_system_title: Optional[bytes] = None,
        client_initial_invocation_counter: int = 0,
        meter_initial_invocation_counter: int = 0,
    ):
        tcp_transport = BlockingTcpTransport(
            host=host,
            port=port,
            client_logical_address=client_logical_address,
            server_logical_address=server_logical_address,
        )
        return cls(
            client_logical_address=client_logical_address,
            server_logical_address=server_logical_address,
            authentication_method=authentication_method,
            password=password,
            encryption_key=encryption_key,
            authentication_key=authentication_key,
            security_suite=security_suite,
            dedicated_ciphering=dedicated_ciphering,
            block_transfer=block_transfer,
            max_pdu_size=max_pdu_size,
            client_system_title=client_system_title,
            client_initial_invocation_counter=client_initial_invocation_counter,
            meter_initial_invocation_counter=meter_initial_invocation_counter,
            io_interface=tcp_transport,
        )

    @contextlib.contextmanager
    def session(self) -> "DlmsClient":
        self.connect()
        self.associate()
        yield self
        self.release_association()
        self.disconnect()

    def get(
        self,
        cosem_attribute: cosem.CosemAttribute,
        access_descriptor: Optional[RangeDescriptor] = None,
    ) -> bytes:
        self.send(
            xdlms.GetRequestNormal(
                cosem_attribute=cosem_attribute, access_selection=access_descriptor
            )
        )
        all_data_received = False
        data = bytearray()
        while not all_data_received:
            get_response = self.next_event()
            if isinstance(get_response, xdlms.GetResponseNormal):
                data.extend(get_response.data)
                all_data_received = True
                continue
            if isinstance(get_response, xdlms.GetResponseWithBlock):
                data.extend(get_response.data)
                self.send(
                    xdlms.GetRequestNext(
                        invoke_id_and_priority=get_response.invoke_id_and_priority,
                        block_number=get_response.block_number,
                    )
                )
                continue
            if isinstance(get_response, xdlms.GetResponseLastBlock):
                data.extend(get_response.data)
                all_data_received = True
                continue

            if isinstance(get_response, xdlms.GetResponseLastBlockWithError):
                raise DataResultError(
                    f"Error in blocktransfer of GET response: {get_response.error!r}"
                )

            if isinstance(get_response, xdlms.GetResponseNormalWithError):
                raise DataResultError(
                    f"Could not perform GET request: {get_response.error!r}"
                )

        return bytes(data)

    def set(self, cosem_attribute: cosem.CosemAttribute, data: bytes):
        self.send(xdlms.SetRequestNormal(cosem_attribute=cosem_attribute, data=data))
        return self.next_event()

    def action(self, method: cosem.CosemMethod, data: bytes):
        self.send(xdlms.ActionRequestNormal(cosem_method=method, data=data))
        response = self.next_event()

        if isinstance(response, xdlms.ActionResponseNormalWithError):
            raise ActionError(response.error.name)
        elif isinstance(response, xdlms.ActionResponseNormalWithData):
            if response.status != enumerations.ActionResultStatus.SUCCESS:
                raise ActionError(f"Unsuccessful ActionRequest: {response.status.name}")
            return response.data
        else:
            if response.status != enumerations.ActionResultStatus.SUCCESS:
                raise ActionError(f"Unsuccessful ActionRequest: {response.status.name}")
        return

    def associate(
        self,
        association_request: Optional[acse.ApplicationAssociationRequest] = None,
    ) -> acse.ApplicationAssociationResponse:

        # the aarq can be overridden or the standard one from the connection is used.
        aarq = association_request or self.dlms_connection.get_aarq()

        self.send(aarq)
        response = self.next_event()
        # we could have received an exception from the meter.
        if isinstance(response, xdlms.ExceptionResponse):
            raise exceptions.DlmsClientException(
                f"DLMS Exception: {response.state_error!r}:{response.service_error!r}"
            )
        # the association might not be accepted by the meter
        if isinstance(response, acse.ApplicationAssociationResponse):
            if response.result is not enumerations.AssociationResult.ACCEPTED:
                # there could be an error suppled with the reject.
                extra_error = None
                if response.user_information:
                    if isinstance(
                        response.user_information.content, ConfirmedServiceError
                    ):
                        extra_error = response.user_information.content.error
                raise exceptions.DlmsClientException(
                    f"Unable to perform Association: {response.result!r} and "
                    f"{response.result_source_diagnostics!r}, extra info: {extra_error}"
                )
        else:
            raise exceptions.LocalDlmsProtocolError(
                "Did not receive an AARE after sending AARQ"
            )

        if self.should_send_hls_reply():
            try:
                hls_response = self.send_hls_reply()
            except ActionError as e:
                raise HLSError from e

            hls_data = utils.parse_as_dlms_data(hls_response)
            if not hls_response:
                raise HLSError("Did not receive any HLS response data")

            if not self.dlms_connection.hls_response_valid(hls_data):
                raise HLSError(
                    f"Meter did not respond with correct challenge calculation"
                )

        return response

    def should_send_hls_reply(self) -> bool:
        return (
            self.dlms_connection.state.current_state
            == state.SHOULD_SEND_HLS_SEVER_CHALLENGE_RESULT
        )

    def send_hls_reply(self) -> Optional[bytes]:
        return self.action(
            method=cosem.CosemMethod(
                enumerations.CosemInterface.ASSOCIATION_LN,
                cosem.Obis(0, 0, 40, 0, 0),
                1,
            ),
            data=dlms_data.OctetStringData(
                self.dlms_connection.get_hls_reply()
            ).to_bytes(),
        )

    def release_association(self) -> acse.ReleaseResponse:
        rlrq = self.dlms_connection.get_rlrq()
        self.send(rlrq)
        rlre = self.next_event()
        return rlre

    def connect(self):
        self.io_interface.connect()

    def disconnect(self):
        self.io_interface.disconnect()

    def send(self, *events):
        for event in events:
            data = self.dlms_connection.send(event)
            response_bytes = self.io_interface.send(data)

            self.dlms_connection.receive_data(response_bytes)

    def next_event(self):
        event = self.dlms_connection.next_event()
        LOG.info(f"Received {event}")
        return event
def test_conformance_protection_is_set_when_passing_encryption_key():
    c = DlmsConnection(global_encryption_key=b"1234", client_system_title=b"12345678")
    assert c.conformance.general_protection
    assert c.state.current_state == state.NO_ASSOCIATION