Repository: u9n/dlms-cosem (language: python)

Issue:
Support TCP/IP Connection
Suppport TCP/IP connections



Discussion / hints:

Fix patch:
diff --git a/.flake8 b/.flake8
new file mode 100644
index 0000000..20f7b9c
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,5 @@
+[flake8]
+ignore = E203, E266, E501, W503, F403, F401, F405, F541
+max-line-length = 89
+max-complexity = 18
+select = B,C,E,F,W,T4,B9
diff --git a/.gitignore b/.gitignore
index de040da..ee759a4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -104,4 +104,4 @@ venv.bak/
 .mypy_cache/
 
 # pycharm
-.idea/
\ No newline at end of file
+.idea/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..f86f588
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,20 @@
+repos:
+  - repo: https://github.com/PyCQA/isort
+    rev: 5.7.0
+    hooks:
+      - id: isort  # needs to run before black
+        language_version: python3.6
+        args: ["--profile", "black", "--filter-files"]
+  - repo: https://github.com/psf/black
+    rev: 20.8b1
+    hooks:
+      - id: black
+        language_version: python3.6
+
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v2.3.0
+    hooks:
+      - id: check-yaml
+      - id: end-of-file-fixer
+      - id: trailing-whitespace
+      - id: flake8
diff --git a/HISTORY.md b/HISTORY.md
index 5e8cf57..d7904be 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -1,24 +1,27 @@
 
 # Changelog
 
-The format is based on `Keep a Changelog: https://keepachangelog.com/en/1.0.0/`,
-and this project adheres to `Semantic Versioning: https://semver.org/spec/v2.0.0.html`
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Calendar Versioning](https://calver.org/)
 
 ## Unreleased
 
 
 ### Added
 
-* HDLC client implementation
+* HDLC transport implementation
+* TCP transport implementation
 * DlMS client implementation
 * Support for Get service including service specific block transfer
 * Support for selective access via range descriptor
 * Support for HLS authentication using HLS-GMAC.
-* Support for GlobalCiphering 
+* Support for GlobalCiphering
 * Parsing of ProfileGeneric buffer
 
 ### Changed
 
+* Changed project versioning scheme to Calendar versioning
+
 
 ### Deprecated
 
diff --git a/LICENSE b/LICENSE
index b71ac2a..9f0f149 100644
--- a/LICENSE
+++ b/LICENSE
@@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
\ No newline at end of file
+SOFTWARE.
diff --git a/README.md b/README.md
index 29212f2..81db905 100644
--- a/README.md
+++ b/README.md
@@ -21,17 +21,17 @@ Full documentation can be found at [www.dlms.dev](https://www.dlms.dev)
 
 # About
 
-`dlms-cosem` is designed to be a tool with a simple API for working with DLMS/COSEM 
-enabled energy meters. It provides the lowest level function, as protocol state 
+`dlms-cosem` is designed to be a tool with a simple API for working with DLMS/COSEM
+enabled energy meters. It provides the lowest level function, as protocol state
 management, APDU encoding/decoding, APDU encryption/decryption.
 
-The library aims to provide a [sans-io](https://sans-io.readthedocs.io/) implementation 
-of the DLMS/COSEM protocol so that the protocol code can be reused with several 
-different io-paradigms. As of now we provide a simple client implementation based on 
+The library aims to provide a [sans-io](https://sans-io.readthedocs.io/) implementation
+of the DLMS/COSEM protocol so that the protocol code can be reused with several
+different io-paradigms. As of now we provide a simple client implementation based on
 blocking I/O.
 
-We have not implemented full support to be able to build a server (meter) emulator. If 
-this is a use-case you need, consider sponsoring the development and contact us.    
+We have not implemented full support to be able to build a server (meter) emulator. If
+this is a use-case you need, consider sponsoring the development and contact us.
 
 # Supported features
 
@@ -50,123 +50,16 @@ Current Work:
 
 # Example use:
 
-Reading the billing data from an IDIS Electricity meter, over HDLC via an USB optical 
-probe.
-
-```python
-encryption_key = bytes.fromhex("D0D1D2D3D4D5D6D7D8D9DADBDCDDDEDF")
-authentication_key = bytes.fromhex("000102030405060708090A0B0C0D0E0F")
-auth = enumerations.AuthenticationMechanism.HLS_GMAC
-
-# simple partial to creat a public client. physical address depends on meter model.
-public_client = partial(
-    SerialDlmsClient,
-    server_logical_address=1,
-    server_physical_address=17,
-    client_logical_address=16,
-)
-
-# simple partial to create a managment client. physical address depends on meter model
-management_client = partial(
-    SerialDlmsClient,
-    server_logical_address=1,
-    server_physical_address=17,
-    client_logical_address=1,
-    authentication_method=auth,
-    encryption_key=encryption_key,
-    authentication_key=authentication_key,
-)
-
-port = "/dev/tty.usbserial-A704H991"
-
-# we need to read the current invocation counter from the public client to be able to 
-# set up a correct authenticated and encrypted session with the meter.
-with public_client(serial_port=port).session() as client:
-    
-    response_data = client.get(
-       cosem.CosemAttribute(
-            interface=enumerations.CosemInterface.DATA,
-            instance=cosem.Obis(0, 0, 0x2B, 1, 0),
-            attribute=2,
-        )
-    )
-    
-    data_decoder = a_xdr.AXdrDecoder(
-        encoding_conf=a_xdr.EncodingConf(
-            attributes=[a_xdr.Sequence(attribute_name="data")]
-        )
-    )
-    invocation_counter = data_decoder.decode(response_data)["data"]
-    print(f"meter_initial_invocation_counter = {invocation_counter}")
-
-
-# Now that we have the initial invocation counter we can create a management client 
-# to read data from the protected parts of the meter.
-with management_client(
-    serial_port=port, client_initial_invocation_counter=invocation_counter + 1
-).session() as client:
-
-    # Get on billing profile. Blockwise transfer handled automatically.
-    # Requesting data in a date range.
-    profile = client.get(
-        cosem.CosemAttribute(
-            interface=enumerations.CosemInterface.PROFILE_GENERIC,
-            instance=cosem.Obis(1, 0, 99, 1, 0),
-            attribute=2,
-        ),
-        access_descriptor=RangeDescriptor(
-            restricting_object=selective_access.CaptureObject(
-                cosem_attribute=cosem.CosemAttribute(
-                    interface=enumerations.CosemInterface.CLOCK,
-                    instance=cosem.Obis.from_dotted("0.0.1.0.0.255"),
-                    attribute=2,
-                ),
-                data_index=0,
-            ),
-            from_value=dateparse("2020-01-01T00:03:00-02:00"),
-            to_value=dateparse("2020-01-06T00:03:00-01:00"),
-        ),
-    )
-    
-    # Defining profile data parser
-    parser = ProfileGenericBufferParser(
-        capture_objects=[
-            cosem.CosemAttribute(
-                interface=enumerations.CosemInterface.CLOCK,
-                instance=cosem.Obis(0, 0, 1, 0, 0, 255),
-                attribute=2,
-            ),
-            cosem.CosemAttribute(
-                interface=enumerations.CosemInterface.DATA,
-                instance=cosem.Obis(0, 0, 96, 10, 1, 255),
-                attribute=2,
-            ),
-            cosem.CosemAttribute(
-                interface=enumerations.CosemInterface.REGISTER,
-                instance=cosem.Obis(1, 0, 1, 8, 0, 255),
-                attribute=2,
-            ),
-            cosem.CosemAttribute(
-                interface=enumerations.CosemInterface.REGISTER,
-                instance=cosem.Obis(1, 0, 2, 8, 0, 255),
-                attribute=2,
-            ),
-        ],
-        capture_period=60,
-    )
-    
-    result = parser.parse_bytes(profile)
-    pprint(result)
+Look at the different file in the `examples` folder to how to use the library.
 
-```
-    
 # Supported meters
 
-Technically we aim to support any DLMS enabled meter. But since the library is low 
-level DLMS you might need an abstraction layer to support everything in your meter.
+Technically we aim to support any DLMS enabled meter. The library is implementing all
+the low level DLMS, and you might need an abstraction layer to support everything in
+your meter.
 
-DLMS/COSEM specifies many different ways of performing tasks on a meter. It is 
-customary that a meter also adheres to a companion standard. In the companion standard 
+DLMS/COSEM specifies many ways of performing tasks on a meter. It is
+customary that a meter also adheres to a companion standard. In the companion standard
 it is defined exactly how certain use-cases are to be performed and how data is modeled.
 
 Examples of companion standards are:
@@ -174,8 +67,8 @@ Examples of companion standards are:
 * IDIS (all Europe)
 * UNI/TS 11291 (Italy)
 
-On top of it all your DSO (Distribution Service Operator) might have ordered their 
-meters with extra functionality or reduced functionality from one of the companion 
+On top of it all your DSO (Distribution Service Operator) might have ordered their
+meters with extra functionality or reduced functionality from one of the companion
 standards.
 
 We have some meters we have run tests on or know the library is used for in production
@@ -188,21 +81,17 @@ We have some meters we have run tests on or know the library is used for in prod
 This library is developed by Palmlund Wahlgren Innovative Technology AB. We are
 based in Sweden and are members of the DLMS User Association.
 
-If you find an bug pleas raise an issue on Github.
+If you find a bug pleas raise an issue on Github.
 
 We welcome contributions of any kind.
 
-We add features depending on our own use cases and our clients use cases. If you 
+We add features depending on our own and our clients use cases. If you
 need a feature implemented please contact us.
 
 # Training / Consultancy / Commercial Support
 
-We offer consultancy service and training services around this library and general DLMS/COSEM. 
-If you are interested in our services just reach out to us. 
+We offer consultancy service and training services around this library and general DLMS/COSEM.
+If you are interested in our services just reach out to us.
 
-If you have implemented a solution based on this library we also offer a commercial 
+If you have implemented a solution based on this library we also offer a commercial
 support scheme.
-
-
-
-
diff --git a/dlms_cosem/protocol/a_xdr.py b/dlms_cosem/a_xdr.py
similarity index 99%
rename from dlms_cosem/protocol/a_xdr.py
rename to dlms_cosem/a_xdr.py
index 519eff8..094404e 100644
--- a/dlms_cosem/protocol/a_xdr.py
+++ b/dlms_cosem/a_xdr.py
@@ -38,9 +38,11 @@
 
 """
 
-import attr
 from typing import *
-from dlms_cosem.protocol import dlms_data
+
+import attr
+
+from dlms_cosem import dlms_data
 
 VARIABLE_LENGTH = -1
 
@@ -81,7 +83,7 @@ def decode_variable_integer(bytes_input: bytes):
     if is_mutliple_bytes:
         length_length = int(bytes_input[0] & 0b01111111)
         length_data = bytes_input[1 : (length_length + 1)]
-        length = int.from_bytes(length_data, 'big')
+        length = int.from_bytes(length_data, "big")
         return length, bytes_input[length_length + 1 :]
 
     else:
diff --git a/dlms_cosem/api_design.md b/dlms_cosem/api_design.md
deleted file mode 100644
index 73dd52c..0000000
--- a/dlms_cosem/api_design.md
+++ /dev/null
@@ -1,80 +0,0 @@
-# API Design
-
-We want a simple API for the actions you perform on objects in a DLMS meter.
-DLSM follows a request/response flow.
-
-Simillar to how redis can pipeline actions there is the possiblite to "pipeline" 
-commands in DLMS with the ACCESS service where several actions can 
-be "clumped together in on request" 
-
-The client should take care of association aquire and release without the user needing 
-to set it up. It should also be possible to set up a predefined assosiation and skip the 
-aquire and release.
-
-```python 
-with Client() as client:
-    response = client.set("1.2.3.4.5", 2, "test")
-``` 
-
-But this is a bit too simple since we need to know how to model the objects in the meter.
-
-There should be a way of defining the objects available in the meter beforehand or 
-set it up as the request goes away. Then if something is wrong in the calling a proper 
-error can be generated. 
-
-```python 
-with Client() as client:
-    response = client.set(
-        logical_name=LogicalNameReference(a=1, b=2, c=3, d=4, e=5, f=5)
-        interface=PushInterfaceV2,
-        attribute=2,
-        value="test"
-        )
-    # above should be the same as
-    response = client.set("1.2.3.4.5", 2, "test") 
-    # if we know that the PushInterface is on LN 1.2.3.4.5 beforehand
-    # It could be a simple dict.
-
-    # it would also make it possible to use something like:
-    response = client["1.2.3.4.5"][0] # to initate a get in the attribute.
-    # or
-    response = client["1.2.3.4.5"].set(2, "test")
-    
-``` 
-
-Since we want automatic handling of assiciation aquire and relase we need to set up 
-the client with data so it can handle it for us.
-
-```python
-
-with Client(encryption_key="0f0f", security_setup=x, proposed_conformance=Conformance() as client:
-    response = client.set("1.2.3.4.5", 2, "test")
-```
-
-Should result in an AARQ is sent according to association. Set up dedicated key 
-encryption, handle HLS password transfer, etc.
-When you then send a SET (WriteRequestApdu) it should be automatically encrypted 
-with the global or dedicated key. If block is supported in conformance it should be 
-split into blocks and sent. The response should be received and when it is fully 
-received it should be decrypted and returned to the user. 
-
-By having the "heavy stuff" handled in the background it makes it possbile to keep a 
-simple API while adding incfreasing functionallity to the middlestack.
-
-We also need to add a certain transport to the client so it can send its data to the 
-meter. HDLC and IP should be implemented. Maybe HDLC_over_IP. But since that is in the 
-IO part it can easily be broken out and extended as needed, supporting asyncio or 
-whatever.
-
-The main part of the protocol implementation should still be sans-io.
-
-client.set()
- -> SetRequest
-   -> GlobalCipheredApdu
-      -> Block, Block, Block
-      -> connection.send("data")
-      -> data is sent over wire.
-      <- data is received.
-      <- Block, Block, Block
-   <- GlobalCipheredApdu
- <- SetResponse
diff --git a/dlms_cosem/protocol/ber.py b/dlms_cosem/ber.py
similarity index 85%
rename from dlms_cosem/protocol/ber.py
rename to dlms_cosem/ber.py
index 621cb6b..4f03d1e 100644
--- a/dlms_cosem/protocol/ber.py
+++ b/dlms_cosem/ber.py
@@ -1,5 +1,6 @@
 from typing import *
 
+
 class BER:
 
     """
@@ -13,7 +14,7 @@ def encode(tag: Union[int, bytes], data: Union[bytearray, bytes]):
         if isinstance(tag, int):
             # Simplification since we now just use ints as tags when they are single
             # bytes.
-            _tag_bytes = tag.to_bytes(1, 'big')
+            _tag_bytes = tag.to_bytes(1, "big")
         else:
             _tag_bytes = tag
 
@@ -22,9 +23,10 @@ def encode(tag: Union[int, bytes], data: Union[bytearray, bytes]):
 
         if not isinstance(data, (bytes, bytearray)):
             raise ValueError(
-                f"BER encoding requires bytes or bytearray, got {data!r} of {type(data)}")
+                f"BER encoding requires bytes or bytearray, got {data!r} of {type(data)}"
+            )
 
-        length = len(data).to_bytes(1, 'big')
+        length = len(data).to_bytes(1, "big")
         if length == 0:
             return b""
 
@@ -33,7 +35,7 @@ def encode(tag: Union[int, bytes], data: Union[bytearray, bytes]):
     @staticmethod
     def decode(_bytes: bytes, tag_length: int = 1) -> Tuple[bytes, int, bytes]:
         input = bytearray(_bytes)
-        tag = b"".join([input.pop(0).to_bytes(1, 'big') for _ in range(tag_length)])
+        tag = b"".join([input.pop(0).to_bytes(1, "big") for _ in range(tag_length)])
         length = input.pop(0)
         data = input
         if len(data) != length:
diff --git a/dlms_cosem/clients/blocking_tcp_transport.py b/dlms_cosem/clients/blocking_tcp_transport.py
new file mode 100644
index 0000000..f7ee862
--- /dev/null
+++ b/dlms_cosem/clients/blocking_tcp_transport.py
@@ -0,0 +1,65 @@
+import logging
+import socket
+from typing import *
+
+import attr
+
+from dlms_cosem.protocol.wrappers import WrapperHeader, WrapperProtocolDataUnit
+
+LOG = logging.getLogger(__name__)
+
+
+@attr.s(auto_attribs=True)
+class BlockingTcpTransport:
+
+    host: str
+    port: int
+    client_logical_address: int
+    server_logical_address: int
+    timeout: int = attr.ib(default=5)
+    tcp_socket: Optional[socket.socket] = attr.ib(init=False, default=None)
+
+    @property
+    def address(self) -> Tuple[str, int]:
+        return self.host, self.port
+
+    def wrap(self, bytes_to_wrap: bytes) -> bytes:
+        """
+        When sending data over TCP or UDP it is necessary to wrap the data in the in
+        the DLMS IP wrapper. This is so the server (meter) knows where the data is
+        intended and how long the message is since there is no "final/poll" bit like
+        in HDLC.
+        """
+        header = WrapperHeader(
+            source_wport=self.client_logical_address,
+            destination_wport=self.server_logical_address,
+            length=len(bytes_to_wrap),
+        )
+        return WrapperProtocolDataUnit(bytes_to_wrap, header).to_bytes()
+
+    def connect(self):
+        """Create a new socket and set it on the transport"""
+
+        self.tcp_socket = socket.create_connection(
+            address=self.address, timeout=self.timeout
+        )
+        LOG.info(f"Connected to {self.address}")
+
+    def disconnect(self):
+        """Close socket and remove it from the transport"""
+        self.tcp_socket.close()
+        self.tcp_socket = None
+
+    def send(self, bytes_to_send: bytes) -> bytes:
+        if not self.tcp_socket:
+            raise RuntimeError("TCP transport not connected.")
+
+        self.tcp_socket.sendall(self.wrap(bytes_to_send))
+        return self.recv()
+
+    def recv(self) -> bytes:
+        if not self.tcp_socket:
+            raise RuntimeError("TCP transport not connected.")
+        header = WrapperHeader.from_bytes(self.tcp_socket.recv(8))
+        data = self.tcp_socket.recv(header.length)
+        return data
diff --git a/dlms_cosem/clients/dlms_client.py b/dlms_cosem/clients/dlms_client.py
new file mode 100644
index 0000000..f5eed2d
--- /dev/null
+++ b/dlms_cosem/clients/dlms_client.py
@@ -0,0 +1,321 @@
+import contextlib
+import logging
+from typing import *
+
+import attr
+from typing_extensions import Protocol  # type: ignore
+
+from dlms_cosem import cosem, dlms_data, enumerations, exceptions, state, utils
+from dlms_cosem.clients.blocking_tcp_transport import BlockingTcpTransport
+from dlms_cosem.clients.hdlc_transport import SerialHdlcTransport
+from dlms_cosem.connection import DlmsConnection
+from dlms_cosem.protocol import acse, xdlms
+from dlms_cosem.protocol.xdlms import ConfirmedServiceErrorApdu
+from dlms_cosem.protocol.xdlms.selective_access import RangeDescriptor
+
+LOG = logging.getLogger(__name__)
+
+
+class DataResultError(Exception):
+    """ Error retrieveing data"""
+
+
+class HLSError(Exception):
+    """error in HLS procedure"""
+
+
+class DlmsIOInterface(Protocol):
+    """
+    Protocol for a class that should be used for transport.
+    """
+
+    def connect(self) -> None:
+        ...
+
+    def disconnect(self) -> None:
+        ...
+
+    def send(self, bytes_to_send: bytes) -> bytes:
+        ...
+
+
+@attr.s(auto_attribs=True)
+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):
+        self.associate()
+        yield self
+        self.release_association()
+
+    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):
+        pass
+
+    def action(self, method: cosem.CosemMethod, data: bytes):
+        self.send(xdlms.ActionRequestNormal(cosem_method=method, data=data))
+        return self.next_event()
+
+    def associate(
+        self,
+        association_request: Optional[acse.ApplicationAssociationRequestApdu] = None,
+    ) -> acse.ApplicationAssociationResponseApdu:
+
+        self.io_interface.connect()
+
+        # the aarq can be overridden or the standard one from the connection is used.
+        aarq = association_request or self.dlms_connection.get_aarq()
+
+        try:
+            self.send(aarq)
+            response = self.next_event()
+            # we could have received an exception from the meter.
+            if isinstance(response, xdlms.ExceptionResponseApdu):
+                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.ApplicationAssociationResponseApdu):
+                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, ConfirmedServiceErrorApdu
+                        ):
+                            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():
+                hls_response = self.send_hls_reply()
+
+                hls_data = utils.parse_as_dlms_data(hls_response.data)
+
+                if not isinstance(hls_response, xdlms.ActionResponseNormalWithData):
+                    raise exceptions.LocalDlmsProtocolError(
+                        "Received an incorrect ActionResponse to HLS"
+                    )
+
+                if hls_response.status != enumerations.ActionResultStatus.SUCCESS:
+                    raise HLSError(
+                        f"HLS authentication failed: {hls_response.status!r}"
+                    )
+
+                if not self.dlms_connection.hls_response_valid(hls_data):
+                    raise HLSError(
+                        f"Meter did not respond with correct challenge calculation"
+                    )
+        except (exceptions.DlmsClientException, HLSError):
+            self.io_interface.disconnect()
+            raise
+        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) -> xdlms.ActionRequestNormal:
+        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.ReleaseResponseApdu:
+        rlrq = self.dlms_connection.get_rlrq()
+        self.send(rlrq)
+        rlre = self.next_event()
+        self.io_interface.disconnect()
+        return rlre
+
+    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
diff --git a/dlms_cosem/clients/experimental_meter.py b/dlms_cosem/clients/experimental_meter.py
new file mode 100644
index 0000000..6186f8d
--- /dev/null
+++ b/dlms_cosem/clients/experimental_meter.py
@@ -0,0 +1,110 @@
+from contextlib import contextmanager
+from decimal import Decimal
+from typing import *
+
+import attr
+
+from dlms_cosem import cosem, utils
+from dlms_cosem.clients.dlms_client import DlmsClient
+from dlms_cosem.cosem import Obis
+from dlms_cosem.cosem.profile_generic import Data, ProfileGeneric
+from dlms_cosem.protocol.xdlms.selective_access import RangeDescriptor
+
+
+def force_obis(maybe_obis: Union[str, Obis]):
+    if isinstance(maybe_obis, Obis):
+        return maybe_obis
+    else:
+        return Obis.from_dotted(maybe_obis)
+
+
+@attr.s(auto_attribs=True)
+class NumericalMeterValue:
+    """
+    When parsing data from a DlmsMeter it can be a simple value that we know the meta
+    data around before since it is predefined. For example static attribute on an
+    interface class. For example capture_period on ProfileGeneric is an integer in
+    minutes.
+    Other attributes can be objects that is returned so that we may correctly parse
+    data in other attributes: Ex. capture_objects in Profile Generic.
+    Modeling values when they are not part of an attribute on an interface class is
+    usually done via interface classes DATA and REGISTER.
+    We also have numerial based values and string-based values.
+    """
+
+    # To avoid floating point rounding error we should always use Decimal to
+    # represent a numerical value in a meter.
+    value: Decimal
+
+    # Scalar is used if the value provides one
+    scalar: Optional[int]
+
+    # see table 4 in Blue Book.
+    # TODO: simple Enum or a factory that returns Unit objects?
+    unit: Any
+
+
+@attr.s(auto_attribs=True)
+class StringlikeMeterValue:
+    """
+    For meter values that are string-like. Bytes should be represented as hexadecimal
+    strings If we can't decode them to strings.
+    """
+
+    pass
+
+
+@attr.s(auto_attribs=True)
+class Meter:
+
+    dlms_client: DlmsClient
+    objects: Dict[str, Union[ProfileGeneric, Data]]
+
+    def object_exists(self, object_obis: Obis) -> bool:
+        return object_obis.dotted_repr() in self.objects.keys()
+
+    @contextmanager
+    def session(self):
+        self.dlms_client.associate()
+        yield self
+        self.dlms_client.release_association()
+
+    def get(
+        self,
+        logical_name: Union[str, Obis],
+        attribute: int,
+        selective_access: Optional[RangeDescriptor] = None,
+    ):
+        obis = force_obis(logical_name)
+        instance = self.objects.get(obis.dotted_repr(), None)
+        if instance is None:
+            raise ValueError(
+                f"Object with logical name {obis.dotted_repr()} does not exist on meter"
+            )
+        if instance.is_static_attribute(attribute):
+            # check if the value is already present on the meter
+            value = getattr(
+                instance, instance.STATIC_ATTRIBUTES[attribute].attribute_name
+            )
+            if value:
+                return value
+
+        value = utils.parse_as_dlms_data(
+            self.dlms_client.get(
+                cosem_attribute=cosem.CosemAttribute(
+                    interface=instance.INTERFACE_CLASS_ID,
+                    instance=obis,
+                    attribute=attribute,
+                )
+            )
+        )
+
+        if instance.is_static_attribute(attribute):
+            setattr(
+                instance, instance.STATIC_ATTRIBUTES[attribute].attribute_name, value
+            )
+
+        converter = instance.DYNAMIC_CONVERTERS.get(attribute, None)
+        if converter:
+            return converter(instance, value)
+        return value
diff --git a/dlms_cosem/clients/serial_hdlc.py b/dlms_cosem/clients/hdlc_transport.py
similarity index 97%
rename from dlms_cosem/clients/serial_hdlc.py
rename to dlms_cosem/clients/hdlc_transport.py
index a64f181..d6ad0e8 100644
--- a/dlms_cosem/clients/serial_hdlc.py
+++ b/dlms_cosem/clients/hdlc_transport.py
@@ -1,30 +1,25 @@
-from typing import Optional
 import logging
+from typing import Optional
+
 import attr
 import serial
 
-from dlms_cosem.protocol.hdlc import (
-    address,
-    state,
-    connection,
-    frames,
-    exceptions as hdlc_exception,
-)
-
+from dlms_cosem.hdlc import address, connection, frames, state
 
 LOG = logging.getLogger(__name__)
 
 LLC_COMMAND_HEADER = b"\xe6\xe6\x00"
 LLC_RESPONSE_HEADER = b"\xe6\xe7\x00"
 
+
 class ClientError(Exception):
     """General error in client"""
 
 
 @attr.s(auto_attribs=True)
-class SerialHdlcClient:
+class SerialHdlcTransport:
     """
-    HDLC client to send data over serial.
+    HDLC transport to send data over serial.
     """
 
     client_logical_address: int
@@ -152,7 +147,8 @@ def send(self, telegram: bytes) -> bytes:
                 rr = frames.ReceiveReadyFrame(
                     destination_address=self.server_hdlc_address,
                     source_address=self.client_hdlc_address,
-                    receive_sequence_number=self.hdlc_connection.server_rsn)
+                    receive_sequence_number=self.hdlc_connection.server_rsn,
+                )
                 self.out_buffer += self.hdlc_connection.send(rr)
                 self.drain_out_buffer()
 
