Repository: XKNX/xknx (python)

Issue:
Support for KNXnet/IP Device Management Connection
Currently `ConnectRequest` only supports tunnelling (basic or extended tunnel CRI), would it be possible to add support for opening device management connections?

Pull request description:
Support different connection types in `ConnectRequest` and `ConnectResponse` frames
<!--
  You are awesome! Thanks for contributing to our project!
  Please, DO NOT DELETE ANY TEXT from this template!.
-->
## Description
<!--
Please include a summary of the change and which issue is fixed.
Please also include relevant motivation and context.
List any dependencies that are required for this change.
-->

This PR adds support for a Device Management Connection to an KNXnet/IP server.

Fixes #1197

## Type of change
<!--
Please tick the applicable options.
NOTE: Ticking multiple options most likely indicates
that your change is to big and it is suggested to split it into several smaller PRs.
-->

- [ ] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Code quality improvements to existing code or addition of tests

## Checklist

- [ ] The documentation has been adjusted accordingly
- [x] Tests have been added that prove the fix is effective or that the feature works
- [ ] The changes are documented in the changelog (docs/changelog.md)

Patch:
diff --git a/xknx/io/request_response/connect.py b/xknx/io/request_response/connect.py
index 355d41d1..bbe1a4e8 100644
--- a/xknx/io/request_response/connect.py
+++ b/xknx/io/request_response/connect.py
@@ -8,9 +8,9 @@ from xknx.knxip import (
     ConnectRequest,
     ConnectRequestInformation,
     ConnectResponse,
+    ConnectResponseData,
     KNXIPFrame,
 )
-from xknx.telegram import IndividualAddress
 
 from .request_response import RequestResponse
 
@@ -29,24 +29,23 @@ class Connect(RequestResponse):
         self,
         transport: KNXIPTransport,
         local_hpai: HPAI,
-        individual_address: IndividualAddress | None = None,
+        cri: ConnectRequestInformation | None = None,
     ):
         """Initialize Connect class."""
         super().__init__(transport, ConnectResponse)
         self.communication_channel = 0
         self.data_endpoint = HPAI()
-        self.identifier = 0
-        self.individual_address = individual_address
         self.local_hpai = local_hpai
+        self.cri = cri or ConnectRequestInformation()
+        self.crd = ConnectResponseData()
 
     def create_knxipframe(self) -> KNXIPFrame:
         """Create KNX/IP Frame object to be sent to device."""
-        cri = ConnectRequestInformation(individual_address=self.individual_address)
         # use the same HPAI for control_endpoint and data_endpoint
         connect_request = ConnectRequest(
             control_endpoint=self.local_hpai,
             data_endpoint=self.local_hpai,
-            cri=cri,
+            cri=self.cri,
         )
         return KNXIPFrame.init_from_body(connect_request)
 
@@ -55,4 +54,4 @@ class Connect(RequestResponse):
         assert isinstance(knxipframe.body, ConnectResponse)
         self.communication_channel = knxipframe.body.communication_channel
         self.data_endpoint = knxipframe.body.data_endpoint
-        self.identifier = knxipframe.body.identifier
+        self.crd = knxipframe.body.crd
diff --git a/xknx/io/tunnel.py b/xknx/io/tunnel.py
index 3e4979d0..83e98841 100644
--- a/xknx/io/tunnel.py
+++ b/xknx/io/tunnel.py
@@ -19,6 +19,7 @@ from xknx.exceptions import (
 )
 from xknx.knxip import (
     HPAI,
+    ConnectRequestInformation,
     DisconnectRequest,
     DisconnectResponse,
     HostProtocol,
@@ -190,7 +191,7 @@ class _Tunnel(Interface):
         connect = Connect(
             transport=self.transport,
             local_hpai=self.local_hpai,
-            individual_address=self._requested_address,
+            cri=ConnectRequestInformation(individual_address=self._requested_address),
         )
         await connect.start()
         if connect.success:
@@ -202,7 +203,7 @@ class _Tunnel(Interface):
                 else connect.data_endpoint.addr_tuple
             )
             # Use the individual address provided by the tunnelling server
