Repository: XKNX/xknx
Pull request #1210 (2023-03-01 09:44:30)

Support Device Management CEMI 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 classes/types used to send and receive Device Configuration Requests. 
According to the KNX specifications, these requests use CEMI M_Prop type frames for reading/writing device properties.

## 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
- [x] The changes are documented in the changelog (docs/changelog.md)

codecov[bot]: # [Codecov](https://codecov.io/gh/XKNX/xknx/pull/1210?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) Report
> Merging [#1210](https://codecov.io/gh/XKNX/xknx/pull/1210?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) (7da1382) into [main](https://codecov.io/gh/XKNX/xknx/commit/5840e6386a389f555597a5a4b2aad2a54e7511ee?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) (5840e63) will **decrease** coverage by `0.33%`.
> The diff coverage is `82.41%`.

[![Impacted file tree graph](https://codecov.io/gh/XKNX/xknx/pull/1210/graphs/tree.svg?width=650&height=150&src=pr&token=irWbIygS84&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX)](https://codecov.io/gh/XKNX/xknx/pull/1210?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX)

```diff
@@            Coverage Diff             @@
##             main    #1210      +/-   ##
==========================================
- Coverage   96.45%   96.12%   -0.33%     
==========================================
  Files         143      146       +3     
  Lines        9027     9172     +145     
==========================================
+ Hits         8707     8817     +110     
- Misses        320      355      +35     
```


| [Impacted Files](https://codecov.io/gh/XKNX/xknx/pull/1210?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) | Coverage Δ | |
|---|---|---|
| [xknx/knxip/device\_configuration\_request.py](https://codecov.io/gh/XKNX/xknx/pull/1210?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9rbnhpcC9kZXZpY2VfY29uZmlndXJhdGlvbl9yZXF1ZXN0LnB5) | `45.83% <45.83%> (ø)` | |
| [xknx/knxip/device\_configuration\_ack.py](https://codecov.io/gh/XKNX/xknx/pull/1210?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9rbnhpcC9kZXZpY2VfY29uZmlndXJhdGlvbl9hY2sucHk=) | `48.00% <48.00%> (ø)` | |
| [xknx/io/request\_response/device\_configuration.py](https://codecov.io/gh/XKNX/xknx/pull/1210?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9pby9yZXF1ZXN0X3Jlc3BvbnNlL2RldmljZV9jb25maWd1cmF0aW9uLnB5) | `64.28% <64.28%> (ø)` | |
| [xknx/knxip/knxip.py](https://codecov.io/gh/XKNX/xknx/pull/1210?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9rbnhpcC9rbnhpcC5weQ==) | `98.07% <66.66%> (-1.93%)` | :arrow_down: |
| [xknx/cemi/cemi\_frame.py](https://codecov.io/gh/XKNX/xknx/pull/1210?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9jZW1pL2NlbWlfZnJhbWUucHk=) | `98.64% <98.19%> (-1.36%)` | :arrow_down: |
| [xknx/\_\_version\_\_.py](https://codecov.io/gh/XKNX/xknx/pull/1210?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9fX3ZlcnNpb25fXy5weQ==) | `100.00% <100.00%> (ø)` | |
| [xknx/cemi/\_\_init\_\_.py](https://codecov.io/gh/XKNX/xknx/pull/1210?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9jZW1pL19faW5pdF9fLnB5) | `100.00% <100.00%> (ø)` | |
| [xknx/cemi/const.py](https://codecov.io/gh/XKNX/xknx/pull/1210?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9jZW1pL2NvbnN0LnB5) | `100.00% <100.00%> (ø)` | |
| [xknx/io/request\_response/\_\_init\_\_.py](https://codecov.io/gh/XKNX/xknx/pull/1210?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9pby9yZXF1ZXN0X3Jlc3BvbnNlL19faW5pdF9fLnB5) | `100.00% <100.00%> (ø)` | |
| [xknx/knxip/\_\_init\_\_.py](https://codecov.io/gh/XKNX/xknx/pull/1210?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9rbnhpcC9fX2luaXRfXy5weQ==) | `100.00% <100.00%> (ø)` | |
| ... and [1 more](https://codecov.io/gh/XKNX/xknx/pull/1210?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) | |


Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index d2d4ae9e..48fb0b14 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -6,6 +6,13 @@ nav_order: 2
 
 # Changelog
 
+# Unreleased Changes
+
+### Protocol
+
+- Add support for Device Management Configuration service.
+- Support CEMI M_Prop messages.
+
 # 2.6.0 Connection information 2023-02-27
 
 ### Connection
diff --git a/requirements/production.txt b/requirements/production.txt
index 69360bbf..05df0766 100644
--- a/requirements/production.txt
+++ b/requirements/production.txt
@@ -1,2 +1,2 @@
-cryptography==39.0.1
+cryptography==39.0.2
 ifaddr==0.2.0
diff --git a/xknx/cemi/__init__.py b/xknx/cemi/__init__.py
index d3da0314..ad2a97fc 100644
--- a/xknx/cemi/__init__.py
+++ b/xknx/cemi/__init__.py
@@ -1,5 +1,13 @@
 """Module for handling CEMI Frames."""
 # flake8: noqa
-from .cemi_frame import CEMIFrame
+from .cemi_frame import (
+    CEMIFrame,
+    CEMILData,
+    CEMIMPropInfo,
+    CEMIMPropReadRequest,
+    CEMIMPropReadResponse,
+    CEMIMPropWriteRequest,
+    CEMIMPropWriteResponse,
+)
 from .cemi_handler import CEMIHandler
-from .const import CEMIFlags, CEMIMessageCode
+from .const import CEMIErrorCode, CEMIFlags, CEMIMessageCode
diff --git a/xknx/cemi/cemi_frame.py b/xknx/cemi/cemi_frame.py
index f6592636..7825d5e5 100644
--- a/xknx/cemi/cemi_frame.py
+++ b/xknx/cemi/cemi_frame.py
@@ -13,150 +13,178 @@ Documentation within:
 """
 from __future__ import annotations
 
-from xknx.exceptions import ConversionError, UnsupportedCEMIMessage
+from abc import ABC, abstractmethod
+
+from xknx.exceptions import ConversionError, CouldNotParseCEMI, UnsupportedCEMIMessage
+from xknx.profile.const import ResourceObjectType, ResourcePropertyId
 from xknx.telegram import GroupAddress, IndividualAddress, Telegram
 from xknx.telegram.apci import APCI
 from xknx.telegram.tpci import TPCI, TDataBroadcast
 
-from .const import CEMIFlags, CEMIMessageCode
+from .const import CEMIErrorCode, CEMIFlags, CEMIMessageCode
 
 
-class CEMIFrame:
-    """Representation of a CEMI Frame."""
+class CEMIInfo:
+    """
+    Raw helper class for all CEMI additional info.
+
+    As specified within KNX Chapter 3.6.3/4.1.4.3 "Additional information".
+    """
+
+    def __init__(self, raw: bytes = bytes(0)):
+        """Initialize CEMILInfo object."""
+        self.raw = raw
+
+    def calculated_length(self) -> int:
+        """Get length of CEMI info."""
+        return 1 + len(self.raw)
+
+    @staticmethod
+    def from_knx(raw: bytes) -> tuple[CEMIInfo, bytes]:
+        """Parse/deserialize from CEMI raw data."""
+        length = raw[0]
+        return CEMIInfo(raw[1 : length + 1]), raw[length + 1 :]
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        return bytes((len(self.raw),)) + self.raw
+
+    def __repr__(self) -> str:
+        """Return object as readable string."""
+        return f'CEMIInfo("{self.raw.hex()}")'
+
+    def __eq__(self, other: object) -> bool:
+        """Equal operator."""
+        return self.__dict__ == other.__dict__
+
+
+class CEMIData(ABC):
+    """Base class for all CEMI data."""
+
+    @abstractmethod
+    def calculated_length(self) -> int:
+        """Get length of CEMI data."""
+
+    @abstractmethod
+    def to_knx(self) -> bytes:
+        """Serialize to CEMI raw data."""
+
+    @classmethod
+    @abstractmethod
+    def from_knx(cls, raw: bytes) -> CEMIData:
+        """Parse/deserialize from KNX/IP raw data."""
+
+    @abstractmethod
+    def __repr__(self) -> str:
+        """Return object as readable string."""
+
+    def __eq__(self, other: object) -> bool:
+        """Equal operator."""
+        if self.__class__ != other.__class__:
+            return False
+        return self.__dict__ == other.__dict__
+
+
+class CEMILData(CEMIData):
+    """Representation of CEMI Link Layer Data."""
 
     def __init__(
         self,
         *,
-        code: CEMIMessageCode,
         flags: int,
         src_addr: IndividualAddress,
         dst_addr: GroupAddress | IndividualAddress,
         tpci: TPCI,
         payload: APCI | None,
     ):
-        """Initialize CEMIFrame object."""
-        self.code = code
+        """Initialize CEMILData object."""
         self.flags = flags
         self.src_addr = src_addr
         self.dst_addr = dst_addr
         self.tpci = tpci
         self.payload = payload
 
-    @staticmethod
-    def init_from_telegram(
-        telegram: Telegram,
-        code: CEMIMessageCode = CEMIMessageCode.L_DATA_IND,
-        src_addr: IndividualAddress | None = None,
-    ) -> CEMIFrame:
-        """Return CEMIFrame from a Telegram."""
-        flags = (
-            CEMIFlags.FRAME_TYPE_STANDARD
-            | CEMIFlags.DO_NOT_REPEAT
-            | CEMIFlags.BROADCAST
-            | CEMIFlags.NO_ACK_REQUESTED
-            | CEMIFlags.CONFIRM_NO_ERROR
-            | CEMIFlags.HOP_COUNT_1ST
-        )
-        if isinstance(telegram.destination_address, GroupAddress):
-            flags |= CEMIFlags.DESTINATION_GROUP_ADDRESS
-            if isinstance(telegram.tpci, TDataBroadcast):
-                flags |= CEMIFlags.PRIORITY_SYSTEM
-            else:
-                flags |= CEMIFlags.PRIORITY_LOW
-        elif isinstance(telegram.destination_address, IndividualAddress):
-            flags |= (
-                CEMIFlags.DESTINATION_INDIVIDUAL_ADDRESS | CEMIFlags.PRIORITY_SYSTEM
-            )
-        else:
-            raise TypeError()
-
-        return CEMIFrame(
-            code=code,
-            flags=flags,
-            src_addr=src_addr or telegram.source_address,
-            dst_addr=telegram.destination_address,
-            tpci=telegram.tpci,
-            payload=telegram.payload,
-        )
-
     @property
-    def telegram(self) -> Telegram:
-        """Return telegram."""
-
-        return Telegram(
-            destination_address=self.dst_addr,
-            payload=self.payload,
-            source_address=self.src_addr,
-            tpci=self.tpci,
-        )
+    def hops(self) -> int:
+        """Return hops."""
+        return (self.flags & 0x0070) >> 4
 
-    def set_hops(self, hops: int) -> None:
+    @hops.setter
+    def hops(self, val: int) -> None:
         """Set hops."""
         # Resetting hops
         self.flags &= 0xFFFF ^ 0x0070
         # Setting new hops
-        self.flags |= hops << 4
+        self.flags |= val << 4
 
     def calculated_length(self) -> int:
         """Get length of KNX/IP body."""
         if not self.tpci.control and self.payload is not None:
-            return 10 + self.payload.calculated_length()
+            return 8 + self.payload.calculated_length()
         if self.tpci.control and self.payload is None:
-            return 10
+            return 8
         raise TypeError("Data TPDU must have a payload; control TPDU must not.")
 
-    @staticmethod
-    def from_knx(raw: bytes) -> CEMIFrame:
-        """Parse/deserialize from KNX/IP raw data."""
-        try:
-            code = CEMIMessageCode(raw[0])
-        except ValueError:
-            raise UnsupportedCEMIMessage(
-                f"CEMIMessageCode not implemented: {raw[0]} in CEMI: {raw.hex()}"
-            )
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        if self.tpci.control:
+            tpdu = self.tpci.to_knx().to_bytes(1, "big")
+            npdu_len = 0
+        else:
+            if not isinstance(self.payload, APCI):
+                raise ConversionError(
+                    f"Invalid payload set for data TPDU: {type(self.payload)}"
+                )
+            tpdu = self.payload.to_knx()
+            tpdu[0] |= self.tpci.to_knx()
+            npdu_len = self.payload.calculated_length()
 
-        if code in (
-            CEMIMessageCode.L_DATA_IND,
-            CEMIMessageCode.L_DATA_REQ,
-            CEMIMessageCode.L_DATA_CON,
-        ):
-            return CEMIFrame.from_knx_data_link_layer(raw, code)
+        return (
+            self.flags.to_bytes(2, "big")
+            + self.src_addr.to_knx()
+            + self.dst_addr.to_knx()
+            + npdu_len.to_bytes(1, "big")
+            + tpdu
+        )
 
-        raise UnsupportedCEMIMessage(
-            f"Could not handle CEMIMessageCode: {code} / {raw[0]} in CEMI: {raw.hex()}"
+    @property
+    def telegram(self) -> Telegram:
+        """Return telegram."""
+
+        return Telegram(
+            destination_address=self.dst_addr,
+            payload=self.payload,
+            source_address=self.src_addr,
+            tpci=self.tpci,
         )
 
-    @staticmethod
-    def from_knx_data_link_layer(cemi: bytes, code: CEMIMessageCode) -> CEMIFrame:
+    @classmethod
+    def from_knx(cls, raw: bytes) -> CEMILData:
         """Parse L_DATA_IND, CEMIMessageCode.L_DATA_REQ, CEMIMessageCode.L_DATA_CON."""
-        if len(cemi) < 10:
-            raise UnsupportedCEMIMessage(
-                f"CEMI too small. Length: {len(cemi)}; CEMI: {cemi.hex()}"
+        if len(raw) < 8:
+            raise CouldNotParseCEMI(
+                f"CEMI too small. Length: {len(raw)}; CEMI: {raw.hex()}"
             )
 
-        # AddIL (Additional Info Length), as specified within
-        # KNX Chapter 3.6.3/4.1.4.3 "Additional information."
-        # Additional information is not yet parsed.
-        addil = cemi[1]
-        # Control field 1 and Control field 2 - first 2 octets after Additional information
-        flags = cemi[2 + addil] * 256 + cemi[3 + addil]
+        # Control field 1 and Control field 2 - first 2 octets
+        flags = int.from_bytes(raw[0:2], "big")
 
-        src_addr = IndividualAddress.from_knx(cemi[4 + addil : 6 + addil])
+        src_addr = IndividualAddress.from_knx(raw[2:4])
 
         _dst_is_group_address = bool(flags & CEMIFlags.DESTINATION_GROUP_ADDRESS)
         dst_addr: GroupAddress | IndividualAddress = (
-            GroupAddress.from_knx(cemi[6 + addil : 8 + addil])
+            GroupAddress.from_knx(raw[4:6])
             if _dst_is_group_address
-            else IndividualAddress.from_knx(cemi[6 + addil : 8 + addil])
+            else IndividualAddress.from_knx(raw[4:6])
         )
 
-        _npdu_len = cemi[8 + addil]
-        _tpdu = cemi[9 + addil :]
+        _npdu_len = raw[6]
+        _tpdu = raw[7:]
         _apdu = bytes([_tpdu[0] & 0b11]) + _tpdu[1:]  # clear TPCI bits
         if len(_apdu) != (_npdu_len + 1):  # TCPI octet not included in NPDU length
-            raise UnsupportedCEMIMessage(
+            raise CouldNotParseCEMI(
                 f"APDU LEN should be {_npdu_len} but is {len(_apdu) - 1} "
-                f"from {src_addr} in CEMI: {cemi.hex()}"
+                f"from {src_addr} in CEMI: {raw.hex()}"
             )
 
         # TPCI (transport layer control information)
@@ -172,17 +200,16 @@ class CEMIFrame:
         except ConversionError as err:
             raise UnsupportedCEMIMessage(
                 f"TPCI not supported: {_tpdu[0]:#10b} "
-                f"from {src_addr} in CEMI: {cemi.hex()}"
+                f"from {src_addr} in CEMI: {raw.hex()}"
             ) from err
 
         if tpci.control:
             if _npdu_len:
-                raise UnsupportedCEMIMessage(
+                raise CouldNotParseCEMI(
                     f"Invalid length for control TPDU {tpci}: {_npdu_len}"
-                    f" from {src_addr} in CEMI: {cemi.hex()}"
+                    f" from {src_addr} in CEMI: {raw.hex()}"
                 )
-            return CEMIFrame(
-                code=code,
+            return cls(
                 flags=flags,
                 src_addr=src_addr,
                 dst_addr=dst_addr,
@@ -195,11 +222,10 @@ class CEMIFrame:
         except ConversionError as err:
             raise UnsupportedCEMIMessage(
                 f"APDU not supported from {src_addr} "
-                f"from {src_addr} in CEMI: {cemi.hex()}"
+                f"from {src_addr} in CEMI: {raw.hex()}"
             ) from err
 
-        return CEMIFrame(
-            code=code,
+        return cls(
             flags=flags,
             src_addr=src_addr,
             dst_addr=dst_addr,
@@ -207,46 +233,449 @@ class CEMIFrame:
             payload=payload,
         )
 
+    def __repr__(self) -> str:
+        """Return object as readable string."""
+        return (
+            "CEMILData("
+            f'src_addr="{self.src_addr.__repr__()}" '
+            f'dst_addr="{self.dst_addr.__repr__()}" '
+            f'flags="{self.flags:16b}" '
+            f'tpci="{self.tpci}" '
+            f'payload="{self.payload}")'
+        )
+
+
+class CEMIMPropInfo:
+    """Representation of CEMI Device Management Property."""
+
+    LENGTH = 6
+
+    def __init__(
+        self,
+        *,
+        object_type: ResourceObjectType,
+        object_instance: int = 1,
+        property_id: ResourcePropertyId | int,
+        number_of_elements: int = 1,
+        start_index: int = 1,
+    ):
+        """Initialize CEMIMProp object."""
+        self.object_type = object_type
+        self.object_instance = object_instance
+        self.property_id = (
+            property_id.value
+            if isinstance(property_id, ResourcePropertyId)
+            else property_id
+        )
+        self.number_of_elements = number_of_elements
+        self.start_index = start_index
+
+    def calculated_length(self) -> int:
+        """Get length of CEMI data."""
+        return CEMIMPropInfo.LENGTH
+
+    @staticmethod
+    def from_knx(raw: bytes) -> CEMIMPropInfo:
+        """Parse/deserialize from KNX/IP raw data."""
+        if len(raw) != CEMIMPropInfo.LENGTH:
+            raise CouldNotParseCEMI(
+                f"Invalid CEMI length: {len(raw)}; CEMI: {raw.hex()}"
+            )
+
+        try:
+            object_type = ResourceObjectType(int.from_bytes(raw[0:2], "big"))
+        except ValueError:
+            raise UnsupportedCEMIMessage(
+                f"CEMIMProp Object Type not supported: {raw[0:2].hex()} in CEMI: {raw.hex()}"
+            )
+
+        return CEMIMPropInfo(
+            object_type=object_type,
+            object_instance=raw[2],
+            property_id=raw[3],
+            number_of_elements=(raw[4] >> 4),
+            start_index=(int.from_bytes(raw[4:6], "big") % 0x1000),
+        )
+
     def to_knx(self) -> bytes:
-        """Serialize to KNX/IP raw data."""
-        if self.tpci.control:
-            tpdu = self.tpci.to_knx().to_bytes(1, "big")
-            npdu_len = 0
-        else:
-            if not isinstance(self.payload, APCI):
-                raise ConversionError(
-                    f"Invalid payload set for data TPDU: {type(self.payload)}"
-                )
-            tpdu = self.payload.to_knx()
-            tpdu[0] |= self.tpci.to_knx()
-            npdu_len = self.payload.calculated_length()
+        """Serialize to CEMI raw data."""
+        return (
+            self.object_type.value.to_bytes(2, "big")
+            + self.object_instance.to_bytes(1, "big")
+            + self.property_id.to_bytes(1, "big")
+            + ((self.number_of_elements << 12) + self.start_index).to_bytes(2, "big")
+        )
 
+    def __repr__(self) -> str:
+        """Return object as readable string."""
         return (
-            bytes(
-                (
-                    self.code.value,
-                    0x00,  # Additional information length
-                )
+            f'object_type="{self.object_type.value}" '
+            f'object_instance="{self.object_instance}" '
+            f'property_id="{self.property_id}" '
+            f'number_of_elements="{self.number_of_elements}" '
+            f'start_index="{self.start_index}" '
+        )
+
+    def __eq__(self, other: object) -> bool:
+        """Equal operator."""
+        return self.__dict__ == other.__dict__
+
+
+class CEMIMPropReadRequest(CEMIData):
+    """Representation of CEMI Device Management Property Read Request."""
+
+    def __init__(self, *, property_info: CEMIMPropInfo):
+        """Initialize CEMIMPropReadRequest object."""
+        self.property_info = property_info
+
+    def calculated_length(self) -> int:
+        """Get length of CEMI data."""
+        return self.property_info.calculated_length()
+
+    def to_knx(self) -> bytes:
+        """Serialize to CEMI raw data."""
+        return self.property_info.to_knx()
+
+    @classmethod
+    def from_knx(cls, raw: bytes) -> CEMIData:
+        """Parse/deserialize from KNX/IP raw data."""
+        return cls(property_info=CEMIMPropInfo.from_knx(raw))
+
+    def __repr__(self) -> str:
+        """Return object as readable string."""
+        return f"CEMIMPropReadRequest({self.property_info})"
+
+
+class CEMIMPropReadResponse(CEMIData):
+    """Representation of CEMI Device Management Property Read Response."""
+
+    def __init__(
+        self,
+        *,
+        property_info: CEMIMPropInfo,
+        data: bytes,
+    ):
+        """Initialize CEMIMPropReadResponse object."""
+        self.property_info = property_info
+        self.data = data
+
+    @property
+    def error_code(self) -> CEMIErrorCode | None:
+        """Return an optional CEMI error code."""
+        if self.property_info.number_of_elements == 0:
+            return CEMIErrorCode(self.data[0])
+
+        return None
+
+    def calculated_length(self) -> int:
+        """Get length of CEMI data."""
+        return CEMIMPropInfo.LENGTH + len(self.data)
+
+    def to_knx(self) -> bytes:
+        """Serialize to CEMI raw data."""
+        return self.property_info.to_knx() + self.data
+
+    @classmethod
+    def from_knx(cls, raw: bytes) -> CEMIData:
+        """Parse/deserialize from KNX/IP raw data."""
+        if len(raw) <= CEMIMPropInfo.LENGTH:
+            raise CouldNotParseCEMI(
+                f"CEMI Property Read Response too small. Length: {len(raw)}; CEMI: {raw.hex()}"
             )
-            + self.flags.to_bytes(2, "big")
-            + self.src_addr.to_knx()
-            + self.dst_addr.to_knx()
-            + npdu_len.to_bytes(1, "big")
-            + tpdu
+
+        property_info = CEMIMPropInfo.from_knx(raw[0 : CEMIMPropInfo.LENGTH])
+
+        # Did we get an error?
+        if property_info.number_of_elements == 0 and len(raw) != 7:
+            raise CouldNotParseCEMI(
+                f"Invalid CEMI error response length: {len(raw)}; CEMI: {raw.hex()}"
+            )
+
+        return cls(property_info=property_info, data=raw[CEMIMPropInfo.LENGTH :])
+
+    def __repr__(self) -> str:
+        """Return object as readable string."""
+        _data = (
+            f'data="{self.data.hex()}" '
+            if self.property_info.number_of_elements != 0
+            else ""
+        )
+        return (
+            f"CEMIMPropReadResponse("
+            f"{self.property_info}"
+            f'error_code="{self.error_code}" '
+            f"{_data})"
+        )
+
+
+class CEMIMPropWriteRequest(CEMIData):
+    """Representation of CEMI Device Management Property Write Request."""
+
+    def __init__(
+        self,
+        *,
+        property_info: CEMIMPropInfo,
+        data: bytes,
+    ):
+        """Initialize CEMIMPropWriteRequest object."""
+        self.property_info = property_info
+        self.data = data
+
+    def calculated_length(self) -> int:
+        """Get length of CEMI data."""
+        return CEMIMPropInfo.LENGTH + len(self.data)
+
+    def to_knx(self) -> bytes:
+        """Serialize to CEMI raw data."""
+        return self.property_info.to_knx() + self.data
+
+    @classmethod
+    def from_knx(cls, raw: bytes) -> CEMIData:
+        """Parse/deserialize from KNX/IP raw data."""
+        if len(raw) <= CEMIMPropInfo.LENGTH:
+            raise CouldNotParseCEMI(
+                f"CEMI Property Write Request too small. Length: {len(raw)}; CEMI: {raw.hex()}"
+            )
+
+        property_info = CEMIMPropInfo.from_knx(raw[0 : CEMIMPropInfo.LENGTH])
+
+        return cls(property_info=property_info, data=raw[CEMIMPropInfo.LENGTH :])
+
+    def __repr__(self) -> str:
+        """Return object as readable string."""
+        return (
+            f"CEMIMPropWriteRequest("
+            f"{self.property_info}"
+            f'data="{self.data.hex()}" )'
         )
 
+
+class CEMIMPropWriteResponse(CEMIData):
+    """Representation of CEMI Device Management Property Write Response."""
+
+    def __init__(
+        self,
+        *,
+        property_info: CEMIMPropInfo,
+        error_code: CEMIErrorCode | None = None,
+    ):
+        """Initialize CEMIMPropWriteResponse object."""
+        self.property_info = property_info
+        self._error_code = error_code
+
+    def calculated_length(self) -> int:
+        """Get length of CEMI data."""
+        return CEMIMPropInfo.LENGTH + (1 if self._error_code else 0)
+
+    def to_knx(self) -> bytes:
+        """Serialize to CEMI raw data."""
+        if self._error_code:
+            return self.property_info.to_knx() + self._error_code.value.to_bytes(
+                1, "big"
+            )
+
+        return self.property_info.to_knx()
+
+    @property
+    def error_code(self) -> CEMIErrorCode | None:
+        """Return an optional CEMI error code."""
+        return self._error_code
+
+    @classmethod
+    def from_knx(cls, raw: bytes) -> CEMIData:
+        """Parse/deserialize from KNX/IP raw data."""
+        if len(raw) < CEMIMPropInfo.LENGTH:
+            raise CouldNotParseCEMI(
+                f"CEMI Property Write Response too small. Length: {len(raw)}; CEMI: {raw.hex()}"
+            )
+
+        property_info = CEMIMPropInfo.from_knx(raw[0 : CEMIMPropInfo.LENGTH])
+
+        # Did we get an error?
+        if property_info.number_of_elements == 0:
+            if len(raw) != 7:
+                raise CouldNotParseCEMI(
+                    f"Invalid CEMI error response length: {len(raw)}; CEMI: {raw.hex()}"
+                )
+            return cls(
+                property_info=property_info,
+                error_code=CEMIErrorCode(raw[CEMIMPropInfo.LENGTH]),
+            )
+
+        if len(raw) != CEMIMPropInfo.LENGTH:
+            raise CouldNotParseCEMI(
+                f"Invalid CEMI response length: {len(raw)}; CEMI: {raw.hex()}"
+            )
+
+        return cls(property_info=property_info)
+
     def __repr__(self) -> str:
         """Return object as readable string."""
         return (
-            "<CEMIFrame "
-            f'code="{self.code.name}" '
-            f'src_addr="{self.src_addr.__repr__()}" '
-            f'dst_addr="{self.dst_addr.__repr__()}" '
-            f'flags="{self.flags:16b}" '
-            f'tpci="{self.tpci}" '
-            f'payload="{self.payload}" />'
+            f"CEMIMPropWriteResponse("
+            f"{self.property_info}"
+            f'error_code="{self.error_code}" )'
+        )
+
+
+class CEMIFrame:
+    """Representation of a CEMI Frame."""
+
+    def __init__(
+        self,
+        *,
+        code: CEMIMessageCode,
+        info: CEMIInfo | None = None,
+        data: CEMIData | None,
+    ):
+        """Initialize CEMIFrame object."""
+        self.code = code
+        self.info = info or CEMIInfo()
+        self.data = data
+
+    @staticmethod
+    def _has_info(code: CEMIMessageCode) -> bool:
+        return code not in (
+            CEMIMessageCode.M_PROP_READ_REQ,
+            CEMIMessageCode.M_PROP_READ_CON,
+            CEMIMessageCode.M_PROP_WRITE_REQ,
+            CEMIMessageCode.M_PROP_WRITE_CON,
+            CEMIMessageCode.M_PROP_INFO_IND,
+            CEMIMessageCode.M_FUNC_PROP_COMMAND_REQ,
+            CEMIMessageCode.M_FUNC_PROP_STATE_READ_REQ,
+            CEMIMessageCode.M_FUNC_PROP_COMMAND_CON,
+            CEMIMessageCode.M_FUNC_PROP_STATE_READ_CON,
+            CEMIMessageCode.M_RESET_REQ,
+            CEMIMessageCode.M_RESET_IND,
+        )
+
+    @staticmethod
+    def _has_data(code: CEMIMessageCode) -> bool:
+        return code not in (
+            CEMIMessageCode.M_RESET_REQ,
+            CEMIMessageCode.M_RESET_IND,
+        )
+
+    @staticmethod
+    def init_from_telegram(
+        telegram: Telegram,
+        code: CEMIMessageCode = CEMIMessageCode.L_DATA_IND,
+        src_addr: IndividualAddress | None = None,
+    ) -> CEMIFrame:
+        """Return CEMIFrame from a Telegram."""
+        flags = (
+            CEMIFlags.FRAME_TYPE_STANDARD
+            | CEMIFlags.DO_NOT_REPEAT
+            | CEMIFlags.BROADCAST
+            | CEMIFlags.NO_ACK_REQUESTED
+            | CEMIFlags.CONFIRM_NO_ERROR
+            | CEMIFlags.HOP_COUNT_1ST
+        )
+        if isinstance(telegram.destination_address, GroupAddress):
+            flags |= CEMIFlags.DESTINATION_GROUP_ADDRESS
+            if isinstance(telegram.tpci, TDataBroadcast):
+                flags |= CEMIFlags.PRIORITY_SYSTEM
+            else:
+                flags |= CEMIFlags.PRIORITY_LOW
+        elif isinstance(telegram.destination_address, IndividualAddress):
+            flags |= (
+                CEMIFlags.DESTINATION_INDIVIDUAL_ADDRESS | CEMIFlags.PRIORITY_SYSTEM
+            )
+        else:
+            raise TypeError()
+
+        return CEMIFrame(
+            code=code,
+            data=CEMILData(
+                flags=flags,
+                src_addr=src_addr or telegram.source_address,
+                dst_addr=telegram.destination_address,
+                tpci=telegram.tpci,
+                payload=telegram.payload,
+            ),
+        )
+
+    def calculated_length(self) -> int:
+        """Get length of KNX/IP body."""
+        length = 1
+
+        if self._has_info(self.code):
+            length += self.info.calculated_length()
+
+        if self._has_data(self.code):
+            if self.data is None:
+                raise UnsupportedCEMIMessage(
+                    f"CEMI data required for code: {self.code}"
+                )
+            length += self.data.calculated_length()
+
+        return length
+
+    @staticmethod
+    def from_knx(raw: bytes) -> CEMIFrame:
+        """Parse/deserialize from KNX/IP raw data."""
+        try:
+            code = CEMIMessageCode(raw[0])
+        except ValueError:
+            raise UnsupportedCEMIMessage(
+                f"CEMIMessageCode not implemented: {raw[0]} in CEMI: {raw.hex()}"
+            )
+
+        if code in (
+            CEMIMessageCode.L_DATA_IND,
+            CEMIMessageCode.L_DATA_REQ,
+            CEMIMessageCode.L_DATA_CON,
+        ):
+            info, remainder = CEMIInfo.from_knx(raw[1:])
+            return CEMIFrame(
+                code=code,
+                info=info,
+                data=CEMILData.from_knx(remainder),
+            )
+        if code == CEMIMessageCode.M_PROP_READ_REQ:
+            return CEMIFrame(code=code, data=CEMIMPropReadRequest.from_knx(raw[1:]))
+        if code == CEMIMessageCode.M_PROP_READ_CON:
+            return CEMIFrame(code=code, data=CEMIMPropReadResponse.from_knx(raw[1:]))
+        if code == CEMIMessageCode.M_PROP_WRITE_REQ:
+            return CEMIFrame(code=code, data=CEMIMPropWriteRequest.from_knx(raw[1:]))
+        if code == CEMIMessageCode.M_PROP_WRITE_CON:
+            return CEMIFrame(code=code, data=CEMIMPropWriteResponse.from_knx(raw[1:]))
+
+        raise UnsupportedCEMIMessage(
+            f"Could not handle CEMIMessageCode: {code} / {raw[0]} in CEMI: {raw.hex()}"
         )
 
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        raw = bytes((self.code.value,))
+
+        if self._has_info(self.code):
+            raw += self.info.to_knx()
+
+        if self._has_data(self.code):
+            if self.data is None:
+                raise UnsupportedCEMIMessage(
+                    f"CEMI data required for code: {self.code}"
+                )
+            raw += self.data.to_knx()
+
+        return raw
+
+    def __repr__(self) -> str:
+        """Return object as readable string."""
+        _info = f'info="{self.info.__repr__()}" ' if self._has_info(self.code) else ""
+        _data = f'data="{self.data.__repr__()}" ' if self._has_data(self.code) else ""
+
+        return f'<CEMIFrame code="{self.code.name}" {_info}{_data}/>'
+
     def __eq__(self, other: object) -> bool:
         """Equal operator."""
         return self.__dict__ == other.__dict__
+
+    @property
+    def telegram(self) -> Telegram:
+        """Return telegram."""
+        if not isinstance(self.data, CEMILData):
+            raise TypeError()
+
+        return self.data.telegram
diff --git a/xknx/cemi/cemi_handler.py b/xknx/cemi/cemi_handler.py
index 3a118817..a31269ad 100644
--- a/xknx/cemi/cemi_handler.py
+++ b/xknx/cemi/cemi_handler.py
@@ -15,6 +15,7 @@ from xknx.exceptions import (
     CommunicationError,
     ConfirmationError,
     ConversionError,
+    CouldNotParseCEMI,
     DataSecureError,
     UnsupportedCEMIMessage,
 )
@@ -22,7 +23,7 @@ from xknx.secure.data_secure import DataSecure
 from xknx.secure.keyring import Keyring
 from xknx.telegram import IndividualAddress, Telegram, TelegramDirection, tpci
 
-from .cemi_frame import CEMIFrame
+from .cemi_frame import CEMIFrame, CEMILData
 from .const import CEMIMessageCode
 
 if TYPE_CHECKING:
@@ -88,6 +89,10 @@ class CEMIHandler:
         """Parse and handle incoming raw CEMI Frames."""
         try:
             cemi = CEMIFrame.from_knx(raw_cemi)
+        except CouldNotParseCEMI as cemi_parse_err:
+            logger.warning("CEMI Frame failed to parse: %s", cemi_parse_err)
+            self.xknx.connection_manager.cemi_count_incoming_error += 1
+            return
         except UnsupportedCEMIMessage as unsupported_cemi_err:
             logger.info("CEMI not supported: %s", unsupported_cemi_err)
             self.xknx.connection_manager.cemi_count_incoming_error += 1
@@ -96,6 +101,9 @@ class CEMIHandler:
 
     def handle_cemi_frame(self, cemi: CEMIFrame) -> None:
         """Handle incoming CEMI Frames."""
+        if not isinstance(cemi.data, CEMILData):
+            return
+
         if cemi.code is CEMIMessageCode.L_DATA_CON:
             # L_DATA_CON confirmation frame signals ready to send next telegram
             self._l_data_confirmation_event.set()
@@ -106,7 +114,10 @@ class CEMIHandler:
             logger.warning("Received unexpected L_DATA_REQ frame: %s", cemi)
             self.xknx.connection_manager.cemi_count_incoming_error += 1
             return
-        if cemi.src_addr == self.xknx.current_address:
+        if (
+            cemi.code is CEMIMessageCode.L_DATA_IND
+            and cemi.data.src_addr == self.xknx.current_address
+        ):
             # L_DATA_IND frames from our own address should be ignored (may occur form routing)
             logger.debug("Ignoring own CEMI: %s", cemi)
             self.xknx.connection_manager.cemi_count_incoming_error += 1
diff --git a/xknx/cemi/const.py b/xknx/cemi/const.py
index a6ed4920..1b4031cc 100644
--- a/xknx/cemi/const.py
+++ b/xknx/cemi/const.py
@@ -1,5 +1,5 @@
 """Constants for CEMI."""
-from enum import Enum
+from enum import Enum, IntEnum
 
 
 class CEMIMessageCode(Enum):
@@ -25,6 +25,19 @@ class CEMIMessageCode(Enum):
     # (does not indicate a successful receive though)
     L_RAW_CON = 0x2F
 
+    # Management Configuration message types
+    M_PROP_READ_REQ = 0xFC  # Read Property Request
+    M_PROP_READ_CON = 0xFB  # Read Property Confirmation
+    M_PROP_WRITE_REQ = 0xF6  # Write Property Request
+    M_PROP_WRITE_CON = 0xF5  # Write Property Confirmation
+    M_PROP_INFO_IND = 0xF7  # Property Indication
+    M_FUNC_PROP_COMMAND_REQ = 0xF8  # Call Property Function
+    M_FUNC_PROP_STATE_READ_REQ = 0xF9  # Read status of Property Function
+    M_FUNC_PROP_COMMAND_CON = 0xFA  # Property Function Confirmation
+    M_FUNC_PROP_STATE_READ_CON = 0xFA  # Read status of Property Function Confirmation
+    M_RESET_REQ = 0xF1  # Reset Request
+    M_RESET_IND = 0xF0  # Reset confirmation
+
 
 class CEMIFlags:
     """Enum class for CEMI Flags."""
@@ -69,3 +82,23 @@ class CEMIFlags:
     # Bit 0/3+2+1+0
     STANDARD_FRAME_FORMAT = 0x0000
     EXTENDED_FRAME_FORMAT = 0x0001
+
+
+class CEMIErrorCode(IntEnum):
+    """Enum class for CEMI Error Codes."""
+
+    CEMI_ERROR_UNSPECIFIED = 0x00
+    CEMI_ERROR_OUT_OF_RANGE = 0x01
+    CEMI_ERROR_OUT_OF_MAX_RANGE = 0x02
+    CEMI_ERROR_OUT_OF_MIN_RANGE = 0x03
+    CEMI_ERROR_MEMORY = 0x04
+    CEMI_ERROR_READ_ONLY = 0x05
+    CEMI_ERROR_ILLEGAL_COMMAND = 0x06
+    CEMI_ERROR_VOID_DP = 0x07
+    CEMI_ERROR_TYPE_CONFLICT = 0x08
+    CEMI_ERROR_PROP_INDEX_RANGE = 0x09
+    CEMI_ERROR_TEMPORARILY_READ_ONLY = 0x0A
+
+    def __repr__(self) -> str:
+        """Return object as readable string."""
+        return str(self.value)
diff --git a/xknx/exceptions/__init__.py b/xknx/exceptions/__init__.py
index 90e576cc..5b132ee4 100644
--- a/xknx/exceptions/__init__.py
+++ b/xknx/exceptions/__init__.py
@@ -5,6 +5,7 @@ from .exception import (
     ConfirmationError,
     ConversionError,
     CouldNotParseAddress,
+    CouldNotParseCEMI,
     CouldNotParseKNXIP,
     CouldNotParseTelegram,
     DataSecureError,
@@ -26,6 +27,7 @@ __all__ = [
     "ConfirmationError",
     "ConversionError",
     "CouldNotParseAddress",
+    "CouldNotParseCEMI",
     "CouldNotParseKNXIP",
     "CouldNotParseTelegram",
     "DataSecureError",
diff --git a/xknx/exceptions/exception.py b/xknx/exceptions/exception.py
index 35080e4b..092369f3 100644
--- a/xknx/exceptions/exception.py
+++ b/xknx/exceptions/exception.py
@@ -95,6 +95,19 @@ class IncompleteKNXIPFrame(CouldNotParseKNXIP):
         return f'<IncompleteKNXIPFrame description="{self.description}" />'
 
 
+class CouldNotParseCEMI(XKNXException):
+    """Exception class for wrong CEMI data."""
+
+    def __init__(self, description: str = "") -> None:
+        """Initialize CouldNotParseCEMI class."""
+        super().__init__()
+        self.description = description
+
+    def __str__(self) -> str:
+        """Return object as readable string."""
+        return f'<CouldNotParseCEMI description="{self.description}" />'
+
+
 class UnsupportedCEMIMessage(XKNXException):
     """Exception class for unsupported CEMI Messages."""
 
diff --git a/xknx/io/request_response/__init__.py b/xknx/io/request_response/__init__.py
index 443ce2f0..a2a24b32 100644
--- a/xknx/io/request_response/__init__.py
+++ b/xknx/io/request_response/__init__.py
@@ -6,6 +6,7 @@ of specific KNX/IP Packets used for tunnelling connections.
 from .authenticate import Authenticate
 from .connect import Connect
 from .connectionstate import ConnectionState
+from .device_configuration import DeviceConfiguration
 from .disconnect import Disconnect
 from .request_response import RequestResponse
 from .session import Session
diff --git a/xknx/io/request_response/device_configuration.py b/xknx/io/request_response/device_configuration.py
new file mode 100644
index 00000000..d9d5233d
--- /dev/null
+++ b/xknx/io/request_response/device_configuration.py
@@ -0,0 +1,36 @@
+"""Abstraction to send a DeviceConfigurationRequest and wait for DeviceConfigurationAck."""
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from xknx.knxip import DeviceConfigurationAck, DeviceConfigurationRequest, KNXIPFrame
+
+from .request_response import RequestResponse
+
+if TYPE_CHECKING:
+    from xknx.io.transport import UDPTransport
+
+
+class DeviceConfiguration(RequestResponse):
+    """Class to send DeviceConfigurationRequest and wait for DeviceConfigurationAck (UDP only)."""
+
+    transport: UDPTransport
+
+    def __init__(
+        self,
+        transport: UDPTransport,
+        data_endpoint: tuple[str, int] | None,
+        device_configuration_request: DeviceConfigurationRequest,
+    ):
+        """Initialize DeviceConfiguration class."""
+        self.data_endpoint_addr = data_endpoint
+        self.device_configuration_request = device_configuration_request
+        super().__init__(transport, DeviceConfigurationAck)
+
+    async def send_request(self) -> None:
+        """Build knxipframe (within derived class) and send via UDP."""
+        self.transport.send(self.create_knxipframe(), addr=self.data_endpoint_addr)
+
+    def create_knxipframe(self) -> KNXIPFrame:
+        """Create KNX/IP Frame object to be sent to device."""
+        return KNXIPFrame.init_from_body(self.device_configuration_request)
diff --git a/xknx/knxip/__init__.py b/xknx/knxip/__init__.py
index 00fc76af..3e4f13f2 100644
--- a/xknx/knxip/__init__.py
+++ b/xknx/knxip/__init__.py
@@ -7,6 +7,8 @@ from .connectionstate_request import ConnectionStateRequest
 from .connectionstate_response import ConnectionStateResponse
 from .description_request import DescriptionRequest
 from .description_response import DescriptionResponse
+from .device_configuration_ack import DeviceConfigurationAck
+from .device_configuration_request import DeviceConfigurationRequest
 from .dib import (
     DIB,
     DIBDeviceInformation,
@@ -59,6 +61,8 @@ __all__ = [
     "ConnectionStateResponse",
     "DescriptionRequest",
     "DescriptionResponse",
+    "DeviceConfigurationAck",
+    "DeviceConfigurationRequest",
     "DIB",
     "DIBDeviceInformation",
     "DIBGeneric",
diff --git a/xknx/knxip/device_configuration_ack.py b/xknx/knxip/device_configuration_ack.py
new file mode 100644
index 00000000..d5bc74d6
--- /dev/null
+++ b/xknx/knxip/device_configuration_ack.py
@@ -0,0 +1,61 @@
+"""Module for Serialization and Deserialization of a KNX Device Configuration ACK."""
+from __future__ import annotations
+
+from xknx.exceptions import CouldNotParseKNXIP
+
+from .body import KNXIPBodyResponse
+from .error_code import ErrorCode
+from .knxip_enum import KNXIPServiceType
+
+
+class DeviceConfigurationAck(KNXIPBodyResponse):
+    """Representation of a KNX Device Configuration Ack."""
+
+    SERVICE_TYPE = KNXIPServiceType.DEVICE_CONFIGURATION_ACK
+    BODY_LENGTH = 4
+
+    def __init__(
+        self,
+        communication_channel_id: int = 1,
+        sequence_counter: int = 0,
+        status_code: ErrorCode = ErrorCode.E_NO_ERROR,
+    ):
+        """Initialize DeviceConfigurationAck object."""
+        self.communication_channel_id = communication_channel_id
+        self.sequence_counter = sequence_counter
+        self.status_code = status_code
+
+    def calculated_length(self) -> int:
+        """Get length of KNX/IP body."""
+        return DeviceConfigurationAck.BODY_LENGTH
+
+    def from_knx(self, raw: bytes) -> int:
+        """Parse/deserialize from KNX/IP raw data."""
+        if raw[0] != DeviceConfigurationAck.BODY_LENGTH:  # structure_length field
+            raise CouldNotParseKNXIP("DeviceConfigurationAck body has invalid length")
+        if len(raw) != DeviceConfigurationAck.BODY_LENGTH:
+            raise CouldNotParseKNXIP("DeviceConfigurationAck body has wrong length")
+        self.communication_channel_id = raw[1]
+        self.sequence_counter = raw[2]
+        self.status_code = ErrorCode(raw[3])
+        return DeviceConfigurationAck.BODY_LENGTH
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        return bytes(
+            (
+                DeviceConfigurationAck.BODY_LENGTH,
+                self.communication_channel_id,
+                self.sequence_counter,
+                self.status_code.value,
+            )
+        )
+
+    def __repr__(self) -> str:
+        """Return object as readable string."""
+        return (
+            "<DeviceConfigurationAck "
+            f'communication_channel_id="{self.communication_channel_id}" '
+            f'sequence_counter="{self.sequence_counter}" '
+            f'status_code="{self.status_code}" />'
+        )
diff --git a/xknx/knxip/device_configuration_request.py b/xknx/knxip/device_configuration_request.py
new file mode 100644
index 00000000..36dd7774
--- /dev/null
+++ b/xknx/knxip/device_configuration_request.py
@@ -0,0 +1,64 @@
+"""Module for Serialization and Deserialization of a KNX Device Configuration Request."""
+from __future__ import annotations
+
+from xknx.exceptions import CouldNotParseKNXIP
+
+from .body import KNXIPBody
+from .knxip_enum import KNXIPServiceType
+
+
+class DeviceConfigurationRequest(KNXIPBody):
+    """Representation of a KNX Device Configuration Request."""
+
+    SERVICE_TYPE = KNXIPServiceType.DEVICE_CONFIGURATION_REQUEST
+    HEADER_LENGTH = 4
+
+    def __init__(
+        self,
+        communication_channel_id: int = 1,
+        sequence_counter: int = 0,
+        raw_cemi: bytes = b"",
+    ):
+        """Initialize DeviceConfigurationRequest object."""
+        self.communication_channel_id = communication_channel_id
+        self.sequence_counter = sequence_counter
+        self.raw_cemi = raw_cemi
+
+    def calculated_length(self) -> int:
+        """Get length of KNX/IP body."""
+        return DeviceConfigurationRequest.HEADER_LENGTH + len(self.raw_cemi)
+
+    def from_knx(self, raw: bytes) -> int:
+        """Parse/deserialize from KNX/IP raw data."""
+        if raw[0] != DeviceConfigurationRequest.HEADER_LENGTH:
+            raise CouldNotParseKNXIP("connection header wrong length")
+        if len(raw) < DeviceConfigurationRequest.HEADER_LENGTH:
+            raise CouldNotParseKNXIP("connection header wrong length")
+        self.communication_channel_id = raw[1]
+        self.sequence_counter = raw[2]
+        # raw[3] is reserved
+        self.raw_cemi = raw[DeviceConfigurationRequest.HEADER_LENGTH :]
+        return len(raw)
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        return (
+            bytes(
+                (
+                    DeviceConfigurationRequest.HEADER_LENGTH,
+                    self.communication_channel_id,
+                    self.sequence_counter,
+                    0x00,  # Reserved
+                )
+            )
+            + self.raw_cemi
+        )
+
+    def __repr__(self) -> str:
+        """Return object as readable string."""
+        return (
+            "<DeviceConfigurationRequest "
+            f'communication_channel_id="{self.communication_channel_id}" '
+            f'sequence_counter="{self.sequence_counter}" '
+            f'cemi="{self.raw_cemi.hex()}" />'
+        )
diff --git a/xknx/knxip/knxip.py b/xknx/knxip/knxip.py
index 298629ef..79cd00b3 100644
--- a/xknx/knxip/knxip.py
+++ b/xknx/knxip/knxip.py
@@ -15,6 +15,8 @@ from .connectionstate_request import ConnectionStateRequest
 from .connectionstate_response import ConnectionStateResponse
 from .description_request import DescriptionRequest
 from .description_response import DescriptionResponse
+from .device_configuration_ack import DeviceConfigurationAck
+from .device_configuration_request import DeviceConfigurationRequest
 from .disconnect_request import DisconnectRequest
 from .disconnect_response import DisconnectResponse
 from .header import KNXIPHeader
@@ -95,6 +97,11 @@ class KNXIPFrame:
             body = DisconnectRequest()
         elif header.service_type_ident == KNXIPServiceType.DISCONNECT_RESPONSE:
             body = DisconnectResponse()
+        # Device Management
+        elif header.service_type_ident == KNXIPServiceType.DEVICE_CONFIGURATION_REQUEST:
+            body = DeviceConfigurationRequest()
+        elif header.service_type_ident == KNXIPServiceType.DEVICE_CONFIGURATION_ACK:
+            body = DeviceConfigurationAck()
         # Tunneling
         elif header.service_type_ident == KNXIPServiceType.TUNNELLING_REQUEST:
             body = TunnellingRequest()
diff --git a/xknx/profile/__init__.py b/xknx/profile/__init__.py
new file mode 100644
index 00000000..ce255348
--- /dev/null
+++ b/xknx/profile/__init__.py
@@ -0,0 +1,19 @@
+"""Module for Profile and resource management ."""
+# flake8: noqa
+from .const import (
+    ResourceDevicePropertyId,
+    ResourceEModeChannelPropertyId,
+    ResourceEModeDevicePropertyId,
+    ResourceGenericPropertyId,
+    ResourceGroupObjectTablePropertyId,
+    ResourceKNXNETIPPropertyId,
+    ResourceLTERoutingTablePropertyId,
+    ResourceObjectType,
+    ResourcePollingMasterInterfacePropertyId,
+    ResourceProgramPropertyId,
+    ResourcePropertyId,
+    ResourceRFMediumPropertyId,
+    ResourceRouterPropertyId,
+    ResourceSecureInterfacePropertyId,
+    ResourceTextCataloguePropertyId,
+)
diff --git a/xknx/profile/const.py b/xknx/profile/const.py
new file mode 100644
index 00000000..e251cb1d
--- /dev/null
+++ b/xknx/profile/const.py
@@ -0,0 +1,317 @@
+"""Constants for Profile resources."""
+from enum import IntEnum
+
+
+class ResourceObjectType(IntEnum):
+    """Enum class for Resource Object Types."""
+
+    OBJECT_DEVICE = 0x0000
+    OBJECT_ADDRESS_TABLE = 0x0001
+    OBJECT_ASSOCIATION_TABLE = 0x0002
+    OBJECT_APPLICATION_PROGRAM = 0x0003
+    OBJECT_INTERFACE_PROGRAM = 0x0004
+    OBJECT_KNX_OBJECT_ASSOCIATION_TABLE = 0x0005
+    OBJECT_ROUTER = 0x0006
+    OBJECT_LTE_ADDRESS_ROUTING_TABLE = 0x0007
+    OBJECT_CEMI_SERVER = 0x0008
+    OBJECT_GROUP_OBJECT_TABLE = 0x0009
+    OBJECT_POLLING_MASTER = 0x000A
+    OBJECT_KNXNETIP_PARAMETER = 0x000B
+    OBJECT_FILESERVER = 0x000D
+    OBJECT_E_MODE_CHANNEL = 0x000E
+    OBJECT_ADJUSTED_E_MODE_CHANNEL = 0x000F
+    OBJECT_TEXT_CATALOGUE = 0x0010
+    OBJECT_SECURITY = 0x0011
+    OBJECT_E_MODE_DEVICE = 0x0012
+    OBJECT_RF_MEDIUM_OBJECT = 0x0013
+
+
+ResourcePropertyId = IntEnum
+
+
+class ResourceGenericPropertyId(ResourcePropertyId):
+    """Enum base class for Property Ids."""
+
+    PID_OBJECT_TYPE = 1  # PDT_UNSIGNED_INT
+    PID_OBJECT_NAME = 2  # PDT_UNSIGNED_CHAR[]
+    PID_SEMAPHOR = 3
+    PID_GROUP_OBJECT_REFERENCE = 4
+    PID_LOAD_STATE_CONTROL = 5  # PDT_CONTROL
+    PID_RUN_STATE_CONTROL = 6  # PDT_CONTROL
+    PID_TABLE_REFERENCE = 7  # PDT_UNSIGNED_LONG
+    PID_SERVICE_CONTROL = 8  # PDT_UNSIGNED_INT
+    PID_FIRMWARE_REVISION = 9  # PDT_UNSIGNED_CHAR
+    PID_SERVICES_SUPPORTED = 10
+    PID_SERIAL_NUMBER = 11  # PDT_GENERIC_06
+    PID_MANUFACTURER_ID = 12  # PDT_UNSIGNED_INT
+    PID_PROGRAM_VERSION = 13  # PDT_GENERIC_05
+    PID_DEVICE_CONTROL = 14  # PDT_BITSET8/PDT_GENERIC_01
+    PID_ORDER_INFO = 15  # PDT_GENERIC_10
+    PID_PEI_TYPE = 16  # PDT_UNSIGNED_CHAR
+    PID_PORT_CONFIGURATION = 17  # PDT_UNSIGNED_CHAR
+    PID_POLL_GROUP_SETTINGS = 18  # PDT_POLL_GROUP_SETTINGS
+    PID_MANUFACTURER_DATA = 19  # PDT_GENERIC_04
+    PID_ENABLE = 20
+    PID_DESCRIPTION = 21  # PDT_UNSIGNED_CHAR[]
+    PID_TABLE = 23
+    PID_ENROL = 24  # PDT_FUNCTION
+    PID_VERSION = 25  # PDT_VERSION/PDT_GENERIC_02
+    PID_GROUP_OBJECT_LINK = 26  # PDT_FUNCTION
+    PID_MCB_TABLE = 27  # PDT_GENERIC_08[]
+    PID_ERROR_CODE = 28  # PDT_ENUM8/PDT_UNSIGNED_CHAR
+    PID_OBJECT_INDEX = 29  # PDT_UNSIGNED_CHAR
+    PID_DOWNLOAD_COUNTER = 30  # PDT_UNSIGNED_INT
+
+
+class ResourceDevicePropertyId(ResourcePropertyId):
+    """Extended class for Device Object (Type 0) Property Ids."""
+
+    PID_ROUTING_COUNT = 51  # PDT_UNSIGNED_CHAR
+    PID_MAX_RETRY_COUNT = 52  # PDT_GENERIC_01
+    PID_ERROR_FLAGS = 53  # PDT_UNSIGNED_CHAR
+    PID_PROGMODE = 54  # PDT_GENERIC_01
+    PID_PRODUCT_ID = 55  # PDT_GENERIC_10
+    PID_MAX_APDU_LENGTH = 56  # PDT_UNSIGNED_INT
+    PID_SUBNET_ADDR = 57  # PDT_UNSIGNED_CHAR
+    PID_DEVICE_ADDR = 58  # PDT_UNSIGNED_CHAR
+    PID_PB_CONFIG = 59  # PDT_GENERIC_04
+    PID_ADDR_REPORT = 60  # PDT_GENERIC_06
+    PID_ADDR_CHECK = 61  # PDT_GENERIC_01
+    PID_OBJECT_VALUE = 62  # PDT_FUNCTION
+    PID_OBJECTLINK = 63  # PDT_FUNCTION
+    PID_APPLICATION = 64  # PDT_FUNCTION
+    PID_PARAMETER = 65  # PDT_FUNCTION
+    PID_OBJECTADDRESS = 66  # PDT_FUNCTION
+    PID_PSU_TYPE = 67  # PDT_UNSIGNED_INT
+    PID_PSU_STATUS = 68  # PDT_BINARY_INFORMATION
+    PID_PSU_ENABLE = 69  # PDT_ENUM8
+    PID_DOMAIN_ADDRESS = 70  # PDT_UNSIGNED_INT
+    PID_IO_LIST = 71  # PDT_UNSIGNED_INT
+    PID_MGT_DESCRIPTOR_01 = 72  # PDT_GENERIC_10
+    PID_PL110_PARAM = 73  # PDT_GENERIC_01
+    PID_RF_REPEAT_COUNTER = 74  # PDT_UNSIGNED_CHAR
+    PID_RECEIVE_BLOCK_TABLE = 75  # PDT_UNSIGNED_CHAR[16]
+    PID_RANDOM_PAUSE_TABLE = 76  # PDT_UNSIGNED_CHAR[12]
+    PID_RECEIVE_BLOCK_NR = 77  # PDT_UNSIGNED_CHAR
+    PID_HARDWARE_TYPE = 78  # PDT_GENERIC_06
+    PID_RETRANSMITTER_NUMBER = 79  # PDT_UNSIGNED_CHAR
+    PID_SERIAL_NR_TABLE = 80  # PDT_GENERIC_06[]
+    PID_BIBAT_MASTER_ADDRESS = 81  # PDT_UNSIGNED_INT
+    PID_RF_DOMAIN_ADDRESS = 82  # PDT_GENERIC_06
+    PID_DEVICE_DESCRIPTOR = 83  # PDT_GENERIC_02
+    PID_METERING_FILTER_TABLE = 84  # PDT_GENERIC_08[]
+    PID_GROUP_TELEGR_RATE_LIMIT_TIME_BASE = 85  # PDT_UNSIGNED_INT
+    PID_GROUP_TELEGR_RATE_LIMIT_NO_OF_TELEGR = 86  # PDT_UNSIGNED_INT
+    PID_FEATURES_SUPPORTED = 89  # PDT_GENERIC_10
+    PID_CHANNEL_01_PARAM = 101  # PDT_GENERIC_01[]
+    PID_CHANNEL_02_PARAM = 102  # PDT_GENERIC_01[]
+    PID_CHANNEL_03_PARAM = 103  # PDT_GENERIC_01[]
+    PID_CHANNEL_04_PARAM = 104  # PDT_GENERIC_01[]
+    PID_CHANNEL_05_PARAM = 105  # PDT_GENERIC_01[]
+    PID_CHANNEL_06_PARAM = 106  # PDT_GENERIC_01[]
+    PID_CHANNEL_07_PARAM = 107  # PDT_GENERIC_01[]
+    PID_CHANNEL_08_PARAM = 108  # PDT_GENERIC_01[]
+    PID_CHANNEL_09_PARAM = 109  # PDT_GENERIC_01[]
+    PID_CHANNEL_10_PARAM = 110  # PDT_GENERIC_01[]
+    PID_CHANNEL_11_PARAM = 111  # PDT_GENERIC_01[]
+    PID_CHANNEL_12_PARAM = 112  # PDT_GENERIC_01[]
+    PID_CHANNEL_13_PARAM = 113  # PDT_GENERIC_01[]
+    PID_CHANNEL_14_PARAM = 114  # PDT_GENERIC_01[]
+    PID_CHANNEL_15_PARAM = 115  # PDT_GENERIC_01[]
+    PID_CHANNEL_16_PARAM = 116  # PDT_GENERIC_01[]
+    PID_CHANNEL_17_PARAM = 117  # PDT_GENERIC_01[]
+    PID_CHANNEL_18_PARAM = 118  # PDT_GENERIC_01[]
+    PID_CHANNEL_19_PARAM = 119  # PDT_GENERIC_01[]
+    PID_CHANNEL_20_PARAM = 120  # PDT_GENERIC_01[]
+    PID_CHANNEL_21_PARAM = 121  # PDT_GENERIC_01[]
+    PID_CHANNEL_22_PARAM = 122  # PDT_GENERIC_01[]
+    PID_CHANNEL_23_PARAM = 123  # PDT_GENERIC_01[]
+    PID_CHANNEL_24_PARAM = 124  # PDT_GENERIC_01[]
+    PID_CHANNEL_25_PARAM = 125  # PDT_GENERIC_01[]
+    PID_CHANNEL_26_PARAM = 126  # PDT_GENERIC_01[]
+    PID_CHANNEL_27_PARAM = 127  # PDT_GENERIC_01[]
+    PID_CHANNEL_28_PARAM = 128  # PDT_GENERIC_01[]
+    PID_CHANNEL_29_PARAM = 129  # PDT_GENERIC_01[]
+    PID_CHANNEL_30_PARAM = 130  # PDT_GENERIC_01[]
+    PID_CHANNEL_31_PARAM = 131  # PDT_GENERIC_01[]
+    PID_CHANNEL_32_PARAM = 132  # PDT_GENERIC_01[]
+
+
+class ResourceProgramPropertyId(ResourcePropertyId):
+    """Extended class for Program Object (Type 3 & 4) Property Ids."""
+
+    PID_OPERATION_MODE = 51  # PDT_FUNCTION/PDT_VARIABLE_LENGTH
+
+
+class ResourceRouterPropertyId(ResourcePropertyId):
+    """Extended class for Router Object (Type 6) Property Ids."""
+
+    PID_MEDIUM_STATUS = 51  # PDT_GENERIC_01
+    PID_MAIN_LCCONFIG = 52
+    PID_SUB_LCCONFIG = 53
+    PID_MAIN_LCGRPCONFIG = 54
+    PID_SUB_LCGRPCONFIG = 55
+    PID_ROUTETABLE_CONTROL = 56
+    PID_COUPL_SERV_CONTROL = 57
+    PID_MAX_APDU_LENGTH_ROUTER = 58  # PDT_UNSIGNED_INT
+    PID_L2_COUPLER_TYPE = 59  # PDT_BITSET8
+    PID_HOP_COUNT = 61  # PDT_UNSIGNED_INT
+    PID_MEDIUM = 63  # PDT_ENUM8
+    PID_FILTER_TABLE_USE = 67  # PDT_BINARY_INFORMATION
+    PID_PL110_SBC_CONTROL = 104  # PDT_FUNCTION
+    PID_RF_SBC_CONTROL = 112  # PDT_FUNCTION
+    PID_IP_SBC_CONTROL = 120  # PDT_FUNCTION
+
+
+class ResourceLTERoutingTablePropertyId(ResourcePropertyId):
+    """Extended class for LTE Address Routing Table Object (Object Type 7) Property Ids."""
+
+    PID_LTE_ROUTESELECT = 51
+    PID_LTE_ROUTETABLE = 52  # PDT_GENERIC_05
+
+
+class ResourceCEMIServerPropertyId(ResourcePropertyId):
+    """Extended class for cEMI Server Object (Object Type 8) Property Ids."""
+
+    PID_MEDIUM_TYPE = 51  # PDT_BITSET16
+    PID_COMM_MODE = 52  # PDT_ENUM8
+    PID_MEDIUM_AVAILABILITY = 53  # PDT_BITSET16
+    PID_ADD_INFO_TYPES = 54  # PDT_ENUM8[]
+    PID_TIME_BASE = 55  # PDT_UNSIGNED_INT
+    PID_TRANSP_ENABLE = 56  # PDT_BINARY_INFORMATION
+    PID_CEMI_SERVER_DEVICE_ADDRESS = 58  # PDT_UNSIGNED_CHAR
+    PID_BIBAT_NEXTBLOCK = 59  # PDT_UNSIGNED_CHAR
+    PID_RF_MODE_SELECT = 60  # PDT_ENUM8/PDT_UNSIGNED_CHAR
+    PID_RF_MODE_SUPPORT = 61  # PDT_BITSET8/PDT_GENERIC_01
+    PID_RF_FILTERING_MODE_SELECT = 62  # PDT_ENUM8/PDT_UNSIGNED_CHAR
+    PID_RF_FILTERING_MODE_SUPPORT = 63  # PDT_BITSET8/PDT_GENERIC_01
+    PID_COMM_MODES_SUPPORTED = 64  # PDT_BITSET16
+    PID_FILTERING_MODE_SUPPORT = 65  # PDT_BITSET16/PDT_GENERIC_02
+    PID_FILTERING_MODE_SELECT = 66  # PDT_BITSET16/PDT_GENERIC_02
+    PID_MAX_INTERFACE_APDU_LENGTH = 68  # PDT_UNSIGNED_INT
+    PID_MAX_LOCAL_APDU_LENGTH = 69  # PDT_UNSIGNED_INT
+
+
+class ResourceGroupObjectTablePropertyId(ResourcePropertyId):
+    """Extended class for Group Object Table Object (Object Type 9) Property Ids."""
+
+    PID_GO_DIAGNOSTICS = 66  # PDT_FUNCTION
+
+
+class ResourcePollingMasterInterfacePropertyId(ResourcePropertyId):
+    """Extended class for Polling Master Interface Object (Object Type 10) Property Ids."""
+
+    PID_POLLING_STATE = 51
+    PID_POLLING_SLAVE_ADDR = 52
+    PID_POLL_CYCLE = 53
+
+
+class ResourceKNXNETIPPropertyId(ResourcePropertyId):
+    """Extended class for KNXNet/IP Object (Object Type 11) Property Ids."""
+
+    PID_PROJECT_INSTALLATION_ID = 51  # PDT_UNSIGNED_INT
+    PID_KNX_INDIVIDUAL_ADDRESS = 52  # PDT_UNSIGNED_INT
+    PID_ADDITIONAL_INDIVIDUAL_ADDRESSES = 53  # PDT_UNSIGNED_INT[]
+    PID_CURRENT_IP_ASSIGNMENT_METHOD = 54  # PDT_UNSIGNED_CHAR
+    PID_IP_ASSIGNMENT_METHOD = 55  # PDT_UNSIGNED_CHAR
+    PID_IP_CAPABILITIES = 56  # PDT_BITSET8
+    PID_CURRENT_IP_ADDRESS = 57  # PDT_UNSIGNED_LONG
+    PID_CURRENT_SUBNET_MASK = 58  # PDT_UNSIGNED_LONG
+    PID_CURRENT_DEFAULT_GATEWAY = 59  # PDT_UNSIGNED_LONG
+    PID_IP_ADDRESS = 60  # PDT_UNSIGNED_LONG
+    PID_SUBNET_MASK = 61  # PDT_UNSIGNED_LONG
+    PID_DEFAULT_GATEWAY = 62  # PDT_UNSIGNED_LONG
+    PID_DHCP_BOOTP_SERVER = 63  # PDT_UNSIGNED_LONG
+    PID_MAC_ADDRESS = 64  # PDT_GENERIC_06
+    PID_SYSTEM_SETUP_MULTICAST_ADDRESS = 65  # PDT_UNSIGNED_LONG
+    PID_ROUTING_MULTICAST_ADDRESS = 66  # PDT_UNSIGNED_LONG
+    PID_TTL = 67  # PDT_UNSIGNED_CHAR
+    PID_KNXNETIP_DEVICE_CAPABILITIES = 68  # PDT_BITSET16
+    PID_KNXNETIP_DEVICE_STATE = 69  # PDT_UNSIGNED_CHAR
+    PID_KNXNETIP_ROUTING_CAPABILITIES = 70  # PDT_UNSIGNED_CHAR
+    PID_PRIORITY_FIFO_ENABLED = 71  # PDT_BINARY_INFORMATION
+    PID_QUEUE_OVERFLOW_TO_IP = 72  # PDT_UNSIGNED_INT
+    PID_QUEUE_OVERFLOW_TO_KNX = 73  # PDT_UNSIGNED_INT
+    PID_MSG_TRANSMIT_TO_IP = 74  # PDT_UNSIGNED_LONG
+    PID_MSG_TRANSMIT_TO_KNX = 75  # PDT_UNSIGNED_LONG
+    PID_FRIENDLY_NAME = 76  # PDT_UNSIGNED_CHAR[30]
+    PID_ROUTING_BUSY_WAIT_TIME = 78  # PDT_UNSIGNED_INT
+    PID_TUNNELLING_ADDRESSES = 79  # PDT_UNSIGNED_CHAR[]
+
+
+class ResourceEModeChannelPropertyId(ResourcePropertyId):
+    """Extended class for E-Mode Channel Object (Object Type 14 & 15) Property Ids."""
+
+    PID_CHAN_NUMBER = 51  # PDT_UNSIGNED_INT
+    PID_CHAN_CODE = 52  # PDT_UNSIGNED_INT
+    PID_CHAN_FLAGS = 53  # PDT_GENERIC_02
+    PID_CHAN_FB_LIST = 54  # PDT_UNSIGNED_INT[]
+    PID_CHAN_ADJ_LISTS = 55  # PDT_UNSIGNED_CHAR[]
+    PID_GO_CCODES_LIST = 61  # PDT_GENERIC_08[]
+    PID_GO_CFLAGS_LIST = 62  # PDT_BITSET16[]
+    PID_OBJECTLINK = 63  # PDT_FUNCTION
+    PID_GO_SUBUNIT = 64  # PDT_UNSIGNED_CHAR[]
+    PID_GO_NAME_LIST = 65  # PDT_GENERIC_10[]/PDT_REFERENCE[]
+    PID_GO_DIAGNOSTICS = 66  # PDT_FUNCTION
+    PID_PARAM_TYPES = 70  # PDT_GENERIC_10[]
+    PID_PARAM_FLAGS = 71  # PDT_BITSET16[]
+    PID_PARAM_NAMES = 72  # PDT_GENERIC_10[]/PDT_REFERENCE[]
+    PID_PARAM_UNITS = 73  # PDT_GENERIC_10[]/PDT_REFERENCE[]
+    PID_PARAM_VALUES = (
+        79  # PDT_GENERIC_01[]/PDT_GENERIC_02[]/PDT_GENERIC_04[]/PDT_GENERIC_010[]
+    )
+
+
+class ResourceTextCataloguePropertyId(ResourcePropertyId):
+    """Extended class for Text Catalogue Object (Object Type 16) Property Ids."""
+
+    PID_LOCALE_LIST = 51  # PDT_GENERIC_04[]
+    PID_LOCALE_SELECTION = 52  # PDT_GENERIC_04
+    PID_ACTIVE_LOCALE = 53  # PDT_GENERIC_04
+    # PID_STRING_001 to PID_STRING_141 is [60-200]
+    PID_STRING_001 = 60  # PDT_UTF_8
+
+
+class ResourceSecureInterfacePropertyId(ResourcePropertyId):
+    """Extended class for Secure Interface Object (Object Type 17) Property Ids."""
+
+    PID_SECURITY_MODE = 51  # PDT_FUNCTION
+    PID_P2P_KEY_TABLE = 52  # PDT_GENERIC_20[]
+    PID_GRP_KEY_TABLE = 53  # PDT_GENERIC_18[]
+    PID_SECURITY_INDIVIDUAL_ADDRESS_TABLE = 54  # PDT_GENERIC_08[]
+    PID_SECURITY_FAILURES_LOG = 55  # PDT_FUNCTION
+    PID_TOOL_KEY = 56  # PDT_GENERIC_16
+    PID_SECURITY_REPORT = 57  # PDT_BITSET8
+    PID_SECURITY_REPORT_CONTROL = 58  # PDT_BINARY_INFORMATION
+    PID_SEQUENCE_NUMBER_SENDING = 59  # PDT_GENERIC_06
+    PID_ZONE_KEY_TABLE = 60  # PDT_GENERIC_19[]
+    PID_GO_SECURITY_FLAGS = 61  # PDT_GENERIC_01[]
+
+
+class ResourceEModeDevicePropertyId(ResourcePropertyId):
+    """Extended class for E-Mode Device Object (Object Type 18) Property Ids."""
+
+    PID_LOCALISATION_MODE = 60  # PDT_BINARY_INFORMATION/PDT_UNSIGNED_CHAR
+    PID_LOCALISATION_REPORT = 61  # PDT_UNSIGNED_INT
+    PID_LOCALISATION_COMMAND = 62  # PDT_GENERIC_03
+
+
+class ResourceRFMediumPropertyId(ResourcePropertyId):
+    """Extended class for RF Medium Object (Object Type 19) Property Ids."""
+
+    PID_RF_MULTI_TYPE = 51  # PDT_BITSET8
+    PID_RF_MULTI_PHYSICAL_FEATURES = 52  # PDT_BITSET8
+    PID_RF_MULTI_CALL_CHANNEL = 53  # PDT_GENERIC_01
+    PID_RF_MULTI_OBJECT_LINK = 54  # PDT_FUNCTION
+    PID_RF_MULTI_EXT_GA_REPEATED = 55  # PDT_FUNCTION
+    PID_RF_RETRANSMITTER = 57  # PDT_BINARY_INFORMATION
+    PID_RF_BIDIR_TIMEOUT = 60  # PDT_FUNCTION
+    PID_RF_DIAG_SA_FILTER_TABLE = 61  # PDT_GENERIC_03[]
+    PID_RF_DIAG_QUALITY_TABLE = 62  # PDT_GENERIC_04[]
+    PID_RF_DIAG_PROBE = 63  # PDT_FUNCTION
+    PID_TRANSMISSION_MODE = 70  # PDT_ENUM8
+    PID_RECEPTION_MODE = 71  # PDT_ENUM8
+    PID_TEST_SIGNAL = 72  # PDT_GENERIC_02
+    PID_FAST_ACK = 73  # PDT_GENERIC_02[]
+    PID_FAST_ACK_ACTIVATE = 74  # PDT_BINARY_INFORMATION
+    PID_RF_TYPES_SUPPORTED = 75  # PDT_BITSET8/PDT_GENERIC_01
diff --git a/xknx/secure/data_secure.py b/xknx/secure/data_secure.py
index ac44b0c0..fa1e4e22 100644
--- a/xknx/secure/data_secure.py
+++ b/xknx/secure/data_secure.py
@@ -3,13 +3,13 @@ from __future__ import annotations
 
 from collections.abc import Iterator
 from contextlib import contextmanager
-from copy import copy
+from copy import deepcopy
 from datetime import datetime, timezone
 import logging
 import time
 
-from xknx.cemi import CEMIFrame
-from xknx.exceptions import DataSecureError
+from xknx.cemi import CEMIFrame, CEMILData
+from xknx.exceptions import DataSecureError, UnsupportedCEMIMessage
 from xknx.telegram.address import GroupAddress, IndividualAddress
 from xknx.telegram.apci import APCI, SecureAPDU
 
@@ -131,12 +131,15 @@ class DataSecure:
 
     def received_cemi(self, cemi: CEMIFrame) -> CEMIFrame:
         """Handle received CEMI frame."""
+        # No data frame
+        if not isinstance(cemi.data, CEMILData):
+            return cemi
         # Data Secure frame
-        if isinstance(cemi.payload, SecureAPDU):
-            return self._received_secure_cemi(cemi, cemi.payload)
+        if isinstance(cemi.data.payload, SecureAPDU):
+            return self._received_secure_cemi(cemi, cemi.data.payload)
         # Plain group communication frame
-        if isinstance(cemi.dst_addr, GroupAddress):
-            if cemi.dst_addr in self._group_key_table:
+        if isinstance(cemi.data.dst_addr, GroupAddress):
+            if cemi.data.dst_addr in self._group_key_table:
                 raise DataSecureError(
                     f"Discarding frame with plain APDU for secure group address: {cemi}",
                     log_level=logging.WARNING,
@@ -163,11 +166,15 @@ class DataSecure:
                 log_level=logging.DEBUG,
             )
 
+        # Sanity check for Link Layer Data frame
+        if not isinstance(cemi.data, CEMILData):
+            raise UnsupportedCEMIMessage(f"Secure CEMI data frames only {cemi}")
+
         # Secure group communication frame
-        if isinstance(cemi.dst_addr, GroupAddress):
-            if not (key := self._group_key_table.get(cemi.dst_addr)):
+        if isinstance(cemi.data.dst_addr, GroupAddress):
+            if not (key := self._group_key_table.get(cemi.data.dst_addr)):
                 raise DataSecureError(
-                    f"No key found for group address {cemi.dst_addr} from {cemi.src_addr}",
+                    f"No key found for group address {cemi.data.dst_addr} from {cemi.data.src_addr}",
                     log_level=logging.INFO,
                 )
         # Secure point-to-point frame
@@ -179,30 +186,39 @@ class DataSecure:
             )
 
         with self.check_sequence_number(
-            source_address=cemi.src_addr,
+            source_address=cemi.data.src_addr,
             received_sequence_number=int.from_bytes(
                 s_apdu.secured_data.sequence_number_bytes, "big"
             ),
         ):
-            _address_fields_raw = cemi.src_addr.to_knx() + cemi.dst_addr.to_knx()
+            _address_fields_raw = (
+                cemi.data.src_addr.to_knx() + cemi.data.dst_addr.to_knx()
+            )
             plain_apdu_raw = s_apdu.secured_data.get_plain_apdu(
                 key=key,
                 scf=s_apdu.scf,
                 address_fields_raw=_address_fields_raw,
-                frame_flags=cemi.flags,
-                tpci=cemi.tpci,
+                frame_flags=cemi.data.flags,
+                tpci=cemi.data.tpci,
             )
         decrypted_payload = APCI.from_knx(plain_apdu_raw)
         _LOGGER.debug("Unpacked APDU %s from %s", decrypted_payload, s_apdu)
-        plain_cemi = copy(cemi)
-        plain_cemi.payload = decrypted_payload
+        plain_cemi = deepcopy(cemi)
+
+        # Sanity check for Link Layer Data frame
+        if not isinstance(plain_cemi.data, CEMILData):
+            raise UnsupportedCEMIMessage(f"Secure CEMI data frames only {plain_cemi}")
+
+        plain_cemi.data.payload = decrypted_payload
         return plain_cemi
 
     def outgoing_cemi(self, cemi: CEMIFrame) -> CEMIFrame:
         """Handle outgoing CEMI frame. Pass through as plain frame or encrypt."""
         # Outgoing  group communication frame
-        if isinstance(cemi.dst_addr, GroupAddress):
-            if key := self._group_key_table.get(cemi.dst_addr):
+        if isinstance(cemi.data, CEMILData) and isinstance(
+            cemi.data.dst_addr, GroupAddress
+        ):
+            if key := self._group_key_table.get(cemi.data.dst_addr):
                 scf = SecurityControlField(
                     algorithm=SecurityAlgorithmIdentifier.CCM_ENCRYPTION,
                     service=SecurityALService.S_A_DATA,
@@ -223,8 +239,11 @@ class DataSecure:
     ) -> CEMIFrame:
         """Wrap encrypted payload of a plain CEMIFrame in a SecureAPDU."""
         plain_apdu_raw: bytes | bytearray
-        if cemi.payload is not None:
-            plain_apdu_raw = cemi.payload.to_knx()
+        if not isinstance(cemi.data, CEMILData):
+            raise UnsupportedCEMIMessage(f"CEMI L_Data frame required {cemi}.")
+
+        if cemi.data.payload is not None:
+            plain_apdu_raw = cemi.data.payload.to_knx()
         else:
             # TODO: test if this is correct
             plain_apdu_raw = b""  # used ein point-to-point eg. TConnect
@@ -233,11 +252,18 @@ class DataSecure:
             apdu=plain_apdu_raw,
             scf=scf,
             sequence_number=self.get_sequence_number(),
-            address_fields_raw=cemi.src_addr.to_knx() + cemi.dst_addr.to_knx(),
-            frame_flags=cemi.flags,
-            tpci=cemi.tpci,
+            address_fields_raw=cemi.data.src_addr.to_knx()
+            + cemi.data.dst_addr.to_knx(),
+            frame_flags=cemi.data.flags,
+            tpci=cemi.data.tpci,
+        )
+        secure_cemi = deepcopy(cemi)
+
+        if not isinstance(secure_cemi.data, CEMILData):
+            raise UnsupportedCEMIMessage(f"CEMI L_Data frame required {secure_cemi}.")
+
+        secure_cemi.data.payload = SecureAPDU(scf=scf, secured_data=secure_asdu)
+        _LOGGER.debug(
+            "Secured APDU %s with %s", cemi.data.payload, secure_cemi.data.payload
         )
-        secure_cemi = copy(cemi)
-        secure_cemi.payload = SecureAPDU(scf=scf, secured_data=secure_asdu)
-        _LOGGER.debug("Secured APDU %s with %s", cemi.payload, secure_cemi.payload)
         return secure_cemi


Test patch:
diff --git a/requirements/testing.txt b/requirements/testing.txt
index db4b4e77..6f419c33 100644
--- a/requirements/testing.txt
+++ b/requirements/testing.txt
@@ -1,13 +1,13 @@
 -r production.txt
-pre-commit==3.1.0
+pre-commit==3.1.1
 isort==5.12.0
 flake8==6.0.0
 flake8-isort==6.0.0
 pydocstyle==6.3.0
-pylint==2.16.2
-pytest==7.2.1
+pylint==2.16.3
+pytest==7.2.2
 pytest-asyncio==0.20.3
 pytest-cov==4.0.0
-setuptools==67.4.0
+setuptools==67.5.0
 tox==4.4.6
 mypy==1.0.1
diff --git a/test/cemi_tests/cemi_frame_test.py b/test/cemi_tests/cemi_frame_test.py
index b82a81ce..4443c770 100644
--- a/test/cemi_tests/cemi_frame_test.py
+++ b/test/cemi_tests/cemi_frame_test.py
@@ -1,8 +1,19 @@
 """Tests for the CEMIFrame object."""
 import pytest
 
-from xknx.cemi import CEMIFlags, CEMIFrame, CEMIMessageCode
-from xknx.exceptions import ConversionError, UnsupportedCEMIMessage
+from xknx.cemi import (
+    CEMIFlags,
+    CEMIFrame,
+    CEMILData,
+    CEMIMessageCode,
+    CEMIMPropReadRequest,
+    CEMIMPropReadResponse,
+    CEMIMPropWriteRequest,
+    CEMIMPropWriteResponse,
+)
+from xknx.cemi.const import CEMIErrorCode
+from xknx.exceptions import ConversionError, CouldNotParseCEMI, UnsupportedCEMIMessage
+from xknx.profile.const import ResourceKNXNETIPPropertyId, ResourceObjectType
 from xknx.telegram import GroupAddress, IndividualAddress, Telegram
 from xknx.telegram.apci import GroupValueRead
 from xknx.telegram.tpci import TConnect, TDataBroadcast, TDataGroup
@@ -33,11 +44,13 @@ def test_valid_command():
     raw = get_data(0x29, 0, 0x0080, 1, 1, 1, 0, [])
     frame = CEMIFrame.from_knx(raw)
     assert frame.code == CEMIMessageCode.L_DATA_IND
-    assert frame.flags == 0x0080
-    assert frame.src_addr == IndividualAddress(1)
-    assert frame.dst_addr == GroupAddress(1)
-    assert frame.payload == GroupValueRead()
-    assert frame.tpci == TDataGroup()
+    assert isinstance(frame.data, CEMILData)
+    assert frame.data.flags == 0x0080
+    assert frame.data.hops == 0
+    assert frame.data.src_addr == IndividualAddress(1)
+    assert frame.data.dst_addr == GroupAddress(1)
+    assert frame.data.payload == GroupValueRead()
+    assert frame.data.tpci == TDataGroup()
     assert frame.calculated_length() == 11
     assert frame.to_knx() == raw
 
@@ -47,11 +60,13 @@ def test_valid_tpci_control():
     raw = bytes((0x29, 0, 0, 0, 0, 0, 0, 0, 0, 0x80))
     frame = CEMIFrame.from_knx(raw)
     assert frame.code == CEMIMessageCode.L_DATA_IND
-    assert frame.flags == 0
-    assert frame.payload is None
-    assert frame.src_addr == IndividualAddress(0)
-    assert frame.dst_addr == IndividualAddress(0)
-    assert frame.tpci == TConnect()
+    assert isinstance(frame.data, CEMILData)
+    assert frame.data.flags == 0
+    assert frame.data.hops == 0
+    assert frame.data.payload is None
+    assert frame.data.src_addr == IndividualAddress(0)
+    assert frame.data.dst_addr == IndividualAddress(0)
+    assert frame.data.tpci == TConnect()
     assert frame.calculated_length() == 10
     assert frame.to_knx() == raw
 
@@ -63,6 +78,17 @@ def test_valid_tpci_control():
             get_data(0x29, 0, 0, 0, 0, 1, 0xFFC0, []),
             r".*Invalid length for control TPDU.*",
         ),
+    ],
+)
+def test_invalid_tpci_apci(raw, err_msg):
+    """Test for invalid APCIService."""
+    with pytest.raises(CouldNotParseCEMI, match=err_msg):
+        CEMIFrame.from_knx(raw)
+
+
+@pytest.mark.parametrize(
+    "raw,err_msg",
+    [
         (
             get_data(0x29, 0, 0, 0, 0, 1, 0x08C0, []),
             r".*TPCI not supported.*",
@@ -73,15 +99,15 @@ def test_valid_tpci_control():
         ),
     ],
 )
-def test_invalid_tpci_apci(raw, err_msg):
+def test_unsupported_tpci_apci(raw, err_msg):
     """Test for invalid APCIService."""
     with pytest.raises(UnsupportedCEMIMessage, match=err_msg):
-        CEMIFrame.from_knx_data_link_layer(raw, code=CEMIMessageCode.L_DATA_IND)
+        CEMIFrame.from_knx(raw)
 
 
 def test_invalid_apdu_len():
     """Test for invalid apdu len."""
-    with pytest.raises(UnsupportedCEMIMessage, match=r".*APDU LEN should be .*"):
+    with pytest.raises(CouldNotParseCEMI, match=r".*APDU LEN should be .*"):
         CEMIFrame.from_knx(get_data(0x29, 0, 0, 0, 0, 2, 0, []))
 
 
@@ -89,11 +115,13 @@ def test_invalid_payload():
     """Test for having wrong payload set."""
     frame = CEMIFrame(
         code=CEMIMessageCode.L_DATA_IND,
-        flags=0,
-        src_addr=IndividualAddress(0),
-        dst_addr=IndividualAddress(0),
-        tpci=TDataGroup(),
-        payload=None,
+        data=CEMILData(
+            flags=0,
+            src_addr=IndividualAddress(0),
+            dst_addr=IndividualAddress(0),
+            tpci=TDataGroup(),
+            payload=None,
+        ),
     )
 
     with pytest.raises(TypeError):
@@ -103,6 +131,20 @@ def test_invalid_payload():
         frame.to_knx()
 
 
+def test_missing_data():
+    """Test for having no data set."""
+    frame = CEMIFrame(
+        code=CEMIMessageCode.L_DATA_IND,
+        data=None,
+    )
+
+    with pytest.raises(UnsupportedCEMIMessage):
+        frame.calculated_length()
+
+    with pytest.raises(UnsupportedCEMIMessage):
+        frame.to_knx()
+
+
 def test_from_knx_with_not_handleable_cemi():
     """Test for having unhandlebale cemi set."""
     with pytest.raises(
@@ -123,31 +165,31 @@ def test_from_knx_with_not_implemented_cemi():
 
 def test_invalid_invalid_len():
     """Test for invalid cemi len."""
-    with pytest.raises(UnsupportedCEMIMessage, match=r".*CEMI too small.*"):
-        CEMIFrame.from_knx_data_link_layer(
-            get_data(0x29, 0, 0, 0, 0, 2, 0, [])[:5],
-            code=CEMIMessageCode.L_DATA_IND,
-        )
+    with pytest.raises(CouldNotParseCEMI, match=r".*CEMI too small.*"):
+        CEMIFrame.from_knx(get_data(0x29, 0, 0, 0, 0, 2, 0, [])[:5])
 
 
 def test_from_knx_group_address():
     """Test conversion for a cemi with a group address as destination."""
     frame = CEMIFrame.from_knx(get_data(0x29, 0, 0x80, 0, 0, 1, 0, []))
-    assert frame.dst_addr == GroupAddress(0)
+    assert isinstance(frame.data, CEMILData)
+    assert frame.data.dst_addr == GroupAddress(0)
 
 
 def test_from_knx_individual_address():
     """Test conversion for a cemi with a individual address as destination."""
     frame = CEMIFrame.from_knx(get_data(0x29, 0, 0x00, 0, 0, 1, 0, []))
-    assert frame.dst_addr == IndividualAddress(0)
+    assert isinstance(frame.data, CEMILData)
+    assert frame.data.dst_addr == IndividualAddress(0)
 
 
 def test_telegram_group_address():
     """Test telegram conversion flags with a group address."""
     _telegram = Telegram(destination_address=GroupAddress(1))
     frame = CEMIFrame.init_from_telegram(_telegram)
-    assert frame.flags & 0x0080 == CEMIFlags.DESTINATION_GROUP_ADDRESS
-    assert frame.flags & 0x0C00 == CEMIFlags.PRIORITY_LOW
+    assert isinstance(frame.data, CEMILData)
+    assert frame.data.flags & 0x0080 == CEMIFlags.DESTINATION_GROUP_ADDRESS
+    assert frame.data.flags & 0x0C00 == CEMIFlags.PRIORITY_LOW
     # test CEMIFrame.telegram property
     assert frame.telegram == _telegram
 
@@ -156,9 +198,10 @@ def test_telegram_broadcast():
     """Test telegram conversion flags with a group address."""
     _telegram = Telegram(destination_address=GroupAddress(0))
     frame = CEMIFrame.init_from_telegram(_telegram)
-    assert frame.flags & 0x0080 == CEMIFlags.DESTINATION_GROUP_ADDRESS
-    assert frame.flags & 0x0C00 == CEMIFlags.PRIORITY_SYSTEM
-    assert frame.tpci == TDataBroadcast()
+    assert isinstance(frame.data, CEMILData)
+    assert frame.data.flags & 0x0080 == CEMIFlags.DESTINATION_GROUP_ADDRESS
+    assert frame.data.flags & 0x0C00 == CEMIFlags.PRIORITY_SYSTEM
+    assert frame.data.tpci == TDataBroadcast()
     # test CEMIFrame.telegram property
     assert frame.telegram == _telegram
 
@@ -167,9 +210,10 @@ def test_telegram_individual_address():
     """Test telegram conversion flags with a individual address."""
     _telegram = Telegram(destination_address=IndividualAddress(0), tpci=TConnect())
     frame = CEMIFrame.init_from_telegram(_telegram)
-    assert frame.flags & 0x0080 == CEMIFlags.DESTINATION_INDIVIDUAL_ADDRESS
-    assert frame.flags & 0x0C00 == CEMIFlags.PRIORITY_SYSTEM
-    assert frame.flags & 0x0200 == CEMIFlags.NO_ACK_REQUESTED
+    assert isinstance(frame.data, CEMILData)
+    assert frame.data.flags & 0x0080 == CEMIFlags.DESTINATION_INDIVIDUAL_ADDRESS
+    assert frame.data.flags & 0x0C00 == CEMIFlags.PRIORITY_SYSTEM
+    assert frame.data.flags & 0x0200 == CEMIFlags.NO_ACK_REQUESTED
     # test CEMIFrame.telegram property
     assert frame.telegram == _telegram
 
@@ -178,3 +222,210 @@ def test_telegram_unsupported_address():
     """Test telegram conversion flags with an unsupported address."""
     with pytest.raises(TypeError):
         CEMIFrame.init_from_telegram(Telegram(destination_address=object()))
+
+
+def get_prop(code, obj_id, obj_inst, prop_id, num, six, payload):
+    """Encode to cemi prop raw bytes."""
+    return bytes(
+        [
+            code,
+            (obj_id >> 8) & 255,  # Interface Object Type
+            obj_id & 255,  # Interface Object Type
+            obj_inst & 255,  # Object instance
+            prop_id & 255,  # Property ID
+            (num << 4) | (six >> 8),  # Number of Elements (4bit) Start index (hsb 4bit)
+            six & 255,  # Start index (lsb 8bit)
+            *payload,  # payload
+        ]
+    )
+
+
+def test_valid_read_req():
+    """Test for valid frame parsing."""
+    raw = get_prop(0xFC, 0x000B, 1, 52, 1, 1, [])
+    frame = CEMIFrame.from_knx(raw)
+    assert frame.code == CEMIMessageCode.M_PROP_READ_REQ
+    assert isinstance(frame.data, CEMIMPropReadRequest)
+    assert (
+        frame.data.property_info.object_type
+        == ResourceObjectType.OBJECT_KNXNETIP_PARAMETER
+    )
+    assert frame.data.property_info.object_instance == 1
+    assert (
+        frame.data.property_info.property_id
+        == ResourceKNXNETIPPropertyId.PID_KNX_INDIVIDUAL_ADDRESS
+    )
+    assert frame.data.property_info.number_of_elements == 1
+    assert frame.data.property_info.start_index == 1
+    assert frame.calculated_length() == 7
+    assert frame.to_knx() == raw
+    with pytest.raises(TypeError):
+        frame.telegram
+
+
+def test_valid_read_con():
+    """Test for valid frame parsing."""
+    raw = get_prop(0xFB, 0x000B, 1, 52, 1, 1, [0x12, 0x03])
+    frame = CEMIFrame.from_knx(raw)
+    assert frame.code == CEMIMessageCode.M_PROP_READ_CON
+    assert isinstance(frame.data, CEMIMPropReadResponse)
+    assert (
+        frame.data.property_info.object_type
+        == ResourceObjectType.OBJECT_KNXNETIP_PARAMETER
+    )
+    assert frame.data.property_info.object_instance == 1
+    assert (
+        frame.data.property_info.property_id
+        == ResourceKNXNETIPPropertyId.PID_KNX_INDIVIDUAL_ADDRESS
+    )
+    assert frame.data.property_info.number_of_elements == 1
+    assert frame.data.property_info.start_index == 1
+    assert frame.data.error_code == None
+    assert IndividualAddress.from_knx(frame.data.data) == IndividualAddress("1.2.3")
+    assert frame.calculated_length() == 9
+    assert frame.to_knx() == raw
+    with pytest.raises(TypeError):
+        frame.telegram
+
+
+def test_valid_error_read_con():
+    """Test for valid frame parsing."""
+    raw = get_prop(0xFB, 0x000B, 1, 52, 0, 1, [0x07])
+    frame = CEMIFrame.from_knx(raw)
+    assert frame.code == CEMIMessageCode.M_PROP_READ_CON
+    assert isinstance(frame.data, CEMIMPropReadResponse)
+    assert (
+        frame.data.property_info.object_type
+        == ResourceObjectType.OBJECT_KNXNETIP_PARAMETER
+    )
+    assert frame.data.property_info.object_instance == 1
+    assert (
+        frame.data.property_info.property_id
+        == ResourceKNXNETIPPropertyId.PID_KNX_INDIVIDUAL_ADDRESS
+    )
+    assert frame.data.property_info.number_of_elements == 0
+    assert frame.data.property_info.start_index == 1
+    assert frame.data.error_code == CEMIErrorCode.CEMI_ERROR_VOID_DP
+    assert frame.calculated_length() == 8
+    assert frame.to_knx() == raw
+    with pytest.raises(TypeError):
+        frame.telegram
+
+
+def test_valid_write_req():
+    """Test for valid frame parsing."""
+    raw = get_prop(0xF6, 0x000B, 1, 52, 1, 1, [0x12, 0x03])
+    frame = CEMIFrame.from_knx(raw)
+    assert frame.code == CEMIMessageCode.M_PROP_WRITE_REQ
+    assert isinstance(frame.data, CEMIMPropWriteRequest)
+    assert (
+        frame.data.property_info.object_type
+        == ResourceObjectType.OBJECT_KNXNETIP_PARAMETER
+    )
+    assert frame.data.property_info.object_instance == 1
+    assert (
+        frame.data.property_info.property_id
+        == ResourceKNXNETIPPropertyId.PID_KNX_INDIVIDUAL_ADDRESS
+    )
+    assert frame.data.property_info.number_of_elements == 1
+    assert frame.data.property_info.start_index == 1
+    assert IndividualAddress.from_knx(frame.data.data) == IndividualAddress("1.2.3")
+    assert frame.calculated_length() == 9
+    assert frame.to_knx() == raw
+    with pytest.raises(TypeError):
+        frame.telegram
+
+
+def test_valid_empty_write_con():
+    """Test for valid frame parsing."""
+    raw = get_prop(0xF5, 0x000B, 1, 52, 1, 1, [])
+    frame = CEMIFrame.from_knx(raw)
+    assert frame.code == CEMIMessageCode.M_PROP_WRITE_CON
+    assert isinstance(frame.data, CEMIMPropWriteResponse)
+    assert (
+        frame.data.property_info.object_type
+        == ResourceObjectType.OBJECT_KNXNETIP_PARAMETER
+    )
+    assert frame.data.property_info.object_instance == 1
+    assert (
+        frame.data.property_info.property_id
+        == ResourceKNXNETIPPropertyId.PID_KNX_INDIVIDUAL_ADDRESS
+    )
+    assert frame.data.property_info.number_of_elements == 1
+    assert frame.data.property_info.start_index == 1
+    assert frame.data.error_code == None
+    assert frame.calculated_length() == 7
+    assert frame.to_knx() == raw
+    with pytest.raises(TypeError):
+        frame.telegram
+
+
+def test_valid_error_write_con():
+    """Test for valid frame parsing."""
+    raw = get_prop(0xF5, 0x000B, 1, 52, 0, 1, [0x07])
+    frame = CEMIFrame.from_knx(raw)
+    assert frame.code == CEMIMessageCode.M_PROP_WRITE_CON
+    assert isinstance(frame.data, CEMIMPropWriteResponse)
+    assert (
+        frame.data.property_info.object_type
+        == ResourceObjectType.OBJECT_KNXNETIP_PARAMETER
+    )
+    assert frame.data.property_info.object_instance == 1
+    assert (
+        frame.data.property_info.property_id
+        == ResourceKNXNETIPPropertyId.PID_KNX_INDIVIDUAL_ADDRESS
+    )
+    assert frame.data.property_info.number_of_elements == 0
+    assert frame.data.property_info.start_index == 1
+    assert frame.data.error_code == CEMIErrorCode.CEMI_ERROR_VOID_DP
+    assert frame.calculated_length() == 8
+    assert frame.to_knx() == raw
+    with pytest.raises(TypeError):
+        frame.telegram
+
+
+@pytest.mark.parametrize(
+    "raw,err_msg",
+    [
+        (
+            get_prop(0xFC, 0x000B, 1, 52, 1, 1, [])[:5],
+            r".*Invalid CEMI length:*",
+        ),
+        (
+            get_prop(0xFB, 0x000B, 1, 52, 1, 1, [])[:5],
+            r".*CEMI Property Read Response too small.*",
+        ),
+        (
+            get_prop(0xFB, 0x000B, 1, 52, 0, 1, [0x07, 0x00]),
+            r".*Invalid CEMI error response length:.*",
+        ),
+        (
+            get_prop(0xF6, 0x000B, 1, 52, 1, 1, [])[:5],
+            r".*CEMI Property Write Request too small.*",
+        ),
+        (
+            get_prop(0xF5, 0x000B, 1, 52, 1, 1, [])[:5],
+            r".*CEMI Property Write Response too small.*",
+        ),
+        (
+            get_prop(0xF5, 0x000B, 1, 52, 0, 1, [0x07, 0x00]),
+            r".*Invalid CEMI error response length:.*",
+        ),
+        (
+            get_prop(0xF5, 0x000B, 1, 52, 1, 1, [0x07]),
+            r".*Invalid CEMI response length:.*",
+        ),
+    ],
+)
+def test_invalid_length(raw, err_msg):
+    """Test for invalid frame parsing."""
+    with pytest.raises(CouldNotParseCEMI, match=err_msg):
+        CEMIFrame.from_knx(raw)
+
+
+def test_invalid_resource_object():
+    """Test for invalid frame parsing."""
+    with pytest.raises(
+        UnsupportedCEMIMessage, match=r".*CEMIMProp Object Type not supported:.*"
+    ):
+        CEMIFrame.from_knx(get_prop(0xFC, 0x1234, 1, 52, 1, 1, []))
diff --git a/test/cemi_tests/cemi_handler_test.py b/test/cemi_tests/cemi_handler_test.py
index 31bda573..32272523 100644
--- a/test/cemi_tests/cemi_handler_test.py
+++ b/test/cemi_tests/cemi_handler_test.py
@@ -124,10 +124,7 @@ def test_incoming_management_telegram(telegram):
 @pytest.mark.parametrize(
     "raw",
     [
-        # LDataInd Unsupported Extended APCI from 0.0.1 to 0/0/0 broadcast
-        # <UnsupportedCEMIMessage description="APCI not supported: 0b1111111000 in CEMI: 2900b0d0000100000103f8" />
-        bytes.fromhex("2900b0d0000100000103f8"),
-        # <UnsupportedCEMIMessage description="CEMI too small. Length: 9; CEMI: 2900b06010fa10ff00" />
+        # <CouldNotParseCEMI description="CEMI too small. Length: 9; CEMI: 2900b06010fa10ff00" />
         # communication_channel_id: 0x02   sequence_counter: 0x81
         bytes.fromhex("2900b06010fa10ff00"),
     ],
@@ -136,6 +133,27 @@ def test_invalid_cemi(raw):
     """Test incoming invalid CEMI Frames."""
     xknx = XKNX()
 
+    with patch("logging.Logger.warning") as mock_info, patch.object(
+        xknx.cemi_handler, "handle_cemi_frame"
+    ) as mock_handle_cemi_frame:
+        xknx.cemi_handler.handle_raw_cemi(raw)
+        mock_info.assert_called_once()
+        mock_handle_cemi_frame.assert_not_called()
+        assert xknx.connection_manager.cemi_count_incoming_error == 1
+
+
+@pytest.mark.parametrize(
+    "raw",
+    [
+        # LDataInd Unsupported Extended APCI from 0.0.1 to 0/0/0 broadcast
+        # <UnsupportedCEMIMessage description="APCI not supported: 0b1111111000 in CEMI: 2900b0d0000100000103f8" />
+        bytes.fromhex("2900b0d0000100000103f8"),
+    ],
+)
+def test_unsupported_cemi(raw):
+    """Test incoming unsupported CEMI Frames."""
+    xknx = XKNX()
+
     with patch("logging.Logger.info") as mock_info, patch.object(
         xknx.cemi_handler, "handle_cemi_frame"
     ) as mock_handle_cemi_frame:
diff --git a/test/io_tests/request_response_tests/device_configuration_test.py b/test/io_tests/request_response_tests/device_configuration_test.py
new file mode 100644
index 00000000..7e7a5806
--- /dev/null
+++ b/test/io_tests/request_response_tests/device_configuration_test.py
@@ -0,0 +1,80 @@
+"""Unit test for KNX/IP DeviceConfiguration Request/Response."""
+from unittest.mock import patch
+
+from xknx.cemi import CEMIFrame, CEMIMPropInfo, CEMIMPropReadRequest
+from xknx.cemi.const import CEMIMessageCode
+from xknx.io.request_response import DeviceConfiguration
+from xknx.io.transport import UDPTransport
+from xknx.knxip import (
+    HPAI,
+    ConnectionStateRequest,
+    DeviceConfigurationAck,
+    DeviceConfigurationRequest,
+    ErrorCode,
+    KNXIPFrame,
+)
+from xknx.profile.const import ResourceKNXNETIPPropertyId, ResourceObjectType
+
+
+class TestDeviceConfiguration:
+    """Test class for xknx/io/DeviceConfiguration objects."""
+
+    async def test_device_configuration(self):
+        """Test device_configuration from KNX bus."""
+        data_endpoint = ("192.168.1.2", 4567)
+        udp_transport = UDPTransport(("192.168.1.1", 0), ("192.168.1.2", 1234))
+        raw_cemi = CEMIFrame(
+            code=CEMIMessageCode.M_PROP_READ_REQ,
+            data=CEMIMPropReadRequest(
+                property_info=CEMIMPropInfo(
+                    object_type=ResourceObjectType.OBJECT_KNXNETIP_PARAMETER,
+                    object_instance=1,
+                    property_id=ResourceKNXNETIPPropertyId.PID_KNX_INDIVIDUAL_ADDRESS,
+                    start_index=1,
+                    number_of_elements=1,
+                )
+            ),
+        ).to_knx()
+        device_configuration_request = DeviceConfigurationRequest(
+            communication_channel_id=23,
+            sequence_counter=42,
+            raw_cemi=raw_cemi,
+        )
+        device_configuration = DeviceConfiguration(
+            udp_transport, data_endpoint, device_configuration_request
+        )
+        device_configuration.timeout_in_seconds = 0
+
+        assert device_configuration.awaited_response_class == DeviceConfigurationAck
+
+        exp_knxipframe = KNXIPFrame.init_from_body(device_configuration_request)
+        with patch("xknx.io.transport.UDPTransport.send") as mock_udp_send, patch(
+            "xknx.io.transport.UDPTransport.getsockname"
+        ) as mock_udp_getsockname:
+            mock_udp_getsockname.return_value = ("192.168.1.3", 4321)
+            await device_configuration.start()
+            mock_udp_send.assert_called_with(exp_knxipframe, addr=data_endpoint)
+
+        # Response KNX/IP-Frame with wrong type
+        wrong_knxipframe = KNXIPFrame.init_from_body(ConnectionStateRequest())
+        with patch("logging.Logger.warning") as mock_warning:
+            device_configuration.response_rec_callback(wrong_knxipframe, HPAI(), None)
+            mock_warning.assert_called_with("Could not understand knxipframe")
+
+        # Response KNX/IP-Frame with error:
+        err_knxipframe = KNXIPFrame.init_from_body(
+            DeviceConfigurationAck(status_code=ErrorCode.E_CONNECTION_ID)
+        )
+        with patch("logging.Logger.debug") as mock_warning:
+            device_configuration.response_rec_callback(err_knxipframe, HPAI(), None)
+            mock_warning.assert_called_with(
+                "Error: KNX bus responded to request of type '%s' with error in '%s': %s",
+                type(device_configuration).__name__,
+                type(err_knxipframe.body).__name__,
+                ErrorCode.E_CONNECTION_ID,
+            )
+
+        # Correct Response KNX/IP-Frame:
+        res_knxipframe = KNXIPFrame.init_from_body(DeviceConfigurationAck())
+        device_configuration.response_rec_callback(res_knxipframe, HPAI(), None)
+        assert device_configuration.success
diff --git a/test/knxip_tests/device_configuration_ack_test.py b/test/knxip_tests/device_configuration_ack_test.py
new file mode 100644
index 00000000..7f85392a
--- /dev/null
+++ b/test/knxip_tests/device_configuration_ack_test.py
@@ -0,0 +1,39 @@
+"""Unit test for KNX/IP DeviceConfigurationAck objects."""
+import pytest
+
+from xknx.exceptions import CouldNotParseKNXIP
+from xknx.knxip import DeviceConfigurationAck, ErrorCode, KNXIPFrame
+
+
+class TestKNXIPDeviceConfigurationAck:
+    """Test class for KNX/IP DeviceConfigurationAck objects."""
+
+    def test_device_configuration_ack(self):
+        """Test parsing and streaming device configuration ACK KNX/IP packet."""
+        raw = bytes((0x06, 0x10, 0x03, 0x11, 0x00, 0x0A, 0x04, 0x2A, 0x17, 0x00))
+        knxipframe, _ = KNXIPFrame.from_knx(raw)
+
+        assert isinstance(knxipframe.body, DeviceConfigurationAck)
+        assert knxipframe.body.communication_channel_id == 42
+        assert knxipframe.body.sequence_counter == 23
+        assert knxipframe.body.status_code == ErrorCode.E_NO_ERROR
+
+        device_configuration_ack = DeviceConfigurationAck(
+            communication_channel_id=42,
+            sequence_counter=23,
+        )
+        knxipframe2 = KNXIPFrame.init_from_body(device_configuration_ack)
+
+        assert knxipframe2.to_knx() == raw
+
+    def test_from_knx_wrong_ack_information(self):
+        """Test parsing and streaming wrong DeviceConfigurationAck (wrong length byte)."""
+        raw = bytes((0x06, 0x10, 0x03, 0x11, 0x00, 0x0A, 0x03, 0x2A, 0x17, 0x00))
+        with pytest.raises(CouldNotParseKNXIP):
+            KNXIPFrame.from_knx(raw)
+
+    def test_from_knx_wrong_ack_information2(self):
+        """Test parsing and streaming wrong DeviceConfigurationAck (wrong length)."""
+        raw = bytes((0x06, 0x10, 0x03, 0x11, 0x00, 0x0A, 0x04, 0x2A, 0x17))
+        with pytest.raises(CouldNotParseKNXIP):
+            KNXIPFrame.from_knx(raw)
diff --git a/test/knxip_tests/device_configuration_request_test.py b/test/knxip_tests/device_configuration_request_test.py
new file mode 100644
index 00000000..6b2a2aeb
--- /dev/null
+++ b/test/knxip_tests/device_configuration_request_test.py
@@ -0,0 +1,67 @@
+"""Unit test for KNX/IP DeviceConfigurationRequest objects."""
+import pytest
+
+from xknx.cemi import CEMIFrame, CEMIMessageCode, CEMIMPropInfo, CEMIMPropReadRequest
+from xknx.cemi.const import CEMIMessageCode
+from xknx.exceptions import CouldNotParseKNXIP
+from xknx.knxip import DeviceConfigurationRequest, KNXIPFrame
+from xknx.profile.const import ResourceKNXNETIPPropertyId, ResourceObjectType
+
+
+class TestKNXIPDeviceConfigurationRequest:
+    """Test class for KNX/IP DeviceConfigurationRequest objects."""
+
+    def test_device_configuration_request(self):
+        """Test parsing and streaming device configuration ACK KNX/IP packet."""
+        raw = bytes.fromhex("06 10 03 10 00 11 04 2A 17 00 FC 00 0B 01 34 10 01")
+        knxipframe, _ = KNXIPFrame.from_knx(raw)
+
+        assert isinstance(knxipframe.body, DeviceConfigurationRequest)
+        assert knxipframe.body.communication_channel_id == 42
+        assert knxipframe.body.sequence_counter == 23
+        assert isinstance(knxipframe.body.raw_cemi, bytes)
+
+        incoming_cemi = CEMIFrame.from_knx(knxipframe.body.raw_cemi)
+        assert incoming_cemi.code == CEMIMessageCode.M_PROP_READ_REQ
+        assert incoming_cemi.data == CEMIMPropReadRequest(
+            property_info=CEMIMPropInfo(
+                object_type=ResourceObjectType.OBJECT_KNXNETIP_PARAMETER,
+                object_instance=1,
+                property_id=ResourceKNXNETIPPropertyId.PID_KNX_INDIVIDUAL_ADDRESS,
+                start_index=1,
+                number_of_elements=1,
+            )
+        )
+
+        outgoing_cemi = CEMIFrame(
+            code=CEMIMessageCode.M_PROP_READ_REQ,
+            data=CEMIMPropReadRequest(
+                property_info=CEMIMPropInfo(
+                    object_type=ResourceObjectType.OBJECT_KNXNETIP_PARAMETER,
+                    object_instance=1,
+                    property_id=ResourceKNXNETIPPropertyId.PID_KNX_INDIVIDUAL_ADDRESS,
+                    start_index=1,
+                    number_of_elements=1,
+                )
+            ),
+        )
+        device_configuration_req = DeviceConfigurationRequest(
+            communication_channel_id=42,
+            sequence_counter=23,
+            raw_cemi=outgoing_cemi.to_knx(),
+        )
+        knxipframe2 = KNXIPFrame.init_from_body(device_configuration_req)
+
+        assert knxipframe2.to_knx() == raw
+
+    def test_from_knx_wrong_header(self):
+        """Test parsing and streaming wrong DeviceConfigurationRequest (wrong length byte)."""
+        raw = bytes((0x06, 0x10, 0x03, 0x10, 0x00, 0x15, 0x03))
+        with pytest.raises(CouldNotParseKNXIP):
+            KNXIPFrame.from_knx(raw)
+
+    def test_from_knx_wrong_header2(self):
+        """Test parsing and streaming wrong DeviceConfigurationRequest (wrong length)."""
+        raw = bytes((0x06, 0x10, 0x03, 0x10, 0x00, 0x15, 0x04))
+        with pytest.raises(CouldNotParseKNXIP):
+            KNXIPFrame.from_knx(raw)
diff --git a/test/knxip_tests/routing_indication_test.py b/test/knxip_tests/routing_indication_test.py
index 265820bf..97197d17 100644
--- a/test/knxip_tests/routing_indication_test.py
+++ b/test/knxip_tests/routing_indication_test.py
@@ -1,7 +1,7 @@
 """Unit test for KNX/IP RountingIndication objects."""
 import time
 
-from xknx.cemi import CEMIFrame
+from xknx.cemi import CEMIFrame, CEMILData
 from xknx.dpt import DPTArray, DPTBinary, DPTTemperature, DPTTime
 from xknx.knxip import KNXIPFrame, KNXIPServiceType, RoutingIndication
 from xknx.telegram import GroupAddress, IndividualAddress, Telegram
@@ -41,7 +41,8 @@ class TestKNXIPRountingIndication:
             telegram,
             src_addr=IndividualAddress("1.2.2"),
         )
-        cemi.set_hops(5)
+        assert isinstance(cemi.data, CEMILData)
+        cemi.data.hops = 5
         routing_indication = RoutingIndication(raw_cemi=cemi.to_knx())
         knxipframe = KNXIPFrame.init_from_body(routing_indication)
 
diff --git a/test/secure_tests/data_secure_test.py b/test/secure_tests/data_secure_test.py
index 1c91fa13..26c4806d 100644
--- a/test/secure_tests/data_secure_test.py
+++ b/test/secure_tests/data_secure_test.py
@@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, patch
 import pytest
 
 from xknx import XKNX
-from xknx.cemi import CEMIFrame, CEMIMessageCode
+from xknx.cemi import CEMIFrame, CEMILData, CEMIMessageCode
 from xknx.dpt import DPTArray
 from xknx.exceptions import DataSecureError
 from xknx.secure.data_secure_asdu import (
@@ -136,8 +136,9 @@ class TestDataSecure:
             src_addr=self.xknx.current_address,
         )
         secured_frame = self.data_secure.outgoing_cemi(test_cemi)
-        assert isinstance(secured_frame.payload, apci.SecureAPDU)
-        secured_asdu = secured_frame.payload.secured_data
+        assert isinstance(secured_frame.data, CEMILData)
+        assert isinstance(secured_frame.data.payload, apci.SecureAPDU)
+        secured_asdu = secured_frame.data.payload.secured_data
 
         assert int.from_bytes(secured_asdu.sequence_number_bytes, "big") == 160170101607
         assert secured_asdu.secured_apdu == bytes.fromhex("cd18")
@@ -155,11 +156,15 @@ class TestDataSecure:
             self.data_secure._individual_address_table[IndividualAddress("4.0.9")]
             == 155806854915
         )
-        assert test_group_response_cemi.src_addr == IndividualAddress("4.0.9")
-        assert isinstance(test_group_response_cemi.payload, apci.SecureAPDU)
+        assert isinstance(test_group_response_cemi.data, CEMILData)
+        assert test_group_response_cemi.data.src_addr == IndividualAddress("4.0.9")
+        assert isinstance(test_group_response_cemi.data.payload, apci.SecureAPDU)
 
         plain_frame = self.data_secure.received_cemi(test_group_response_cemi)
-        assert plain_frame.payload == apci.GroupValueResponse(DPTArray((116, 41, 41)))
+        assert isinstance(plain_frame.data, CEMILData)
+        assert plain_frame.data.payload == apci.GroupValueResponse(
+            DPTArray((116, 41, 41))
+        )
         # individual_address_table sequence number was updated
         assert (
             self.data_secure._individual_address_table[IndividualAddress("4.0.9")]
@@ -169,7 +174,7 @@ class TestDataSecure:
     def test_data_secure_individual_receive_tool_key(self, test_point_to_point_cemi):
         """Test incoming DataSecure point-to-point communication via tool key."""
         self.xknx.current_address = IndividualAddress("15.15.0")
-        assert isinstance(test_point_to_point_cemi.payload, apci.SecureAPDU)
+        assert isinstance(test_point_to_point_cemi.data.payload, apci.SecureAPDU)
 
         with pytest.raises(
             DataSecureError, match=r"System broadcast and tool access not supported.*"
@@ -183,11 +188,11 @@ class TestDataSecure:
     def test_data_secure_individual_receive(self, test_point_to_point_cemi):
         """Test incoming DataSecure point-to-point communication."""
         self.xknx.current_address = IndividualAddress("15.15.0")
-        assert isinstance(test_point_to_point_cemi.payload, apci.SecureAPDU)
+        assert isinstance(test_point_to_point_cemi.data.payload, apci.SecureAPDU)
         # don't use tool key or system broadcast
         # further validation is skipped so we can use the same test data
-        test_point_to_point_cemi.payload.scf.tool_access = False
-        test_point_to_point_cemi.payload.scf.system_broadcast = False
+        test_point_to_point_cemi.data.payload.scf.tool_access = False
+        test_point_to_point_cemi.data.payload.scf.system_broadcast = False
         with pytest.raises(
             DataSecureError,
             match=r"Secure Point-to-Point communication not supported.*",
@@ -200,7 +205,7 @@ class TestDataSecure:
 
     def test_data_secure_group_receive_unknown_source(self, test_group_response_cemi):
         """Test incoming DataSecure group communication from unknown source."""
-        test_group_response_cemi.src_addr = IndividualAddress("1.2.3")
+        test_group_response_cemi.data.src_addr = IndividualAddress("1.2.3")
         with pytest.raises(
             DataSecureError,
             match=r"Source address not found in Security Individual Address Table.*",
@@ -211,7 +216,7 @@ class TestDataSecure:
         self, test_group_response_cemi
     ):
         """Test incoming DataSecure group communication for unknown destination."""
-        test_group_response_cemi.dst_addr = GroupAddress("1/2/3")
+        test_group_response_cemi.data.dst_addr = GroupAddress("1/2/3")
         with pytest.raises(
             DataSecureError,
             match=r"No key found for group address.*",
@@ -224,7 +229,7 @@ class TestDataSecure:
         """Test incoming DataSecure group communication with wrong sequence number."""
         seq_num = 155806854986
         assert (
-            test_group_response_cemi.payload.secured_data.sequence_number_bytes
+            test_group_response_cemi.data.payload.secured_data.sequence_number_bytes
             == seq_num.to_bytes(6, "big")
         )
         # sequence number already used
@@ -237,8 +242,8 @@ class TestDataSecure:
 
     def test_data_secure_group_receive_wrong_mac(self, test_group_response_cemi):
         """Test incoming DataSecure group communication with wrong MAC."""
-        test_group_response_cemi.payload.secured_data.message_authentication_code = (
-            bytes(4)
+        test_group_response_cemi.data.payload.secured_data.message_authentication_code = bytes(
+            4
         )
         with pytest.raises(
             DataSecureError,
@@ -369,17 +374,19 @@ class TestDataSecure:
         outgoing_signed_cemi = self.data_secure._secure_data_cemi(
             key=key, scf=scf, cemi=test_cemi
         )
-        assert outgoing_signed_cemi.payload.secured_data is not None
+        assert isinstance(outgoing_signed_cemi.data, CEMILData)
+        assert outgoing_signed_cemi.data.payload.secured_data is not None
 
         # create new cemi to avoid mixed bytearray / byte parts
         incoming_cemi = CEMIFrame.from_knx(outgoing_signed_cemi.to_knx())
+        assert isinstance(incoming_cemi.data, CEMILData)
         # receive same cemi - fake individual address table entry
-        self.data_secure._individual_address_table[incoming_cemi.src_addr] = 1
+        self.data_secure._individual_address_table[incoming_cemi.data.src_addr] = 1
         assert self.data_secure.received_cemi(incoming_cemi) == test_cemi
 
         # Test wrong MAC
-        self.data_secure._individual_address_table[incoming_cemi.src_addr] = 1
-        incoming_cemi.payload.secured_data.message_authentication_code = bytes(4)
+        self.data_secure._individual_address_table[incoming_cemi.data.src_addr] = 1
+        incoming_cemi.data.payload.secured_data.message_authentication_code = bytes(4)
         with pytest.raises(
             DataSecureError,
             match=r"Data Secure MAC verification failed.*",
diff --git a/test/str_test.py b/test/str_test.py
index 1da8cfc5..a1a402a7 100644
--- a/test/str_test.py
+++ b/test/str_test.py
@@ -2,7 +2,7 @@
 from unittest.mock import patch
 
 from xknx import XKNX
-from xknx.cemi import CEMIFrame
+from xknx.cemi import CEMIFrame, CEMIMessageCode, CEMIMPropInfo, CEMIMPropReadResponse
 from xknx.devices import (
     BinarySensor,
     Climate,
@@ -54,6 +54,7 @@ from xknx.knxip import (
     TunnellingAck,
     TunnellingRequest,
 )
+from xknx.profile.const import ResourceKNXNETIPPropertyId, ResourceObjectType
 from xknx.remote_value import RemoteValue
 from xknx.telegram import GroupAddress, IndividualAddress, Telegram, TelegramDirection
 from xknx.telegram.apci import GroupValueWrite
@@ -642,7 +643,7 @@ class TestStringRepresentations:
             == '<TunnellingAck communication_channel_id="23" sequence_counter="42" status_code="ErrorCode.E_NO_ERROR" />'
         )
 
-    def test_cemi_frame(self):
+    def test_cemi_ldata_frame(self):
         """Test string representation of KNX/IP CEMI Frame."""
         cemi_frame = CEMIFrame.init_from_telegram(
             Telegram(
@@ -653,8 +654,27 @@ class TestStringRepresentations:
         )
         assert (
             str(cemi_frame)
-            == '<CEMIFrame code="L_DATA_IND" src_addr="IndividualAddress("1.2.3")" dst_addr="GroupAddress("1/2/5")" '
-            'flags="1011110011100000" tpci="TDataGroup()" payload="<GroupValueWrite value="<DPTBinary value="7" />" />" />'
+            == '<CEMIFrame code="L_DATA_IND" info="CEMIInfo("")" data="CEMILData(src_addr="IndividualAddress("1.2.3")" '
+            'dst_addr="GroupAddress("1/2/5")" flags="1011110011100000" tpci="TDataGroup()" '
+            'payload="<GroupValueWrite value="<DPTBinary value="7" />" />")" />'
+        )
+
+    def test_cemi_mprop_frame(self):
+        """Test string representation of KNX/IP CEMI Frame."""
+        cemi_frame = CEMIFrame(
+            code=CEMIMessageCode.M_PROP_READ_REQ,
+            data=CEMIMPropReadResponse(
+                property_info=CEMIMPropInfo(
+                    object_type=ResourceObjectType.OBJECT_KNXNETIP_PARAMETER,
+                    property_id=ResourceKNXNETIPPropertyId.PID_KNX_INDIVIDUAL_ADDRESS,
+                ),
+                data=IndividualAddress("1.2.3").to_knx(),
+            ),
+        )
+        assert (
+            str(cemi_frame)
+            == '<CEMIFrame code="M_PROP_READ_REQ" data="CEMIMPropReadResponse(object_type="11" object_instance="1" '
+            'property_id="52" number_of_elements="1" start_index="1" error_code="None" data="1203" )" />'
         )
 
     def test_knxip_frame(self):