@@ -199,8 +195,6 @@ def drain_out_buffer(self):
                     continue
             break
 
-
-
     def generate_information_frame(
         self, payload: bytes, segmented: bool, final: bool
     ) -> frames.InformationFrame:
diff --git a/dlms_cosem/clients/serial_dlms.py b/dlms_cosem/clients/serial_dlms.py
deleted file mode 100644
index aad4243..0000000
--- a/dlms_cosem/clients/serial_dlms.py
+++ /dev/null
@@ -1,222 +0,0 @@
-import attr
-from typing import *
-from dlms_cosem.clients.serial_hdlc import SerialHdlcClient
-from dlms_cosem.protocol.connection import DlmsConnection
-from dlms_cosem.protocol import xdlms, cosem, exceptions, enumerations, dlms_data, a_xdr
-from dlms_cosem.protocol import acse, state
-import logging
-import contextlib
-
-from dlms_cosem.protocol.xdlms import ConfirmedServiceErrorApdu
-from dlms_cosem.protocol.xdlms.selective_access import EntryDescriptor, RangeDescriptor
-
-LOG = logging.getLogger(__name__)
-
-
-class DataResultError(Exception):
-    """ Error retrieveing data"""
-
-
-class HLSError(Exception):
-    """error in HLS procedure"""
-
-
-@attr.s(auto_attribs=True)
-class SerialDlmsClient:
-    client_logical_address: int
-    server_logical_address: int
-    serial_port: str
-    serial_baud_rate: int = attr.ib(default=9600)
-    server_physical_address: Optional[int] = attr.ib(default=None)
-    client_physical_address: Optional[int] = attr.ib(default=None)
-    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[bytes] = 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,
-        )
-    )
-    io_interface: SerialHdlcClient = attr.ib(
-        default=attr.Factory(
-            lambda self: SerialHdlcClient(
-                client_logical_address=self.client_logical_address,
-                client_physical_address=self.client_physical_address,
-                server_logical_address=self.server_logical_address,
-                server_physical_address=self.server_physical_address,
-                serial_port=self.serial_port,
-                serial_baud_rate=self.serial_baud_rate,
-            ),
-            takes_self=True,
-        )
-    )
-
-    @contextlib.contextmanager
-    def session(self):
-        self.associate()
-        yield self
-        self.release_association()
-
-    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):
-        pass
-
-    def action(self):
-        pass
-
-    def associate(
-        self,
-        association_request: Optional[acse.ApplicationAssociationRequestApdu] = None,
-    ) -> acse.ApplicationAssociationResponseApdu:
-
-        # set up hdlc
-        self.io_interface.connect()
-        aarq = association_request or self.dlms_connection.get_aarq()
-
-        try:
-            self.send(aarq)
-            aare = self.next_event()
-            # we could have recieved an exception from the meter.
-            if isinstance(aare, xdlms.ExceptionResponseApdu):
-                raise exceptions.DlmsClientException(
-                    f"DLMS Exception: {aare.state_error!r}:{aare.service_error!r}"
-                )
-            # the association might not be accepted by the meter
-            if aare.result is not enumerations.AssociationResult.ACCEPTED:
-                # there could be an error suppled with the reject.
-                extra_error = None
-                if aare.user_information:
-                    if isinstance(
-                        aare.user_information.content, ConfirmedServiceErrorApdu
-                    ):
-                        extra_error = aare.user_information.content.error
-                raise exceptions.DlmsClientException(
-                    f"Unable to perform Association: {aare.result!r} and "
-                    f"{aare.result_source_diagnostics!r}, extra info: {extra_error}"
-                )
-
-            if self.should_send_hls_reply():
-
-                self.send(self.get_hls_reply())
-                action_response = self.next_event()
-
-                if action_response.result != enumerations.ActionResult.SUCCESS:
-                    raise HLSError(
-                        f"HLS authentication failed: {action_response.result!r}"
-                    )
-
-                if not self.dlms_connection.hls_response_valid(
-                    action_response.result_data
-                ):
-                    raise HLSError(
-                        f"Meter did not respond with correct challenge calculation"
-                    )
-        except (exceptions.DlmsClientException, HLSError):
-            self.io_interface.disconnect()
-            raise
-        return aare
-
-    def should_send_hls_reply(self) -> bool:
-        return (
-            self.dlms_connection.state.current_state
-            == state.SHOULD_SEND_HLS_SEVER_CHALLENGE_RESULT
-        )
-
-    def get_hls_reply(self) -> xdlms.ActionRequest:
-        return xdlms.ActionRequest(
-            cosem_method=cosem.CosemMethod(
-                enumerations.CosemInterface.ASSOCIATION_LN,
-                cosem.Obis(0, 0, 40, 0, 0),
-                1,
-            ),
-            action_type=enumerations.ActionType.NORMAL,
-            parameters=dlms_data.OctetStringData(
-                self.dlms_connection.get_hls_reply()
-            ).to_bytes(),
-        )
-
-    def release_association(self) -> acse.ReleaseResponseApdu:
-        rlrq = self.dlms_connection.get_rlrq()
-        self.send(rlrq)
-        rlre = self.next_event()
-        self.io_interface.disconnect()
-        return rlre
-
-    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
diff --git a/dlms_cosem/protocol/connection.py b/dlms_cosem/connection.py
similarity index 87%
rename from dlms_cosem/protocol/connection.py
rename to dlms_cosem/connection.py
index 0187725..dc0026d 100644
--- a/dlms_cosem/protocol/connection.py
+++ b/dlms_cosem/connection.py
@@ -1,14 +1,16 @@
+import logging
+import os
 from typing import *
 
 import attr
 
-from dlms_cosem.protocol.xdlms.conformance import Conformance
-from dlms_cosem.protocol import acse, xdlms, exceptions, security, cosem, dlms_data
-from dlms_cosem.protocol import enumerations as enums
-from dlms_cosem.protocol import state as dlms_state
+from dlms_cosem import enumerations as enums
+from dlms_cosem import exceptions, security
+from dlms_cosem import state as dlms_state
+from dlms_cosem import utils
+from dlms_cosem.protocol import acse, xdlms
 from dlms_cosem.protocol.xdlms.base import AbstractXDlmsApdu
-import logging
-import os
+from dlms_cosem.protocol.xdlms.conformance import Conformance
 
 LOG = logging.getLogger(__name__)
 
@@ -38,9 +40,9 @@ class XDlmsApduFactory:
         98: acse.ReleaseRequestApdu,
         99: acse.ReleaseResponseApdu,
         192: xdlms.GetRequestFactory,
-        195: xdlms.ActionRequest,
+        195: xdlms.ActionRequestFactory,
         196: xdlms.GetResponseFactory,
-        199: xdlms.ActionResponse,
+        199: xdlms.ActionResponseFactory,
     }
 
     @classmethod
@@ -100,6 +102,10 @@ def make_conformance(encryption_key: Optional[bytes], use_block_transfer: bool):
     )
 
 
+class ProtectionError(Exception):
+    """Unable to perform cryptographic function"""
+
+
 @attr.s(auto_attribs=True)
 class DlmsConnection:
     """
@@ -109,7 +115,9 @@ class DlmsConnection:
     # Client system title can be any combination of 8 bytes.
     # But is should not be the same as the metering the connection is set up too.
     client_system_title: bytes = attr.ib(
-        converter=attr.converters.default_if_none(factory=default_system_title)
+        converter=attr.converters.default_if_none(
+            factory=default_system_title
+        )  # type: ignore
     )
 
     global_encryption_key: Optional[bytes] = attr.ib(default=None)
@@ -358,15 +366,24 @@ def next_event(self):
             elif apdu.authentication == enums.AuthenticationMechanism.HLS_GMAC:
                 self.state.process_event(dlms_state.HlsStart())
 
-        if isinstance(apdu, xdlms.ActionResponse):
-            if self.state.current_state == dlms_state.HLS_DONE:
-                if apdu.result != enums.ActionResult.SUCCESS:
+        # Handle HLS verification
+        if self.state.current_state == dlms_state.HLS_DONE:
+            if isinstance(apdu, xdlms.ActionResponseNormalWithData):
+                if apdu.status != enums.ActionResultStatus.SUCCESS:
                     self.state.process_event(dlms_state.HlsFailed())
+                if self.hls_response_valid(utils.parse_as_dlms_data(apdu.data)):
+                    self.state.process_event(dlms_state.HlsSuccess())
                 else:
-                    if self.hls_response_valid(apdu.result_data):
-                        self.state.process_event(dlms_state.HlsSuccess())
-                    else:
-                        self.state.process_event(dlms_state.HlsFailed())
+                    self.state.process_event(dlms_state.HlsFailed())
+            elif isinstance(
+                apdu, (xdlms.ActionResponseNormalWithError, xdlms.ActionResponseNormal)
+            ):
+                self.state.process_event(dlms_state.HlsFailed())
+
+            else:
+                raise exceptions.LocalDlmsProtocolError(
+                    "Received a non Action response when in HLS DONE"
+                )
 
         return apdu
 
@@ -433,6 +450,15 @@ def encrypt(self, plain_text: bytes):
         """
         Encrypts plain bytes according to the current association and connection.
         """
+        if not self.global_encryption_key:
+            raise ProtectionError(
+                "Unable to encrypt plain text. Missing global_encryption_key"
+            )
+        if not self.global_authentication_key:
+            raise ProtectionError(
+                "Unable to encrypt plain text. Missing global_authentication_key"
+            )
+
         return security.encrypt(
             self.security_control,
             system_title=self.client_system_title,
@@ -446,6 +472,20 @@ def decrypt(self, ciphered_text: bytes):
         """
         Encrypts ciphered bytes according to the current association and connection.
         """
+
+        if not self.global_encryption_key:
+            raise ProtectionError(
+                "Unable to decrypt ciphered text. Missing global_encryption_key"
+            )
+        if not self.global_authentication_key:
+            raise ProtectionError(
+                "Unable to decrypt ciphered text. Missing global_authentication_key"
+            )
+        if not self.meter_system_title:
+            raise ProtectionError(
+                "Unable to decrypt ciphered text. Have not received the meters system title."
+            )
+
         return security.decrypt(
             self.security_control,
             system_title=self.meter_system_title,
@@ -481,8 +521,8 @@ def unprotect(self, event):
                         auth_key=self.global_authentication_key,
                         cipher_text=event.user_information.content.ciphered_text,
                     )
-                    event.user_information.content = xdlms.InitiateResponseApdu.from_bytes(
-                        plain_text
+                    event.user_information.content = (
+                        xdlms.InitiateResponseApdu.from_bytes(plain_text)
                     )
 
         elif isinstance(event, xdlms.GeneralGlobalCipherApdu):
@@ -496,6 +536,7 @@ def unprotect(self, event):
                 auth_key=self.global_authentication_key,
                 cipher_text=event.ciphered_text,
             )
+            print(plain_text)
             event = XDlmsApduFactory.apdu_from_bytes(plain_text)
 
         else:
@@ -588,6 +629,14 @@ def get_hls_reply(self) -> bytes:
         """
         if not self.meter_to_client_challenge:
             raise exceptions.LocalDlmsProtocolError("Meter has not send challenge")
+        if not self.global_encryption_key:
+            raise ProtectionError(
+                "Unable to create GMAC. Missing global_encryption_key"
+            )
+        if not self.global_authentication_key:
+            raise ProtectionError(
+                "Unable to create GMAC. Missing global_authentication_key"
+            )
         if self.authentication_method == enums.AuthenticationMechanism.HLS_GMAC:
             only_auth_security_control = security.SecurityControlField(
                 security_suite=self.security_suite, authenticated=True, encrypted=False
@@ -629,6 +678,23 @@ def hls_response_valid(self, response_to_client_challenge: bytes) -> bool:
         invocation_counter = int.from_bytes(response_to_client_challenge[1:5], "big")
         gmac_result = response_to_client_challenge[-12:]
 
+        if not self.global_encryption_key:
+            raise ProtectionError(
+                "Unable to verify GMAC. Missing global_encryption_key"
+            )
+        if not self.global_authentication_key:
+            raise ProtectionError(
+                "Unable to verify GMAC. Missing global_authentication_key"
+            )
+        if not self.meter_system_title:
+            raise ProtectionError(
+                "Unable to verify GMAC. Have not received the meters system title."
+            )
+        if not self.client_to_meter_challenge:
+            raise ProtectionError(
+                "Unable to verify GMAC. Have not received the meters system title."
+            )
+
         correct_gmac = security.gmac(
             security_control=security_control,
             system_title=self.meter_system_title,
diff --git a/dlms_cosem/protocol/cosem/__init__.py b/dlms_cosem/cosem/__init__.py
similarity index 95%
rename from dlms_cosem/protocol/cosem/__init__.py
rename to dlms_cosem/cosem/__init__.py
index 3ad9436..1b59b05 100644
--- a/dlms_cosem/protocol/cosem/__init__.py
+++ b/dlms_cosem/cosem/__init__.py
@@ -2,8 +2,8 @@
 
 import attr
 
-from dlms_cosem.protocol.cosem.obis import Obis
-from dlms_cosem.protocol import enumerations
+from dlms_cosem import enumerations
+from dlms_cosem.cosem.obis import Obis
 
 
 @attr.s(auto_attribs=True)
@@ -37,6 +37,7 @@ def to_bytes(self) -> bytes:
             ]
         )
 
+
 @attr.s(auto_attribs=True)
 class CosemMethod:
 
diff --git a/dlms_cosem/cosem/association.py b/dlms_cosem/cosem/association.py
new file mode 100644
index 0000000..e07340d
--- /dev/null
+++ b/dlms_cosem/cosem/association.py
@@ -0,0 +1,41 @@
+from enum import IntEnum
+from typing import *
+
+import attr
+
+from dlms_cosem import cosem, enumerations
+
+
+class AccessRight(IntEnum):
+    READ_ACCESS = 0
+    WRITE_ACCESS = 1
+    AUTHENTICATED_REQUEST = 2
+    ENCRYPTED_REQUEST = 3
+    DIGITALLY_SIGNED_REQUEST = 4
+    AUTHENTICATED_RESPONSE = 5
+    ENCRYPTED_RESPONSE = 6
+    DIGITALLY_SIGNED_RESPONSE = 7
+
+
+@attr.s(auto_attribs=True)
+class AttributeAccessRights:
+    attribute: int
+    access_rights: List[AccessRight]
+    access_selectors: List[int] = attr.ib(
+        factory=list, converter=attr.converters.default_if_none(factory=list)
+    )
+
+
+@attr.s(auto_attribs=True)
+class MethodAccessRights:
+    method: int
+    access_rights: List[AccessRight]
+
+
+@attr.s(auto_attribs=True)
+class AssociationObjectListItem:
+    interface: enumerations.CosemInterface
+    logical_name: cosem.Obis
+    version: int
+    attribute_access_rights: Dict[int, AttributeAccessRights]
+    method_access_rights: Dict[int, MethodAccessRights]
diff --git a/dlms_cosem/protocol/cosem/obis.py b/dlms_cosem/cosem/obis.py
similarity index 99%
rename from dlms_cosem/protocol/cosem/obis.py
rename to dlms_cosem/cosem/obis.py
index 11d490f..e34ced2 100644
--- a/dlms_cosem/protocol/cosem/obis.py
+++ b/dlms_cosem/cosem/obis.py
@@ -1,6 +1,7 @@
-import attr
 from typing import *
 
+import attr
+
 
 def allowed_range_for_obis_code(instance, attribute, value: int):
 
diff --git a/dlms_cosem/cosem/profile_generic.py b/dlms_cosem/cosem/profile_generic.py
new file mode 100644
index 0000000..930af8c
--- /dev/null
+++ b/dlms_cosem/cosem/profile_generic.py
@@ -0,0 +1,106 @@
+from enum import IntEnum
+from typing import *
+
+import attr
+
+from dlms_cosem import cosem
+from dlms_cosem import enumerations as enums
+from dlms_cosem import parsers
+from dlms_cosem.protocol.xdlms.selective_access import CaptureObject, RangeDescriptor
+
+
+class SortMethod(IntEnum):
+    FIFO = 1
+    LIFO = 2
+    LARGEST = 3
+    SMALLEST = 4
+    NEAREST_TO_ZERO = 5
+    FARTHEST_FROM_ZERO = 6
+
+
+@attr.s(auto_attribs=True)
+class AttributeDescription:
+    attribute_id: int
+    attribute_name: str
+    data_parser: Optional[Any] = attr.ib(default=None)
+    data_converter: Optional[Any] = attr.ib(default=None)  # callable?
+
+
+@attr.s(auto_attribs=True)
+class Data:
+    INTERFACE_CLASS_ID: ClassVar[enums.CosemInterface] = enums.CosemInterface.DATA
+
+    logical_name: cosem.Obis
+    value: Any
+
+    STATIC_ATTRIBUTES: ClassVar[Dict[int, AttributeDescription]] = {
+        1: AttributeDescription(attribute_id=1, attribute_name="logical_name"),
+    }
+
+    DYNAMIC_ATTRIBUTES: ClassVar[Dict[int, AttributeDescription]] = {
+        2: AttributeDescription(attribute_id=2, attribute_name="value"),
+    }
+
+    SELECTIVE_ACCESS: ClassVar[Dict[int, Type[RangeDescriptor]]] = {}
+
+    METHODS: ClassVar[Dict[int, str]] = {1: "reset", 2: "capture"}
+
+    DYNAMIC_CONVERTERS: ClassVar[Dict[int, Callable]] = {}
+
+    def is_static_attribute(self, attribute_id: int) -> bool:
+        return attribute_id in self.STATIC_ATTRIBUTES.keys()
+
+
+def convert_load_profile(instance, data):
+    parser = parsers.ProfileGenericBufferParser(
+        capture_objects=[x.cosem_attribute for x in instance.capture_objects],
+        capture_period=instance.capture_period,
+    )
+    return parser.parse_entries(data)
+
+
+@attr.s(auto_attribs=True)
+class ProfileGeneric:
+    INTERFACE_CLASS_ID: ClassVar[
+        enums.CosemInterface
+    ] = enums.CosemInterface.PROFILE_GENERIC
+
+    logical_name: cosem.Obis
+    buffer = List[List[Any]]
+    capture_objects: List[CaptureObject]
+    capture_period: int
+    sort_method: Optional[SortMethod] = attr.ib(default=None)
+    sort_object: Optional[CaptureObject] = attr.ib(default=None)
+    entries_in_use: Optional[int] = attr.ib(default=None)
+    profile_entries: Optional[int] = attr.ib(default=None)
+
+    STATIC_ATTRIBUTES: ClassVar[Dict[int, AttributeDescription]] = {
+        1: AttributeDescription(attribute_id=1, attribute_name="logical_name"),
+        3: AttributeDescription(attribute_id=3, attribute_name="capture_objects"),
+        4: AttributeDescription(attribute_id=4, attribute_name="capture_period"),
+        5: AttributeDescription(attribute_id=5, attribute_name="sort_method"),
+        6: AttributeDescription(attribute_id=6, attribute_name="sort_object"),
+        8: AttributeDescription(attribute_id=8, attribute_name="profile_entries"),
+    }
+
+    DYNAMIC_ATTRIBUTES: ClassVar[Dict[int, AttributeDescription]] = {
+        2: AttributeDescription(attribute_id=2, attribute_name="buffer"),
+        7: AttributeDescription(attribute_id=7, attribute_name="entries_in_use"),
+    }
+
+    SELECTIVE_ACCESS: ClassVar[Dict[int, Type[RangeDescriptor]]] = {2: RangeDescriptor}
+
+    METHODS: ClassVar[Dict[int, str]] = {1: "reset", 2: "capture"}
+
+    # Todo: needs to take instance of the class
+    DYNAMIC_CONVERTERS: ClassVar[Dict[int, Callable]] = {2: convert_load_profile}
+
+    def reset(self, data: int = 0):
+        """clears the buffer"""
+        ...
+
+    def capture(self, data: int = 0):
+        """initiate a capture"""
+
+    def is_static_attribute(self, attribute_id: int) -> bool:
+        return attribute_id in self.STATIC_ATTRIBUTES.keys()
diff --git a/dlms_cosem/protocol/crc.py b/dlms_cosem/crc.py
similarity index 90%
rename from dlms_cosem/protocol/crc.py
rename to dlms_cosem/crc.py
index 8e9d500..814a50a 100644
--- a/dlms_cosem/protocol/crc.py
+++ b/dlms_cosem/crc.py
@@ -9,6 +9,7 @@
 from ctypes import c_ushort
 from typing import *
 
+
 class CRCCCITT:
     crc_ccitt_table: List[int] = list()
 
@@ -39,11 +40,11 @@ def calculate_for(self, input_data, lsb_first=False) -> bytes:
         lsb_rev = reversed_crc & 0x00FF
         lsb: int = ord(reverse_byte(lsb_rev))
         lsb ^= 0xFF
-        lsb_byte = lsb.to_bytes(1, 'big')
+        lsb_byte = lsb.to_bytes(1, "big")
         msb_rev = (reversed_crc & 0xFF00) >> 8
         msb = ord(reverse_byte(msb_rev))
         msb ^= 0xFF
-        msb_byte = msb.to_bytes(1, 'big')
+        msb_byte = msb.to_bytes(1, "big")
 
         if lsb_first:
             return b"".join([lsb_byte, msb_byte])
@@ -55,8 +56,8 @@ def _calculate(self, input_data: bytes):
         crc_value = self.starting_value
 
         for c in input_data:
-            tmp = ((crc_value >> 8) & 0xff) ^ c
-            crc_shifted = ((crc_value << 8) & 0xff00)
+            tmp = ((crc_value >> 8) & 0xFF) ^ c
+            crc_shifted = (crc_value << 8) & 0xFF00
             crc_value = crc_shifted ^ self.crc_ccitt_table[tmp]
 
         return crc_value
@@ -85,13 +86,11 @@ def reverse_byte(byte_to_reverse):
         reversed_byte += ((byte_to_reverse & and_value) >> i) * (2 ** (7 - i))
         and_value += and_value
 
-    return (chr(reversed_byte)).encode('latin-1')
+    return (chr(reversed_byte)).encode("latin-1")
 
 
 def reverse_byte_message(msg):
-    reversed_mgs = b''
+    reversed_mgs = b""
     for char in msg:
         reversed_mgs += reverse_byte(char)
     return reversed_mgs
-
-
diff --git a/dlms_cosem/protocol/dlms_data.py b/dlms_cosem/dlms_data.py
similarity index 92%
rename from dlms_cosem/protocol/dlms_data.py
rename to dlms_cosem/dlms_data.py
index f97202b..8cf38ed 100644
--- a/dlms_cosem/protocol/dlms_data.py
+++ b/dlms_cosem/dlms_data.py
@@ -1,11 +1,12 @@
-import datetime
 import abc
+import datetime
 from typing import *
+
 import attr
-from dlms_cosem.protocol import time
 
-VARIABLE_LENGTH = -1
+from dlms_cosem import time
 
+VARIABLE_LENGTH = -1
 
 
 class AbstractDlmsData(abc.ABC):
@@ -100,9 +101,7 @@ class DoubleLongData(BaseDlmsData):
 
     @classmethod
     def from_bytes(cls, bytes_data: bytes):
-        return cls(
-            value=int.from_bytes(bytes_data, "big", signed=True)
-        )
+        return cls(value=int.from_bytes(bytes_data, "big", signed=True))
 
 
 @attr.s(auto_attribs=True)
@@ -117,7 +116,7 @@ def from_bytes(cls, bytes_data: bytes):
         return cls(value=int.from_bytes(bytes_data, "big"))
 
     def value_to_bytes(self) -> bytes:
-        return self.value.to_bytes(4, 'big')
+        return self.value.to_bytes(4, "big")
 
 
 @attr.s(auto_attribs=True)
@@ -163,12 +162,10 @@ class IntegerData(BaseDlmsData):
 
     @classmethod
     def from_bytes(cls, bytes_data: bytes):
-        return cls(
-            value=int.from_bytes(bytes_data, "big", signed=True)
-        )
+        return cls(value=int.from_bytes(bytes_data, "big", signed=True))
 
     def value_to_bytes(self) -> bytes:
-        return self.value.to_bytes(1, 'big')
+        return self.value.to_bytes(1, "big")
 
 
 @attr.s(auto_attribs=True)
@@ -180,9 +177,7 @@ class LongData(BaseDlmsData):
 
     @classmethod
     def from_bytes(cls, bytes_data: bytes):
-        return cls(
-            value=int.from_bytes(bytes_data, "big", signed=True)
-        )
+        return cls(value=int.from_bytes(bytes_data, "big", signed=True))
 
 
 @attr.s(auto_attribs=True)
@@ -209,7 +204,7 @@ def from_bytes(cls, bytes_data: bytes):
         return cls(value=int.from_bytes(bytes_data, "big"))
 
     def value_to_bytes(self) -> bytes:
-        return self.value.to_bytes(2, 'big')
+        return self.value.to_bytes(2, "big")
 
 
 @attr.s(auto_attribs=True)
@@ -235,9 +230,7 @@ class Long64Data(BaseDlmsData):
 
     @classmethod
     def from_bytes(cls, bytes_data: bytes):
-        return cls(
-            value=int.from_bytes(bytes_data, "big", signed=True)
-        )
+        return cls(value=int.from_bytes(bytes_data, "big", signed=True))
 
 
 @attr.s(auto_attribs=True)
@@ -290,8 +283,7 @@ class Float64Data(BaseDlmsData):
 
 @attr.s(auto_attribs=True)
 class DateTimeData(BaseDlmsData):
-    """Octet string of 12 bytes
-    """
+    """Octet string of 12 bytes"""
 
     TAG = 25
     LENGTH = 12
@@ -303,7 +295,6 @@ def from_bytes(cls, bytes_data: bytes):
         return cls(time.datetime_from_bytes(bytes_data))
 
 
-
 @attr.s(auto_attribs=True)
 class DateData(BaseDlmsData):
     """Octet string of 5 bytes"""
diff --git a/dlms_cosem/protocol/enumerations.py b/dlms_cosem/enumerations.py
similarity index 99%
rename from dlms_cosem/protocol/enumerations.py
rename to dlms_cosem/enumerations.py
index 957bb37..1098f12 100644
--- a/dlms_cosem/protocol/enumerations.py
+++ b/dlms_cosem/enumerations.py
@@ -26,11 +26,13 @@ class GetRequestType(IntEnum):
     NEXT = 2
     WITH_LIST = 3
 
+
 class GetResponseType(IntEnum):
     NORMAL = 1
     WITH_BLOCK = 2
     WITH_LIST = 3
 
+
 class ActionType(IntEnum):
     NORMAL = 1
     NEXT_PBLOCK = 2
@@ -330,7 +332,8 @@ class AssociationResult(IntEnum):
     REJECTED_TRANSIENT = 2
     # TODO: What does transient rejection mean?
 
-class ActionResult(IntEnum):
+
+class ActionResultStatus(IntEnum):
     SUCCESS = 0
     HARDWARE_FAULT = 1
     TEMPORARY_FAILURE = 2