-            self._src_address = IndividualAddress(connect.identifier)
+            self._src_address = connect.crd.individual_address or IndividualAddress(0)
             self.xknx.current_address = self._src_address
             logger.debug(
                 "Tunnel established. communication_channel=%s, address=%s",
diff --git a/xknx/knxip/__init__.py b/xknx/knxip/__init__.py
index 07b0e121..00fc76af 100644
--- a/xknx/knxip/__init__.py
+++ b/xknx/knxip/__init__.py
@@ -2,7 +2,7 @@
 # flake8: noqa
 from .body import KNXIPBody, KNXIPBodyResponse
 from .connect_request import ConnectRequest, ConnectRequestInformation
-from .connect_response import ConnectResponse
+from .connect_response import ConnectResponse, ConnectResponseData
 from .connectionstate_request import ConnectionStateRequest
 from .connectionstate_response import ConnectionStateResponse
 from .description_request import DescriptionRequest
@@ -54,6 +54,7 @@ __all__ = [
     "ConnectRequest",
     "ConnectRequestInformation",
     "ConnectResponse",
+    "ConnectResponseData",
     "ConnectionStateRequest",
     "ConnectionStateResponse",
     "DescriptionRequest",
diff --git a/xknx/knxip/connect_request.py b/xknx/knxip/connect_request.py
index f8e56344..30f26b14 100644
--- a/xknx/knxip/connect_request.py
+++ b/xknx/knxip/connect_request.py
@@ -67,8 +67,9 @@ class ConnectRequestInformation:
     supported by Tunnelling v2 devices.
     """
 
-    CRI_BASIC_LENGTH = 4
-    CRI_EXTENDED_LENGTH = 6
+    CRI_LENGTH = 2
+    CRI_TUNNEL_LENGTH = 4
+    CRI_TUNNEL_EXT_LENGTH = 6
 
     def __init__(
         self,
@@ -81,29 +82,40 @@ class ConnectRequestInformation:
         self.knx_layer = knx_layer
         self.individual_address = individual_address
 
+    def _is_tunnel_cri(self) -> bool:
+        return self.connection_type == ConnectRequestType.TUNNEL_CONNECTION
+
     def calculated_length(self) -> int:
         """Get length of KNX/IP body."""
-        return (
-            ConnectRequestInformation.CRI_EXTENDED_LENGTH
-            if self.individual_address
-            else ConnectRequestInformation.CRI_BASIC_LENGTH
-        )
+        if self._is_tunnel_cri():
+            return (
+                ConnectRequestInformation.CRI_TUNNEL_EXT_LENGTH
+                if self.individual_address
+                else ConnectRequestInformation.CRI_TUNNEL_LENGTH
+            )
+        return ConnectRequestInformation.CRI_LENGTH
 
     def from_knx(self, raw: bytes) -> int:
         """Parse/deserialize from KNX/IP raw data."""
         cri_length = raw[0]
-        if cri_length == ConnectRequestInformation.CRI_BASIC_LENGTH:
-            extended = False
-        elif cri_length == ConnectRequestInformation.CRI_EXTENDED_LENGTH:
-            extended = True
-        else:
-            raise CouldNotParseKNXIP("CRI has wrong length")
         if len(raw) < cri_length:
             raise CouldNotParseKNXIP("CRI data has wrong length")
+        if cri_length < ConnectRequestInformation.CRI_LENGTH:
+            raise CouldNotParseKNXIP("CRI length too small")
         self.connection_type = ConnectRequestType(raw[1])
-        self.knx_layer = TunnellingLayer(raw[2])
-        if extended:
-            self.individual_address = IndividualAddress.from_knx(raw[4:6])
+        if self._is_tunnel_cri():
+            if cri_length == ConnectRequestInformation.CRI_TUNNEL_LENGTH:
+                extended = False
+            elif cri_length == ConnectRequestInformation.CRI_TUNNEL_EXT_LENGTH:
+                extended = True
+            else:
+                raise CouldNotParseKNXIP("CRI has wrong length")
+            self.knx_layer = TunnellingLayer(raw[2])
+            self.individual_address = (
+                IndividualAddress.from_knx(raw[4:6]) if extended else None
+            )
+        elif cri_length != ConnectRequestInformation.CRI_LENGTH:
+            raise CouldNotParseKNXIP("CRI has wrong length")
         return cri_length
 
     def to_knx(self) -> bytes:
@@ -112,13 +124,19 @@ class ConnectRequestInformation:
             (
                 self.calculated_length(),
                 self.connection_type.value,
-                self.knx_layer.value,
-                0x00,  # Reserved
             )
         )
-        if self.individual_address is None:
-            return _cri
-        return _cri + self.individual_address.to_knx()
+        if self._is_tunnel_cri():
+            _cri = _cri + bytes(
+                (
+                    self.knx_layer.value,
+                    0x00,  # Reserved
+                )
+            )
+            if self.individual_address:
+                _cri = _cri + self.individual_address.to_knx()
+
+        return _cri
 
     def __eq__(self, other: object) -> bool:
         """Equal operator."""
@@ -126,14 +144,17 @@ class ConnectRequestInformation:
 
     def __repr__(self) -> str:
         """Return object as readable string."""
+        _tunnel_layer = (
+            f'knx_layer="{self.knx_layer.name}" ' if self._is_tunnel_cri() else ""
+        )
         _extended = (
             f'individual_address="{self.individual_address}" '
-            if self.individual_address
+            if self._is_tunnel_cri() and self.individual_address
             else ""
         )
         return (
             "<ConnectRequestInformation "
             f'connection_type="{self.connection_type.name}" '
-            f'knx_layer="{self.knx_layer.name}" '
+            f"{_tunnel_layer}"
             f"{_extended}/>"
         )
diff --git a/xknx/knxip/connect_response.py b/xknx/knxip/connect_response.py
index f053de36..6661b9fe 100644
--- a/xknx/knxip/connect_response.py
+++ b/xknx/knxip/connect_response.py
@@ -8,6 +8,7 @@ assigns a communication channel and an individual address for the client.
 from __future__ import annotations
 
 from xknx.exceptions import CouldNotParseKNXIP
+from xknx.telegram import IndividualAddress
 
 from .body import KNXIPBodyResponse
 from .error_code import ErrorCode
@@ -19,48 +20,34 @@ class ConnectResponse(KNXIPBodyResponse):
     """Representation of a KNX Connect Response."""
 
     SERVICE_TYPE = KNXIPServiceType.CONNECT_RESPONSE
-    CRD_LENGTH = 4
 
     def __init__(
         self,
         communication_channel: int = 0,
         status_code: ErrorCode = ErrorCode.E_NO_ERROR,
-        request_type: ConnectRequestType = ConnectRequestType.TUNNEL_CONNECTION,
         data_endpoint: HPAI | None = None,
-        identifier: int = 0,
+        crd: ConnectResponseData | None = None,
     ):
         """Initialize ConnectResponse class."""
         self.communication_channel = communication_channel
         self.status_code = status_code
-        self.request_type = request_type
         self.data_endpoint = data_endpoint or HPAI()
-        # identifier shall contain KNX Individual Address assigned to this KNXnet/IP Tunnelling connection
-        self.identifier = identifier
+        self.crd = crd or ConnectResponseData()
 
     def calculated_length(self) -> int:
         """Get length of KNX/IP body."""
-        return 2 + HPAI.LENGTH + ConnectResponse.CRD_LENGTH
+        return 2 + HPAI.LENGTH + self.crd.calculated_length()
 
     def from_knx(self, raw: bytes) -> int:
         """Parse/deserialize from KNX/IP raw data."""
 
-        def crd_from_knx(crd: bytes) -> int:
-            """Parse CRD (Connection Response Data Block)."""
-            if crd[0] != ConnectResponse.CRD_LENGTH:
-                raise CouldNotParseKNXIP("CRD has wrong length")
-            if len(crd) < ConnectResponse.CRD_LENGTH:
-                raise CouldNotParseKNXIP("CRD data has wrong length")
-            self.request_type = ConnectRequestType(crd[1])
-            self.identifier = crd[2] * 256 + crd[3]
-            return 4
-
         self.communication_channel = raw[0]
         self.status_code = ErrorCode(raw[1])
         pos = 2
 
         if self.status_code == ErrorCode.E_NO_ERROR:
             pos += self.data_endpoint.from_knx(raw[pos:])
-            pos += crd_from_knx(raw[pos:])
+            pos += self.crd.from_knx(raw[pos:])
         else:
             # do not parse HPAI and CRD in case of errors - just check length
             pos = len(raw)
@@ -69,18 +56,10 @@ class ConnectResponse(KNXIPBodyResponse):
     def to_knx(self) -> bytes:
         """Serialize to KNX/IP raw data."""
 
-        def crd_to_knx() -> bytes:
-            """Serialize CRD (Connect Response Data Block)."""
-            assert self.identifier is not None
-
-            return bytes(
-                (ConnectResponse.CRD_LENGTH, self.request_type.value)
-            ) + self.identifier.to_bytes(2, "big")
-
         return (
             bytes((self.communication_channel, self.status_code.value))
             + self.data_endpoint.to_knx()
-            + crd_to_knx()
+            + self.crd.to_knx()
         )
 
     def __repr__(self) -> str:
@@ -90,6 +69,79 @@ class ConnectResponse(KNXIPBodyResponse):
             f'communication_channel="{self.communication_channel}" '
             f'status_code="{self.status_code}" '
             f'data_endpoint="{self.data_endpoint}" '
+            f'crd="{self.crd}" />'
+        )
+
+
+class ConnectResponseData:
+    """Representation of a KNX Connect Response Data block (CRD)."""
+
+    CRD_LENGTH = 2
+    CRD_TUNNEL_LENGTH = 4
+
+    def __init__(
+        self,
+        request_type: ConnectRequestType = ConnectRequestType.TUNNEL_CONNECTION,
+        individual_address: IndividualAddress | None = None,
+    ):
+        """Initialize ConnectResponseData object."""
+        self.request_type = request_type
+        self.individual_address = individual_address
+
+    def _is_tunnel_crd(self) -> bool:
+        return self.request_type == ConnectRequestType.TUNNEL_CONNECTION
+
+    def calculated_length(self) -> int:
+        """Get length of KNX/IP body."""
+        return (
+            ConnectResponseData.CRD_TUNNEL_LENGTH
+            if self._is_tunnel_crd()
+            else ConnectResponseData.CRD_LENGTH
+        )
+
+    def from_knx(self, raw: bytes) -> int:
+        """Parse/deserialize from KNX/IP raw data."""
+        crd_length = raw[0]
+        if len(raw) < crd_length:
+            raise CouldNotParseKNXIP("CRD has wrong length")
+        if crd_length < ConnectResponseData.CRD_LENGTH:
+            raise CouldNotParseKNXIP("CRD length too small")
+        self.request_type = ConnectRequestType(raw[1])
+        if self._is_tunnel_crd():
+            if crd_length != ConnectResponseData.CRD_TUNNEL_LENGTH:
+                raise CouldNotParseKNXIP("CRD has wrong length")
+            self.individual_address = IndividualAddress.from_knx(raw[2:4])
+        elif crd_length != ConnectResponseData.CRD_LENGTH:
+            raise CouldNotParseKNXIP("CRD has wrong length")
+        return crd_length
+
+    def to_knx(self) -> bytes:
+        """Serialize CRD (Connect Response Data Block)."""
+        _crd = bytes(
+            (
+                self.calculated_length(),
+                self.request_type.value,
+            )
+        )
+        if self._is_tunnel_crd():
+            assert self.individual_address is not None
+            return _crd + self.individual_address.to_knx()
+
+        return _crd
+
+    def __eq__(self, other: object) -> bool:
+        """Equal operator."""
+        return self.__dict__ == other.__dict__
+
+    def __repr__(self) -> str:
+        """Return object as readable string."""
+        _address = (
+            f'individual_address="{self.individual_address}" '
+            if self._is_tunnel_crd() and self.individual_address
+            else ""
+        )
+        return (
+            "<ConnectResponseData "
             f'request_type="{self.request_type}" '
-            f'identifier="{self.identifier}" />'
+            f"{_address}/>"
         )


Test patch:
diff --git a/test/io_tests/request_response_tests/connect_test.py b/test/io_tests/request_response_tests/connect_test.py
index 6c77b373..7bda3eaa 100644
--- a/test/io_tests/request_response_tests/connect_test.py
+++ b/test/io_tests/request_response_tests/connect_test.py
@@ -8,9 +8,11 @@ from xknx.knxip import (
     ConnectionStateRequest,
     ConnectRequest,
     ConnectResponse,
+    ConnectResponseData,
     ErrorCode,
     KNXIPFrame,
 )
+from xknx.telegram import IndividualAddress
 
 
 class TestConnect:
@@ -63,13 +65,13 @@ class TestConnect:
         res_knxipframe = KNXIPFrame.init_from_body(
             ConnectResponse(
                 communication_channel=23,
-                identifier=7,
+                crd=ConnectResponseData(individual_address=IndividualAddress(7)),
             )
         )
         connect.response_rec_callback(res_knxipframe, HPAI(), None)
         assert connect.success
         assert connect.communication_channel == 23
-        assert connect.identifier == 7
+        assert connect.crd.individual_address.raw == 7
 
     async def test_connect_route_back_true(self):
         """Test connecting from KNX bus."""
@@ -112,10 +114,10 @@ class TestConnect:
         res_knxipframe = KNXIPFrame.init_from_body(
             ConnectResponse(
                 communication_channel=23,
-                identifier=7,
+                crd=ConnectResponseData(individual_address=IndividualAddress(7)),
             )
         )
         connect.response_rec_callback(res_knxipframe, HPAI(), None)
         assert connect.success
         assert connect.communication_channel == 23
-        assert connect.identifier == 7
+        assert connect.crd.individual_address.raw == 7
diff --git a/test/io_tests/tunnel_test.py b/test/io_tests/tunnel_test.py
index 2c6d1759..54fe6180 100644
--- a/test/io_tests/tunnel_test.py
+++ b/test/io_tests/tunnel_test.py
@@ -18,6 +18,7 @@ from xknx.knxip import (
     ConnectionStateResponse,
     ConnectRequest,
     ConnectResponse,
+    ConnectResponseData,
     DescriptionRequest,
     DescriptionResponse,
     DisconnectRequest,
@@ -297,7 +298,7 @@ class TestUDPTunnel:
             ConnectResponse(
                 communication_channel=23,
                 data_endpoint=gateway_data_endpoint,
-                identifier=7,
+                crd=ConnectResponseData(individual_address=IndividualAddress(7)),
             )
         )
         self.tunnel.transport.handle_knxipframe(connect_response_frame, remote_addr)
@@ -410,7 +411,7 @@ class TestTCPTunnel:
             ConnectResponse(
                 communication_channel=23,
                 data_endpoint=HPAI(protocol=HostProtocol.IPV4_TCP),
-                identifier=7,
+                crd=ConnectResponseData(individual_address=IndividualAddress(7)),
             )
         )
         self.tunnel.transport.handle_knxipframe(connect_response_frame, remote_hpai)
@@ -464,7 +465,7 @@ class TestTCPTunnel:
             ConnectResponse(
                 communication_channel=23,
                 data_endpoint=HPAI(protocol=HostProtocol.IPV4_TCP),
-                identifier=7,
+                crd=ConnectResponseData(individual_address=IndividualAddress(7)),
             )
         )
         self.tunnel.transport.handle_knxipframe(connect_response_frame, remote_hpai)
@@ -516,7 +517,7 @@ class TestTCPTunnel:
             ConnectResponse(
                 communication_channel=23,
                 data_endpoint=HPAI(protocol=HostProtocol.IPV4_TCP),
-                identifier=7,
+                crd=ConnectResponseData(individual_address=IndividualAddress(7)),
             )
         )
         self.tunnel.transport.handle_knxipframe(connect_response_frame, remote_hpai)
diff --git a/test/knxip_tests/connect_request_test.py b/test/knxip_tests/connect_request_test.py
index 13db0894..b3b8300f 100644
--- a/test/knxip_tests/connect_request_test.py
+++ b/test/knxip_tests/connect_request_test.py
@@ -16,7 +16,7 @@ from xknx.telegram import IndividualAddress
 class TestKNXIPConnectRequest:
     """Test class for KNX/IP ConnectRequests."""
 
-    def test_connect_request(self):
+    def test_tunnel_connect_request(self):
         """Test parsing and streaming connection request KNX/IP packet."""
         raw = bytes.fromhex(
             "06 10 02 05 00 1A 08 01 C0 A8 2A 01 84 95 08 01"
@@ -49,6 +49,37 @@ class TestKNXIPConnectRequest:
 
         assert knxipframe2.to_knx() == raw
 
+    def test_mgmt_connect_request(self):
+        """Test parsing and streaming connection request KNX/IP packet."""
+        raw = bytes.fromhex(
+            "06 10 02 05 00 18 08 01 C0 A8 2A 01 84 95 08 01 C0 A8 2A 01 CC A9 02 03"
+        )
+        knxipframe, _ = KNXIPFrame.from_knx(raw)
+
+        assert isinstance(knxipframe.body, ConnectRequest)
+        assert knxipframe.body.control_endpoint == HPAI(
+            ip_addr="192.168.42.1", port=33941
+        )
+        assert knxipframe.body.data_endpoint == HPAI(ip_addr="192.168.42.1", port=52393)
+        assert (
+            knxipframe.body.cri.connection_type
+            is ConnectRequestType.DEVICE_MGMT_CONNECTION
+        )
+        assert knxipframe.body.cri.individual_address is None
+
+        cri = ConnectRequestInformation(
+            connection_type=ConnectRequestType.DEVICE_MGMT_CONNECTION,
+            individual_address=None,
+        )
+        connect_request = ConnectRequest(
+            control_endpoint=HPAI(ip_addr="192.168.42.1", port=33941),
+            data_endpoint=HPAI(ip_addr="192.168.42.1", port=52393),
+            cri=cri,
+        )
+        knxipframe2 = KNXIPFrame.init_from_body(connect_request)
+
+        assert knxipframe2.to_knx() == raw
+
     def test_connect_request_extended_cri(self):
         """Test parsing and streaming connection request KNX/IP packet."""
         raw = bytes.fromhex(
@@ -91,6 +122,30 @@ class TestKNXIPConnectRequest:
         with pytest.raises(CouldNotParseKNXIP):
             KNXIPFrame.from_knx(raw)
 
+    def test_from_knx_wrong_length_of_cri2(self):
+        """Test parsing and streaming wrong ConnectRequest."""
+        raw = bytes.fromhex(
+            "06 10 02 05 00 18 08 01 C0 A8 2A 01 84 95 08 01 C0 A8 2A 01 CC A9 03 03"
+        )
+        with pytest.raises(CouldNotParseKNXIP):
+            KNXIPFrame.from_knx(raw)
+
+    def test_from_knx_wrong_length_of_cri3(self):
+        """Test parsing and streaming wrong ConnectRequest."""
+        raw = bytes.fromhex(
+            "06 10 02 05 00 18 08 01 C0 A8 2A 01 84 95 08 01 C0 A8 2A 01 CC A9 01 03"
+        )
+        with pytest.raises(CouldNotParseKNXIP):
+            KNXIPFrame.from_knx(raw)
+
+    def test_from_knx_wrong_length_of_cri4(self):
+        """Test parsing and streaming wrong ConnectRequest."""
+        raw = bytes.fromhex(
+            "06 10 02 05 00 19 08 01 C0 A8 2A 01 84 95 08 01 C0 A8 2A 01 CC A9 03 03 01"
+        )
+        with pytest.raises(CouldNotParseKNXIP):
+            KNXIPFrame.from_knx(raw)
+
     def test_from_knx_wrong_cri(self):
         """Test parsing and streaming wrong ConnectRequest."""
         raw = bytes.fromhex(
diff --git a/test/knxip_tests/connect_response_test.py b/test/knxip_tests/connect_response_test.py
index be19bab8..d45f11ba 100644
--- a/test/knxip_tests/connect_response_test.py
+++ b/test/knxip_tests/connect_response_test.py
@@ -2,13 +2,21 @@
 import pytest
 
 from xknx.exceptions import CouldNotParseKNXIP
-from xknx.knxip import HPAI, ConnectRequestType, ConnectResponse, ErrorCode, KNXIPFrame
+from xknx.knxip import (
+    HPAI,
+    ConnectRequestType,
+    ConnectResponse,
+    ConnectResponseData,
+    ErrorCode,
+    KNXIPFrame,
+)
+from xknx.telegram import IndividualAddress
 
 
 class TestKNXIPConnectResponse:
     """Test class for KNX/IP ConnectResponses."""
 
-    def test_connect_response(self):
+    def test_tunnel_connect_response(self):
         """Test parsing and streaming connection response KNX/IP packet."""
         raw = bytes.fromhex(
             "06 10 02 06 00 14 01 00 08 01 C0 A8 2A 0A 0E 57 04 04 11 FF"
@@ -18,15 +26,43 @@ class TestKNXIPConnectResponse:
         assert knxipframe.body.communication_channel == 1
         assert knxipframe.body.status_code == ErrorCode.E_NO_ERROR
         assert knxipframe.body.data_endpoint == HPAI(ip_addr="192.168.42.10", port=3671)
-        assert knxipframe.body.request_type == ConnectRequestType.TUNNEL_CONNECTION
-        assert knxipframe.body.identifier == 4607
+        assert knxipframe.body.crd.request_type == ConnectRequestType.TUNNEL_CONNECTION
+        assert knxipframe.body.crd.individual_address.raw == 4607
 
         connect_response = ConnectResponse(
             communication_channel=1,
             status_code=ErrorCode.E_NO_ERROR,
-            request_type=ConnectRequestType.TUNNEL_CONNECTION,
             data_endpoint=HPAI(ip_addr="192.168.42.10", port=3671),
-            identifier=4607,
+            crd=ConnectResponseData(
+                request_type=ConnectRequestType.TUNNEL_CONNECTION,
+                individual_address=IndividualAddress(4607),
+            ),
+        )
+        knxipframe2 = KNXIPFrame.init_from_body(connect_response)
+
+        assert knxipframe2.to_knx() == raw
+
+    def test_mgmt_connect_response(self):
+        """Test parsing and streaming connection response KNX/IP packet."""
+        raw = bytes.fromhex("06 10 02 06 00 12 01 00 08 01 C0 A8 2A 0A 0E 57 02 03")
+        knxipframe, _ = KNXIPFrame.from_knx(raw)
+        assert isinstance(knxipframe.body, ConnectResponse)
+        assert knxipframe.body.communication_channel == 1
+        assert knxipframe.body.status_code == ErrorCode.E_NO_ERROR
+        assert knxipframe.body.data_endpoint == HPAI(ip_addr="192.168.42.10", port=3671)
+        assert (
+            knxipframe.body.crd.request_type
+            == ConnectRequestType.DEVICE_MGMT_CONNECTION
+        )
+        assert knxipframe.body.crd.individual_address == None
+
+        connect_response = ConnectResponse(
+            communication_channel=1,
+            status_code=ErrorCode.E_NO_ERROR,
+            data_endpoint=HPAI(ip_addr="192.168.42.10", port=3671),
+            crd=ConnectResponseData(
+                request_type=ConnectRequestType.DEVICE_MGMT_CONNECTION,
+            ),
         )
         knxipframe2 = KNXIPFrame.init_from_body(connect_response)
 
@@ -42,7 +78,25 @@ class TestKNXIPConnectResponse:
 
     def test_from_knx_wrong_crd2(self):
         """Test parsing and streaming wrong ConnectRequest (wrong CRD length)."""
-        raw = bytes.fromhex("06 10 02 06 00 14 01 00 08 01 C0 A8 2A 0A 0E 57 04 04")
+        raw = bytes.fromhex("06 10 02 06 00 12 01 00 08 01 C0 A8 2A 0A 0E 57 04 04")
+        with pytest.raises(CouldNotParseKNXIP):
+            KNXIPFrame.from_knx(raw)
+
+    def test_from_knx_wrong_crd3(self):
+        """Test parsing and streaming wrong ConnectRequest (CRD length too small)."""
+        raw = bytes.fromhex("06 10 02 06 00 12 01 00 08 01 C0 A8 2A 0A 0E 57 01 04")
+        with pytest.raises(CouldNotParseKNXIP):
+            KNXIPFrame.from_knx(raw)
+
+    def test_from_knx_wrong_crd4(self):
+        """Test parsing and streaming wrong ConnectRequest (wrong CRD length)."""
+        raw = bytes.fromhex("06 10 02 06 00 12 01 00 08 01 C0 A8 2A 0A 0E 57 04 03")
+        with pytest.raises(CouldNotParseKNXIP):
+            KNXIPFrame.from_knx(raw)
+
+    def test_from_knx_wrong_crd5(self):
+        """Test parsing and streaming wrong ConnectRequest (wrong CRD length)."""
+        raw = bytes.fromhex("06 10 02 06 00 13 01 00 08 01 C0 A8 2A 0A 0E 57 03 03 01")
         with pytest.raises(CouldNotParseKNXIP):
             KNXIPFrame.from_knx(raw)
 
@@ -63,9 +117,11 @@ class TestKNXIPConnectResponse:
         connect_response = ConnectResponse(
             communication_channel=192,
             status_code=ErrorCode.E_NO_MORE_CONNECTIONS,
-            request_type=ConnectRequestType.TUNNEL_CONNECTION,
             data_endpoint=HPAI(ip_addr="10.1.0.41", port=3671),
-            identifier=0,
+            crd=ConnectResponseData(
+                request_type=ConnectRequestType.TUNNEL_CONNECTION,
+                individual_address=IndividualAddress(0),
+            ),
         )
         knxipframe2 = KNXIPFrame.init_from_body(connect_response)
 
diff --git a/test/str_test.py b/test/str_test.py
index 8ad71aec..1da8cfc5 100644
--- a/test/str_test.py
+++ b/test/str_test.py
@@ -35,6 +35,7 @@ from xknx.knxip import (
     ConnectRequest,
     ConnectRequestType,
     ConnectResponse,
+    ConnectResponseData,
     DIBDeviceInformation,
     DIBGeneric,
     DIBServiceFamily,
@@ -533,14 +534,16 @@ class TestStringRepresentations:
         """Test string representatoin of KNX/IP ConnectResponse."""
         connect_response = ConnectResponse()
         connect_response.communication_channel = 13
-        connect_response.request_type = ConnectRequestType.TUNNEL_CONNECTION
         connect_response.data_endpoint = HPAI(ip_addr="192.168.42.1", port=33941)
-        connect_response.identifier = 42
+        connect_response.crd = ConnectResponseData(
+            request_type=ConnectRequestType.TUNNEL_CONNECTION,
+            individual_address=IndividualAddress("1.2.3"),
+        )
         assert (
             str(connect_response)
             == '<ConnectResponse communication_channel="13" status_code="ErrorCode.E_NO_ERROR" '
             'data_endpoint="192.168.42.1:33941/udp" '
-            'request_type="ConnectRequestType.TUNNEL_CONNECTION" identifier="42" />'
+            'crd="<ConnectResponseData request_type="ConnectRequestType.TUNNEL_CONNECTION" individual_address="1.2.3" />" />'
         )
 
     def test_disconnect_request(self):