Repository: XKNX/xknx
Pull request #1220 (2023-03-08 22:32:48)

Use CEMILData instead of CEMIFrame in DataSecure
<!--
  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.
-->
Data Secure is only used for link layer data.

Fixes # (issue)

## 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)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [x] 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/1220?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) Report
> Merging [#1220](https://codecov.io/gh/XKNX/xknx/pull/1220?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) (6903268) into [main](https://codecov.io/gh/XKNX/xknx/commit/18eddddb0f9feda463624dfcdaf3d643073a3bd8?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) (18edddd) will **increase** coverage by `0.13%`.
> The diff coverage is `99.02%`.

[![Impacted file tree graph](https://codecov.io/gh/XKNX/xknx/pull/1220/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/1220?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX)

```diff
@@            Coverage Diff             @@
##             main    #1220      +/-   ##
==========================================
+ Coverage   96.45%   96.58%   +0.13%     
==========================================
  Files         143      148       +5     
  Lines        9027     9553     +526     
==========================================
+ Hits         8707     9227     +520     
- Misses        320      326       +6     
```


| [Impacted Files](https://codecov.io/gh/XKNX/xknx/pull/1220?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) | Coverage Δ | |
|---|---|---|
| [xknx/exceptions/\_\_init\_\_.py](https://codecov.io/gh/XKNX/xknx/pull/1220?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9leGNlcHRpb25zL19faW5pdF9fLnB5) | `100.00% <ø> (ø)` | |
| [xknx/cemi/cemi\_handler.py](https://codecov.io/gh/XKNX/xknx/pull/1220?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9jZW1pL2NlbWlfaGFuZGxlci5weQ==) | `97.46% <81.81%> (-2.54%)` | :arrow_down: |
| [xknx/knxip/device\_configuration\_request.py](https://codecov.io/gh/XKNX/xknx/pull/1220?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9rbnhpcC9kZXZpY2VfY29uZmlndXJhdGlvbl9yZXF1ZXN0LnB5) | `91.66% <91.66%> (ø)` | |
| [xknx/knxip/device\_configuration\_ack.py](https://codecov.io/gh/XKNX/xknx/pull/1220?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9rbnhpcC9kZXZpY2VfY29uZmlndXJhdGlvbl9hY2sucHk=) | `96.00% <96.00%> (ø)` | |
| [xknx/cemi/cemi\_frame.py](https://codecov.io/gh/XKNX/xknx/pull/1220?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9jZW1pL2NlbWlfZnJhbWUucHk=) | `99.60% <99.54%> (-0.40%)` | :arrow_down: |
| [xknx/cemi/\_\_init\_\_.py](https://codecov.io/gh/XKNX/xknx/pull/1220?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/1220?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/exceptions/exception.py](https://codecov.io/gh/XKNX/xknx/pull/1220?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9leGNlcHRpb25zL2V4Y2VwdGlvbi5weQ==) | `100.00% <100.00%> (ø)` | |
| [xknx/io/request\_response/\_\_init\_\_.py](https://codecov.io/gh/XKNX/xknx/pull/1220?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/io/request\_response/device\_configuration.py](https://codecov.io/gh/XKNX/xknx/pull/1220?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9pby9yZXF1ZXN0X3Jlc3BvbnNlL2RldmljZV9jb25maWd1cmF0aW9uLnB5) | `100.00% <100.00%> (ø)` | |
| ... and [6 more](https://codecov.io/gh/XKNX/xknx/pull/1220?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 48fb0b14..3781d74b 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -13,6 +13,10 @@ nav_order: 2
 - Add support for Device Management Configuration service.
 - Support CEMI M_Prop messages.
 
+### Internals
+
+- Use CEMILData instead of CEMIFrame in DataSecure.
+
 # 2.6.0 Connection information 2023-02-27
 
 ### Connection
diff --git a/xknx/cemi/cemi_handler.py b/xknx/cemi/cemi_handler.py
index a31269ad..7ec0712f 100644
--- a/xknx/cemi/cemi_handler.py
+++ b/xknx/cemi/cemi_handler.py
@@ -61,10 +61,13 @@ class CEMIHandler:
                 self.xknx.current_address if telegram.source_address.raw == 0 else None
             ),
         )
+        if not isinstance(cemi.data, CEMILData):
+            logger.warning("Invalid CEMI data from Telegram: %s", cemi)
+            return
+
         logger.debug("Outgoing CEMI: %s", cemi)
         if self.data_secure is not None:
-            cemi = self.data_secure.outgoing_cemi(cemi=cemi)
-
+            cemi.data = self.data_secure.outgoing_cemi(cemi_data=cemi.data)
         self._l_data_confirmation_event.clear()
         try:
             await self.xknx.knxip_interface.send_cemi(cemi)
@@ -102,6 +105,7 @@ class CEMIHandler:
     def handle_cemi_frame(self, cemi: CEMIFrame) -> None:
         """Handle incoming CEMI Frames."""
         if not isinstance(cemi.data, CEMILData):
+            logger.debug("Ignoring incoming non-link-layer CEMI: %s", cemi)
             return
 
         if cemi.code is CEMIMessageCode.L_DATA_CON:
@@ -126,7 +130,7 @@ class CEMIHandler:
 
         if self.data_secure is not None:
             try:
-                cemi = self.data_secure.received_cemi(cemi=cemi)
+                cemi.data = self.data_secure.received_cemi(cemi_data=cemi.data)
             except DataSecureError as err:
                 data_secure_logger.log(
                     err.log_level,
diff --git a/xknx/secure/data_secure.py b/xknx/secure/data_secure.py
index fa1e4e22..a6f55c51 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 deepcopy
+from copy import copy
 from datetime import datetime, timezone
 import logging
 import time
 
-from xknx.cemi import CEMIFrame, CEMILData
-from xknx.exceptions import DataSecureError, UnsupportedCEMIMessage
+from xknx.cemi import CEMILData
+from xknx.exceptions import DataSecureError
 from xknx.telegram.address import GroupAddress, IndividualAddress
 from xknx.telegram.apci import APCI, SecureAPDU
 
@@ -129,121 +129,108 @@ class DataSecure:
         # Don't increment sequence number if exception is raised while decrypting (yield)
         self._individual_address_table[source_address] = received_sequence_number
 
-    def received_cemi(self, cemi: CEMIFrame) -> CEMIFrame:
+    def received_cemi(self, cemi_data: CEMILData) -> CEMILData:
         """Handle received CEMI frame."""
-        # No data frame
-        if not isinstance(cemi.data, CEMILData):
-            return cemi
         # Data Secure frame
-        if isinstance(cemi.data.payload, SecureAPDU):
-            return self._received_secure_cemi(cemi, cemi.data.payload)
+        if isinstance(cemi_data.payload, SecureAPDU):
+            return self._received_secure_cemi(cemi_data, cemi_data.payload)
         # Plain group communication frame
-        if isinstance(cemi.data.dst_addr, GroupAddress):
-            if cemi.data.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}",
+                    f"Discarding frame with plain APDU for secure group address: {cemi_data}",
                     log_level=logging.WARNING,
                 )
-            return cemi
+            return cemi_data
         # Plain point-to-point frame
         #   No point to point key table is implemented at the moment as ETS can't even configure this
         #   only way to communicate point-to-point with data secure currently is with tool key
         #   - which we don't have
-        return cemi
+        return cemi_data
 
-    def _received_secure_cemi(self, cemi: CEMIFrame, s_apdu: SecureAPDU) -> CEMIFrame:
+    def _received_secure_cemi(
+        self, cemi_data: CEMILData, s_apdu: SecureAPDU
+    ) -> CEMILData:
         """Handle received secured CEMI frame."""
         if s_apdu.scf.service is not SecurityALService.S_A_DATA:
             raise DataSecureError(
-                f"Only SecurityALService.S_A_DATA supported {cemi}",
+                f"Only SecurityALService.S_A_DATA supported {cemi_data}",
                 log_level=logging.DEBUG,
             )
         if s_apdu.scf.system_broadcast or s_apdu.scf.tool_access:
             # TODO: handle incoming responses with tool key of sending device
             # when we can send with tool key
             raise DataSecureError(
-                f"System broadcast and tool access not supported {cemi}",
+                f"System broadcast and tool access not supported {cemi_data}",
                 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.data.dst_addr, GroupAddress):
-            if not (key := self._group_key_table.get(cemi.data.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.data.dst_addr} from {cemi.data.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
         else:
             # TODO: maybe possible to implement this over tool key
             raise DataSecureError(
-                f"Secure Point-to-Point communication not supported {cemi}",
+                f"Secure Point-to-Point communication not supported {cemi_data}",
                 log_level=logging.DEBUG,
             )
 
         with self.check_sequence_number(
-            source_address=cemi.data.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.data.src_addr.to_knx() + cemi.data.dst_addr.to_knx()
+                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.data.flags,
-                tpci=cemi.data.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 = 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
+        plain_cemi_data = copy(cemi_data)
+        plain_cemi_data.payload = decrypted_payload
+        return plain_cemi_data
 
-    def outgoing_cemi(self, cemi: CEMIFrame) -> CEMIFrame:
+    def outgoing_cemi(self, cemi_data: CEMILData) -> CEMILData:
         """Handle outgoing CEMI frame. Pass through as plain frame or encrypt."""
         # Outgoing  group communication frame
-        if isinstance(cemi.data, CEMILData) and isinstance(
-            cemi.data.dst_addr, GroupAddress
-        ):
-            if key := self._group_key_table.get(cemi.data.dst_addr):
+        if 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,
                     system_broadcast=False,
                     tool_access=False,
                 )
-                return self._secure_data_cemi(key=key, scf=scf, cemi=cemi)
-            return cemi
+                return self._secure_data_cemi(key=key, scf=scf, cemi_data=cemi_data)
+            return cemi_data
         # Outgoing secure point-to-point frames are sent plain.
         # Data Secure point-to-point is not supported.
-        return cemi
+        return cemi_data
 
     def _secure_data_cemi(
         self,
         key: bytes,
         scf: SecurityControlField,
-        cemi: CEMIFrame,
-    ) -> CEMIFrame:
-        """Wrap encrypted payload of a plain CEMIFrame in a SecureAPDU."""
+        cemi_data: CEMILData,
+    ) -> CEMILData:
+        """Wrap encrypted payload of a plain CEMILData in a SecureAPDU."""
         plain_apdu_raw: bytes | bytearray
-        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()
+        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
@@ -252,18 +239,14 @@ class DataSecure:
             apdu=plain_apdu_raw,
             scf=scf,
             sequence_number=self.get_sequence_number(),
-            address_fields_raw=cemi.data.src_addr.to_knx()
-            + cemi.data.dst_addr.to_knx(),
-            frame_flags=cemi.data.flags,
-            tpci=cemi.data.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)
+        secure_cemi_data = copy(cemi_data)
+        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
+            "Secured APDU %s with %s", cemi_data.payload, secure_cemi_data.payload
         )
-        return secure_cemi
+        return secure_cemi_data


Test patch:
diff --git a/test/secure_tests/data_secure_test.py b/test/secure_tests/data_secure_test.py
index 26c4806d..4439d746 100644
--- a/test/secure_tests/data_secure_test.py
+++ b/test/secure_tests/data_secure_test.py
@@ -107,7 +107,9 @@ class TestDataSecure:
 
         test_cemi = CEMIFrame.init_from_telegram(test_telegram)
         with patch.object(
-            self.data_secure, "received_cemi"
+            self.data_secure,
+            "received_cemi",
+            return_value=test_cemi.data,  # Reuse incoming plain APDU. Decryption is not tested here
         ) as mock_ds_received_cemi, patch.object(
             self.xknx.cemi_handler, "telegram_received"
         ) as mock_telegram_received:  # suppress forwarding to telegras/management
@@ -135,10 +137,10 @@ class TestDataSecure:
             code=CEMIMessageCode.L_DATA_REQ,
             src_addr=self.xknx.current_address,
         )
-        secured_frame = self.data_secure.outgoing_cemi(test_cemi)
-        assert isinstance(secured_frame.data, CEMILData)
-        assert isinstance(secured_frame.data.payload, apci.SecureAPDU)
-        secured_asdu = secured_frame.data.payload.secured_data
+        secured_frame_data = self.data_secure.outgoing_cemi(test_cemi.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")
@@ -146,8 +148,8 @@ class TestDataSecure:
         # sequence number sending was incremented
         assert self.data_secure._sequence_number_sending == 160170101608
 
-        assert secured_frame.to_knx() == bytes.fromhex(
-            "1100bce0500104000e03f11000254ae1cb67cd184afe5744"
+        assert secured_frame_data.to_knx() == bytes.fromhex(
+            "bce0500104000e03f11000254ae1cb67cd184afe5744"
         )
 
     def test_data_secure_group_receive(self, test_group_response_cemi):
@@ -160,9 +162,9 @@ class TestDataSecure:
         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 isinstance(plain_frame.data, CEMILData)
-        assert plain_frame.data.payload == apci.GroupValueResponse(
+        plain_frame_data = self.data_secure.received_cemi(test_group_response_cemi.data)
+        assert isinstance(plain_frame_data, CEMILData)
+        assert plain_frame_data.payload == apci.GroupValueResponse(
             DPTArray((116, 41, 41))
         )
         # individual_address_table sequence number was updated
@@ -179,7 +181,7 @@ class TestDataSecure:
         with pytest.raises(
             DataSecureError, match=r"System broadcast and tool access not supported.*"
         ):
-            self.data_secure.received_cemi(test_point_to_point_cemi)
+            self.data_secure.received_cemi(test_point_to_point_cemi.data)
         # don't raise through handle_cemi_frame()
         assert (
             self.xknx.cemi_handler.handle_cemi_frame(test_point_to_point_cemi) is None
@@ -197,7 +199,7 @@ class TestDataSecure:
             DataSecureError,
             match=r"Secure Point-to-Point communication not supported.*",
         ):
-            self.data_secure.received_cemi(test_point_to_point_cemi)
+            self.data_secure.received_cemi(test_point_to_point_cemi.data)
         # don't raise through handle_cemi_frame()
         assert (
             self.xknx.cemi_handler.handle_cemi_frame(test_point_to_point_cemi) is None
@@ -210,7 +212,7 @@ class TestDataSecure:
             DataSecureError,
             match=r"Source address not found in Security Individual Address Table.*",
         ):
-            self.data_secure.received_cemi(test_group_response_cemi)
+            self.data_secure.received_cemi(test_group_response_cemi.data)
 
     def test_data_secure_group_receive_unknown_destination(
         self, test_group_response_cemi
@@ -221,7 +223,7 @@ class TestDataSecure:
             DataSecureError,
             match=r"No key found for group address.*",
         ):
-            self.data_secure.received_cemi(test_group_response_cemi)
+            self.data_secure.received_cemi(test_group_response_cemi.data)
 
     def test_data_secure_group_receive_wrong_sequence_number(
         self, test_group_response_cemi
@@ -238,7 +240,7 @@ class TestDataSecure:
             DataSecureError,
             match=r"Sequence number too low.*",
         ):
-            self.data_secure.received_cemi(test_group_response_cemi)
+            self.data_secure.received_cemi(test_group_response_cemi.data)
 
     def test_data_secure_group_receive_wrong_mac(self, test_group_response_cemi):
         """Test incoming DataSecure group communication with wrong MAC."""
@@ -249,7 +251,7 @@ class TestDataSecure:
             DataSecureError,
             match=r"Data Secure MAC verification failed.*",
         ):
-            self.data_secure.received_cemi(test_group_response_cemi)
+            self.data_secure.received_cemi(test_group_response_cemi.data)
 
     def test_data_secure_group_receive_plain_frame(self):
         """Test incoming DataSecure group communication with plain frame."""
@@ -268,7 +270,7 @@ class TestDataSecure:
             DataSecureError,
             match=r"Discarding frame with plain APDU for secure group address.*",
         ):
-            self.data_secure.received_cemi(test_cemi)
+            self.data_secure.received_cemi(test_cemi.data)
 
     def test_non_secure_group_receive_plain_frame(self):
         """Test incoming non-secure group communication with plain frame."""
@@ -283,7 +285,7 @@ class TestDataSecure:
             src_addr=IndividualAddress("4.0.9"),
         )
         assert dst_addr not in self.data_secure._group_key_table
-        assert self.data_secure.received_cemi(test_cemi) == test_cemi
+        assert self.data_secure.received_cemi(test_cemi.data) == test_cemi.data
 
     def test_non_secure_group_send_plain_frame(self):
         """Test outgoing non-secure group communication with plain frame."""
@@ -298,7 +300,7 @@ class TestDataSecure:
             src_addr=self.xknx.current_address,
         )
         assert dst_addr not in self.data_secure._group_key_table
-        assert self.data_secure.outgoing_cemi(test_cemi) == test_cemi
+        assert self.data_secure.outgoing_cemi(test_cemi.data) == test_cemi.data
 
     def test_non_secure_individual_receive_plain_frame(self):
         """Test incoming non-secure group communication with plain frame."""
@@ -322,7 +324,7 @@ class TestDataSecure:
             src_addr=IndividualAddress("4.0.9"),
         )
         assert src_addr not in self.data_secure._individual_address_table
-        assert self.data_secure.received_cemi(test_cemi) == test_cemi
+        assert self.data_secure.received_cemi(test_cemi.data) == test_cemi.data
 
     def test_non_secure_individual_send_plain_frame(self):
         """Test outgoing non-secure group communication with plain frame."""
@@ -346,7 +348,7 @@ class TestDataSecure:
             src_addr=self.xknx.current_address,
         )
         assert dst_addr not in self.data_secure._individual_address_table
-        assert self.data_secure.outgoing_cemi(test_cemi) == test_cemi
+        assert self.data_secure.outgoing_cemi(test_cemi.data) == test_cemi.data
 
     def test_data_secure_authentication_only(self):
         """Test frame de-/serialization for DataSecure authentication only."""
@@ -371,18 +373,19 @@ class TestDataSecure:
         )
         key = self.data_secure._group_key_table[dst_addr]
 
-        outgoing_signed_cemi = self.data_secure._secure_data_cemi(
-            key=key, scf=scf, cemi=test_cemi
+        outgoing_signed_cemi_data = self.data_secure._secure_data_cemi(
+            key=key, scf=scf, cemi_data=test_cemi.data
         )
-        assert isinstance(outgoing_signed_cemi.data, CEMILData)
-        assert outgoing_signed_cemi.data.payload.secured_data is not None
+        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())
+        incoming_cemi = CEMIFrame.from_knx(
+            b"\x11\x00" + outgoing_signed_cemi_data.to_knx()
+        )
         assert isinstance(incoming_cemi.data, CEMILData)
         # receive same cemi - fake individual address table entry
         self.data_secure._individual_address_table[incoming_cemi.data.src_addr] = 1
-        assert self.data_secure.received_cemi(incoming_cemi) == test_cemi
+        assert self.data_secure.received_cemi(incoming_cemi.data) == test_cemi.data
 
         # Test wrong MAC
         self.data_secure._individual_address_table[incoming_cemi.data.src_addr] = 1
@@ -391,4 +394,4 @@ class TestDataSecure:
             DataSecureError,
             match=r"Data Secure MAC verification failed.*",
         ):
-            self.data_secure.received_cemi(incoming_cemi)
+            self.data_secure.received_cemi(incoming_cemi.data)