Skip to content

Commit

Permalink
feat(connection): Added use_rlrq_rlre on DlmsConnectionSetting
Browse files Browse the repository at this point in the history
Some devices does not use the ReleaseRequest and ReleaseResponse to release an association and the lower layer can be disconnecter right away.
Implementing the new setting, how it handled in the connection and in the client.

Fixes #78
  • Loading branch information
Henrik Wahlgren committed Feb 26, 2024
1 parent fb3a669 commit 9a968b3
Show file tree
Hide file tree
Showing 6 changed files with 74 additions and 32 deletions.
2 changes: 2 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ and this project adheres to [Calendar Versioning](https://calver.org/)


### Added
* `use_rlrq_rlre` added to DlmsConnectionSettings. If `False` no ReleaseRequest is sent to server/device and lower
layer can be disconnected right away.

### Changed

Expand Down
33 changes: 18 additions & 15 deletions dlms_cosem/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ def session(self) -> "DlmsClient":
self.disconnect()

def get(
self,
cosem_attribute: cosem.CosemAttribute,
access_descriptor: Optional[RangeDescriptor] = None,
self,
cosem_attribute: cosem.CosemAttribute,
access_descriptor: Optional[RangeDescriptor] = None,
) -> bytes:
self.send(
xdlms.GetRequestNormal(
Expand Down Expand Up @@ -115,7 +115,7 @@ def get(
return bytes(data)

def get_many(
self, cosem_attributes_with_selection: List[cosem.CosemAttributeWithSelection]
self, cosem_attributes_with_selection: List[cosem.CosemAttributeWithSelection]
):
"""
Make a GET.WITH_LIST call. Get many items in one request.
Expand Down Expand Up @@ -153,8 +153,8 @@ def action(self, method: cosem.CosemMethod, data: bytes):
return

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

# the aarq can be overridden or the standard one from the connection is used.
Expand All @@ -174,7 +174,7 @@ def associate(
extra_error = None
if response.user_information:
if isinstance(
response.user_information.content, ConfirmedServiceError
response.user_information.content, ConfirmedServiceError
):
extra_error = response.user_information.content.error
raise exceptions.DlmsClientException(
Expand Down Expand Up @@ -203,7 +203,7 @@ def associate(
raise HLSError("Did not receive any HLS response data")

if not self.dlms_connection.authentication.hls_meter_data_is_valid(
hls_data, self.dlms_connection
hls_data, self.dlms_connection
):
raise HLSError(
f"Meter did not respond with correct challenge calculation"
Expand All @@ -213,8 +213,8 @@ def associate(

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

def send_hls_reply(self) -> Optional[bytes]:
Expand All @@ -231,11 +231,15 @@ def send_hls_reply(self) -> Optional[bytes]:
).to_bytes(),
)

def release_association(self) -> acse.ReleaseResponse:
def release_association(self) -> Optional[acse.ReleaseResponse]:

rlrq = self.dlms_connection.get_rlrq()
self.send(rlrq)
rlre = self.next_event()
return rlre
try:
self.send(rlrq)
rlre = self.next_event()
return rlre
except exceptions.NoRlrqRlreError:
return None

def connect(self):
self.transport.connect()
Expand All @@ -247,7 +251,6 @@ def send(self, *events):
for event in events:
data = self.dlms_connection.send(event)
response_bytes = self.transport.send_request(data)

self.dlms_connection.receive_data(response_bytes)

def next_event(self):
Expand Down
19 changes: 17 additions & 2 deletions dlms_cosem/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,13 @@ class DlmsConnectionSettings:
server implementations and manufacturers specific irregularity.
"""

# If false there should be no RLRQ and RLRE to release an association. It is fine to disconnect the lower layer
# directly
use_rlrq_rlre: bool = attr.ib(default=True)
# In Pietro Fiorentini local communication over HDLC the system title in GeneralGlobalCiphering is omitted.
empty_system_title_in_general_glo_ciphering: bool = attr.ib(default=False)



@attr.s(auto_attribs=True)
class DlmsConnection:
"""
Expand Down Expand Up @@ -274,6 +276,17 @@ def send(self, event) -> bytes:
f"pre-established "
)

if not self.settings.use_rlrq_rlre and isinstance(event, acse.ReleaseRequest):
# Client has issued a release request but the connection is not using them
# Connection states goes to NO_ASSOCIATION directly

LOG.info("Stopped ReleaseRequest as settings.use_rlrq_rlre is False. Ending association",
settings=self.settings, rlrq=event)
self.state.process_event(dlms_state.EndAssociation())
raise exceptions.NoRlrqRlreError(
"Connection settings does not allow ReleaseRequest and ReleaseResponse"
)

self.state.process_event(event)
LOG.debug(f"Preparing to send DLMS Request", request=event)

Expand Down Expand Up @@ -314,6 +327,7 @@ def next_event(self):
the IP wrapper element so it is possible to can keep on trying until all data
is received.
"""

apdu = XDlmsApduFactory.apdu_from_bytes(self.buffer)

LOG.info("Received DLMS Response", response=apdu)
Expand Down Expand Up @@ -572,8 +586,9 @@ def get_aarq(self) -> acse.ApplicationAssociationRequest:

def get_rlrq(self) -> acse.ReleaseRequest:
"""
Returns a ReleaseRequestApdu to release the current association.
Returns a ReleaseRequestApdu to release the current association if one should be used.
"""

initiate_request = xdlms.InitiateRequest(
proposed_conformance=self.conformance,
client_max_receive_pdu_size=self.max_pdu_size,
Expand Down
7 changes: 7 additions & 0 deletions dlms_cosem/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,10 @@ class DecryptionError(CryptographyError):
because the ciphertext has changed or that the key, nonce or associated data is
wrong
"""


class NoRlrqRlreError(Exception):
"""
Is raised from connection when a ReleaseRequest is issued on a connection that has use_rlrq_rlre==False
Control for client to just skip Release and disconnect the lower layer.
"""
9 changes: 9 additions & 0 deletions dlms_cosem/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ class RejectAssociation:
pass


@attr.s()
class EndAssociation:
"""
Is used when settings.use_rlrq_rlre == False to send the state to NO_ASSOCIATION
"""
pass


def make_sentinel(name):
cls = _SentinelBase(name, (_SentinelBase,), {})
cls.__class__ = cls
Expand Down Expand Up @@ -94,6 +102,7 @@ def make_sentinel(name):
RejectAssociation: NO_ASSOCIATION,
xdlms.ActionRequestNormal: AWAITING_ACTION_RESPONSE,
xdlms.DataNotification: READY,
EndAssociation: NO_ASSOCIATION,
},
SHOULD_SEND_HLS_SEVER_CHALLENGE_RESULT: {
xdlms.ActionRequestNormal: AWAITING_HLS_CLIENT_CHALLENGE_RESULT
Expand Down
36 changes: 21 additions & 15 deletions tests/test_dlms_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ def test_settings_exists_on_simple_init():
)
assert c.settings is not None


def test_settings_empty_system_title_in_general_glo_cipher_false(get_request: xdlms.GetRequestNormal):
"""
Make sure that system_title is is used when protecting APDUs with default connection settings.
Expand Down Expand Up @@ -167,7 +168,7 @@ def test_receive_get_response_sets_state_to_ready():


def test_set_request_sets_state_in_waiting_for_set_response(
set_request: xdlms.SetRequestNormal,
set_request: xdlms.SetRequestNormal,
):
c = DlmsConnection(
state=state.DlmsConnectionState(current_state=state.READY),
Expand Down Expand Up @@ -203,7 +204,6 @@ def test_can_send_action_request_in_ready(action_request: xdlms.ActionRequestNor


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",
Expand All @@ -223,7 +223,6 @@ def test_action_response_normal_sets_ready_when_awaiting_action_resoponse():


def test_action_response_normal_with_error_sets_ready_when_awaiting_action_resoponse():

c = DlmsConnection(
state=state.DlmsConnectionState(current_state=state.AWAITING_ACTION_RESPONSE),
client_system_title=b"12345678",
Expand All @@ -244,7 +243,6 @@ def test_action_response_normal_with_error_sets_ready_when_awaiting_action_resop


def test_action_response_normal_with_data_sets_ready_when_awaiting_action_resoponse():

c = DlmsConnection(
state=state.DlmsConnectionState(current_state=state.AWAITING_ACTION_RESPONSE),
client_system_title=b"12345678",
Expand All @@ -265,7 +263,7 @@ def test_action_response_normal_with_data_sets_ready_when_awaiting_action_resopo


def test_receive_exception_response_sets_state_to_ready(
exception_response: xdlms.ExceptionResponse,
exception_response: xdlms.ExceptionResponse,
):
c = DlmsConnection(
state=state.DlmsConnectionState(current_state=state.AWAITING_GET_RESPONSE),
Expand All @@ -278,16 +276,16 @@ def test_receive_exception_response_sets_state_to_ready(


def test_hls_is_started_automatically(
connection_with_hls: DlmsConnection,
ciphered_hls_aare: acse.ApplicationAssociationResponse,
connection_with_hls: DlmsConnection,
ciphered_hls_aare: acse.ApplicationAssociationResponse,
):
# 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
connection_with_hls.state.current_state
== state.SHOULD_SEND_HLS_SEVER_CHALLENGE_RESULT
)


Expand Down Expand Up @@ -319,8 +317,8 @@ def test_hls_fails(connection_with_hls: DlmsConnection):


def test_rejection_resets_connection_state(
connection_with_hls: DlmsConnection,
ciphered_hls_aare: acse.ApplicationAssociationResponse,
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
Expand All @@ -329,10 +327,19 @@ def test_rejection_resets_connection_state(
assert connection_with_hls.state.current_state == state.NO_ASSOCIATION


# what happens if the gmac provided by the meter is wrong
# -> we get an error
def test_rlrq_raises_norlrqrlreerror_when_settings_use_rlrq_rlre_is_false():
settings = DlmsConnectionSettings(use_rlrq_rlre=False)
c = DlmsConnection(
state=state.DlmsConnectionState(current_state=state.READY),
client_system_title=b"12345678",
authentication=NoSecurityAuthentication(),
settings=settings
)
rlrq = c.get_rlrq()
with pytest.raises(exceptions.NoRlrqRlreError):
c.send(rlrq)

# what happens if the gmac provided by the client is wrong
assert c.state.current_state == state.NO_ASSOCIATION


class TestPreEstablishedAssociation:
Expand Down Expand Up @@ -387,7 +394,6 @@ def test_hls_gmac_returns_correct_bytes(self):
assert type(challenge) == bytes

def test_too_short_length_raises_value_error(self):

with pytest.raises(ValueError):
make_client_to_server_challenge(7)

Expand Down

0 comments on commit 9a968b3

Please sign in to comment.