diff --git a/dlms_cosem/protocol/exceptions.py b/dlms_cosem/exceptions.py
similarity index 100%
rename from dlms_cosem/protocol/exceptions.py
rename to dlms_cosem/exceptions.py
diff --git a/dlms_cosem/protocol/hdlc/__init__.py b/dlms_cosem/hdlc/__init__.py
similarity index 100%
rename from dlms_cosem/protocol/hdlc/__init__.py
rename to dlms_cosem/hdlc/__init__.py
diff --git a/dlms_cosem/protocol/hdlc/address.py b/dlms_cosem/hdlc/address.py
similarity index 96%
rename from dlms_cosem/protocol/hdlc/address.py
rename to dlms_cosem/hdlc/address.py
index ba1b5fe..131e36d 100644
--- a/dlms_cosem/protocol/hdlc/address.py
+++ b/dlms_cosem/hdlc/address.py
@@ -2,7 +2,7 @@
 
 import attr
 
-from dlms_cosem.protocol.hdlc import validators
+from dlms_cosem.hdlc import validators
 
 
 @attr.s(auto_attribs=True)
@@ -91,9 +91,11 @@ def destination_from_bytes(cls, frame_bytes: bytes, address_type: str):
         destination_address_data, _ = HdlcAddress.find_address_in_frame_bytes(
             frame_bytes
         )
-        destination_logical, destination_physical, destination_length = (
-            destination_address_data
-        )
+        (
+            destination_logical,
+            destination_physical,
+            destination_length,
+        ) = destination_address_data
 
         return cls(destination_logical, destination_physical, address_type)
 
@@ -106,7 +108,7 @@ def source_from_bytes(cls, frame_bytes: bytes, address_type: str):
 
     @staticmethod
     def find_address_in_frame_bytes(
-        hdlc_frame_bytes: bytes
+        hdlc_frame_bytes: bytes,
     ) -> Tuple[Tuple[int, Optional[int], int], Tuple[int, Optional[int], int]]:
         """
         address can be 1, 2 or 4 bytes long. the end byte is indicated by the of
diff --git a/dlms_cosem/protocol/hdlc/connection.py b/dlms_cosem/hdlc/connection.py
similarity index 96%
rename from dlms_cosem/protocol/hdlc/connection.py
rename to dlms_cosem/hdlc/connection.py
index cfd654d..215eb14 100644
--- a/dlms_cosem/protocol/hdlc/connection.py
+++ b/dlms_cosem/hdlc/connection.py
@@ -2,16 +2,15 @@
 
 import attr
 
-from dlms_cosem.protocol.hdlc import address, exceptions, frames
-from dlms_cosem.protocol.hdlc.state import (
+from dlms_cosem.hdlc import address, exceptions, frames
+from dlms_cosem.hdlc.exceptions import LocalProtocolError
+from dlms_cosem.hdlc.state import (
     AWAITING_CONNECTION,
     AWAITING_DISCONNECT,
     AWAITING_RESPONSE,
     NEED_DATA,
     HdlcConnectionState,
 )
-from dlms_cosem.protocol.hdlc.exceptions import LocalProtocolError
-
 
 LOG = logging.getLogger(__name__)
 
@@ -29,12 +28,12 @@ def read_ua_frame(frame_data: bytes):
     def read_information_frame(frame_data: bytes):
         try:
             return frames.InformationFrame.from_bytes(frame_data)
-        except exceptions.HdlcParsingError as e:
-            #LOG.debug(
+        except exceptions.HdlcParsingError:
+            # LOG.debug(
             #    f"Unable to load and information frame from frame_data: {frame_data!r}."
             #    f"Information carried in frame might have contained the HDLC flag and "
             #    f"we have not recieved the full frame yet."
-            #)
+            # )
             return None
 
 
diff --git a/dlms_cosem/protocol/hdlc/exceptions.py b/dlms_cosem/hdlc/exceptions.py
similarity index 86%
rename from dlms_cosem/protocol/hdlc/exceptions.py
rename to dlms_cosem/hdlc/exceptions.py
index 83ab453..83c1f65 100644
--- a/dlms_cosem/protocol/hdlc/exceptions.py
+++ b/dlms_cosem/hdlc/exceptions.py
@@ -1,4 +1,3 @@
-
 class LocalProtocolError(Exception):
     """Error in HDLC Protocol"""
 
@@ -12,4 +11,4 @@ class HdlcParsingError(HdlcException):
 
 
 class MissingHdlcFlags(HdlcParsingError):
-    """Frame is not enclosed byt HDLC flags"""
\ No newline at end of file
+    """Frame is not enclosed byt HDLC flags"""
diff --git a/dlms_cosem/protocol/hdlc/fields.py b/dlms_cosem/hdlc/fields.py
similarity index 98%
rename from dlms_cosem/protocol/hdlc/fields.py
rename to dlms_cosem/hdlc/fields.py
index da2c777..b5ce576 100644
--- a/dlms_cosem/protocol/hdlc/fields.py
+++ b/dlms_cosem/hdlc/fields.py
@@ -2,7 +2,8 @@
 
 import attr
 
-from dlms_cosem.protocol.hdlc import exceptions as hdlc_exceptions, validators
+from dlms_cosem.hdlc import exceptions as hdlc_exceptions
+from dlms_cosem.hdlc import validators
 
 
 class _AbstractHdlcControlField(abc.ABC):
@@ -32,6 +33,7 @@ class SnrmControlField(_AbstractHdlcControlField):
     """
     S-frame fo SNRM request.
     """
+
     def is_final(self):
         """ 'Almost' all the time a SNRM frame is contaned in single frame."""
         # TODO: Handle multi frame
@@ -97,7 +99,8 @@ class ReceiveReadyControlField(_AbstractHdlcControlField):
     """
 
     receive_sequence_number: int = attr.ib(
-        validator=[validators.validate_information_sequence_number])
+        validator=[validators.validate_information_sequence_number]
+    )
 
     def is_final(self):
         """
@@ -158,6 +161,7 @@ class InformationControlField(_AbstractHdlcControlField):
 
 
     """
+
     send_sequence_number: int = attr.ib(
         validator=[validators.validate_information_sequence_number]
     )
diff --git a/dlms_cosem/protocol/hdlc/frames.py b/dlms_cosem/hdlc/frames.py
similarity index 98%
rename from dlms_cosem/protocol/hdlc/frames.py
rename to dlms_cosem/hdlc/frames.py
index 9d75292..5c08e59 100644
--- a/dlms_cosem/protocol/hdlc/frames.py
+++ b/dlms_cosem/hdlc/frames.py
@@ -3,10 +3,11 @@
 
 import attr
 
-from dlms_cosem.protocol.crc import CRCCCITT
-from dlms_cosem.protocol.hdlc import exceptions as hdlc_exceptions, address, validators
-from dlms_cosem.protocol.hdlc.address import HdlcAddress
-from dlms_cosem.protocol.hdlc import fields
+from dlms_cosem.crc import CRCCCITT
+from dlms_cosem.hdlc import address
+from dlms_cosem.hdlc import exceptions as hdlc_exceptions
+from dlms_cosem.hdlc import fields, validators
+from dlms_cosem.hdlc.address import HdlcAddress
 
 HDLC_FLAG = b"\x7e"
 
@@ -143,7 +144,7 @@ def get_control_field(self):
 
     @staticmethod
     def extract_format_field_from_bytes(
-        frame_bytes: bytes
+        frame_bytes: bytes,
     ) -> fields.DlmsHdlcFrameFormatField:
         """
         The format field is always on the same place in all frames.
@@ -315,6 +316,7 @@ class InformationFrame(BaseHdlcFrame):
     receive_sequence_number: int = attr.ib(
         validator=[validators.validate_information_sequence_number], default=0
     )
+
     @property
     def information(self) -> bytes:
         """
diff --git a/dlms_cosem/protocol/hdlc/state.py b/dlms_cosem/hdlc/state.py
similarity index 91%
rename from dlms_cosem/protocol/hdlc/state.py
rename to dlms_cosem/hdlc/state.py
index 5213232..74f4a57 100644
--- a/dlms_cosem/protocol/hdlc/state.py
+++ b/dlms_cosem/hdlc/state.py
@@ -2,9 +2,8 @@
 
 import attr
 
-from dlms_cosem.protocol.hdlc import frames
-from dlms_cosem.protocol.hdlc.exceptions import LocalProtocolError
-
+from dlms_cosem.hdlc import frames
+from dlms_cosem.hdlc.exceptions import LocalProtocolError
 
 LOG = logging.getLogger(__name__)
 
@@ -21,7 +20,8 @@ class _SentinelBase(type):
      next_event() and do some sort of dispatch based on type(event).
 
      Taken from h11.
-     """
+    """
+
     def __repr__(self):
         return self.__name__
 
@@ -58,12 +58,9 @@ def make_sentinel(name):
     IDLE: {
         frames.InformationFrame: AWAITING_RESPONSE,
         frames.DisconnectFrame: AWAITING_DISCONNECT,
-        frames.ReceiveReadyFrame: AWAITING_RESPONSE
-    },
-    AWAITING_RESPONSE: {
-        frames.InformationFrame: IDLE,
-        frames.ReceiveReadyFrame: IDLE,
+        frames.ReceiveReadyFrame: AWAITING_RESPONSE,
     },
+    AWAITING_RESPONSE: {frames.InformationFrame: IDLE, frames.ReceiveReadyFrame: IDLE},
     AWAITING_DISCONNECT: {frames.UnNumberedAcknowledgmentFrame: NOT_CONNECTED},
 }
 
diff --git a/dlms_cosem/protocol/hdlc/validators.py b/dlms_cosem/hdlc/validators.py
similarity index 99%
rename from dlms_cosem/protocol/hdlc/validators.py
rename to dlms_cosem/hdlc/validators.py
index 8c4a93e..837f638 100644
--- a/dlms_cosem/protocol/hdlc/validators.py
+++ b/dlms_cosem/hdlc/validators.py
@@ -1,14 +1,8 @@
-
-
-
-
 def validate_information_sequence_number(instance, attribute, value):
     if not 0 <= value <= 7:
         raise ValueError(f"Sequence number can only be between 0-7. Got {value}")
 
 
-
-
 def validate_hdlc_address_type(instance, attribute, value):
     if value not in ["client", "server"]:
         raise ValueError("HdlcAddress type can only be client or server.")
diff --git a/dlms_cosem/parsers.py b/dlms_cosem/parsers.py
new file mode 100644
index 0000000..ee9c8dd
--- /dev/null
+++ b/dlms_cosem/parsers.py
@@ -0,0 +1,200 @@
+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)
+        if bool(access_right & 0b00000010):
+            parsed_access_rights.append(AccessRight.WRITE_ACCESS)
+        if bool(access_right & 0b00000100):
+            parsed_access_rights.append(AccessRight.AUTHENTICATED_REQUEST)
+        if bool(access_right & 0b00001000):
+            parsed_access_rights.append(AccessRight.ENCRYPTED_REQUEST)
+        if bool(access_right & 0b00010000):
+            parsed_access_rights.append(AccessRight.DIGITALLY_SIGNED_REQUEST)
+        if bool(access_right & 0b00100000):
+            parsed_access_rights.append(AccessRight.AUTHENTICATED_RESPONSE)
+        if bool(access_right & 0b01000000):
+            parsed_access_rights.append(AccessRight.ENCRYPTED_RESPONSE)
+        if bool(access_right & 0b10000000):
+            parsed_access_rights.append(AccessRight.DIGITALLY_SIGNED_RESPONSE)
+
+        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])
+            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
diff --git a/dlms_cosem/protocol/acse/__init__.py b/dlms_cosem/protocol/acse/__init__.py
index ca7514d..8fa2ed5 100644
--- a/dlms_cosem/protocol/acse/__init__.py
+++ b/dlms_cosem/protocol/acse/__init__.py
@@ -1,8 +1,8 @@
-from dlms_cosem.protocol.acse.aarq import ApplicationAssociationRequestApdu
 from dlms_cosem.protocol.acse.aare import ApplicationAssociationResponseApdu
-from dlms_cosem.protocol.acse.rlrq import ReleaseRequestApdu
-from dlms_cosem.protocol.acse.rlre import ReleaseResponseApdu
+from dlms_cosem.protocol.acse.aarq import ApplicationAssociationRequestApdu
 from dlms_cosem.protocol.acse.base import *
+from dlms_cosem.protocol.acse.rlre import ReleaseResponseApdu
+from dlms_cosem.protocol.acse.rlrq import ReleaseRequestApdu
 from dlms_cosem.protocol.acse.user_information import UserInformation
 
 __all__ = [
diff --git a/dlms_cosem/protocol/acse/aare.py b/dlms_cosem/protocol/acse/aare.py
index 6a8ccdc..0afa2c7 100644
--- a/dlms_cosem/protocol/acse/aare.py
+++ b/dlms_cosem/protocol/acse/aare.py
@@ -3,11 +3,11 @@
 import attr
 from asn1crypto.core import Choice, Integer
 
+from dlms_cosem import enumerations
+from dlms_cosem.ber import BER
 from dlms_cosem.protocol.acse import base as acse_base
-from dlms_cosem.protocol.acse.user_information import UserInformation
-from dlms_cosem.protocol import enumerations
 from dlms_cosem.protocol.acse.aarq import aarq_should_set_authenticated
-from dlms_cosem.protocol.ber import BER
+from dlms_cosem.protocol.acse.user_information import UserInformation
 
 
 @attr.s(auto_attribs=True)
@@ -120,10 +120,7 @@ class ApplicationAssociationResponseApdu(acse_base.AbstractAcseApdu):
         0x89: ("mechanism_name", acse_base.MechanismName),
         170: ("responding_authentication_value", acse_base.AuthenticationValue),
         189: ("implementation_information", None),
-        0xBE: (
-            "user_information",
-            UserInformation,
-        ),  # Context specific, constructed 30
+        0xBE: ("user_information", UserInformation),  # Context specific, constructed 30
     }
 
     result: enumerations.AssociationResult
@@ -295,8 +292,6 @@ def from_bytes(cls, source_bytes: bytes):
         else:
             object_dict["public_cert"] = None
 
-
-
         auth_value: Optional[acse_base.AuthenticationValue] = object_dict.pop(
             "responding_authentication_value", None
         )
diff --git a/dlms_cosem/protocol/acse/aarq.py b/dlms_cosem/protocol/acse/aarq.py
index c58deac..1c09929 100644
--- a/dlms_cosem/protocol/acse/aarq.py
+++ b/dlms_cosem/protocol/acse/aarq.py
@@ -2,17 +2,19 @@
 
 import attr
 
-from dlms_cosem.protocol.ber import BER
-
+from dlms_cosem import enumerations
+from dlms_cosem.ber import BER
+from dlms_cosem.protocol import xdlms
 from dlms_cosem.protocol.acse import base as acse_base
 from dlms_cosem.protocol.acse.user_information import UserInformation
-from dlms_cosem.protocol import xdlms, enumerations
 
 
 def user_information_holds_initiate_request(
     instance, attribute, value: UserInformation
 ):
-    if not isinstance(value.content, (xdlms.InitiateRequestApdu, xdlms.GlobalCipherInitiateRequest)):
+    if not isinstance(
+        value.content, (xdlms.InitiateRequestApdu, xdlms.GlobalCipherInitiateRequest)
+    ):
         raise ValueError(
             f"ApplicationAssociationRequestApdu.user_information should "
             f"only hold a UserInformation where .content is a "
@@ -21,7 +23,7 @@ def user_information_holds_initiate_request(
 
 
 def aarq_should_set_authenticated(
-    mechanism: Optional[enumerations.AuthenticationMechanism]
+    mechanism: Optional[enumerations.AuthenticationMechanism],
 ):
     """
     * If Lowest Level Scurity (None) is used it shall not be present.
@@ -136,10 +138,7 @@ class ApplicationAssociationRequestApdu:
         0x8B: ("mechanism_name", acse_base.MechanismName),
         0xAC: ("calling_authentication_value", acse_base.AuthenticationValue),
         0xBD: ("implementation_information", None),
-        0xBE: (
-            "user_information",
-            UserInformation,
-        ),  # Context specific, constructed 30
+        0xBE: ("user_information", UserInformation),  # Context specific, constructed 30
     }
 
     user_information: UserInformation = attr.ib(
@@ -285,9 +284,7 @@ def from_bytes(cls, aarq_bytes):
         else:
             object_dict["system_title"] = None
 
-        client_public_cert = object_dict.pop(
-            "calling_ae_qualifier", None
-        )
+        client_public_cert = object_dict.pop("calling_ae_qualifier", None)
         if client_public_cert:
             # it is ber encoded universal tag ocetctring. simple handling
             object_dict["public_cert"] = client_public_cert[2:]
diff --git a/dlms_cosem/protocol/acse/base.py b/dlms_cosem/protocol/acse/base.py
index fd3913b..4313ca4 100644
--- a/dlms_cosem/protocol/acse/base.py
+++ b/dlms_cosem/protocol/acse/base.py
@@ -3,8 +3,8 @@
 
 import attr
 
-from dlms_cosem.protocol import enumerations, xdlms
-from dlms_cosem.protocol.ber import BER
+from dlms_cosem import enumerations
+from dlms_cosem.ber import BER
 
 
 class AbstractAcseApdu(abc.ABC):
@@ -186,9 +186,8 @@ def from_bytes(cls, _bytes):
                 f"Authentication Functional Unit data should by 2 "
                 f"bytes. Got: {_bytes}"
             )
-        last_byte = _bytes[-1]
-        # should I check anything?
-        return cls(authentication=True)
+        last_byte = bool(_bytes[-1])
+        return cls(authentication=last_byte)
 
     def to_bytes(self):
         if self.authentication:
@@ -197,5 +196,3 @@ def to_bytes(self):
             # when not using authentication this the sender-acse-requirements
             # should not be in the data.
             return None
-
-
diff --git a/dlms_cosem/protocol/acse/rlre.py b/dlms_cosem/protocol/acse/rlre.py
index 346363b..ad5d769 100644
--- a/dlms_cosem/protocol/acse/rlre.py
+++ b/dlms_cosem/protocol/acse/rlre.py
@@ -1,13 +1,12 @@
-from enum import Enum, IntEnum, unique
 from functools import partial
 from typing import *
 
 import attr
 
-from dlms_cosem.protocol import enumerations
+from dlms_cosem import enumerations
+from dlms_cosem.ber import BER
 from dlms_cosem.protocol.acse.base import AbstractAcseApdu
 from dlms_cosem.protocol.acse.user_information import UserInformation
-from dlms_cosem.protocol.ber import BER
 
 # TODO: It might be a better approach to give the callable and not the class to make a
 #   object from bytes. This means we could jack into the creation if needed
diff --git a/dlms_cosem/protocol/acse/rlrq.py b/dlms_cosem/protocol/acse/rlrq.py
index 6817327..aae635f 100644
--- a/dlms_cosem/protocol/acse/rlrq.py
+++ b/dlms_cosem/protocol/acse/rlrq.py
@@ -3,10 +3,10 @@
 
 import attr
 
-from dlms_cosem.protocol import enumerations
+from dlms_cosem import enumerations
+from dlms_cosem.ber import BER
 from dlms_cosem.protocol.acse.base import AbstractAcseApdu
 from dlms_cosem.protocol.acse.user_information import UserInformation
-from dlms_cosem.protocol.ber import BER
 
 # TODO: It might be a better approach to give the callable and not the class to make a
 #   object from bytes. This means we could jack into the creation if needed
diff --git a/dlms_cosem/protocol/acse/user_information.py b/dlms_cosem/protocol/acse/user_information.py
index 1340fa7..8c0b25d 100644
--- a/dlms_cosem/protocol/acse/user_information.py
+++ b/dlms_cosem/protocol/acse/user_information.py
@@ -1,6 +1,9 @@
-import attr
 from typing import *
-from dlms_cosem.protocol import ber
+
+import attr
+
+from dlms_cosem import ber
+
 
 @attr.s(auto_attribs=True)
 class UserInformation:
@@ -19,16 +22,17 @@ class UserInformation:
     tag = b"\x04"  # is encoded as an octetstring
 
     content: Any
-    #Union[
-        #xdlms.InitiateRequestApdu,
-        #xdlms.InitiateResponseApdu,
-        #xdlms.ConfirmedServiceErrorApdu,
-        #xdlms.GlobalCipherInitiateRequest
-    #]
+    # Union[
+    # xdlms.InitiateRequestApdu,
+    # xdlms.InitiateResponseApdu,
+    # xdlms.ConfirmedServiceErrorApdu,
+    # xdlms.GlobalCipherInitiateRequest
+    # ]
 
     @classmethod
     def from_bytes(cls, _bytes):
         from dlms_cosem.protocol import xdlms
+
         tag, length, data = ber.BER.decode(_bytes)
         if tag != UserInformation.tag:
             raise ValueError(
diff --git a/dlms_cosem/protocol/parsers.py b/dlms_cosem/protocol/parsers.py
deleted file mode 100644
index f54e83b..0000000
--- a/dlms_cosem/protocol/parsers.py
+++ /dev/null
@@ -1,87 +0,0 @@
-import attr
-from datetime import timedelta
-from typing import *
-
-from dlms_cosem.protocol import a_xdr, enumerations
-from dlms_cosem.protocol.cosem import CosemAttribute
-from dlms_cosem.protocol.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
diff --git a/dlms_cosem/protocol/wrappers.py b/dlms_cosem/protocol/wrappers.py
index c205a7a..898bd05 100644
--- a/dlms_cosem/protocol/wrappers.py
+++ b/dlms_cosem/protocol/wrappers.py
@@ -1,3 +1,7 @@
+import attr
+
+
+@attr.s(auto_attribs=True)
 class WrapperHeader:
     """
     The wrapper header contains 4 parts. Each is an unsigned 16 bit integer.
@@ -39,53 +43,53 @@ class WrapperHeader:
 
     """
 
-    def __init__(self, source_wport: int, destination_wport: int, length: int,
-                 version: int = 1):
-        self.source_wport = source_wport
-        self.destination_wport = destination_wport
-        self.length = length
-        self.version = version
+    source_wport: int
+    destination_wport: int
+    length: int
+    version: int = attr.ib(default=1)
 
     def to_bytes(self):
-        _version = self.version.to_bytes(2, 'big')
-        _source_wport = self.source_wport.to_bytes(2, 'big')
-        _destination_wport = self.destination_wport.to_bytes(2, 'big')
-        _length = self.length.to_bytes(2, 'big')
+        _version = self.version.to_bytes(2, "big")
+        _source_wport = self.source_wport.to_bytes(2, "big")
+        _destination_wport = self.destination_wport.to_bytes(2, "big")
+        _length = self.length.to_bytes(2, "big")
 
         return _version + _source_wport + _destination_wport + _length
 
     @classmethod
     def from_bytes(cls, in_data):
         if len(in_data) != 8:
-            raise ValueError(f'Wrapper Header can only consists of 8 bytes and '
-                             f'got {len(in_data)}')
-        version = int.from_bytes(in_data[0:2], 'big')
-        source_wport = int.from_bytes(in_data[2:4], 'big')
-        destination_wport = int.from_bytes(in_data[4:6], 'big')
-        length = int.from_bytes(in_data[6:8], 'big')
+            raise ValueError(
+                f"Wrapper Header can only consists of 8 bytes and "
+                f"got {len(in_data)}"
+            )
+        version = int.from_bytes(in_data[0:2], "big")
+        source_wport = int.from_bytes(in_data[2:4], "big")
+        destination_wport = int.from_bytes(in_data[4:6], "big")
+        length = int.from_bytes(in_data[6:8], "big")
 
         return cls(source_wport, destination_wport, length, version)
 
 
+@attr.s(auto_attribs=True)
 class WrapperProtocolDataUnit:
     """
 
-       When sending DLMS data over UDP or TCP you need to include an additional
-       wrapper to:
+    When sending DLMS data over UDP or TCP you need to include an additional
+    wrapper to:
 
-       * Provide additional addressing functionality on top of UDP/TCP port. (Since
-         a physical device can host several logical devices)
-       * Describe the length of the data sent. Especially for TCP where the data
-         can be split up in several packets.
+    * Provide additional addressing functionality on top of UDP/TCP port. (Since
+      a physical device can host several logical devices)
+    * Describe the length of the data sent. Especially for TCP where the data
+      can be split up in several packets.
 
-       :param data: The bytes of the xDLMS APDU transported.
-       :param WrapperHeader wrapper_header: Wrapper header to declare additional
-        information on how to handle the data sent.
-       """
+    :param data: The bytes of the xDLMS APDU transported.
+    :param WrapperHeader wrapper_header: Wrapper header to declare additional
+     information on how to handle the data sent.
+    """
 
-    def __init__(self, data: bytes, wrapper_header: WrapperHeader):
-        self.wrapper_header = wrapper_header
-        self.data = data
+    data: bytes
+    wrapper_header: WrapperHeader
 
     def to_bytes(self):
         return self.wrapper_header.to_bytes() + self.data
@@ -100,10 +104,13 @@ def from_bytes(cls, in_data):
         data_length = len(data)
         if not wrapper_header.length == data_length:
             raise ValueError(
-                (f'Length of data in Wrapper Protocol Data Unit class '
-                 f'{cls.__class__.__name__}, ({data_length}) does not match '
-                 f'the length parameter in the Wrapper Header '
-                 f'({wrapper_header.length})'))
+                (
+                    f"Length of data in Wrapper Protocol Data Unit class "
+                    f"{cls.__class__.__name__}, ({data_length}) does not match "
+                    f"the length parameter in the Wrapper Header "
+                    f"({wrapper_header.length})"
+                )
+            )
 
         return cls(data, wrapper_header)
 
@@ -112,4 +119,5 @@ class DlmsUdpMessage(WrapperProtocolDataUnit):
     """
     Handle UPD messages with DLMS APDU content
     """
+
     pass
diff --git a/dlms_cosem/protocol/xdlms/__init__.py b/dlms_cosem/protocol/xdlms/__init__.py
index 8ab29a4..6242c71 100644
--- a/dlms_cosem/protocol/xdlms/__init__.py
+++ b/dlms_cosem/protocol/xdlms/__init__.py
@@ -1,28 +1,36 @@
-from dlms_cosem.protocol.xdlms.initiate_request import (
-    InitiateRequestApdu,
-    GlobalCipherInitiateRequest,
+from dlms_cosem.protocol.xdlms.action import (
+    ActionRequestFactory,
+    ActionRequestNormal,
+    ActionResponseFactory,
+    ActionResponseNormal,
+    ActionResponseNormalWithData,
+    ActionResponseNormalWithError,
 )
+from dlms_cosem.protocol.xdlms.confirmed_service_error import ConfirmedServiceErrorApdu
+from dlms_cosem.protocol.xdlms.conformance import Conformance
 from dlms_cosem.protocol.xdlms.data_notification import DataNotificationApdu
+from dlms_cosem.protocol.xdlms.exception_response import ExceptionResponseApdu
 from dlms_cosem.protocol.xdlms.general_global_cipher import GeneralGlobalCipherApdu
-from dlms_cosem.protocol.xdlms.initiate_response import (
-    InitiateResponseApdu,
-    GlobalCipherInitiateResponse,
-)
-from dlms_cosem.protocol.xdlms.confirmed_service_error import ConfirmedServiceErrorApdu
 from dlms_cosem.protocol.xdlms.get import (
-    GetRequestNormal,
-    GetResponseNormal,
     GetRequestFactory,
-    GetResponseFactory,
     GetRequestNext,
-    GetResponseWithBlock,
+    GetRequestNormal,
+    GetResponseFactory,
     GetResponseLastBlock,
     GetResponseLastBlockWithError,
+    GetResponseNormal,
     GetResponseNormalWithError,
+    GetResponseWithBlock,
 )
-from dlms_cosem.protocol.xdlms.conformance import Conformance
-from dlms_cosem.protocol.xdlms.exception_response import ExceptionResponseApdu
-from dlms_cosem.protocol.xdlms.action import ActionResponse, ActionRequest
+from dlms_cosem.protocol.xdlms.initiate_request import (
+    GlobalCipherInitiateRequest,
+    InitiateRequestApdu,
+)
+from dlms_cosem.protocol.xdlms.initiate_response import (
+    GlobalCipherInitiateResponse,
+    InitiateResponseApdu,
+)
+from dlms_cosem.protocol.xdlms.invoke_id_and_priority import InvokeIdAndPriority
 
 __all__ = [
     "InitiateRequestApdu",
@@ -39,10 +47,15 @@
     "GetResponseLastBlock",
     "GetResponseLastBlockWithError",
     "GetRequestFactory",
-    "GetResponseFactory"
+    "GetResponseFactory",
     "ExceptionResponseApdu",
     "GlobalCipherInitiateRequest",
     "GlobalCipherInitiateResponse",
-    "ActionResponse",
-    "ActionRequest",
+    "ActionResponseNormal",
+    "ActionResponseNormalWithData",
+    "ActionResponseNormalWithError",
+    "ActionResponseFactory",
+    "ActionRequestNormal",
+    "ActionRequestFactory",
+    "InvokeIdAndPriority",
 ]
diff --git a/dlms_cosem/protocol/xdlms/action.py b/dlms_cosem/protocol/xdlms/action.py
index 9b2bae0..8636c87 100644
--- a/dlms_cosem/protocol/xdlms/action.py
+++ b/dlms_cosem/protocol/xdlms/action.py
@@ -1,92 +1,173 @@
-import attr
 from typing import *
 
-from dlms_cosem.protocol.xdlms.get import get_data_access_result_from_bytes
-from dlms_cosem.protocol.xdlms.invoke_id_and_priority import InvokeIdAndPriority
-from dlms_cosem.protocol import cosem, enumerations, a_xdr
+import attr
+
+from dlms_cosem import cosem, enumerations
 from dlms_cosem.protocol.xdlms.base import AbstractXDlmsApdu
+from dlms_cosem.protocol.xdlms.invoke_id_and_priority import InvokeIdAndPriority
+
+# TODO:  Use same kind of setup as with GET.
+# Several classes depending on the type of Action Request/Response
+# ActionRequestNormal, ActionResponseNormal, ActionResponseNormalWithError,
 
 
 @attr.s(auto_attribs=True)
-class ActionRequest(AbstractXDlmsApdu):
+class ActionRequestNormal(AbstractXDlmsApdu):
     TAG: ClassVar[int] = 195
-    ENCODING_CONF: ClassVar[a_xdr.EncodingConf] = a_xdr.EncodingConf(
-        attributes=[
-            a_xdr.Attribute(
-                attribute_name="invoke_id_and_priority",
-                create_instance=InvokeIdAndPriority.from_bytes,
-            ),
-            a_xdr.Attribute(
-                attribute_name="cosem_method",
-                create_instance=cosem.CosemMethod.from_bytes,
-            ),
-            a_xdr.Attribute(attribute_name="parameters", create_instance=bytes),
-        ]
+    ACTION_TYPE: ClassVar[enumerations.ActionType] = enumerations.ActionType.NORMAL
+
+    cosem_method: cosem.CosemMethod = attr.ib(
+        validator=attr.validators.instance_of(cosem.CosemMethod)
     )
-    cosem_method: cosem.CosemMethod
-    parameters: Optional[bytes]
+    data: Optional[bytes] = attr.ib(default=None)
     invoke_id_and_priority: InvokeIdAndPriority = attr.ib(
-        default=InvokeIdAndPriority(0, True, True)
-    )
-    action_type: enumerations.ActionType = attr.ib(
-        default=enumerations.ActionType.NORMAL
+        default=InvokeIdAndPriority(0, True, True),
+        validator=attr.validators.instance_of(InvokeIdAndPriority),
     )
 
     def to_bytes(self):
         out = bytearray()
         out.append(self.TAG)
-        out.append(self.action_type.value)
+        out.append(self.ACTION_TYPE.value)
         out.extend(self.invoke_id_and_priority.to_bytes())
         out.extend(self.cosem_method.to_bytes())
-        if self.parameters:
+        if self.data:
             out.append(0x01)
-            out.extend(self.parameters)
+            out.extend(self.data)
         else:
             out.append(0x00)
         return bytes(out)
 
     @classmethod
-    def from_bytes(cls):
-        pass
+    def from_bytes(cls, source_bytes: bytes):
+        data = bytearray(source_bytes)
+        tag = data.pop(0)
+        if tag != cls.TAG:
+            raise ValueError(
+                f"Tag {tag} is not the correct tag for an ActionRequest, should "
+                f"be {cls.TAG}"
+            )
+        request_type = enumerations.ActionType(data.pop(0))
+
+        if request_type != enumerations.ActionType.NORMAL:
+            raise ValueError(
+                f"Bytes are not representing a ActionRequestNormal. Action type "
+                f"is {request_type}"
+            )
+        invoke_id_and_priority = InvokeIdAndPriority.from_bytes(
+            data.pop(0).to_bytes(1, "big")
+        )
+        cosem_method = cosem.CosemMethod.from_bytes(data[:9])
+        has_data = bool(data[9])
+        if has_data:
+            request_data = data[10:]
+        else:
+            request_data = None
 
+        return cls(
+            cosem_method=cosem_method,
+            data=request_data,
+            invoke_id_and_priority=invoke_id_and_priority,
+        )
 
-import attr
-from typing import *
 
-from dlms_cosem.protocol.xdlms.invoke_id_and_priority import InvokeIdAndPriority
-from dlms_cosem.protocol.dlms_data import BaseDlmsData
-from dlms_cosem.protocol import cosem, enumerations, a_xdr
+@attr.s(auto_attribs=True)
+class ActionRequestFactory:
+    """
+    Factory that will parse the ActionRequest and return the correct class for the
+    particular instance
+    """
+
+    TAG: ClassVar[int] = 195
+
+    @staticmethod
+    def from_bytes(source_bytes: bytes):
+        data = bytearray(source_bytes)
+        tag = data.pop(0)
+        if tag != ActionRequestFactory.TAG:
+            raise ValueError(
+                f"Tag for GET request is not correct. Got {tag}, should be "
+                f"{ActionRequestFactory.TAG}"
+            )
+        request_type = enumerations.ActionType(data.pop(0))
+        if request_type == enumerations.ActionType.NORMAL:
+            return ActionRequestNormal.from_bytes(source_bytes)
+        else:
+            raise NotImplementedError(
+                f"no class to support action request type {request_type}"
+            )
 
 
 @attr.s(auto_attribs=True)
-class ActionResponse(AbstractXDlmsApdu):
+class ActionResponseNormal(AbstractXDlmsApdu):
     TAG: ClassVar[int] = 199
-    ENCODING_CONF: ClassVar[a_xdr.EncodingConf] = a_xdr.EncodingConf(
-        attributes=[
-            a_xdr.Attribute(
-                attribute_name="invoke_id_and_priority",
-                create_instance=InvokeIdAndPriority.from_bytes,
-            ),
-            a_xdr.Attribute(attribute_name="parameters", create_instance=bytes),
-        ]
-    )
-    result: enumerations.ActionResult
-    result_data: Optional[Any] = attr.ib(default=None)
+    ACTION_TYPE: ClassVar[enumerations.ActionType] = enumerations.ActionType.NORMAL
+
+    status: enumerations.ActionResultStatus
     invoke_id_and_priority: InvokeIdAndPriority = attr.ib(
         default=InvokeIdAndPriority(0, True, True)
     )
-    action_type: enumerations.ActionType = attr.ib(
-        default=enumerations.ActionType.NORMAL
+
+    def to_bytes(self):
+        out = bytearray()
+        out.append(self.TAG)
+        out.append(self.ACTION_TYPE.value)
+        out.extend(self.invoke_id_and_priority.to_bytes())
+        out.append(self.status.value)
+        out.extend(b"\x00")
+
+        return bytes(out)
+
+    @classmethod
+    def from_bytes(cls, source_bytes: bytes):
+        data = bytearray(source_bytes)
+        tag = data.pop(0)
+        if tag != cls.TAG:
+            raise ValueError(
+                f"Tag {tag} is not correct for ActionResponse. Should be {cls.TAG}"
+            )
+        action_type = enumerations.ActionType(data.pop(0))
+
+        if action_type != enumerations.ActionType.NORMAL:
+            raise ValueError(
+                f"Bytes are not representing a ActionResponseNormal. Action type "
+                f"is {action_type}"
+            )
+
+        invoke_id_and_priority = InvokeIdAndPriority.from_bytes(
+            data.pop(0).to_bytes(1, "big")
+        )
+
+        status = enumerations.ActionResultStatus(data.pop(0))
+        has_data = bool(data.pop(0))
+        if has_data:
+            raise ValueError(
+                f"ActionResponse has data and should not be a " f"ActionResponseNormal"
+            )
+
+        return cls(invoke_id_and_priority=invoke_id_and_priority, status=status)
+
+
+@attr.s(auto_attribs=True)
+class ActionResponseNormalWithData(AbstractXDlmsApdu):
+    TAG: ClassVar[int] = 199
+    ACTION_TYPE: ClassVar[enumerations.ActionType] = enumerations.ActionType.NORMAL
+
+    status: enumerations.ActionResultStatus
+    data: bytes = attr.ib(default=None)
+    invoke_id_and_priority: InvokeIdAndPriority = attr.ib(
+        default=InvokeIdAndPriority(0, True, True)
     )
 
     def to_bytes(self):
         out = bytearray()
         out.append(self.TAG)
-        out.append(self.action_type.value)
+        out.append(self.ACTION_TYPE.value)
         out.extend(self.invoke_id_and_priority.to_bytes())
-        out.append(self.result.value)
-        # optinal result data not used.
-        out.append(0)
+        out.append(self.status.value)
+        out.extend(b"\x01")  # has data
+        out.extend(b"\x00")  # data result choice
+        out.extend(self.data)
         return bytes(out)
 
     @classmethod
@@ -98,39 +179,184 @@ def from_bytes(cls, source_bytes: bytes):
                 f"Tag {tag} is not correct for ActionResponse. Should be {cls.TAG}"
             )
         action_type = enumerations.ActionType(data.pop(0))
+
+        if action_type != enumerations.ActionType.NORMAL:
+            raise ValueError(
+                f"Bytes are not representing a ActionResponseNormal. Action type "
+                f"is {action_type}"
+            )
+
         invoke_id_and_priority = InvokeIdAndPriority.from_bytes(
             data.pop(0).to_bytes(1, "big")
         )
-        result = enumerations.ActionResult(data.pop(0))
-        has_parameters = bool(data.pop(0))
-        if has_parameters:
-            decoder = a_xdr.AXdrDecoder(
-                encoding_conf=a_xdr.EncodingConf(
-                    attributes=[
-                        a_xdr.Choice(
-                            choices={
-                                b"\x00": a_xdr.Sequence(attribute_name="result_data"),
-                                b"\x01": a_xdr.Attribute(
-                                    attribute_name="result_data",
-                                    create_instance=get_data_access_result_from_bytes,
-                                    length=1,
-                                ),
-                            }
-                        )
-                    ]
+
+        status = enumerations.ActionResultStatus(data.pop(0))
+        has_data = bool(data.pop(0))
+        if has_data:
+            data_is_result = data.pop(0) == 0
+            if not data_is_result:
+                raise ValueError(
+                    "Data is not a ActionResponseNormalWithData, maybe a "
+                    "ActionResponseNormalWithError"
                 )
+            response_data = data
+
+        else:
+            raise ValueError(
+                f"ActionResponseNormalWithData does not contain any data. "
+                f"Should probably be an ActionResponseNormal"
+            )
+
+        return cls(
+            invoke_id_and_priority=invoke_id_and_priority,
+            status=status,
+            data=response_data,
+        )
+
+
+@attr.s(auto_attribs=True)
+class ActionResponseNormalWithError(AbstractXDlmsApdu):
+    TAG: ClassVar[int] = 199
+    ACTION_TYPE: ClassVar[enumerations.ActionType] = enumerations.ActionType.NORMAL
+
+    status: enumerations.ActionResultStatus
+    error: enumerations.DataAccessResult
+    invoke_id_and_priority: InvokeIdAndPriority = attr.ib(
+        default=InvokeIdAndPriority(0, True, True)
+    )
+
+    def to_bytes(self):
+        out = bytearray()
+        out.append(self.TAG)
+        out.append(self.ACTION_TYPE.value)
+        out.extend(self.invoke_id_and_priority.to_bytes())
+        out.append(self.status.value)
+
+        out.extend(b"\x01")
+        out.extend(b"\x01")  # data result data (error) choice
+        out.append(self.error.value)
+
+        return bytes(out)
+
+    @classmethod
+    def from_bytes(cls, source_bytes: bytes):
+
+        data = bytearray(source_bytes)
+        tag = data.pop(0)
+        if tag != cls.TAG:
+            raise ValueError(
+                f"Tag {tag} is not correct for ActionResponse. Should be {cls.TAG}"
             )
-            result_data = decoder.decode(data).get("result_data", None)
+        action_type = enumerations.ActionType(data.pop(0))
 
-            return cls(
-                invoke_id_and_priority=invoke_id_and_priority,
-                action_type=action_type,
-                result=result,
-                result_data=result_data
+        if action_type != enumerations.ActionType.NORMAL:
+            raise ValueError(
+                f"Bytes are not representing a ActionResponseNormal. Action type "
+                f"is {action_type}"
             )
+
+        invoke_id_and_priority = InvokeIdAndPriority.from_bytes(
+            data.pop(0).to_bytes(1, "big")
+        )
+
+        status = enumerations.ActionResultStatus(data.pop(0))
+        has_data = bool(data.pop(0))
+        if has_data:
+            data_is_error = data.pop(0) == 1
+            if not data_is_error:
+                raise ValueError(
+                    "Data is not a ActionResponseNormalWithError, maybe a "
+                    "ActionResponseNormal"
+                )
+            assert len(data) == 1
+            error = enumerations.DataAccessResult(data.pop(0))
+
+        else:
+            raise ValueError("No error data in ActionResponseWithError")
+
+        return cls(
+            invoke_id_and_priority=invoke_id_and_priority, status=status, error=error
+        )
+
+
+@attr.s(auto_attribs=True)
+class ActionResponseFactory:
+
+    """
+    Action-Response ::= CHOICE
+    {
+    action-response-normal      [1] IMPLICIT Action-Response-Normal
+    action-response-with-pblock [2] IMPLICIT Action-Response-With-Pblock,
+    action-response-with-list   [3] IMPLICIT Action-Response-With-List,
+    action-response-next-pblock [4] IMPLICIT Action-Response-Next-Pblock,
+    }
+
+    Action-Response-Normal ::= SEQUENCE
+    {
+    invoke-id-and-priority  Invoke-Id-And-Priority,
+    single-response         Action-Response-With-Optional-Data
+    }
+
+    Action-Response-With-Pblock ::= SEQUENCE
+    {
+    invoke-id-and-priority  Invoke-Id-And-Priority,
+    pblock                  DataBlock-SA
+    }
+
+    Action-Response-With-List ::= SEQUENCE
+    {
+    invoke-id-and-priority  Invoke-Id-And-Priority,
+    list-of-responses       SEQUENCE OF Action-Response-With-Optional-Data
+    }
+
+    Action-Response-Next-Pblock ::= SEQUENCE
+    {
+    invoke-id-and-priority  Invoke-Id-And-Priority,
+    block-number            Unsigned32
+    }
+
+    Action-Response-With-Optional-Data ::= SEQUENCE
+    {
+    result              Action-Result,
+    return-parameters   Get-Data-Result OPTIONAL
+    }
+
+    Get-Data-Result ::= CHOICE
+    {
+    data                [0] Data,
+    ata-access-result   [1] IMPLICIT Data-Access-Result
+    }
+
+    """
+
+    TAG: ClassVar[int] = 199
+
+    @staticmethod
+    def from_bytes(source_bytes: bytes):
+        data = bytearray(source_bytes)
+        tag = data.pop(0)
+        if tag != ActionResponseFactory.TAG:
+            raise ValueError(
+                f"Tag is not correct. Should be {ActionResponseFactory.TAG} but is {tag}"
+            )
+        response_type = enumerations.ActionType(data.pop(0))
+
+        data.pop(0)  # Invoke id and priority that is not needed for parsing
+
+        if response_type == enumerations.ActionType.NORMAL:
+            data.pop(0)  # Action result status, not needed for parsing
+            # check if it is an error or data response by assesing the choice.
+            has_data = bool(data.pop(0))
+            if has_data:
+                choice = data.pop(0)
+                if choice == 0:
+                    return ActionResponseNormalWithData.from_bytes(source_bytes)
+                elif choice == 1:
+                    return ActionResponseNormalWithError.from_bytes(source_bytes)
+            else:
+                return ActionResponseNormal.from_bytes(source_bytes)
         else:
-            return cls(
-                invoke_id_and_priority=invoke_id_and_priority,
-                action_type=action_type,
-                result=result,
+            raise NotImplementedError(
+                "Only implemented the ActionResponse Normal "
+                "class types is not implemented."
             )
diff --git a/dlms_cosem/protocol/xdlms/base.py b/dlms_cosem/protocol/xdlms/base.py
index 94750d3..9aab9c6 100644
--- a/dlms_cosem/protocol/xdlms/base.py
+++ b/dlms_cosem/protocol/xdlms/base.py
@@ -2,7 +2,6 @@
 
 
 class AbstractXDlmsApdu(abc.ABC):
-
     @classmethod
     @abc.abstractmethod
     def from_bytes(cls, source_bytes: bytes):
diff --git a/dlms_cosem/protocol/xdlms/confirmed_service_error.py b/dlms_cosem/protocol/xdlms/confirmed_service_error.py
index 1d3eecf..a51abcd 100644
--- a/dlms_cosem/protocol/xdlms/confirmed_service_error.py
+++ b/dlms_cosem/protocol/xdlms/confirmed_service_error.py
@@ -1,11 +1,11 @@
 from enum import IntEnum
+from typing import *
 
 import attr
-from typing import *
 
-from dlms_cosem.protocol.a_xdr import EncodingConf, Choice, Attribute, AXdrDecoder
+from dlms_cosem import enumerations
+from dlms_cosem.a_xdr import Attribute, AXdrDecoder, Choice, EncodingConf
 from dlms_cosem.protocol.xdlms.base import AbstractXDlmsApdu
-from dlms_cosem.protocol import enumerations
 
 
 class ErrorFactory:
diff --git a/dlms_cosem/protocol/xdlms/conformance.py b/dlms_cosem/protocol/xdlms/conformance.py
index 077114d..a1e1b99 100644
--- a/dlms_cosem/protocol/xdlms/conformance.py
+++ b/dlms_cosem/protocol/xdlms/conformance.py
@@ -2,8 +2,6 @@
 
 import attr
 
-from dlms_cosem.protocol.ber import BER
-
 # TODO: when using ciphered apdus we will get other apdus. (33 64) global or dedicated cipered iniitate requests
 
 
@@ -97,4 +95,3 @@ def to_bytes(self):
         # It is a bit string so need to encode how many bits that are unused in the
         # last byte. Its none so we can just put 0x00 infront.
         return b"\x00" + out.to_bytes(3, "big")
-
diff --git a/dlms_cosem/protocol/xdlms/data_notification.py b/dlms_cosem/protocol/xdlms/data_notification.py
index acba5a4..6ad36ea 100644
--- a/dlms_cosem/protocol/xdlms/data_notification.py
+++ b/dlms_cosem/protocol/xdlms/data_notification.py
@@ -2,14 +2,14 @@
 
 import attr
 
-from dlms_cosem.protocol.a_xdr import (
+from dlms_cosem.a_xdr import (
     Attribute,
     AXdrDecoder,
     DlmsDataToPythonConverter,
     EncodingConf,
     Sequence,
 )
-from dlms_cosem.protocol.dlms_data import BaseDlmsData, DateTimeData
+from dlms_cosem.dlms_data import BaseDlmsData, DateTimeData
 from dlms_cosem.protocol.xdlms.base import AbstractXDlmsApdu
 
 
@@ -69,9 +69,7 @@ class NotificationBody:
     Sequence of DLMSData
     """
 
-    ENCODING_CONF = EncodingConf(
-        attributes=[Sequence(attribute_name="encoding_conf")]
-    )
+    ENCODING_CONF = EncodingConf(attributes=[Sequence(attribute_name="encoding_conf")])
 
     data: List[BaseDlmsData] = attr.ib(default=None)
     encoding_conf: EncodingConf = attr.ib(
@@ -129,8 +127,8 @@ class DataNotificationApdu(AbstractXDlmsApdu):
             ),
             Sequence(
                 attribute_name="notification_body",
-                #create_instance=NotificationBody.from_bytes,
-                #wrap_end=True,
+                # create_instance=NotificationBody.from_bytes,
+                # wrap_end=True,
             ),
         ]
     )
diff --git a/dlms_cosem/protocol/xdlms/exception_response.py b/dlms_cosem/protocol/xdlms/exception_response.py
index 3bccf4d..4460032 100644
--- a/dlms_cosem/protocol/xdlms/exception_response.py
+++ b/dlms_cosem/protocol/xdlms/exception_response.py
@@ -1,9 +1,9 @@
 from enum import IntEnum
+from typing import *
 
 import attr
-from typing import *
 
-from dlms_cosem.protocol import enumerations
+from dlms_cosem import enumerations
 
 
 @attr.s(auto_attribs=True)
diff --git a/dlms_cosem/protocol/xdlms/general_global_cipher.py b/dlms_cosem/protocol/xdlms/general_global_cipher.py
index ad7ac5d..2e0efc7 100644
--- a/dlms_cosem/protocol/xdlms/general_global_cipher.py
+++ b/dlms_cosem/protocol/xdlms/general_global_cipher.py
@@ -1,14 +1,11 @@
 from functools import partial
-from typing import *
 
 import attr
 
-from dlms_cosem.protocol import a_xdr
-from dlms_cosem.protocol.dlms_data import OctetStringData
+from dlms_cosem import a_xdr
+from dlms_cosem.dlms_data import OctetStringData
 from dlms_cosem.protocol.xdlms.base import AbstractXDlmsApdu
-
-from dlms_cosem.protocol.security import SecurityControlField, decrypt
-
+from dlms_cosem.security import SecurityControlField, decrypt
 
 int_from_bytes = partial(int.from_bytes, "big")
 
diff --git a/dlms_cosem/protocol/xdlms/get.py b/dlms_cosem/protocol/xdlms/get.py
index 0daec81..6e48894 100644
--- a/dlms_cosem/protocol/xdlms/get.py
+++ b/dlms_cosem/protocol/xdlms/get.py
@@ -3,15 +3,9 @@
 
 import attr
 
-from dlms_cosem.protocol import cosem, a_xdr
-from dlms_cosem.protocol import enumerations as enums
-from dlms_cosem.protocol.a_xdr import (
-    Attribute,
-    AXdrDecoder,
-    Choice,
-    EncodingConf,
-    Sequence,
-)
+from dlms_cosem import a_xdr, cosem
+from dlms_cosem import enumerations as enums
+from dlms_cosem.a_xdr import Attribute, AXdrDecoder, EncodingConf
 from dlms_cosem.protocol.xdlms import selective_access
 from dlms_cosem.protocol.xdlms.base import AbstractXDlmsApdu
 from dlms_cosem.protocol.xdlms.invoke_id_and_priority import InvokeIdAndPriority
@@ -189,11 +183,6 @@ def from_bytes(source_bytes: bytes):
             )
 
 
-get_data_access_result_from_bytes = partial(
-    enums.DataAccessResult.from_bytes, byteorder="big"
-)
-
-
 @attr.s(auto_attribs=True)
 class GetResponseNormal(AbstractXDlmsApdu):
     TAG: ClassVar[int] = 196
diff --git a/dlms_cosem/protocol/xdlms/initiate_request.py b/dlms_cosem/protocol/xdlms/initiate_request.py
index 3bddfa2..5451ff6 100644
--- a/dlms_cosem/protocol/xdlms/initiate_request.py
+++ b/dlms_cosem/protocol/xdlms/initiate_request.py
@@ -3,11 +3,10 @@
 
 import attr
 
-from dlms_cosem.protocol import security, a_xdr, dlms_data, acse, xdlms
-from dlms_cosem.protocol.ber import BER
-
-from dlms_cosem.protocol.xdlms.conformance import Conformance
+from dlms_cosem import a_xdr, dlms_data, security
+from dlms_cosem.protocol import xdlms
 from dlms_cosem.protocol.xdlms.base import AbstractXDlmsApdu
+from dlms_cosem.protocol.xdlms.conformance import Conformance
 
 int_from_bytes = partial(int.from_bytes, byteorder="big")
 
@@ -116,7 +115,7 @@ def to_bytes(self):
         out.append(0x06)
         out.extend(b"_\x1f\x04")
         out.extend(self.proposed_conformance.to_bytes())
-        out.extend(self.client_max_receive_pdu_size.to_bytes(2, 'big'))
+        out.extend(self.client_max_receive_pdu_size.to_bytes(2, "big"))
         return bytes(out)
 
 
diff --git a/dlms_cosem/protocol/xdlms/initiate_response.py b/dlms_cosem/protocol/xdlms/initiate_response.py
index a31683a..288dfca 100644
--- a/dlms_cosem/protocol/xdlms/initiate_response.py
+++ b/dlms_cosem/protocol/xdlms/initiate_response.py
@@ -1,8 +1,8 @@
 from typing import *
+
 import attr
 
-from dlms_cosem.protocol import security
-from dlms_cosem.protocol.ber import BER
+from dlms_cosem import security
 from dlms_cosem.protocol.xdlms.base import AbstractXDlmsApdu
 from dlms_cosem.protocol.xdlms.conformance import Conformance
 
@@ -46,7 +46,6 @@ def from_bytes(cls, source_bytes: bytes):
         else:
             quality_of_service = 0
 
-
         dlms_version = data.pop(0)
 
         conformance_tag_and_length = data[:3]
@@ -77,7 +76,6 @@ def to_bytes(self) -> bytes:
         return b"\x08" + bytes(out) + b"\x00\x07"
 
 
-
 @attr.s(auto_attribs=True)
 class GlobalCipherInitiateResponse(AbstractXDlmsApdu):
     TAG: ClassVar[int] = 40
diff --git a/dlms_cosem/protocol/xdlms/invoke_id_and_priority.py b/dlms_cosem/protocol/xdlms/invoke_id_and_priority.py
index b067b07..541ecf3 100644
--- a/dlms_cosem/protocol/xdlms/invoke_id_and_priority.py
+++ b/dlms_cosem/protocol/xdlms/invoke_id_and_priority.py
@@ -1,6 +1,7 @@
-import attr
 from typing import *
 
+import attr
+
 
 @attr.s(auto_attribs=True)
 class InvokeIdAndPriority:
diff --git a/dlms_cosem/protocol/xdlms/selective_access.py b/dlms_cosem/protocol/xdlms/selective_access.py
index 42a7645..1d2ec70 100644
--- a/dlms_cosem/protocol/xdlms/selective_access.py
+++ b/dlms_cosem/protocol/xdlms/selective_access.py
@@ -1,7 +1,9 @@
-import attr
-from typing import *
 from datetime import datetime
-from dlms_cosem.protocol import enumerations, cosem, dlms_data, time
+from typing import *
+
+import attr
+
+from dlms_cosem import cosem, dlms_data, time
 
 
 @attr.s(auto_attribs=True)
@@ -51,12 +53,8 @@ class RangeDescriptor:
     restricting_object: CaptureObject = attr.ib(
         validator=attr.validators.instance_of(CaptureObject)
     )
-    from_value: datetime = attr.ib(
-        validator=attr.validators.instance_of(datetime)
-    )
-    to_value: datetime = attr.ib(
-        validator=attr.validators.instance_of(datetime)
-    )
+    from_value: datetime = attr.ib(validator=attr.validators.instance_of(datetime))
+    to_value: datetime = attr.ib(validator=attr.validators.instance_of(datetime))
     # selected_values: List[CaptureObject] = attr.ib(factory=list)
 
     @classmethod
@@ -130,10 +128,4 @@ def from_bytes(cls, source_bytes):
         pass
 
     def to_bytes(self) -> bytes:
-        out = bytearray()
-        out.append(self.ACCESS_DESCRIPTOR)
-        out.extend(self.from_entry.to_bytes(4, "big"))
-        out.extend(self.to_entry.to_bytes(4, "big"))
-        out.extend(self.from_selected_value.to_bytes(2, "big"))
-        out.extend(self.to_selected_value.to_bytes(2, "big"))
-        return bytes(out)
+        raise NotImplementedError()
diff --git a/dlms_cosem/protocol/security.py b/dlms_cosem/security.py
similarity index 90%
rename from dlms_cosem/protocol/security.py
rename to dlms_cosem/security.py
index 8417ac4..16bc0a6 100644
--- a/dlms_cosem/protocol/security.py
+++ b/dlms_cosem/security.py
@@ -1,8 +1,8 @@
-from cryptography.hazmat.primitives.keywrap import aes_key_wrap, aes_key_unwrap
-from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
-from typing import *
 import attr
-from dlms_cosem.protocol.exceptions import CipheringError
+from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
+from cryptography.hazmat.primitives.keywrap import aes_key_unwrap, aes_key_wrap
+
+from dlms_cosem.exceptions import CipheringError
 
 """
 Security Suites in DLMS/COSEM define what cryptographic algorithms that are
@@ -11,57 +11,11 @@
 The initialization vector is essentially a nonce. In DLMS/COSEM it is
     composed of two parts. The full length is 96 bits (12 bytes)
     The first part (upper 64bit/8bytes) is called the fixed field and shall
-    contain the system titel. The lower (32bit/4byte) part is called the
+    contain the system title. The lower (32bit/4byte) part is called the
     invocation field and contains an integer invocation counter.
     The system title is a unique identifier for the DLMS/COSEM identity. The
     leftmost 3 octets holds the 3 letter manufacturer ID. (FLAG ID) and the
     remaining 5 octets are to ensure uniqueness.
-
-Security Suite 0 or AES-GCM-128 contains the following:
-    Authenticated Encryption:
-        AES-GCM-128
-    Key Transport:
-        AES-128 Key Wrap
-    
-Security Suite 1 or ECDH-ECDSA-AES-GCM-128-SHA-256 contains the following:
-
-     Authenticated Encryption:
-         AES-GCM-128
-
-     Digital Signature:
-         ECDSA with P-256
-
-     Key Agreement:
-         ECDH with P-256
-
-     Hash:
-         SHA-256
-
-     Key Transport:
-         AES-128 Key Wrap
-
-     Compression:
-         V.44
-
-    Security Suite 2 or ECDH-ECDSA-AES-GCM-256-SHA-384 contains the following:
-
-     Authenticated Encryption:
-         AES-GCM-256
-
-     Digital Signature:
-         ECDSA with P-384
-
-     Key Agreement:
-         ECDH with P-384
-
-     Hash:
-         SHA-384
-
-     Key Transport:
-         AES-256 Key Wrap
-
-     Compression:
-         V.44
 """
 
 TAG_LENGTH = 12
@@ -272,7 +226,6 @@ def gmac(
     associated_data = security_control.to_bytes() + auth_key + challenge
     encryptor.authenticate_additional_data(associated_data)
 
-
     # Making sure to add an empty byte string as input. Then it will only be the
     # associated_data that will be authenticated.
     ciphertext = encryptor.update(b"") + encryptor.finalize()
diff --git a/dlms_cosem/protocol/state.py b/dlms_cosem/state.py
similarity index 89%
rename from dlms_cosem/protocol/state.py
rename to dlms_cosem/state.py
index 7bc70a5..6521f62 100644
--- a/dlms_cosem/protocol/state.py
+++ b/dlms_cosem/state.py
@@ -2,7 +2,7 @@
 
 import attr
 
-from dlms_cosem.protocol.exceptions import LocalDlmsProtocolError
+from dlms_cosem.exceptions import LocalDlmsProtocolError
 from dlms_cosem.protocol import acse, xdlms
 
 LOG = logging.getLogger(__name__)
@@ -20,7 +20,7 @@ class _SentinelBase(type):
      next_event() and do some sort of dispatch based on type(event).
 
      Taken from h11.
-     """
+    """
 
     def __repr__(self):
         return self.__name__
@@ -92,19 +92,23 @@ def make_sentinel(name):
         RejectAssociation: NO_ASSOCIATION,
     },
     SHOULD_SEND_HLS_SEVER_CHALLENGE_RESULT: {
-        xdlms.ActionRequest: AWAITING_HLS_CLIENT_CHALLENGE_RESULT
+        xdlms.ActionRequestNormal: AWAITING_HLS_CLIENT_CHALLENGE_RESULT
+    },
+    AWAITING_HLS_CLIENT_CHALLENGE_RESULT: {
+        xdlms.ActionResponseNormalWithData: HLS_DONE,
+        xdlms.ActionResponseNormal: NO_ASSOCIATION,
+        xdlms.ActionResponseNormalWithError: NO_ASSOCIATION,
     },
-    AWAITING_HLS_CLIENT_CHALLENGE_RESULT: {xdlms.ActionResponse: HLS_DONE},
     HLS_DONE: {HlsSuccess: READY, HlsFailed: NO_ASSOCIATION},
     AWAITING_GET_RESPONSE: {
         xdlms.GetResponseNormal: READY,
         xdlms.GetResponseWithBlock: SHOULD_ACK_LAST_GET_BLOCK,
-        xdlms.GetResponseNormalWithError:  READY,
+        xdlms.GetResponseNormalWithError: READY,
         xdlms.ExceptionResponseApdu: READY,
     },
     AWAITING_GET_BLOCK_RESPONSE: {
         xdlms.GetResponseWithBlock: SHOULD_ACK_LAST_GET_BLOCK,
-        xdlms.GetResponseNormalWithError:  READY,
+        xdlms.GetResponseNormalWithError: READY,
         xdlms.ExceptionResponseApdu: READY,
         xdlms.GetResponseLastBlockWithError: READY,
         xdlms.GetResponseLastBlock: READY,
diff --git a/dlms_cosem/protocol/time.py b/dlms_cosem/time.py
similarity index 90%
rename from dlms_cosem/protocol/time.py
rename to dlms_cosem/time.py
index 9ff358f..0fd66ad 100644
--- a/dlms_cosem/protocol/time.py
+++ b/dlms_cosem/time.py
@@ -1,21 +1,22 @@
 from datetime import date, datetime, time, timedelta, timezone
-from dateutil.tz import tzoffset
-import attr
 from typing import *
 
+import attr
+from dateutil.tz import tzoffset
+
 """
 About datetimes in DLMS.
 
-Date,time and date-time may be represented simply as Octetstring (Tag=9) where the length 
-shows what type of data it is. 
+Date,time and date-time may be represented simply as Octetstring (Tag=9) where the length
+shows what type of data it is.
 * 5 bytes = date
 * 4 bytes = time
-* 12 bytes = datetime. 
+* 12 bytes = datetime.
 
-Date, time and datetime may also be represented by concise data classes of Date 
+Date, time and datetime may also be represented by concise data classes of Date
 (tag=26), Time (tag=27) and DateTime (tag=25).
 
-The special cases seems to mostly be used to define "cron-like" date. For example 
+The special cases seems to mostly be used to define "cron-like" date. For example
 if you want to define a date for every year you would leave year undefined.
 Initially we will ignore this functionality.
 
@@ -210,10 +211,10 @@ def time_from_bytes(source_bytes: bytes) -> time:
     if len(source_bytes) != 4:
         raise ValueError(f"Time is represented by 4 bytes, but got {len(source_bytes)}")
 
-    hour = get_optional_value(source_bytes[0], b"\xff", replace_with=0)
-    minute = get_optional_value(source_bytes[1], b"\xff", replace_with=0)
-    seconds = get_optional_value(source_bytes[2], b"\xff", replace_with=0)
-    hundredths = get_optional_value(source_bytes[3], b"\xff", replace_with=0)
+    hour: int = get_optional_value(source_bytes[0], b"\xff", replace_with=0)
+    minute: int = get_optional_value(source_bytes[1], b"\xff", replace_with=0)
+    seconds: int = get_optional_value(source_bytes[2], b"\xff", replace_with=0)
+    hundredths: int = get_optional_value(source_bytes[3], b"\xff", replace_with=0)
     validate_hour(hour)
     validate_minute_or_second(minute)
     validate_minute_or_second(seconds)
@@ -226,24 +227,24 @@ def time_from_bytes(source_bytes: bytes) -> time:
 
 def utc_offset_minutes(offset_minutes: Optional[int]) -> Optional[tzoffset]:
     if offset_minutes:
-        return tzoffset(name=None, offset=offset_minutes*60)
+        return tzoffset(name=None, offset=offset_minutes * 60)
     else:
         return None
 
 
 def datetime_from_bytes(source_bytes: bytes) -> Tuple[datetime, Optional[ClockStatus]]:
     """
-    Datetime is represented byte 12 bytes
-    [date[year highbyte, year lowbyte, month, day of month, day of week],
-    time[hour, minute, second, hundredths], deviation_high, deviation_low, clock_status]
-    }
-
-   date: as above
-   time: as above
-   deviation: deviation from UTC in minutes. Shows the timezone. signed long-integer
-        (-720 < dev < 720)
-        Special case:
-            0x8000: not specified.
+     Datetime is represented byte 12 bytes
+     [date[year highbyte, year lowbyte, month, day of month, day of week],
+     time[hour, minute, second, hundredths], deviation_high, deviation_low, clock_status]
+     }
+
+    date: as above
+    time: as above
+    deviation: deviation from UTC in minutes. Shows the timezone. signed long-integer
+         (-720 < dev < 720)
+         Special case:
+             0x8000: not specified.
 
     """
     if len(source_bytes) != 12:
@@ -282,7 +283,6 @@ def date_to_bytes(d: date) -> bytes:
     month_byte = month.to_bytes(1, "big")
     day_byte = day.to_bytes(1, "big")
     day_of_week_unspecified = b"\xff"
-    dayotw = d.weekday()
 
     return year_bytes + month_byte + day_byte + day_of_week_unspecified
 
diff --git a/dlms_cosem/utils.py b/dlms_cosem/utils.py
new file mode 100644
index 0000000..1aa0981
--- /dev/null
+++ b/dlms_cosem/utils.py
@@ -0,0 +1,10 @@
+from dlms_cosem import a_xdr
+
+
+def parse_as_dlms_data(data: bytes):
+    data_decoder = a_xdr.AXdrDecoder(
+        encoding_conf=a_xdr.EncodingConf(
+            attributes=[a_xdr.Sequence(attribute_name="data")]
+        )
+    )
+    return data_decoder.decode(data)["data"]
diff --git a/docs/CNAME b/docs/CNAME
index 8c21ac6..ae0e05d 100644
--- a/docs/CNAME
+++ b/docs/CNAME
@@ -1,1 +1,1 @@
-www.dlms.dev
\ No newline at end of file
+www.dlms.dev
diff --git a/docs/api_design.md b/docs/api_design.md
new file mode 100644
index 0000000..0819c32
--- /dev/null
+++ b/docs/api_design.md
@@ -0,0 +1,185 @@
+# API Design
+
+We want a simple API for the actions you perform on objects in a DLMS meter.
+DLSM follows a request/response flow.
+
+Simillar to how redis can pipeline actions there is the possiblite to "pipeline"
+commands in DLMS with the ACCESS service where several actions can
+be "clumped together in on request"
+
+The client should take care of association aquire and release without the user needing
+to set it up. It should also be possible to set up a predefined assosiation and skip the
+aquire and release.
+
+```python
+with Client() as client:
+    response = client.set("1.2.3.4.5", 2, "test")
+```
+
+But this is a bit too simple since we need to know how to model the objects in the meter.
+
+There should be a way of defining the objects available in the meter beforehand or
+set it up as the request goes away. Then if something is wrong in the calling a proper
+error can be generated.
+
+```python
+with Client() as client:
+    response = client.set(
+        logical_name=LogicalNameReference(a=1, b=2, c=3, d=4, e=5, f=5)
+        interface=PushInterfaceV2,
+        attribute=2,
+        value="test"
+        )
+    # above should be the same as
+    response = client.set("1.2.3.4.5", 2, "test")
+    # if we know that the PushInterface is on LN 1.2.3.4.5 beforehand
+    # It could be a simple dict.
+
+    # it would also make it possible to use something like:
+    response = client["1.2.3.4.5"][0] # to initate a get in the attribute.
+    # or
+    response = client["1.2.3.4.5"].set(2, "test")
+
+```
+
+Since we want automatic handling of assiciation aquire and relase we need to set up
+the client with data so it can handle it for us.
+
+```python
+
+with Client(encryption_key="0f0f", security_setup=x, proposed_conformance=Conformance() as client:
+    response = client.set("1.2.3.4.5", 2, "test")
+```
+
+Should result in an AARQ is sent according to association. Set up dedicated key
+encryption, handle HLS password transfer, etc.
+When you then send a SET (WriteRequestApdu) it should be automatically encrypted
+with the global or dedicated key. If block is supported in conformance it should be
+split into blocks and sent. The response should be received and when it is fully
+received it should be decrypted and returned to the user.
+
+By having the "heavy stuff" handled in the background it makes it possbile to keep a
+simple API while adding incfreasing functionallity to the middlestack.
+
+We also need to add a certain transport to the client so it can send its data to the
+meter. HDLC and IP should be implemented. Maybe HDLC_over_IP. But since that is in the
+IO part it can easily be broken out and extended as needed, supporting asyncio or
+whatever.
+
+The main part of the protocol implementation should still be sans-io.
+
+client.set()
+ -> SetRequest
+   -> GlobalCipheredApdu
+      -> Block, Block, Block
+      -> connection.send("data")
+      -> data is sent over wire.
+      <- data is received.
+      <- Block, Block, Block
+   <- GlobalCipheredApdu
+ <- SetResponse
+
+
+The client should be in the background and an abstraction should face the user
+(if the want to).
+Ex classname: Meter
+The Meter class holds information on all objects on the meter. By reading the
+object list of the an association it is possible to get a list of all objects and the
+access rights on each attribute and method. Even the selective access information is
+available. But the attribute are different for each interface type and the data you
+read from them or write to them are different. Many interfaces attributes have a static
+value. We want the opportunity to predefine the static data so that we automatically
+can interpret the data returned from the dynamic attributes.
+For example a profile generic:
+To be able to interpret the buffer we need to know the captured_object.
+To be able to interpret the values in the buffer we need to look up the object
+(ic=DATA just holds a value, ic=Register holds a dynamic value and static information
+about the scalar and the unit.)
+
+
+So we want a way to read the current assosiation list
+For every instance object we would like to read all the static information.
+This should then be outputted into a file. yaml for humans or json for machines.
+By supplying the file to the Meter class it is possible to call instances and get
+values back. If we don't have access to do something raise an Exception.
+
+Example file structure:
+````yaml
+objects:
+  1.2.3.4.5:
+    interface_class: 3  # Register
+    version: 0
+    attributes:
+      1: "1.2.3.4.5"  # Logical Name
+      3:
+        scalar: 3
+        unit: 13
+  0.0.99.0.0.255:
+    interface_class: 7  # Profile Generic
+    version: 1
+    attributes:
+      1: "0.0.99.0.0.255"  # Logical Name
+      3:   # capture objects
+        - interface: 3
+          instance: "1.2.3.4.5"
+          attribute: 2
+          data_index: 0
+        - interface: 3
+          instance: "2.2.3.4.5"
+          attribute: 2
+          data_index: 0
+        - interface: 3
+          instance: "3.2.3.4.5"
+          attribute: 2
+          data_index: 0
+      4: 60  # capture period
+      5: 1  # sort method
+      6:   # sort object
+          interface: 3
+          instance: "1.2.3.4.5"
+          attribute: 2
+          data_index: 0
+      8: 30  # profile_entries
+    selective_access:
+      2:
+        - 1
+        - 2
+    access_rights:
+      1:
+        - 1
+        - 2
+        - 3
+        - 4
+        - 5
+        - 6
+        - 7
+        - 8
+
+
+````
+
+```python
+meter = Meter.from_json(my_json_file)
+meter.get("1.2.3.4.5", 2, selective_access=make_range_descriptor())
+
+# pipelineing access
+access = meter.access()
+access.get()
+access.set()
+access.action()
+access.execute()
+
+# same as
+access = meter.access().get().set().action().execute()
+
+load_profile = (
+    meter.objects.get("1.2.3.4.5", 2)
+    .filter_range(from_value="2020-02-03", to_value="2020-03-03")
+    .filter_columns(from_value=2, to_value=3)
+)
+
+
+meter["1.2.3.4.5"].capture_objects
+
+```
+s
diff --git a/docs/connect_to_your_meter.md b/docs/connect_to_your_meter.md
new file mode 100644
index 0000000..925f5b1
--- /dev/null
+++ b/docs/connect_to_your_meter.md
@@ -0,0 +1,137 @@
+# Connect to your meter
+
+## Get an optical probe
+The simplest way to start reading data from your meter is via the optical port.
+You will need an optical probe to read the data, like [these ones](http://www.optical-probe.de/Optical%20probes/product.html).
+
+There are many makes of probes. Usually each meter manufacturer also sells
+a variant, but they can be a bit pricey, however they are usually of good quality.
+
+## Is your meter using direct HDLC or IEC62056-21 Mode E handshake?
+This library, as of now, just supports the direct HDLC enabled meters.
+
+When you have a meter using IEC62045-21 you need to start with an IEC62056-21
+initiation sequence before you can start the HDLC session.
+Meters have it this way to enable users to still read the meter via the optical port
+using the simpler IEC62056-21 protocol. Check out our python library for
+[IEC62056-21](https://github.com/pwitab/iec62056-21)
+
+
+## Find out how to address your meter.
+When using HDLC you will need to know the physical address to use in HDLC communications.
+This is not something that is standardized, and you might have to try different values
+to get it to work.
+
+Here are some values we have gathered:
+
+**Manufacturer** | **Meter** | **client physical address** | **server (meter) physical address**
+--- | --- | --- | ---
+Iskraemeco | AM550 | 1 | 17
+
+## Is your meter protected?
+
+Most meters have at least two association options. You can connect to the public part
+via the `public_client` of the meter to read non sensitive data but for reading current and historical values
+you will need to use the `management client`
+
+These addresses are reffered as logical addresses. A meter can have several logical
+devices in one physical meter (mostly its just one) so the `server_logical_address` is
+to address which logical device in the meter you want to connect to and the
+`client_logical_address` shows with what kind of client privileges you want to connect
+with.
+
+The public client uses `client_logical_address=16` and the management client uses
+`client_logical_address=1`.
+
+Other addresses can be used for clients with specific privileges or for pre-established
+associations.
+This is up the meter manufacturer and/or the companion standard the meter
+supports to define.
+
+### Password
+A meter can use no security, Low Level Security (LLS) or High Level Security for the
+authenticating against the meter
+
+No security means that no password needs to be submitted.
+
+Low Level Security just means a password needs to be submitted.
+
+High level security involves several passes with exchange of challenges between the
+client and meter and then verifying those challenges. Several methods of validating
+the challenge exists.
+
+* Manufacturer specific
+* MD5
+* SHA1
+* GMAC
+* SHA256
+* ECDSA
+
+As of now `dlms-cosem` supports HLS-GMAC
+
+### Encryption and authentication
+
+Your meter might enforce encryption and/or authentication of messages. If you don't
+have the keys it will be impossible to communicate with your meter.
+
+Each encryption key also have an invocation counter. This is to protect the meter from
+replay attacks. After each use the invocation counter needs to be incremented. If
+the meter receives a message with an invocation counter that is the same or lower than
+in the last message it will discard the message.
+
+If you don't know the current invocation counter you can usually read it from the meter
+using the public client.
+
+It is also possible to sign messages and use a public key infrastructure for
+encryption, but it is not yet supported in `dlms-cosem`
+
+### Security Suite
+A meter also usually adheres to a security suite (0-2). All this does is defining what
+cryptographic functions should be used for certain cryptographic operations.
+
+**Operation** | **Security Suite 0** | **Security Suite 1** | **Security Suite 2**
+--- | --- | --- | ---
+Authenticated Encryption | AES-GCM-128 | AES-GCM-128 | AES-GCM-256
+Key Transport | AES-GCM-128 | AES-GCM-128 | AES-GCM-256
+Digital Signature | NA | ECDSA with P-256 | ECDSA with P-384
+Key Agreement | NA | ECDSA with P-256 | ECDSA with P-384
+Hash | NA | SHA-256 | SHA-384
+Compression | NA | v.44 | v.44
+
+For now the most important take away from the security suite is to make sure you are
+using keys of the correct length.
+
+
+## Simple example
+
+```python3
+from dlms_cosem.clients.dlms_client import DlmsClient
+from dlms_cosem import cosem, enumerations
+
+usb_port: str = "/dev/tty.usbserial-A704H991"
+
+# public client
+dlms_client = DlmsClient.with_serial_hdlc_transport(serial_port=usb_port,
+                                                    server_logical_address=1,
+                                                    server_physical_address=17,
+                                                    client_logical_address=16, )
+
+# Send HDLC connection and send an ApplicationAssociationRequest (AARQ)
+dlms_client.associate()
+
+# read an invocation counter
+data: bytes = dlms_client.get(
+    cosem.CosemAttribute(interface=enumerations.CosemInterface.DATA,
+                         instance=cosem.Obis(0, 0, 0x2B, 1, 0), attribute=2, ))
+
+# Release the association by sending a ReleaseRequest and then closing the HDLC connection
+dlms_client.release_association()
+
+# alternatively use the contextmanager .session() to handle the association and
+# connection automatically.
+with dlms_client.session() as client:
+    data: bytes = client.get(
+        cosem.CosemAttribute(interface=enumerations.CosemInterface.DATA,
+                             instance=cosem.Obis(0, 0, 0x2B, 1, 0), attribute=2, ))
+
+```
diff --git a/docs/dlms_cosem.md b/docs/dlms_cosem.md
index 3a02c51..e4dc6ab 100644
--- a/docs/dlms_cosem.md
+++ b/docs/dlms_cosem.md
@@ -1,5 +1,5 @@
 # About DLMS/COSEM
-    
+
 DLMS/COSEM (IEC 62056, EN13757-1) is the global standard for smart energy
 metering, control and management. It specifies an object-oriented data model,
 an application layer protocol and media-specific communication profiles.
@@ -13,10 +13,10 @@ that turns the information held by COSEM objects into messages.
 DLMS/COSEM can be used for all utilities / energy kinds, all market segments,
 all applications and over virtually any communication media.
 
-### COSEM 
+### COSEM
 Companion Specification for Energy Metering - the object model capable of
  describing virtually any application.
-  
+
 ### OBIS
 Object Identification System, the naming system of the objects
 
@@ -63,13 +63,13 @@ clients.
 
 The DLMS/COSEM application layer provides:
 
-*   the ACSE services to connect the clients and the servers.
-*   the xDLMS services to access the data held by the COSEM objects. The xDLMS
-    services are the same for each object; this allows new objects to be added
-    to the model without affecting the application layer.
-*   The application layer also builds the messages (APDUs, Application Protocol
-    Data Units), applies, check and removes cryptographic protection as needed
-    and manages transferring long messages in blocks.
+* the ACSE services to connect the clients and the servers.
+* the xDLMS services to access the data held by the COSEM objects. The xDLMS
+  services are the same for each object; this allows new objects to be added
+  to the model without affecting the application layer.
+* The application layer also builds the messages (APDUs, Application Protocol
+  Data Units), applies, check and removes cryptographic protection as needed
+  and manages transferring long messages in blocks.
 
 The messages can transported over virtually any communication media.
 
diff --git a/docs/index.md b/docs/index.md
index 6285af5..26736e1 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,10 +1,10 @@
-# DLMS.dev 
+# DLMS.dev
 *Resources for the python library `dlms-cosem` and general DLMS/COSEM*
 
 
 ## About
 
-`dlms-cosem` is a protocol and client library for DLMS/COSEM. It is mainly used for 
+`dlms-cosem` is a protocol and client library for DLMS/COSEM. It is mainly used for
 communication with energy meters.
 
 ## Installation
@@ -15,53 +15,39 @@ pip install dlms-cosem
 
 !!! note
     We only support Python 3.6+
-    
+
 
 ## Design
 
-`dlms-cosem` is designed to be a tool with a simple API for working with DLMS/COSEM 
-enabled energy meters. It provides the lowest level function, as protocol state 
+`dlms-cosem` is designed to be a tool with a simple API for working with DLMS/COSEM
+enabled energy meters. It provides the lowest level function, as protocol state
 management, APDU encoding/decoding, APDU encryption/decryption.
 
-The library aims to provide a [sans-io](https://sans-io.readthedocs.io/) implementation 
-of the DLMS/COSEM protocol so that the protocol code can be reused with several 
-different io-paradigms. As of now we provide a simple client implementation based on 
+The library aims to provide a [sans-io](https://sans-io.readthedocs.io/) implementation
+of the DLMS/COSEM protocol so that the protocol code can be reused with several
+different io-paradigms. As of now we provide a simple client implementation based on
 blocking I/O.
 
-We have not implemented full support to be able to build a server (meter) emulator. If 
-this is a use-case you need, consider sponsoring the development and contact us.    
-
-## Supported features
-
-Current release:
+We have not implemented full support to be able to build a server (meter) emulator. If
+this is a use-case you need, consider sponsoring the development and contact us.
 
-    * Parsing DataNotification via UDP.
-
-Current Work:
-
-    * GET, SET, ACTION over pre-established associations.
-    * Interface classes implementation.
-    * DLMS Client to handle communication.
-    * GBT, ACCESS.
-    * Establish Connections.
-    * More Security options.
-    
 ## Supported meters
 
-Technically we aim to support any DLMS enabled meter. But since the library is low 
+Technically we aim to support any DLMS enabled meter. But since the library is low
 level DLMS you might need an abstraction layer to support everything in your meter.
 
-DLMS/COSEM specifies many different ways of performing tasks on a meter. It is 
-customary that a meter also adheres to a companion standard. In the companion standard 
+DLMS/COSEM specifies many different ways of performing tasks on a meter. It is
+customary that a meter also adheres to a companion standard. In the companion standard
 it is defined exactly how certain use-cases are to be performed and how data is modeled.
 
 Examples of companion standards are:
+
 * DSMR (Netherlands)
 * IDIS (all Europe)
 * UNI/TS 11291 (Italy)
 
-On top of it all your DSO (Distribution Service Operator) might have ordered their 
-meters with extra functionality or reduced functionality from one of the companion 
+On top of it all your DSO (Distribution Service Operator) might have ordered their
+meters with extra functionality or reduced functionality from one of the companion
 standards.
 
 We have some meters we have run tests on or know the library is used for in production
@@ -74,17 +60,17 @@ We have some meters we have run tests on or know the library is used for in prod
 This library is developed by Palmlund Wahlgren Innovative Technology AB. We are
 based in Sweden and are members of the DLMS User Association.
 
-If you find an bug pleas raise an issue on Github.
+If you find a bug pleas raise an issue on Github.
 
 We welcome contributions of any kind.
 
-We add features depending on our own use cases and our clients use cases. If you 
+We add features depending on our own use cases and our clients use cases. If you
 need a feature implemented please contact us.
 
 ## Training / Consultancy / Commercial Support
 
-We offer consultancy service and training services around this library and general DLMS/COSEM. 
-If you are interested in our services just reach out to us. 
+We offer consultancy service and training services around this library and general DLMS/COSEM.
+If you are interested in our services just reach out to us.
 
-If you have implemented a solution based on this library we also offer a commercial 
+If you have implemented a solution based on this library we also offer a commercial
 support scheme.
diff --git a/examples/dlms_with_hdlc_example.py b/examples/dlms_with_hdlc_example.py
new file mode 100644
index 0000000..cdd96c7
--- /dev/null
+++ b/examples/dlms_with_hdlc_example.py
@@ -0,0 +1,145 @@
+import logging
+from functools import partial
+from pprint import pprint
+
+from dateutil import parser as dateparser
+
+from dlms_cosem import a_xdr, cosem, enumerations, utils
+from dlms_cosem.clients.dlms_client import DlmsClient
+from dlms_cosem.parsers import AssociationObjectListParser, ProfileGenericBufferParser
+from dlms_cosem.protocol.xdlms import selective_access
+from dlms_cosem.protocol.xdlms.conformance import Conformance
+from dlms_cosem.protocol.xdlms.selective_access import RangeDescriptor
+
+# set up logging so you get a bit nicer printout of what is happening.
+logging.basicConfig(
+    level=logging.DEBUG,
+    format="%(asctime)s,%(msecs)d : %(levelname)s : %(message)s",
+    datefmt="%H:%M:%S",
+)
+
+c = Conformance(
+    general_protection=False,
+    general_block_transfer=False,
+    delta_value_encoding=False,
+    attribute_0_supported_with_set=False,
+    priority_management_supported=False,
+    attribute_0_supported_with_get=False,
+    block_transfer_with_get_or_read=True,
+    block_transfer_with_set_or_write=False,
+    block_transfer_with_action=True,
+    multiple_references=True,
+    data_notification=False,
+    access=False,
+    get=True,
+    set=True,
+    selective_access=True,
+    event_notification=False,
+    action=True,
+)
+
+encryption_key = bytes.fromhex("990EB3136F283EDB44A79F15F0BFCC21")
+authentication_key = bytes.fromhex("EC29E2F4BD7D697394B190827CE3DD9A")
+auth = enumerations.AuthenticationMechanism.HLS_GMAC
+
+
+public_client = partial(
+    DlmsClient.with_serial_hdlc_transport,
+    server_logical_address=1,
+    server_physical_address=17,
+    client_logical_address=16,
+)
+
+management_client = partial(
+    DlmsClient.with_serial_hdlc_transport,
+    server_logical_address=1,
+    server_physical_address=17,
+    client_logical_address=1,
+    authentication_method=auth,
+    encryption_key=encryption_key,
+    authentication_key=authentication_key,
+)
+
+port = "/dev/tty.usbserial-A704H991"
+with public_client(serial_port=port).session() as client:
+
+    response_data = client.get(
+        cosem.CosemAttribute(
+            interface=enumerations.CosemInterface.DATA,
+            instance=cosem.Obis(0, 0, 0x2B, 1, 0),
+            attribute=2,
+        )
+    )
+    data_decoder = a_xdr.AXdrDecoder(
+        encoding_conf=a_xdr.EncodingConf(
+            attributes=[a_xdr.Sequence(attribute_name="data")]
+        )
+    )
+    invocation_counter = data_decoder.decode(response_data)["data"]
+    print(f"meter_initial_invocation_counter = {invocation_counter}")
+
+
+LOAD_PROFILE_BUFFER = cosem.CosemAttribute(
+    interface=enumerations.CosemInterface.PROFILE_GENERIC,
+    instance=cosem.Obis(1, 0, 99, 1, 0),
+    attribute=2,
+)
+
+CURRENT_ASSOCIATION_OBJECTS = cosem.CosemAttribute(
+    interface=enumerations.CosemInterface.ASSOCIATION_LN,
+    instance=cosem.Obis(0, 0, 40, 0, 0),
+    attribute=2,
+)
+
+with management_client(
+    serial_port=port, client_initial_invocation_counter=invocation_counter + 1
+).session() as client:
+
+    profile = client.get(
+        CURRENT_ASSOCIATION_OBJECTS,
+        # access_descriptor=RangeDescriptor(
+        #    restricting_object=selective_access.CaptureObject(
+        #        cosem_attribute=cosem.CosemAttribute(
+        #            interface=enumerations.CosemInterface.CLOCK,
+        #            instance=cosem.Obis.from_dotted("0.0.1.0.0.255"),
+        #            attribute=2,
+        #        ),
+        #        data_index=0,
+        #    ),
+        #    from_value=dateparser.parse("2020-01-01T00:00:00-02:00"),
+        #    to_value=dateparser.parse("2020-01-01T02:00:00-01:00"),
+        # ),
+    )
+
+    parser = ProfileGenericBufferParser(
+        capture_objects=[
+            cosem.CosemAttribute(
+                interface=enumerations.CosemInterface.CLOCK,
+                instance=cosem.Obis(0, 0, 1, 0, 0, 255),
+                attribute=2,
+            ),
+            cosem.CosemAttribute(
+                interface=enumerations.CosemInterface.DATA,
+                instance=cosem.Obis(0, 0, 96, 10, 1, 255),
+                attribute=2,
+            ),
+            cosem.CosemAttribute(
+                interface=enumerations.CosemInterface.REGISTER,
+                instance=cosem.Obis(1, 0, 1, 8, 0, 255),
+                attribute=2,
+            ),
+            cosem.CosemAttribute(
+                interface=enumerations.CosemInterface.REGISTER,
+                instance=cosem.Obis(1, 0, 2, 8, 0, 255),
+                attribute=2,
+            ),
+        ],
+        capture_period=60,
+    )
+    # result = parser.parse_bytes(profile)
+    result = utils.parse_as_dlms_data(profile)
+    meter_objects_list = AssociationObjectListParser.parse_entries(result)
+    meter_objects_dict = {
+        obj.logical_name.dotted_repr(): obj for obj in meter_objects_list
+    }
+    pprint(meter_objects_dict)
diff --git a/examples/dlms_example.py b/examples/dlms_with_tcp_example.py
similarity index 81%
rename from examples/dlms_example.py
rename to examples/dlms_with_tcp_example.py
index eea5cb8..af2fbd9 100644
--- a/examples/dlms_example.py
+++ b/examples/dlms_with_tcp_example.py
@@ -1,14 +1,13 @@
 import logging
-from datetime import datetime
 from functools import partial
 from pprint import pprint
+from time import sleep
 
-from dateutil import parser
+from dateutil import parser as dateparser
 
-from dlms_cosem.clients.serial_dlms import SerialDlmsClient
-from dlms_cosem.protocol import a_xdr, cosem, time
-from dlms_cosem.protocol.acse import enumerations
-from dlms_cosem.protocol.parsers import ProfileGenericBufferParser
+from dlms_cosem import a_xdr, cosem, enumerations
+from dlms_cosem.clients.dlms_client import DlmsClient
+from dlms_cosem.parsers import ProfileGenericBufferParser
 from dlms_cosem.protocol.xdlms import selective_access
 from dlms_cosem.protocol.xdlms.conformance import Conformance
 from dlms_cosem.protocol.xdlms.selective_access import RangeDescriptor
@@ -43,27 +42,23 @@
 encryption_key = bytes.fromhex("990EB3136F283EDB44A79F15F0BFCC21")
 authentication_key = bytes.fromhex("EC29E2F4BD7D697394B190827CE3DD9A")
 auth = enumerations.AuthenticationMechanism.HLS_GMAC
-
+host = "100.119.108.3"
+port = 4059
 
 public_client = partial(
-    SerialDlmsClient,
-    server_logical_address=1,
-    server_physical_address=17,
-    client_logical_address=16,
+    DlmsClient.with_tcp_transport, server_logical_address=1, client_logical_address=16
 )
 
 management_client = partial(
-    SerialDlmsClient,
+    DlmsClient.with_tcp_transport,
     server_logical_address=1,
-    server_physical_address=17,
     client_logical_address=1,
     authentication_method=auth,
     encryption_key=encryption_key,
     authentication_key=authentication_key,
 )
 
-port = "/dev/tty.usbserial-A704H991"
-with public_client(serial_port=port).session() as client:
+with public_client(host=host, port=port).session() as client:
 
     response_data = client.get(
         cosem.CosemAttribute(
@@ -80,9 +75,12 @@
     invocation_counter = data_decoder.decode(response_data)["data"]
     print(f"meter_initial_invocation_counter = {invocation_counter}")
 
+# we are not reusing the socket as of now. We just need to give the meter some time to
+# close the connection on its side
+sleep(1)
 
 with management_client(
-    serial_port=port, client_initial_invocation_counter=invocation_counter + 1
+    host=host, port=port, client_initial_invocation_counter=invocation_counter + 1
 ).session() as client:
 
     profile = client.get(
@@ -100,8 +98,8 @@
                 ),
                 data_index=0,
             ),
-            from_value=parser.parse("2020-01-01T00:00:00-02:00"),
-            to_value=parser.parse("2020-01-01T02:00:00-01:00"),
+            from_value=dateparser.parse("2020-01-01T00:00:00-02:00"),
+            to_value=dateparser.parse("2020-01-02T00:00:00-01:00"),
         ),
     )
 
diff --git a/examples/exterimental_meter.py b/examples/exterimental_meter.py
new file mode 100644
index 0000000..f46ecf9
--- /dev/null
+++ b/examples/exterimental_meter.py
@@ -0,0 +1,226 @@
+import logging
+from functools import partial
+from pprint import pprint
+
+from dateutil import parser as dateparser
+
+from dlms_cosem import a_xdr, cosem, enumerations, utils
+from dlms_cosem.clients.dlms_client import DlmsClient
+from dlms_cosem.clients.experimental_meter import Meter
+from dlms_cosem.cosem.profile_generic import Data, ProfileGeneric
+from dlms_cosem.parsers import AssociationObjectListParser, ProfileGenericBufferParser
+from dlms_cosem.protocol.xdlms import selective_access
+from dlms_cosem.protocol.xdlms.conformance import Conformance
+from dlms_cosem.protocol.xdlms.selective_access import CaptureObject, RangeDescriptor
+
+# set up logging so you get a bit nicer printout of what is happening.
+logging.basicConfig(
+    level=logging.DEBUG,
+    format="%(asctime)s,%(msecs)d : %(levelname)s : %(message)s",
+    datefmt="%H:%M:%S",
+)
+
+c = Conformance(
+    general_protection=False,
+    general_block_transfer=False,
+    delta_value_encoding=False,
+    attribute_0_supported_with_set=False,
+    priority_management_supported=False,
+    attribute_0_supported_with_get=False,
+    block_transfer_with_get_or_read=True,
+    block_transfer_with_set_or_write=False,
+    block_transfer_with_action=True,
+    multiple_references=True,
+    data_notification=False,
+    access=False,
+    get=True,
+    set=True,
+    selective_access=True,
+    event_notification=False,
+    action=True,
+)
+
+encryption_key = bytes.fromhex("990EB3136F283EDB44A79F15F0BFCC21")
+authentication_key = bytes.fromhex("EC29E2F4BD7D697394B190827CE3DD9A")
+auth = enumerations.AuthenticationMechanism.HLS_GMAC
+
+
+public_client = partial(
+    DlmsClient.with_serial_hdlc_transport,
+    server_logical_address=1,
+    server_physical_address=17,
+    client_logical_address=16,
+)
+
+management_client = partial(
+    DlmsClient.with_serial_hdlc_transport,
+    server_logical_address=1,
+    server_physical_address=17,
+    client_logical_address=1,
+    authentication_method=auth,
+    encryption_key=encryption_key,
+    authentication_key=authentication_key,
+)
+
+port = "/dev/tty.usbserial-A704H991"
+
+public_meter = Meter(
+    dlms_client=public_client(serial_port=port),
+    objects={
+        "0.0.43.1.0.255": Data(
+            logical_name=cosem.Obis.from_dotted("0.0.43.1.0.255"), value=None
+        ),
+        "1.0.99.1.0.255": ProfileGeneric(
+            logical_name="1.0.99.1.0.255",
+            capture_objects=[
+                cosem.CosemAttribute(
+                    interface=enumerations.CosemInterface.CLOCK,
+                    instance=cosem.Obis(0, 0, 1, 0, 0, 255),
+                    attribute=2,
+                ),
+                cosem.CosemAttribute(
+                    interface=enumerations.CosemInterface.DATA,
+                    instance=cosem.Obis(0, 0, 96, 10, 1, 255),
+                    attribute=2,
+                ),
+                cosem.CosemAttribute(
+                    interface=enumerations.CosemInterface.REGISTER,
+                    instance=cosem.Obis(1, 0, 1, 8, 0, 255),
+                    attribute=2,
+                ),
+                cosem.CosemAttribute(
+                    interface=enumerations.CosemInterface.REGISTER,
+                    instance=cosem.Obis(1, 0, 2, 8, 0, 255),
+                    attribute=2,
+                ),
+            ],
+            capture_period=60,
+        ),
+    },
+)
+
+
+with public_meter.session() as meter:
+    invocation_counter = meter.get("0.0.43.1.0.255", 2)
+    print(meter.get("0.0.43.1.0.255", 1))
+
+managment_meter = Meter(
+    dlms_client=management_client(
+        serial_port=port, client_initial_invocation_counter=invocation_counter + 1
+    ),
+    objects={
+        "0.0.43.1.0.255": Data(
+            logical_name=cosem.Obis.from_dotted("0.0.43.1.0.255"),
+            value=None,
+        ),
+        "1.0.99.1.0.255": ProfileGeneric(
+            logical_name=cosem.Obis.from_dotted("1.0.99.1.0.255"),
+            capture_objects=[
+                CaptureObject(
+                    cosem_attribute=cosem.CosemAttribute(
+                        interface=enumerations.CosemInterface.CLOCK,
+                        instance=cosem.Obis(0, 0, 1, 0, 0, 255),
+                        attribute=2,
+                    ),
+                    data_index=0,
+                ),
+                CaptureObject(
+                    cosem_attribute=cosem.CosemAttribute(
+                        interface=enumerations.CosemInterface.DATA,
+                        instance=cosem.Obis(0, 0, 96, 10, 1, 255),
+                        attribute=2,
+                    ),
+                    data_index=0,
+                ),
+                CaptureObject(
+                    cosem_attribute=cosem.CosemAttribute(
+                        interface=enumerations.CosemInterface.REGISTER,
+                        instance=cosem.Obis(1, 0, 1, 8, 0, 255),
+                        attribute=2,
+                    ),
+                    data_index=0,
+                ),
+                CaptureObject(
+                    cosem_attribute=cosem.CosemAttribute(
+                        interface=enumerations.CosemInterface.REGISTER,
+                        instance=cosem.Obis(1, 0, 2, 8, 0, 255),
+                        attribute=2,
+                    ),
+                    data_index=0,
+                ),
+            ],
+            capture_period=60,
+        ),
+    },
+)
+
+with managment_meter.session() as meter:
+    load_profile = meter.get("1.0.99.1.0.255", 2)
+    print(load_profile)
+
+
+#
+# LOAD_PROFILE_BUFFER = cosem.CosemAttribute(
+#     interface=enumerations.CosemInterface.PROFILE_GENERIC,
+#     instance=cosem.Obis(1, 0, 99, 1, 0),
+#     attribute=2,
+# )
+#
+# CURRENT_ASSOCIATION_OBJECTS = cosem.CosemAttribute(
+#     interface=enumerations.CosemInterface.ASSOCIATION_LN,
+#     instance=cosem.Obis(0, 0, 40, 0, 0),
+#     attribute=2,
+# )
+#
+# with management_client(
+#     serial_port=port, client_initial_invocation_counter=invocation_counter + 1
+# ).session() as client:
+#
+#     profile = client.get(
+#         CURRENT_ASSOCIATION_OBJECTS,
+#         # access_descriptor=RangeDescriptor(
+#         #    restricting_object=selective_access.CaptureObject(
+#         #        cosem_attribute=cosem.CosemAttribute(
+#         #            interface=enumerations.CosemInterface.CLOCK,
+#         #            instance=cosem.Obis.from_dotted("0.0.1.0.0.255"),
+#         #            attribute=2,
+#         #        ),
+#         #        data_index=0,
+#         #    ),
+#         #    from_value=dateparser.parse("2020-01-01T00:00:00-02:00"),
+#         #    to_value=dateparser.parse("2020-01-01T02:00:00-01:00"),
+#         # ),
+#     )
+#
+#     parser = ProfileGenericBufferParser(
+#         capture_objects=[
+#             cosem.CosemAttribute(
+#                 interface=enumerations.CosemInterface.CLOCK,
+#                 instance=cosem.Obis(0, 0, 1, 0, 0, 255),
+#                 attribute=2,
+#             ),
+#             cosem.CosemAttribute(
+#                 interface=enumerations.CosemInterface.DATA,
+#                 instance=cosem.Obis(0, 0, 96, 10, 1, 255),
+#                 attribute=2,
+#             ),
+#             cosem.CosemAttribute(
+#                 interface=enumerations.CosemInterface.REGISTER,
+#                 instance=cosem.Obis(1, 0, 1, 8, 0, 255),
+#                 attribute=2,
+#             ),
+#             cosem.CosemAttribute(
+#                 interface=enumerations.CosemInterface.REGISTER,
+#                 instance=cosem.Obis(1, 0, 2, 8, 0, 255),
+#                 attribute=2,
+#             ),
+#         ],
+#         capture_period=60,
+#     )
+#     # result = parser.parse_bytes(profile)
+#     result = utils.parse_as_dlms_data(profile)
+#     meter_objects_list = AssociationObjectListParser.parse_entries(result)
+#     meter_objects_dict = {
+#         obj.logical_name.dotted_repr(): obj for obj in meter_objects_list
+#     }
+#     pprint(meter_objects_dict)
diff --git a/examples/hdlc_example.py b/examples/hdlc_example.py
index ea59c41..4e31329 100644
--- a/examples/hdlc_example.py
+++ b/examples/hdlc_example.py
@@ -1,9 +1,9 @@
+import logging
+
 from dlms_cosem.clients.serial_hdlc import SerialHdlcClient
 from dlms_cosem.protocol.acse import ApplicationAssociationRequestApdu, UserInformation
-
 from dlms_cosem.protocol.xdlms.conformance import Conformance
 from dlms_cosem.protocol.xdlms.initiate_request import InitiateRequestApdu
-import logging
 
 # set up logging so you get a bit nicer printout of what is happening.
 logging.basicConfig(
@@ -22,8 +22,6 @@
 with client as c:
 
     aarq = ApplicationAssociationRequestApdu(
-
-
         user_information=UserInformation(
             content=InitiateRequestApdu(
                 proposed_conformance=Conformance(
@@ -51,7 +49,7 @@
                 response_allowed=True,
                 dedicated_key=None,
             )
-        ),
+        )
     )
 
     response = client.send(aarq.to_bytes())
diff --git a/mkdocs.yml b/mkdocs.yml
index cc85c38..057e950 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -5,6 +5,8 @@ site_author: 'Henrik Palmlund Wahlgren @ Utilitarian'
 nav:
   - Home: index.md
   - About DLMS/COSEM: dlms_cosem.md
+  - Getting Started:
+      - Connect to your meter: connect_to_your_meter.md
 
 
 theme:
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..127a78d
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,24 @@
+[tool.black]
+py36 = true
+include = '\.pyi?$'
+exclude = '''
+/(
+    \.git
+  | \.hg
+  | \.mypy_cache
+  | \.tox
+  | \.venv
+  | _build
+  | buck-out
+  | build
+  | dist
+
+  # The following are specific to Black, you probably don't want those.
+  | blib2to3
+  | tests/data
+)/
+'''
+
+[tool.isort]
+profile = "black"
+multi_line_output = 3
diff --git a/setup.py b/setup.py
index d08e260..a5f830b 100644
--- a/setup.py
+++ b/setup.py
@@ -1,8 +1,9 @@
-from setuptools import setup, find_packages, Command
-import sys
 import os
+import sys
 from shutil import rmtree
 
+from setuptools import Command, find_packages, setup
+
 # Package meta-data.
 NAME = "dlms-cosem"
 DESCRIPTION = "A Python library for DLMS/COSEM"
@@ -10,22 +11,41 @@
 EMAIL = "henrik@pwit.se"
 AUTHOR = "Henrik Palmlund Wahlgren @ Palmlund Wahlgren Innovative Technology AB"
 REQUIRES_PYTHON = "~=3.6"
-# VERSION = None
+VERSION = "21.1.0.dev0"
 
 # What packages are required for this module to be executed?
 REQUIRED = [
-    "attrs~=20.3.0",
-    "pyserial~=3.5",
-    "cryptography~=3.3.1",
+    "attrs==20.3.0",
+    "pyserial==3.5",
+    "cryptography==3.3.1",
     "asn1crypto==0.24.0",
-    "python-dateutil~=2.8.1",
+    "python-dateutil==2.8.1",
 ]
 
-# What packages are optional?
+DOC_PACKAGES = ["mkdocs", "mkdocs-material"]
+TEST_PACKAGES = ["pytest", "pytest-cov", "pytest-sugar"]
+DEV_PACKAGES = ["pre-commit"] + DOC_PACKAGES + TEST_PACKAGES
+
 EXTRAS = {
-    # 'fancy feature': ['django'],
+    "docs": DOC_PACKAGES,
+    "test": TEST_PACKAGES,
+    "dev": DEV_PACKAGES,
 }
 
+CLASSIFIERS = [
+    "Intended Audience :: Developers",
+    "Natural Language :: English",
+    "License :: OSI Approved :: MIT License",
+    "Operating System :: OS Independent",
+    "Programming Language :: Python :: 3",
+    "Programming Language :: Python :: 3.6",
+    "Programming Language :: Python :: 3.7",
+    "Programming Language :: Python :: 3.8",
+    "Programming Language :: Python :: 3.9",
+    "Programming Language :: Python :: Implementation :: CPython",
+    "Topic :: Software Development :: Libraries :: Python Modules",
+]
+
 here = os.path.abspath(os.path.dirname(__file__))
 
 
@@ -74,12 +94,14 @@ def run(self):
 
 setup(
     name=NAME,
-    version="0.0.2",
+    version=VERSION,
     python_requires=REQUIRES_PYTHON,
     description=DESCRIPTION,
     long_description=readme + "\n\n" + history,
     author=AUTHOR,
     author_email=EMAIL,
+    maintainer=AUTHOR,
+    maintainer_email=EMAIL,
     url=URL,
     packages=find_packages(exclude=("tests",)),
     entry_points={},
@@ -88,8 +110,8 @@ def run(self):
     include_package_data=True,
     license="MIT",
     zip_safe=False,
-    keywords="AMR, Metering, MDM, dlms, cosem",
-    classifiers=[],
+    keywords="AMR, Metering, smart meters, MDM, dlms, cosem",
+    classifiers=CLASSIFIERS,
     # $ setup.py publish support.
     cmdclass={"upload": UploadCommand},
 )


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 0f60fb1..bf5c28e 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -30,5 +30,3 @@ jobs:
         uses: codecov/codecov-action@v1.0.12
         with:
           fail_ci_if_error: true
-
-
diff --git a/tests/conftest.py b/tests/conftest.py
index 0d09052..b1a0aad 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,13 +1,15 @@
 import pytest
 
-from dlms_cosem.protocol import acse, xdlms, cosem, enumerations, dlms_data
-from dlms_cosem.protocol.connection import DlmsConnection
+from dlms_cosem import cosem, enumerations
+from dlms_cosem.connection import DlmsConnection
+from dlms_cosem.protocol import acse, xdlms
 
 
 @pytest.fixture()
 def system_title() -> bytes:
     return b"HEWATEST"
 
+
 @pytest.fixture()
 def meter_system_title() -> bytes:
     return b"uti123457"
@@ -90,12 +92,9 @@ def aarq():
 
 @pytest.fixture()
 def aare():
-    from dlms_cosem.protocol import acse, xdlms, cosem, enumerations
-    from dlms_cosem.protocol.xdlms import (
-        InitiateResponseApdu,
-        Conformance,
-        InitiateRequestApdu,
-    )
+    from dlms_cosem import enumerations
+    from dlms_cosem.protocol import acse
+    from dlms_cosem.protocol.xdlms import Conformance, InitiateResponseApdu
 
     return acse.ApplicationAssociationResponseApdu(
         result=enumerations.AssociationResult.ACCEPTED,
@@ -136,13 +135,17 @@ def aare():
         responding_ae_invocation_id=None,
     )
 
+
 @pytest.fixture()
-def ciphered_hls_aare(aare: acse.ApplicationAssociationResponseApdu, meter_system_title: bytes) -> acse.ApplicationAssociationResponseApdu:
+def ciphered_hls_aare(
+    aare: acse.ApplicationAssociationResponseApdu, meter_system_title: bytes
+) -> acse.ApplicationAssociationResponseApdu:
     aare.ciphered = True
     aare.system_title = meter_system_title
     aare.authentication = enumerations.AuthenticationMechanism.HLS_GMAC
     return aare
 
+
 @pytest.fixture()
 def rlrq() -> acse.ReleaseRequestApdu:
 
@@ -182,13 +185,14 @@ def exception_response() -> xdlms.ExceptionResponseApdu:
 
 
 @pytest.fixture()
-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 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,
+    )
diff --git a/tests/test_a_xdr.py b/tests/test_a_xdr.py
index 751514c..c681328 100644
--- a/tests/test_a_xdr.py
+++ b/tests/test_a_xdr.py
@@ -1,60 +1,23 @@
-import pytest
+from functools import partial
 
-from dlms_cosem.protocol.a_xdr import (
+from dlms_cosem import enumerations as enums
+from dlms_cosem.a_xdr import (
     Attribute,
+    AXdrDecoder,
     Choice,
     EncodingConf,
     Sequence,
-    AXdrDecoder,
-    get_axdr_length
-)
-from dlms_cosem.protocol.connection import XDlmsApduFactory
-from dlms_cosem.protocol.xdlms.get import (
-    InvokeIdAndPriority,
-    get_data_access_result_from_bytes,
-    get_request_type_from_bytes,
-    get_response_type_from_bytes,
-    GetResponseWithBlock,
+    get_axdr_length,
 )
+from dlms_cosem.connection import XDlmsApduFactory
+from dlms_cosem.protocol.xdlms.get import GetResponseWithBlock
 
-class TestAxdrDecoder:
-    def test_decode_get_request(self):
-        conf = EncodingConf(
-            attributes=[
-                Attribute(
-                    attribute_name="response_type",
-                    create_instance=get_request_type_from_bytes,
-                    length=1,
-                ),
-                Attribute(
-                    attribute_name="invoke_id_and_priority",
-                    create_instance=InvokeIdAndPriority.from_bytes,
-                    length=1,
-                ),
-                Choice(
-                    {
-                        b"\x00": Sequence(attribute_name="result"),
-                        b"\x01": Attribute(
-                            attribute_name="result",
-                            create_instance=get_data_access_result_from_bytes,
-                            length=1,
-                        ),
-                    }
-                ),
-            ]
-        )
-        in_data = b"\x01\xc1\x00\t\x10ISK1030773044321"
-
-        decoder = AXdrDecoder(encoding_conf=conf)
-        result = decoder.decode(in_data)
-
-        assert result == {
-            "response_type": get_request_type_from_bytes(b"\x01"),
-            "invoke_id_and_priority": InvokeIdAndPriority.from_bytes(b"\xc1"),
-            "result": b"ISK1030773044321",
-        }
+get_data_access_result_from_bytes = partial(
+    enums.DataAccessResult.from_bytes, byteorder="big"
+)
 
 
+class TestAxdrDecoder:
     def test_decode_array_and_structure(self):
         # Data from a get response on a load profile request. preped C40100 to get whole
         # APDU.
@@ -81,7 +44,9 @@ def test_decode_array_and_structure(self):
         )
         assert len(data) == 555
 
-        encoding_conf = EncodingConf(attributes=[Choice(
+        encoding_conf = EncodingConf(
+            attributes=[
+                Choice(
                     {
                         b"\x00": Sequence(attribute_name="result"),
                         b"\x01": Attribute(
@@ -90,11 +55,13 @@ def test_decode_array_and_structure(self):
                             length=1,
                         ),
                     }
-                ),])
+                )
+            ]
+        )
 
         decoder = AXdrDecoder(encoding_conf)
 
-        result = decoder.decode(data)['result']
+        result = decoder.decode(data)["result"]
         assert len(result) == 24
         assert len(result[0]) == 3
         assert isinstance(result[0][0], bytearray)
@@ -102,12 +69,11 @@ def test_decode_array_and_structure(self):
         assert isinstance(result[0][2], int)
 
 
-
 def test_get_axdr_length():
 
     data = bytearray(b"\x82\x0f\x0f")
 
-    assert get_axdr_length(data) == int.from_bytes(b"\x0f\x0f", 'big')
+    assert get_axdr_length(data) == int.from_bytes(b"\x0f\x0f", "big")
     assert len(data) == 0
     data = bytearray(b"\x09")
     assert get_axdr_length(data) == 9
@@ -115,10 +81,8 @@ def test_get_axdr_length():
 
 
 def test_decode_long():
-    data = b'\xc4\x02\xc1\x00\x00\x00\x00\x01\x00\x82\x04\x9e\x01\x82\x05\xa0\x02\x04\t\x0c\x07\xe3\x0c\x10\x01\x08\x00\x00\x00\xff\xc4\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x86\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed'
+    data = b"\xc4\x02\xc1\x00\x00\x00\x00\x01\x00\x82\x04\x9e\x01\x82\x05\xa0\x02\x04\t\x0c\x07\xe3\x0c\x10\x01\x08\x00\x00\x00\xff\xc4\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x86\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed\x06\x00\x00\x06T\x02\x04\x00\x11\x06\x06\x00\x00\x05\xed"
 
     apdu = XDlmsApduFactory.apdu_from_bytes(data)
     assert isinstance(apdu, GetResponseWithBlock)
     assert apdu.block_number == 1
-
-
diff --git a/tests/test_asce/test_aare.py b/tests/test_asce/test_aare.py
index 6ab3185..e1e0085 100644
--- a/tests/test_asce/test_aare.py
+++ b/tests/test_asce/test_aare.py
@@ -1,9 +1,11 @@
 import pytest
-from dlms_cosem.protocol import acse, enumerations
+
+from dlms_cosem import enumerations
+from dlms_cosem.protocol import acse
 
 # Example encodings from DLMS Green book v10 page:
 from dlms_cosem.protocol.acse import UserInformation
-from dlms_cosem.protocol.xdlms import InitiateResponseApdu, Conformance
+from dlms_cosem.protocol.xdlms import Conformance, InitiateResponseApdu
 from dlms_cosem.protocol.xdlms.confirmed_service_error import ConfirmedServiceErrorApdu
 
 
@@ -47,7 +49,10 @@ def test_no_cipher_no_security_incorrect_dlms_version(self):
         )
         print(aare.user_information)
         assert aare.user_information.content is not None
-        assert aare.user_information.content.error == enumerations.InitiateError.DLMS_VERSION_TOO_LOW
+        assert (
+            aare.user_information.content.error
+            == enumerations.InitiateError.DLMS_VERSION_TOO_LOW
+        )
 
     def test_no_ciher_hls_ok(self):
         data = bytes.fromhex(
diff --git a/tests/test_asce/test_aarq.py b/tests/test_asce/test_aarq.py
index 6a75796..fac6c68 100644
--- a/tests/test_asce/test_aarq.py
+++ b/tests/test_asce/test_aarq.py
@@ -1,14 +1,16 @@
 import pytest
 
-from dlms_cosem.protocol import enumerations
-from dlms_cosem.protocol.acse import (
-    ApplicationAssociationRequestApdu,
-    UserInformation,
+from dlms_cosem import enumerations
+from dlms_cosem.protocol.acse import ApplicationAssociationRequestApdu, UserInformation
+from dlms_cosem.protocol.xdlms import (
+    Conformance,
+    GlobalCipherInitiateResponse,
+    InitiateRequestApdu,
 )
-from dlms_cosem.protocol.xdlms import InitiateRequestApdu, Conformance
 
 # Example encodings from DLMS Green Book v10: page 444
 
+
 class TestParseAARQ:
     def test_parse_no_ciphering_no_sercurity(self):
         data = b"`\x1d\xa1\t\x06\x07`\x85t\x05\x08\x01\x01\xbe\x10\x04\x0e\x01\x00\x00\x00\x06_\x1f\x04\x00\x00~\x1f\x04\xb0"
@@ -47,7 +49,9 @@ def test_parse_ciphered_low_security(self):
         assert aarq.authentication_value is not None
 
     def test_parse_ciphered_low_security2(self):
-        data = bytes.fromhex("6066a109060760857405080103a60a04084D4D4D0000BC614E8a0207808b0760857405080201ac0a80083132333435363738be34043221303001234567801302FF8A7874133D414CED25B42534D28DB0047720606B175BD52211BE6841DB204D39EE6FDB8E356855")
+        data = bytes.fromhex(
+            "6066a109060760857405080103a60a04084D4D4D0000BC614E8a0207808b0760857405080201ac0a80083132333435363738be34043221303001234567801302FF8A7874133D414CED25B42534D28DB0047720606B175BD52211BE6841DB204D39EE6FDB8E356855"
+        )
         aarq = ApplicationAssociationRequestApdu.from_bytes(data)
         print(aarq)
         assert aarq.ciphered
@@ -60,9 +64,13 @@ def test_parse_ciphered_low_security2(self):
 
     def test_hls(self):
         data = bytes.fromhex(
-            "6036A1090607608574050801018A0207808B0760857405080202AC0A80083132333435363738BE10040E01000000065F1F0400007E1FFFFF")
+            "6036A1090607608574050801018A0207808B0760857405080202AC0A80083132333435363738BE10040E01000000065F1F0400007E1FFFFF"
+        )
         aarq = ApplicationAssociationRequestApdu.from_bytes(data)
-
+        assert aarq.authentication
+        assert aarq.user_information
+        assert isinstance(aarq.user_information.content, InitiateRequestApdu)
+        assert aarq.authentication_value is not None
 
 
 class TestEncodeAARE:
diff --git a/tests/test_asce/test_conformance.py b/tests/test_asce/test_conformance.py
index cf48c35..316b79b 100644
--- a/tests/test_asce/test_conformance.py
+++ b/tests/test_asce/test_conformance.py
@@ -2,9 +2,9 @@
 
 from dlms_cosem.protocol.xdlms import Conformance
 
-
 # Example code found encoding found in DLMS Green Book v10 page:437
 
+
 @pytest.mark.parametrize(
     "conformance,encoded",
     [
@@ -24,7 +24,7 @@
             ),
             b"\x00\x00\x7e\x1f",
         ),
-(
+        (
             Conformance(
                 priority_management_supported=True,
                 block_transfer_with_get_or_read=True,
@@ -35,7 +35,7 @@
                 action=True,
             ),
             b"\x00\x00\x50\x1f",
-        )
+        ),
     ],
 )
 def test_conformance(conformance: Conformance, encoded: bytes):
diff --git a/tests/test_asce/test_rlre.py b/tests/test_asce/test_rlre.py
index d9bd060..cae1c60 100644
--- a/tests/test_asce/test_rlre.py
+++ b/tests/test_asce/test_rlre.py
@@ -1,6 +1,7 @@
 import pytest
 
-from dlms_cosem.protocol import enumerations, xdlms
+from dlms_cosem import enumerations
+from dlms_cosem.protocol import xdlms
 from dlms_cosem.protocol.acse import ReleaseResponseApdu
 
 
@@ -12,11 +13,10 @@ def test_simple(self):
         assert rlre.user_information is None
 
     def test_with_initiate_response(self):
-        data = b'c\x16\x80\x01\x00\xbe\x11\x04\x0f\x08\x01\x00\x06_\x1f\x04\x00\x00\x1e\x1d\x04\xc8\x00\x07'
+        data = b"c\x16\x80\x01\x00\xbe\x11\x04\x0f\x08\x01\x00\x06_\x1f\x04\x00\x00\x1e\x1d\x04\xc8\x00\x07"
         rlre = ReleaseResponseApdu.from_bytes(data)
         assert rlre.reason == enumerations.ReleaseResponseReason.NORMAL
-        assert isinstance(rlre.user_information.content,
-                          xdlms.InitiateResponseApdu)
+        assert isinstance(rlre.user_information.content, xdlms.InitiateResponseApdu)
 
     def test_with_ciphered_initiate_response(self):
         data = bytes.fromhex(
@@ -24,4 +24,6 @@ def test_with_ciphered_initiate_response(self):
         )
         rlre = ReleaseResponseApdu.from_bytes(data)
         assert rlre.reason == enumerations.ReleaseResponseReason.NORMAL
-        assert isinstance(rlre.user_information.content, xdlms.GlobalCipherInitiateResponse)
+        assert isinstance(
+            rlre.user_information.content, xdlms.GlobalCipherInitiateResponse
+        )
diff --git a/tests/test_asce/test_rlrq.py b/tests/test_asce/test_rlrq.py
index 7b2a337..e961666 100644
--- a/tests/test_asce/test_rlrq.py
+++ b/tests/test_asce/test_rlrq.py
@@ -1,6 +1,7 @@
 import pytest
 
-from dlms_cosem.protocol.acse import ReleaseRequestApdu, enumerations
+from dlms_cosem import enumerations
+from dlms_cosem.protocol.acse import ReleaseRequestApdu
 
 
 class TestDecodeRLRQ:
diff --git a/tests/test_cosem.py b/tests/test_cosem.py
index ada1232..65187e1 100644
--- a/tests/test_cosem.py
+++ b/tests/test_cosem.py
@@ -1,10 +1,7 @@
-import pytest
-
-from dlms_cosem.protocol import cosem
+from dlms_cosem import cosem
 
 
 class TestObis:
-
     def test_obis_to_dotted(self):
         obis = cosem.Obis(1, 0, 1, 8, 0, 255)
         assert obis.dotted_repr() == "1.0.1.8.0.255"
@@ -24,5 +21,3 @@ def test_obis_from_bytes(self):
     def test_obis_to_bytes(self):
         data = b"\x00\x00+\x01\x00\xff"
         assert cosem.Obis(0, 0, 43, 1, 0, 255).to_bytes() == data
-
-
diff --git a/tests/test_dlms_connection.py b/tests/test_dlms_connection.py
index 9428a36..df007dc 100644
--- a/tests/test_dlms_connection.py
+++ b/tests/test_dlms_connection.py
@@ -1,20 +1,14 @@
-import os
-
 import pytest
 
-from dlms_cosem.protocol.connection import DlmsConnection
-from dlms_cosem.protocol import (
-    acse,
-    xdlms,
-    state,
-    enumerations,
-    exceptions,
-    cosem,
-    dlms_data,
+from dlms_cosem import enumerations, exceptions, security, state
+from dlms_cosem.connection import (
+    DlmsConnection,
+    XDlmsApduFactory,
+    make_client_to_server_challenge,
 )
-from dlms_cosem.protocol.exceptions import LocalDlmsProtocolError
-from dlms_cosem.protocol.xdlms import Conformance, ActionResponse
-from dlms_cosem.protocol.xdlms.invoke_id_and_priority import InvokeIdAndPriority
+from dlms_cosem.exceptions import LocalDlmsProtocolError
+from dlms_cosem.protocol import acse, xdlms
+from dlms_cosem.protocol.xdlms import Conformance
 
 
 def test_conformance_exists_on_simple_init():
@@ -115,7 +109,7 @@ def test_receive_get_response_sets_state_to_ready():
 
 
 def test_receive_exception_response_sets_state_to_ready(
-    exception_response: xdlms.ExceptionResponseApdu
+    exception_response: xdlms.ExceptionResponseApdu,
 ):
     c = DlmsConnection(
         state=state.DlmsConnectionState(current_state=state.AWAITING_GET_RESPONSE),
@@ -140,6 +134,33 @@ def test_hls_is_started_automatically(
     )
 
 
+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.GeneralGlobalCipherApdu(
+        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_rejection_resets_connection_state(
     connection_with_hls: DlmsConnection,
     ciphered_hls_aare: acse.ApplicationAssociationResponseApdu,
@@ -150,6 +171,7 @@ def test_rejection_resets_connection_state(
     connection_with_hls.next_event()
     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
 
@@ -187,3 +209,45 @@ def test_not_able_to_send_rlrq(self, rlrq: acse.ReleaseRequestApdu):
 
         with pytest.raises(exceptions.PreEstablishedAssociationError):
             c.send(rlrq)
+
+
+class TestXDlmsApduFactory:
+    def test_nonexistent_tag_raises_key_error(self):
+        dumb_data = bytearray([255, 1, 10, 10, 23])
+        with pytest.raises(KeyError):
+            XDlmsApduFactory.apdu_from_bytes(dumb_data)
+
+
+class TestMakeClientToServerChallenge:
+    def test_no_auth_returns_none(self):
+        challenge = make_client_to_server_challenge(
+            enumerations.AuthenticationMechanism.NONE
+        )
+        assert challenge is None
+
+    def test_lls_returns_none(self):
+        challenge = make_client_to_server_challenge(
+            enumerations.AuthenticationMechanism.LLS
+        )
+        assert challenge is None
+
+    def test_hls_gmac_returns_correct_bytes(self):
+        challenge = make_client_to_server_challenge(
+            enumerations.AuthenticationMechanism.HLS_GMAC, 16
+        )
+        assert challenge
+        assert len(challenge) == 16
+        assert type(challenge) == bytes
+
+    def test_too_short_length_raises_value_error(self):
+
+        with pytest.raises(ValueError):
+            make_client_to_server_challenge(
+                enumerations.AuthenticationMechanism.HLS_GMAC, 7
+            )
+
+    def test_too_long_length_raises_value_error(self):
+        with pytest.raises(ValueError):
+            make_client_to_server_challenge(
+                enumerations.AuthenticationMechanism.HLS_GMAC, 65
+            )
diff --git a/tests/test_dlms_state.py b/tests/test_dlms_state.py
index 2a7a9a7..a11d080 100644
--- a/tests/test_dlms_state.py
+++ b/tests/test_dlms_state.py
@@ -1,8 +1,9 @@
 import pytest
 
-from dlms_cosem.protocol import acse, state, enumerations
+from dlms_cosem import enumerations, state
+from dlms_cosem.exceptions import LocalDlmsProtocolError
+from dlms_cosem.protocol import acse
 from dlms_cosem.protocol.acse import UserInformation
-from dlms_cosem.protocol.exceptions import LocalDlmsProtocolError
 from dlms_cosem.protocol.xdlms import Conformance, InitiateRequestApdu
 
 
diff --git a/tests/test_general_global_cipher.py b/tests/test_general_global_cipher.py
index 0a968cf..bd99829 100644
--- a/tests/test_general_global_cipher.py
+++ b/tests/test_general_global_cipher.py
@@ -1,11 +1,6 @@
-import pytest
+from dlms_cosem.connection import XDlmsApduFactory
+from dlms_cosem.protocol.xdlms import DataNotificationApdu, GeneralGlobalCipherApdu
 
-from dlms_cosem.protocol.xdlms import (
-    GeneralGlobalCipherApdu,
-    DataNotificationApdu,
-)
-
-from dlms_cosem.protocol.connection import XDlmsApduFactory
 
 def test_gen_glo_cipher_load():
     dlms_data = b"\xdb\x08/\x19\"\x91\x99\x16A\x03;0\x00\x00\x01\xe5\x02\\\xe9\xd2'\x1f\xd7\x8b\xe8\xc2\x04!\x1a\x91j\x9d\x7fX~\nz\x81L\xad\xea\x89\xe9Y?\x01\xf9.\xa8\xc0\x87\xb5\xbd\xfd\xef\xea\xb6\xbe\xcf(-\xfeI\xc0\x8f[\xe6\xdc\x84\x00"
@@ -18,6 +13,7 @@ def test_gen_glo_cipher_load():
 
     assert apdu.system_title == system_title
 
+
 def test_gen_glo_cipher_to_apdu():
     dlms_data = b"\xdb\x08/\x19\"\x91\x99\x16A\x03;0\x00\x00\x01\xe5\x02\\\xe9\xd2'\x1f\xd7\x8b\xe8\xc2\x04!\x1a\x91j\x9d\x7fX~\nz\x81L\xad\xea\x89\xe9Y?\x01\xf9.\xa8\xc0\x87\xb5\xbd\xfd\xef\xea\xb6\xbe\xcf(-\xfeI\xc0\x8f[\xe6\xdc\x84\x00"
 
@@ -29,11 +25,16 @@ def test_gen_glo_cipher_to_apdu():
 
     assert apdu.system_title == system_title
 
-    unportected_apdu_data = apdu.to_plain_apdu(encryption_key=b"MYDUMMYGLOBALKEY", authentication_key=b"MYDUMMYGLOBALKEY")
-    unportected_apdu = XDlmsApduFactory.apdu_from_bytes(apdu_bytes=unportected_apdu_data)
+    unportected_apdu_data = apdu.to_plain_apdu(
+        encryption_key=b"MYDUMMYGLOBALKEY", authentication_key=b"MYDUMMYGLOBALKEY"
+    )
+    unportected_apdu = XDlmsApduFactory.apdu_from_bytes(
+        apdu_bytes=unportected_apdu_data
+    )
 
     assert isinstance(unportected_apdu, DataNotificationApdu)
 
+
 def test_gen_glo_cipher_to_bytes():
 
     dlms_data = b"\xdb\x08/\x19\"\x91\x99\x16A\x03;0\x00\x00\x01\xe5\x02\\\xe9\xd2'\x1f\xd7\x8b\xe8\xc2\x04!\x1a\x91j\x9d\x7fX~\nz\x81L\xad\xea\x89\xe9Y?\x01\xf9.\xa8\xc0\x87\xb5\xbd\xfd\xef\xea\xb6\xbe\xcf(-\xfeI\xc0\x8f[\xe6\xdc\x84\x00"
diff --git a/tests/test_hdlc/test_hdlc.py b/tests/test_hdlc/test_hdlc.py
index f12cd6d..c51a6ae 100644
--- a/tests/test_hdlc/test_hdlc.py
+++ b/tests/test_hdlc/test_hdlc.py
@@ -1,10 +1,6 @@
 import pytest
 
-from dlms_cosem.protocol.hdlc import (
-    frames,
-    fields,
-    address,
-)
+from dlms_cosem.hdlc import address, fields, frames
 
 
 def test_hdlc_frame_format_field_from_bytes():
@@ -23,7 +19,7 @@ def test_hdlc_frame_format_filed_from_bytes_segmented():
 
 def test_hdlc_frame_format_raises_value_error_too_long():
     with pytest.raises(ValueError):
-        f = fields.DlmsHdlcFrameFormatField(length=999999, segmented=False)
+        fields.DlmsHdlcFrameFormatField(length=999999, segmented=False)
 
 
 def test_hdlc_frame_format_to_bytes_not_segmented():
@@ -36,14 +32,14 @@ def test_hdlc_frame_format_to_bytes_segmented():
     assert f.to_bytes().hex() == "a81d"
 
 
-
 class TestHdlcAddress:
     def test_find_address(self):
         frame = "7ea87e210396a4090f01160002020f02160002020f03160002020f04160002020f05160002020f0616000204120017110009060000160000ff0202010902030f0116010002030f0216010002030f0316010002030f0416010002030f0516010002030f0616010002030f0716010002030f0816010002030f09160100016ff37e"
         frame_bytes = bytes.fromhex(frame)
-        destination_address, source_address = address.HdlcAddress.find_address_in_frame_bytes(
-            frame_bytes
-        )
+        (
+            destination_address,
+            source_address,
+        ) = address.HdlcAddress.find_address_in_frame_bytes(frame_bytes)
         assert destination_address == (16, None, 1)
         assert source_address == (1, None, 1)
 
@@ -79,7 +75,6 @@ def test_crc(self):
 
 
 class TestHdlcFrameValidation:
-
     def test_frame_is_enclosed_by_hdlc_flag(self):
         data = b"\x7effff\x7e"
         assert frames.frame_is_enclosed_by_hdlc_flags(data)
diff --git a/tests/test_parsers.py b/tests/test_parsers.py
index 7c1970a..8208c5e 100644
--- a/tests/test_parsers.py
+++ b/tests/test_parsers.py
@@ -1,7 +1,5 @@
-import pytest
-from datetime import timedelta
-from dlms_cosem.protocol import cosem, enumerations
-from dlms_cosem.protocol.parsers import ProfileGenericBufferParser
+from dlms_cosem import cosem, enumerations
+from dlms_cosem.parsers import ProfileGenericBufferParser
 
 
 def test_parse_buffer():
diff --git a/tests/test_security.py b/tests/test_security.py
index 65d2133..b80f50d 100644
--- a/tests/test_security.py
+++ b/tests/test_security.py
@@ -1,8 +1,7 @@
-import pytest
 from cryptography.hazmat.primitives.ciphers import algorithms, modes
 from cryptography.hazmat.primitives.ciphers.base import Cipher
 
-from dlms_cosem.protocol.security import encrypt, decrypt, SecurityControlField, gmac
+from dlms_cosem.security import SecurityControlField, decrypt, encrypt, gmac
 
 
 def test_encrypt():
@@ -98,9 +97,9 @@ def test_gmac():
     )
     client_invocation_counter = int.from_bytes(bytes.fromhex("00000001"), "big")
     client_system_title = bytes.fromhex("4D4D4D0000000001")
-    server_system_title = bytes.fromhex("4D4D4D0000BC614E")
-    server_invocation_counter = int.from_bytes(bytes.fromhex("01234567"), "big")
-    client_to_server_challenge = bytes.fromhex("4B35366956616759")
+    # server_system_title = bytes.fromhex("4D4D4D0000BC614E")
+    # server_invocation_counter = int.from_bytes(bytes.fromhex("01234567"), "big")
+    # client_to_server_challenge = bytes.fromhex("4B35366956616759")
     server_to_client_challenge = bytes.fromhex("503677524A323146")
     result = gmac(
         security_control=security_control,
@@ -108,8 +107,7 @@ def test_gmac():
         auth_key=authentication_key,
         invocation_counter=client_invocation_counter,
         system_title=client_system_title,
-        challenge=server_to_client_challenge
-
+        challenge=server_to_client_challenge,
     )
     assert len(result) == 12
     assert result == bytes.fromhex("1A52FE7DD3E72748973C1E28")
@@ -118,13 +116,14 @@ def test_gmac():
 def test_gmac2():
     encryption_key = bytes.fromhex("000102030405060708090A0B0C0D0E0F")
     authentication_key = bytes.fromhex("D0D1D2D3D4D5D6D7D8D9DADBDCDDDEDF")
-    security_control = SecurityControlField(security_suite=0, authenticated=True,
-        encrypted=False)
+    security_control = SecurityControlField(
+        security_suite=0, authenticated=True, encrypted=False
+    )
     client_invocation_counter = int.from_bytes(bytes.fromhex("00000001"), "big")
     client_system_title = bytes.fromhex("4D4D4D0000000001")
-    server_system_title = bytes.fromhex("4D4D4D0000BC614E")
-    server_invocation_counter = int.from_bytes(bytes.fromhex("01234567"), "big")
-    client_to_server_challenge = bytes.fromhex("4B35366956616759")
+    # server_system_title = bytes.fromhex("4D4D4D0000BC614E")
+    # server_invocation_counter = int.from_bytes(bytes.fromhex("01234567"), "big")
+    # client_to_server_challenge = bytes.fromhex("4B35366956616759")
     server_to_client_challenge = bytes.fromhex("503677524A323146")
 
     iv = client_system_title + client_invocation_counter.to_bytes(4, "big")
@@ -132,15 +131,20 @@ def test_gmac2():
     assert iv == bytes.fromhex("4D4D4D000000000100000001")
 
     # Construct an AES-GCM Cipher object with the given key and iv
-    encryptor = Cipher(algorithms.AES(encryption_key),
-        modes.GCM(initialization_vector=iv, tag=None,
-                  min_tag_length=12), ).encryptor()
+    encryptor = Cipher(
+        algorithms.AES(encryption_key),
+        modes.GCM(initialization_vector=iv, tag=None, min_tag_length=12),
+    ).encryptor()
 
     # associated_data will be authenticated but not encrypted,
     # it must also be passed in on decryption.
-    associated_data = security_control.to_bytes() + authentication_key + server_to_client_challenge
+    associated_data = (
+        security_control.to_bytes() + authentication_key + server_to_client_challenge
+    )
 
-    assert associated_data == bytes.fromhex("10D0D1D2D3D4D5D6D7D8D9DADBDCDDDEDF503677524A323146")
+    assert associated_data == bytes.fromhex(
+        "10D0D1D2D3D4D5D6D7D8D9DADBDCDDDEDF503677524A323146"
+    )
     encryptor.authenticate_additional_data(associated_data)
 
     # Encrypt the plaintext and get the associated ciphertext.
diff --git a/tests/test_time.py b/tests/test_time.py
index d8ff5bd..af2ce57 100644
--- a/tests/test_time.py
+++ b/tests/test_time.py
@@ -1,9 +1,10 @@
-import pytest
 import datetime
+
+import pytest
 from dateutil.parser import parse as dt_parse
 
+from dlms_cosem.time import datetime_from_bytes, datetime_to_bytes
 
-from dlms_cosem.protocol.time import datetime_from_bytes, datetime_to_bytes
 
 @pytest.mark.parametrize(
     "bytes_representation, dt",
diff --git a/tests/test_udp_message.py b/tests/test_udp_message.py
index b80ddb0..1874dd5 100644
--- a/tests/test_udp_message.py
+++ b/tests/test_udp_message.py
@@ -3,7 +3,7 @@
 from dlms_cosem.protocol.wrappers import DlmsUdpMessage, WrapperHeader
 
 data_examples_encrypted_data_nofication = [
-    b'\x00\x01\x00\x01\x00\x01\x00F\xdb\x08/\x19"\x91\x99\x16A\x03;0\x00\x00\x01\xe5\x02\\\xe9\xd2\'\x1f\xd7\x8b\xe8\xc2\x04!\x1a\x91j\x9d\x7fX~\nz\x81L\xad\xea\x89\xe9Y?\x01\xf9.\xa8\xc0\x87\xb5\xbd\xfd\xef\xea\xb6\xbe\xcf(-\xfeI\xc0\x8f[\xe6\xdc\x84\x00',
+    b"\x00\x01\x00\x01\x00\x01\x00F\xdb\x08/\x19\"\x91\x99\x16A\x03;0\x00\x00\x01\xe5\x02\\\xe9\xd2'\x1f\xd7\x8b\xe8\xc2\x04!\x1a\x91j\x9d\x7fX~\nz\x81L\xad\xea\x89\xe9Y?\x01\xf9.\xa8\xc0\x87\xb5\xbd\xfd\xef\xea\xb6\xbe\xcf(-\xfeI\xc0\x8f[\xe6\xdc\x84\x00",
     b'\x00\x01\x00\x01\x00\x01\x00F\xdb\x08/\x19"\x91\x99\x16A\x03;0\x00\x00\x01\xe6\x03\xd4\xd3W{\x7fd\x994\xe3\xb7\xc7\x19\xa3\xde5\x1a\xb2\x8cz\xc7\xb8\xa1\xe4D\xb8\x96\x91\xe9%\x91\xce\x1e\xb2\x82}\xf97\xa2\xe5@(\x0fb\x11\xf4\x93d\x80/\xa0\xf5\xc4\x13',
     b'\x00\x01\x00\x01\x00\x01\x00F\xdb\x08/\x19"\x91\x99\x16A\x03;0\x00\x00\x01\xe7\x1c+\xbc?\xfb\x9aN9x\xf2k\xfa\xf5\xe9A\xe2i\xa2\xb6\x1dG\xb46\x1b/[\x1d"\xf5\xa0N\xffp\x8c\x9f\xfbI<@\x16:\x0e\x19x\xb7D\x9c\xec\x9c\xca\xe0\x8d\x19D',
     b'\x00\x01\x00\x01\x00\x01\x00F\xdb\x08/\x19"\x91\x99\x16A\x03;0\x00\x00\x01\xe8\xb1\xf9[\xdd.\xdbA\xd3V\xdbW\xeeQ, \xc6\xeace:U\xbb\x18q~A\x9fE\xe8\xd3\xb4\xf3C)\xf4\xce\xb2\x1c\x81A\xa7\xe3\xcc\x00\xf0k~-\x98\xd7j\xf4\xb8\x06',
@@ -16,14 +16,12 @@
     b'\x00\x01\x00\x01\x00\x01\x00F\xdb\x08/\x19"\x91\x99\x16A\x03;0\x00\x00\x01\xf0N@\xc8{\xde\xb0\xc12\xbfI"\xdf\xc2\x98\xae~pt\xf3\xec_\x1e\x0f\x93\xf36\xfd\x84\xa2\xdf\xb2\xbc\x0b\xed\x80\x84\xf4\xf2\xcf\xebzf\xb1\x16\xd2E\xc8\xb1k\x93\xefM\x1f\x88',
     b'\x00\x01\x00\x01\x00\x01\x00F\xdb\x08/\x19"\x91\x99\x16A\x03;0\x00\x00\x01\xf1\xce\xef\x1e-\xb6ad\x9a\xbc?\xc4\x1by+\x9a\xd5\xa9\xf0 J\xa16{i\xd5\xdc\x18\x0f\x8c\xd8\xaf\x8d\x99%\x9d\x1d\xfa\x16[\xaa\tg\xb1\xcej\xb9\x8a\xf8\xa5\xdb\x94(\xd3G',
     b'\x00\x01\x00\x01\x00\x01\x00F\xdb\x08/\x19"\x91\x99\x16A\x03;0\x00\x00\x01\xf2\xeb\xae\xa2s\xd5.\xd6V\xc0\x97wM\x08=G%]\x88b\xb57\x1d\xc0l\xf1 \xdcU\x81z;\x91\xc3\x86\xac/g\xca\xf7\x94\x1a=\x01\xb2\xb6|\xdd\x9d{\xbb\x871\x12K',
-
 ]
 
 
-
 def test_udp_message_from_bytes():
-    udp_header_data = b'\x00\x01\x00\x01\x00\x01\x00F'
-    dlms_data = b'\xdb\x08/\x19"\x91\x99\x16A\x03;0\x00\x00\x01\xe5\x02\\\xe9\xd2\'\x1f\xd7\x8b\xe8\xc2\x04!\x1a\x91j\x9d\x7fX~\nz\x81L\xad\xea\x89\xe9Y?\x01\xf9.\xa8\xc0\x87\xb5\xbd\xfd\xef\xea\xb6\xbe\xcf(-\xfeI\xc0\x8f[\xe6\xdc\x84\x00'
+    udp_header_data = b"\x00\x01\x00\x01\x00\x01\x00F"
+    dlms_data = b"\xdb\x08/\x19\"\x91\x99\x16A\x03;0\x00\x00\x01\xe5\x02\\\xe9\xd2'\x1f\xd7\x8b\xe8\xc2\x04!\x1a\x91j\x9d\x7fX~\nz\x81L\xad\xea\x89\xe9Y?\x01\xf9.\xa8\xc0\x87\xb5\xbd\xfd\xef\xea\xb6\xbe\xcf(-\xfeI\xc0\x8f[\xe6\xdc\x84\x00"
 
     udp_data = udp_header_data + dlms_data
 
@@ -38,11 +36,13 @@ def test_udp_message_from_bytes():
 
 
 def test_upd_message_to_bytes():
-    udp_header_data = b'\x00\x01\x00\x01\x00\x01\x00F'
-    dlms_data = b'\xdb\x08/\x19"\x91\x99\x16A\x03;0\x00\x00\x01\xe5\x02\\\xe9\xd2\'\x1f\xd7\x8b\xe8\xc2\x04!\x1a\x91j\x9d\x7fX~\nz\x81L\xad\xea\x89\xe9Y?\x01\xf9.\xa8\xc0\x87\xb5\xbd\xfd\xef\xea\xb6\xbe\xcf(-\xfeI\xc0\x8f[\xe6\xdc\x84\x00'
+    udp_header_data = b"\x00\x01\x00\x01\x00\x01\x00F"
+    dlms_data = b"\xdb\x08/\x19\"\x91\x99\x16A\x03;0\x00\x00\x01\xe5\x02\\\xe9\xd2'\x1f\xd7\x8b\xe8\xc2\x04!\x1a\x91j\x9d\x7fX~\nz\x81L\xad\xea\x89\xe9Y?\x01\xf9.\xa8\xc0\x87\xb5\xbd\xfd\xef\xea\xb6\xbe\xcf(-\xfeI\xc0\x8f[\xe6\xdc\x84\x00"
 
     udp_data = udp_header_data + dlms_data
-    wrapper_header = WrapperHeader(source_wport=1, destination_wport=1, length=len(dlms_data))
+    wrapper_header = WrapperHeader(
+        source_wport=1, destination_wport=1, length=len(dlms_data)
+    )
     message = DlmsUdpMessage(data=dlms_data, wrapper_header=wrapper_header)
     assert message.wrapper_header.version == 1
     assert message.wrapper_header.source_wport == 1
@@ -53,6 +53,7 @@ def test_upd_message_to_bytes():
 
     assert message.to_bytes() == udp_data
 
+
 # def test_udp_parsing():
 #     udp = UDPRequest(data_examples_encrypted_data_nofication[0])
 #
diff --git a/tests/test_xdlms/test_action.py b/tests/test_xdlms/test_action.py
new file mode 100644
index 0000000..51d4758
--- /dev/null
+++ b/tests/test_xdlms/test_action.py
@@ -0,0 +1,200 @@
+import pytest
+
+from dlms_cosem import cosem, enumerations
+from dlms_cosem.protocol import xdlms
+
+
+class TestActionRequestNormal:
+    def test_transform_bytes(self):
+        data = b'\xc3\x01\xc0\x00\x0f\x00\x00(\x00\x00\xff\x01\x01\t\x11\x10\x00\x00\x1a\x90\xe6\xd2"\x1f\xa2\xfd\x85\xee\xd6\x1a\xcc"'
+        action = xdlms.ActionRequestNormal(
+            cosem_method=cosem.CosemMethod(
+                interface=enumerations.CosemInterface.ASSOCIATION_LN,
+                instance=cosem.Obis(a=0, b=0, c=40, d=0, e=0, f=255),
+                method=1,
+            ),
+            data=b'\t\x11\x10\x00\x00\x1a\x90\xe6\xd2"\x1f\xa2\xfd\x85\xee\xd6\x1a\xcc"',
+            invoke_id_and_priority=xdlms.InvokeIdAndPriority(
+                invoke_id=0, confirmed=True, high_priority=True
+            ),
+        )
+
+        assert data == action.to_bytes()
+        assert action == xdlms.ActionRequestNormal.from_bytes(data)
+
+    def test_transform_bytes_without_data(self):
+        data = b"\xc3\x01\xc0\x00\x0f\x00\x00(\x00\x00\xff\x01\x00"
+        action = xdlms.ActionRequestNormal(
+            cosem_method=cosem.CosemMethod(
+                interface=enumerations.CosemInterface.ASSOCIATION_LN,
+                instance=cosem.Obis(a=0, b=0, c=40, d=0, e=0, f=255),
+                method=1,
+            ),
+            data=None,
+            invoke_id_and_priority=xdlms.InvokeIdAndPriority(
+                invoke_id=0, confirmed=True, high_priority=True
+            ),
+        )
+
+        assert data == action.to_bytes()
+        assert action == xdlms.ActionRequestNormal.from_bytes(data)
+
+    def test_wrong_tag_raises_valueerror(self):
+        data = b'\xc4\x01\xc0\x00\x0f\x00\x00(\x00\x00\xff\x01\x01\t\x11\x10\x00\x00\x1a\x90\xe6\xd2"\x1f\xa2\xfd\x85\xee\xd6\x1a\xcc"'
+
+        with pytest.raises(ValueError):
+            xdlms.ActionRequestNormal.from_bytes(data)
+
+    def test_wrong_request_type_raises_valueerror(self):
+        data = b'\xc3\x02\xc0\x00\x0f\x00\x00(\x00\x00\xff\x01\x01\t\x11\x10\x00\x00\x1a\x90\xe6\xd2"\x1f\xa2\xfd\x85\xee\xd6\x1a\xcc"'
+
+        with pytest.raises(ValueError):
+            xdlms.ActionRequestNormal.from_bytes(data)
+
+
+class TestActionRequestFactory:
+    def test_normal_with_data(self):
+        data = b'\xc3\x01\xc0\x00\x0f\x00\x00(\x00\x00\xff\x01\x01\t\x11\x10\x00\x00\x1a\x90\xe6\xd2"\x1f\xa2\xfd\x85\xee\xd6\x1a\xcc"'
+        action = xdlms.ActionRequestFactory.from_bytes(data)
+        assert isinstance(action, xdlms.ActionRequestNormal)
+
+    def test_normal_without_data(self):
+        data = b"\xc3\x01\xc0\x00\x0f\x00\x00(\x00\x00\xff\x01\x00"
+        action = xdlms.ActionRequestFactory.from_bytes(data)
+        assert isinstance(action, xdlms.ActionRequestNormal)
+        assert action.data is None
+
+    def test_wrong_tag_raises_valueerror(self):
+        data = b"\xc4\x01\xc0\x00\x0f\x00\x00(\x00\x00\xff\x01\x00"
+        with pytest.raises(ValueError):
+            xdlms.ActionRequestFactory.from_bytes(data)
+
+    def test_any_other_type_than_normal_raises_notimplementederror(self):
+        data = b"\xc3\x02\xc0\x00\x0f\x00\x00(\x00\x00\xff\x01\x00"
+        with pytest.raises(NotImplementedError):
+            xdlms.ActionRequestFactory.from_bytes(data)
+
+
+class TestActionResponseNormal:
+    def test_transform_bytes(self):
+        data = b"\xc7\x01\xc0\x00\x00"
+        action = xdlms.ActionResponseNormal(
+            enumerations.ActionResultStatus.SUCCESS,
+            invoke_id_and_priority=xdlms.InvokeIdAndPriority(
+                invoke_id=0, confirmed=True, high_priority=True
+            ),
+        )
+        assert data == action.to_bytes()
+        assert action == xdlms.ActionResponseNormal.from_bytes(data)
+
+    def test_wrong_tag_raises_valueerror(self):
+        data = b"\xc8\x01\xc0\x00\x01\x00\t\x11\x10\x00\x00\x1a\xfd\xe8\x85{r\x8a4\x99\x10j\xa6e\xd1"
+        with pytest.raises(ValueError):
+            xdlms.ActionResponseNormal.from_bytes(data)
+
+    def test_wrong_type_raises_valueerror(self):
+        data = b"\xc7\x02\xc0\x00\x01\x00\t\x11\x10\x00\x00\x1a\xfd\xe8\x85{r\x8a4\x99\x10j\xa6e\xd1"
+        with pytest.raises(ValueError):
+            xdlms.ActionResponseNormal.from_bytes(data)
+
+    def test_has_data_raises_valueerror(self):
+        data = b"\xc7\x01\xc0\x00\x01\x00\t\x11\x10\x00\x00\x1a\xfd\xe8\x85{r\x8a4\x99\x10j\xa6e\xd1"
+        with pytest.raises(ValueError):
+            xdlms.ActionResponseNormal.from_bytes(data)
+
+
+class TestActionResponseNormalWithData:
+    def test_transform_bytes(self):
+        data = b"\xc7\x01\xc0\x00\x01\x00\t\x11\x10\x00\x00\x1a\xfd\xe8\x85{r\x8a4\x99\x10j\xa6e\xd1"
+        action = xdlms.ActionResponseNormalWithData(
+            enumerations.ActionResultStatus.SUCCESS,
+            data=bytearray(
+                b"\t\x11\x10\x00\x00\x1a\xfd\xe8\x85{r\x8a4\x99\x10j\xa6e\xd1"
+            ),
+            invoke_id_and_priority=xdlms.InvokeIdAndPriority(
+                invoke_id=0, confirmed=True, high_priority=True
+            ),
+        )
+        assert data == action.to_bytes()
+        assert action == xdlms.ActionResponseNormalWithData.from_bytes(data)
+
+    def test_wrong_tag_raises_valueerror(self):
+        data = b"\xc8\x01\xc0\x00\x01\x00\t\x11\x10\x00\x00\x1a\xfd\xe8\x85{r\x8a4\x99\x10j\xa6e\xd1"
+        with pytest.raises(ValueError):
+            xdlms.ActionResponseNormalWithData.from_bytes(data)
+
+    def test_wrong_type_raises_valueerror(self):
+        data = b"\xc7\x02\xc0\x00\x01\x00\t\x11\x10\x00\x00\x1a\xfd\xe8\x85{r\x8a4\x99\x10j\xa6e\xd1"
+        with pytest.raises(ValueError):
+            xdlms.ActionResponseNormalWithData.from_bytes(data)
+
+    def test_no_data_raises_valueerror(self):
+        data = b"\xc7\x01\xc0\x00\x00"
+        with pytest.raises(ValueError):
+            xdlms.ActionResponseNormalWithData.from_bytes(data)
+
+    def test_holds_error_instead_of_data_raises_valueerror(self):
+        data = b"\xc7\x01\xc0\x00\x01\x01\x01"
+        with pytest.raises(ValueError):
+            xdlms.ActionResponseNormalWithData.from_bytes(data)
+
+
+class TestActionResponseWithError:
+    def test_transform_bytes(self):
+        data = b"\xc7\x01\xc0\x00\x01\x01\xfa"
+        action = xdlms.ActionResponseNormalWithError(
+            enumerations.ActionResultStatus.SUCCESS,
+            error=enumerations.DataAccessResult.OTHER_REASON,
+            invoke_id_and_priority=xdlms.InvokeIdAndPriority(
+                invoke_id=0, confirmed=True, high_priority=True
+            ),
+        )
+        assert data == action.to_bytes()
+        assert action == xdlms.ActionResponseNormalWithError.from_bytes(data)
+
+    def test_wrong_tag_raises_valueerror(self):
+        data = b"\xc8\x01\xc0\x00\x01\x01\xfa"
+        with pytest.raises(ValueError):
+            xdlms.ActionResponseNormalWithError.from_bytes(data)
+
+    def test_wrong_type_raises_valueerror(self):
+        data = b"\xc7\x02\xc0\x00\x01\x01\xfa"
+        with pytest.raises(ValueError):
+            xdlms.ActionResponseNormalWithError.from_bytes(data)
+
+    def test_no_data_raises_valueerror(self):
+        data = b"\xc7\x01\xc0\x00\x00"
+        with pytest.raises(ValueError):
+            xdlms.ActionResponseNormalWithError.from_bytes(data)
+
+    def test_holds_data_instead_of_error_raises_valueerror(self):
+        data = b"\xc7\x01\xc0\x00\x01\x00\t\x11\x10\x00\x00\x1a\xfd\xe8\x85{r\x8a4\x99\x10j\xa6e\xd1"
+        with pytest.raises(ValueError):
+            xdlms.ActionResponseNormalWithError.from_bytes(data)
+
+
+class TestActionResponseFactory:
+    def test_parse_action_response_normal(self):
+        data = b"\xc7\x01\xc0\x00\x00"
+        action = xdlms.ActionResponseFactory.from_bytes(data)
+        assert isinstance(action, xdlms.ActionResponseNormal)
+
+    def test_parse_action_response_normal_with_data(self):
+        data = b"\xc7\x01\xc0\x00\x01\x00\t\x11\x10\x00\x00\x1a\xfd\xe8\x85{r\x8a4\x99\x10j\xa6e\xd1"
+        action = xdlms.ActionResponseFactory.from_bytes(data)
+        assert isinstance(action, xdlms.ActionResponseNormalWithData)
+
+    def test_parse_action_response_normal_with_error(self):
+        data = b"\xc7\x01\xc0\x00\x01\x01\xfa"
+        action = xdlms.ActionResponseFactory.from_bytes(data)
+        assert isinstance(action, xdlms.ActionResponseNormalWithError)
+
+    def test_wrong_tag_raises_valueerror(self):
+        data = b"\xc8\x01\xc0\x00\x01\x01\xfa"
+        with pytest.raises(ValueError):
+            xdlms.ActionResponseFactory.from_bytes(data)
+
+    def test_type_other_than_normal_raises_not_implemented_error(self):
+        data = b"\xc7\x02\xc0\x00\x01\x01\xfa"
+        with pytest.raises(NotImplementedError):
+            xdlms.ActionResponseFactory.from_bytes(data)
diff --git a/tests/test_xdlms/test_get.py b/tests/test_xdlms/test_get.py
index 2120ee8..e23217c 100644
--- a/tests/test_xdlms/test_get.py
+++ b/tests/test_xdlms/test_get.py
@@ -1,19 +1,19 @@
 import pytest
 
-from dlms_cosem.protocol import enumerations
-from dlms_cosem.protocol.cosem import CosemAttribute, Obis
-from dlms_cosem.protocol.xdlms import GetResponseNormal, GetRequestNormal
+from dlms_cosem import enumerations
+from dlms_cosem.cosem import CosemAttribute, Obis
+from dlms_cosem.dlms_data import DoubleLongUnsignedData
+from dlms_cosem.protocol.xdlms import GetRequestNormal, GetResponseNormal
 from dlms_cosem.protocol.xdlms.get import (
-    InvokeIdAndPriority,
-    GetRequestNext,
     GetRequestFactory,
-    GetResponseNormalWithError,
-    GetResponseWithBlock,
+    GetRequestNext,
+    GetResponseFactory,
     GetResponseLastBlock,
     GetResponseLastBlockWithError,
-    GetResponseFactory,
+    GetResponseNormalWithError,
+    GetResponseWithBlock,
+    InvokeIdAndPriority,
 )
-from dlms_cosem.protocol.dlms_data import DoubleLongUnsignedData
 
 
 class TestGetRequestNormal:
@@ -42,8 +42,8 @@ def test_wrong_tag_raises_valueerror(self):
 
     def test_wrong_request_type_raises_valueerror(self):
         data = (
-            b"\xc0\x02\xc1\x00\x01\x00\x00+\x01\x00\xff\x02\x00"
-        )  # Wrong request type
+            b"\xc0\x02\xc1\x00\x01\x00\x00+\x01\x00\xff\x02\x00"  # Wrong request type
+        )
         with pytest.raises(ValueError):
             GetRequestNormal.from_bytes(data)
 
@@ -96,17 +96,17 @@ def test_get_request_next(self):
     def test_wrong_tag_raises_valuerror(self):
         data = b"\xc1\x02\xc1\x00\x00\x00\x01"  # Wrong tag
         with pytest.raises(ValueError):
-            apdu = GetRequestFactory.from_bytes(data)
+            GetRequestFactory.from_bytes(data)
 
     def test_with_list_raises_notimplemented(self):
         data = b"\xc0\x03\xc1\x00\x00\x00\x01"  # With list type
         with pytest.raises(NotImplementedError):
-            apdu = GetRequestFactory.from_bytes(data)
+            GetRequestFactory.from_bytes(data)
 
     def test_invalid_type_raises_valueerror(self):
         data = b"\xc0\x04\xc1\x00\x00\x00\x01"  # 0x04 type not possible
         with pytest.raises(ValueError):
-            apdu = GetRequestFactory.from_bytes(data)
+            GetRequestFactory.from_bytes(data)
 
 
 class TestGetResponseNormal:
@@ -376,6 +376,7 @@ def test_get_response_last_block_with_error(self):
         apdu = GetResponseFactory.from_bytes(data)
 
         assert isinstance(apdu, GetResponseLastBlockWithError)
+
     def test_wrong_tag_raises_valueerror(self):
         data = b"\xcc\x02\xc1\x01\x00\x00\x00\x13\x01\x01"
         with pytest.raises(ValueError):
diff --git a/tests/test_xdlms/test_initiate.py b/tests/test_xdlms/test_initiate.py
index 780a591..0c275ac 100644
--- a/tests/test_xdlms/test_initiate.py
+++ b/tests/test_xdlms/test_initiate.py
@@ -1,6 +1,6 @@
-import pytest
-from dlms_cosem.protocol import xdlms, security
-from dlms_cosem.protocol.connection import XDlmsApduFactory
+from dlms_cosem import security
+from dlms_cosem.connection import XDlmsApduFactory
+from dlms_cosem.protocol import xdlms
 
 
 class TestInitiateRequest:
diff --git a/tests/test_xdlms/test_selective_access.py b/tests/test_xdlms/test_selective_access.py
index 8cb5d54..d2b701b 100644
--- a/tests/test_xdlms/test_selective_access.py
+++ b/tests/test_xdlms/test_selective_access.py
@@ -1,7 +1,6 @@
-import pytest
 from dateutil import parser
 
-from dlms_cosem.protocol import cosem, enumerations
+from dlms_cosem import cosem, enumerations
 from dlms_cosem.protocol.xdlms import GetRequestFactory, selective_access
 from dlms_cosem.protocol.xdlms.selective_access import RangeDescriptor
 
@@ -45,6 +44,7 @@ def test_range_descriptor1():
         b"\t\x0c\x07\xe5\x01\x06\xff\x00\x03\x00\xff\xff\xc4\x00"  # to date
         b"\x01\x00"  # all columns
     )
+    assert data
 
     data2 = (
         b"\xc0\x01\xc1\x00\x07\x01\x00c\x01\x00\xff\x02\x01\x01"
@@ -58,6 +58,7 @@ def test_range_descriptor1():
         b"\t\x0c\x07\xe3\x02\x0c\xff\x00\x00\x00\x00\x80\x00\x00"
         b"\x01\x00"
     )
+    assert data2
 
 
 def test_range_descriptor_to_bytes():
@@ -102,4 +103,5 @@ def test_range_descriptor_is_not_parsed():
         b"\t\x0c\x07\xe1\n\x01\x07\x01\x00\x00\x00\xff\xc4\x80"  # to date
         b"\x01\x00"  # selected_values empty array.
     )
+    assert access
     assert g.access_selection == b"\x02"