Repository: XKNX/xknx

Issue:
ValueError: [448,256] is not a valid APCICommand
**Description of problem:**
Exception during processing of messages, likely while ETS is communicating with new Secure IP Interface.

- [ ] using xknx standalone
- [X] using Home-Assistant knx plugin

**Version information:**
- xknx / Home-Assistant release with the issue: 0.98
- last working xknx / Home-Assistant release (if known): None

**KNX installation:**
<!--
Please provide details about your installation.
- Manufacturer and model of relevant actors, sensors or interfaces.
- if you have access to ETS: 
  - provide relevant group address parameters (DPT, Flags)
  - if applicable: excerpt of bus monitor output
-->
ETS 5.7.2

**Problem-relevant `xknx.yaml` or `configuration.yaml` entries (fill out even if it seems unimportant):**

**Traceback (if applicable):**
```
Aug 19 19:09:24 hass hass[32967]: Traceback (most recent call last):
Aug 19 19:09:24 hass hass[32967]:   File "/usr/lib/python3.6/asyncio/events.py", line 127, in _run
Aug 19 19:09:24 hass hass[32967]:     self._callback(*self._args)
Aug 19 19:09:24 hass hass[32967]:   File "/usr/lib/python3.6/asyncio/selector_events.py", line 1078, in _read_ready
Aug 19 19:09:24 hass hass[32967]:     self._protocol.datagram_received(data, addr)
Aug 19 19:09:24 hass hass[32967]:   File "/home/homeassistant/env/lib/python3.6/site-packages/xknx/io/udp_client.py", line 55, in datagram_received
Aug 19 19:09:24 hass hass[32967]:     self.data_received_callback(data)
Aug 19 19:09:24 hass hass[32967]:   File "/home/homeassistant/env/lib/python3.6/site-packages/xknx/io/udp_client.py", line 87, in data_received_callback
Aug 19 19:09:24 hass hass[32967]:     knxipframe.from_knx(raw)
Aug 19 19:09:24 hass hass[32967]:   File "/home/homeassistant/env/lib/python3.6/site-packages/xknx/knxip/knxip.py", line 78, in from_knx
Aug 19 19:09:24 hass hass[32967]:     pos += self.body.from_knx(data[pos:])
Aug 19 19:09:24 hass hass[32967]:   File "/home/homeassistant/env/lib/python3.6/site-packages/xknx/knxip/tunnelling_request.py", line 48, in from_knx
Aug 19 19:09:24 hass hass[32967]:     pos += self.cemi.from_knx(raw[pos:])
Aug 19 19:09:24 hass hass[32967]:   File "/home/homeassistant/env/lib/python3.6/site-packages/xknx/knxip/cemi_frame.py", line 113, in from_knx
Aug 19 19:09:24 hass hass[32967]:     return self.from_knx_data_link_layer(raw)
Aug 19 19:09:24 hass hass[32967]:   File "/home/homeassistant/env/lib/python3.6/site-packages/xknx/knxip/cemi_frame.py", line 143, in from_knx_data_link_layer
Aug 19 19:09:24 hass hass[32967]:     self.cmd = APCICommand(tpci_apci & 0xFFC0)
Aug 19 19:09:24 hass hass[32967]:   File "/usr/lib/python3.6/enum.py", line 291, in __call__
Aug 19 19:09:24 hass hass[32967]:     return cls.__new__(cls, value)
Aug 19 19:09:24 hass hass[32967]:   File "/usr/lib/python3.6/enum.py", line 533, in __new__
Aug 19 19:09:24 hass hass[32967]:     return cls._missing_(value)
Aug 19 19:09:24 hass hass[32967]:   File "/usr/lib/python3.6/enum.py", line 546, in _missing_
Aug 19 19:09:24 hass hass[32967]:     raise ValueError("%r is not a valid %s" % (value, cls.__name__))
Aug 19 19:09:24 hass hass[32967]: ValueError: 448 is not a valid APCICommand
```

```
Aug 22 08:15:49 hass hass[35975]: ValueError: 256 is not a valid APCICommand
Aug 22 08:15:49 hass hass[35975]:     raise ValueError("%r is not a valid %s" % (value, cls.__name__))
Aug 22 08:15:49 hass hass[35975]:   File "/usr/lib/python3.6/enum.py", line 546, in _missing_
Aug 22 08:15:49 hass hass[35975]:     return cls._missing_(value)
Aug 22 08:15:49 hass hass[35975]:   File "/usr/lib/python3.6/enum.py", line 533, in __new__
Aug 22 08:15:49 hass hass[35975]:     return cls.__new__(cls, value)
Aug 22 08:15:49 hass hass[35975]:   File "/usr/lib/python3.6/enum.py", line 291, in __call__
Aug 22 08:15:49 hass hass[35975]:     self.cmd = APCICommand(tpci_apci & 0xFFC0)
Aug 22 08:18:24 hass hass[35975]:   File "/usr/lib/python3.6/asyncio/base_events.py", line 1426, in _run_once
Aug 22 08:18:24 hass hass[35975]:     self._run_once()
Aug 22 08:18:24 hass hass[35975]:   File "/usr/lib/python3.6/asyncio/base_events.py", line 421, in run_forever
Aug 22 08:18:24 hass hass[35975]:     self.run_forever()
Aug 22 08:18:24 hass hass[35975]:   File "/usr/lib/python3.6/asyncio/base_events.py", line 454, in run_until_complete
Aug 22 08:18:24 hass hass[35975]:     return loop.run_until_complete(main)
Aug 22 08:18:24 hass hass[35975]:   File "/home/homeassistant/env/lib/python3.6/site-packages/homeassistant/util/async_.py", line 37, in asyncio_run
Aug 22 08:18:24 hass hass[35975]:     exit_code = asyncio_run(setup_and_run_hass(config_dir, args))
Aug 22 08:18:24 hass hass[35975]:   File "/home/homeassistant/env/lib/python3.6/site-packages/homeassistant/__main__.py", line 419, in main
Aug 22 08:18:24 hass hass[35975]:     sys.exit(main())
Aug 22 08:18:24 hass hass[35975]:   File "/home/homeassistant/env/bin/hass", line 10, in <module>

```

Discussion:
elupus: Note coverage will likeley go down. But there was no tests for the CEMIFrame as far as i could tell.
coveralls: 
[![Coverage Status](https://coveralls.io/builds/25634312/badge)](https://coveralls.io/builds/25634312)

Coverage decreased (-0.04%) to 91.794% when pulling **f575108ac2bca25fcaacdce73fad2f474f20bf67 on elupus:fix_valueerror** into **4b71c28db4cde694695da3dfad0072dd5383717f on XKNX:master**.

Julius2342: Wouldn't it make more sense to handle this directly within https://github.com/XKNX/xknx/blob/master/xknx/knxip/cemi_frame.py#L143 ` (catching the value error and raising an XKNXException istead) ?
elupus: The exact line you mention is the line we are modifying in this pull.
Julius2342: Oh my fault! Sorry! \*blush\*
elupus: Well the whole structure was untested. So it seemed messy/large to write it. I'll see if I have some time to look at it.
elupus: I caught the same issue for the CEMIMessageCode too. 
elupus: And added some tests

Patch:
diff --git a/examples/example_light_rgbw.py b/examples/example_light_rgbw.py
new file mode 100644
index 00000000..dd060c98
--- /dev/null
+++ b/examples/example_light_rgbw.py
@@ -0,0 +1,49 @@
+"""Example for setting different colors on a RGBW light."""
+import asyncio
+
+from xknx import XKNX
+from xknx.devices import RemoteValueColorRGBW
+
+
+async def main():
+    """Connect to KNX/IP bus and set different colors."""
+    xknx = XKNX()
+    await xknx.start()
+
+    rgbw = RemoteValueColorRGBW(xknx,
+                                group_address='1/1/40',
+                                group_address_state='1/1/41',
+                                device_name="RGBWLight")
+
+    await rgbw.set([255, 255, 255, 0, 15])  # cold-white
+    await asyncio.sleep(1)
+    await rgbw.set([0, 0, 0, 255, 15])  # warm-white
+    await asyncio.sleep(1)
+    await rgbw.set([0, 0, 0, 0, 15])  # off
+    await asyncio.sleep(1)
+
+    await rgbw.set([255, 0, 0, 0])  # red
+    await asyncio.sleep(1)
+    await rgbw.set([0, 255, 0, 0])  # green
+    await asyncio.sleep(1)
+    await rgbw.set([0, 0, 255, 0])  # blue
+    await asyncio.sleep(1)
+    await rgbw.set([0, 0, 0, 0, 15])  # off
+    await asyncio.sleep(1)
+
+    await rgbw.set([255, 255, 0, 0, 15])
+    await asyncio.sleep(1)
+    await rgbw.set([0, 255, 255, 0, 15])
+    await asyncio.sleep(1)
+    await rgbw.set([255, 0, 255, 0, 15])
+    await asyncio.sleep(1)
+    await rgbw.set([0, 0, 0, 0, 15])  # off
+    await asyncio.sleep(1)
+
+    await xknx.stop()
+
+
+# pylint: disable=invalid-name
+loop = asyncio.get_event_loop()
+loop.run_until_complete(main())
+loop.close()
diff --git a/xknx/devices/remote_value_color_rgbw.py b/xknx/devices/remote_value_color_rgbw.py
index 6c2e436e..beea0ca9 100644
--- a/xknx/devices/remote_value_color_rgbw.py
+++ b/xknx/devices/remote_value_color_rgbw.py
@@ -35,19 +35,23 @@ class RemoteValueColorRGBW(RemoteValue):
         Convert value (4-6 bytes) to payload (6 bytes).
 
         * Structure of DPT 251.600
-        ** Bytes 0, 1:
-        *** Bit 0-11: 0
-        *** Bit 12,13,14,15: R,G,B,W value valid?
-        ** Byte 2: R value
-        ** Byte 3: G value
-        ** Byte 4: B value
-        ** Byte 5: W value
+        ** Byte 0: R value
+        ** Byte 1: G value
+        ** Byte 2: B value
+        ** Byte 3: W value
+        ** Byte 4: 0x00 (reserved)
+        ** Byte 5:
+        *** Bit 0: W value valid?
+        *** Bit 1: B value valid?
+        *** Bit 2: G value valid?
+        *** Bit 3: R value valid?
+        *** Bit 4-7: 0
 
         In case we receive
         * > 6 bytes: error
         * 6 bytes: all bytes are passed through
-        * 5 bytes: 0x00 left padding
-        * 4 bytes: 0x000f left padding
+        * 5 bytes: 0x00?? fill up to 6 bytes
+        * 4 bytes: 0x000f right padding to 6 bytes
         * < 4 bytes: error
         """
         if not isinstance(value, (list, tuple)):
@@ -56,12 +60,16 @@ class RemoteValueColorRGBW(RemoteValue):
         if len(value) < 4 or len(value) > 6:
             raise ConversionError("Cannot serialize value to DPT 251.600 (wrong length, expecting list of 4-6 bytes)",
                                   value=value, type=type(value))
-        rgbw = value[len(value)-4:]
+        rgbw = value[:4]
         if any(not isinstance(color, int) for color in rgbw) \
                 or any(color < 0 for color in rgbw) \
                 or any(color > 255 for color in rgbw):
             raise ConversionError("Cannot serialize DPT 251.600 (wrong RGBW values)", value=value)
-        return DPTArray([0x00, 0x0f][:6-len(value)] + list(value))
+        if len(value) < 5:
+            return DPTArray(list(rgbw) + [0x00, 0x0f])
+        if len(value) < 6:
+            return DPTArray(list(rgbw) + [0x00] + list(value[4:]))
+        return DPTArray(value)
 
     def from_knx(self, payload):
         """
@@ -72,7 +80,7 @@ class RemoteValueColorRGBW(RemoteValue):
         """
         result = []
         for i in range(0, len(payload.value) - 2):
-            valid = payload.value[1] & (0x08 >> i) != 0
-            result.append(payload.value[2 + i] if valid else self.previous_value[i])
+            valid = (payload.value[5] & (0x08 >> i)) != 0  # R,G,B,W value valid?
+            result.append(payload.value[i] if valid else self.previous_value[i])
         self.previous_value = result
         return result
diff --git a/xknx/exceptions/exception.py b/xknx/exceptions/exception.py
index 879e14b2..626299bd 100644
--- a/xknx/exceptions/exception.py
+++ b/xknx/exceptions/exception.py
@@ -6,7 +6,15 @@ class XKNXException(Exception):
 
     def __eq__(self, other):
         """Equal operator."""
-        return self.__dict__ == other.__dict__
+        return repr(self) == repr(other)
+
+    def __hash__(self):
+        """Hash function."""
+        return hash(str(self))
+
+    def __repr__(self):
+        """Representation of object."""
+        return str(self)
 
 
 class CouldNotParseTelegram(XKNXException):
@@ -14,7 +22,7 @@ class CouldNotParseTelegram(XKNXException):
 
     def __init__(self, description, **kwargs):
         """Initialize CouldNotParseTelegram class."""
-        super().__init__("Could not parse Telegram")
+        super().__init__()
         self.description = description
         self.parameter = kwargs
 
@@ -32,7 +40,7 @@ class CouldNotParseKNXIP(XKNXException):
 
     def __init__(self, description=""):
         """Initialize CouldNotParseTelegram class."""
-        super().__init__("Could not parse KNXIP")
+        super().__init__()
         self.description = description
 
     def __str__(self):
@@ -46,7 +54,7 @@ class ConversionError(XKNXException):
 
     def __init__(self, description, **kwargs):
         """Initialize ConversionError class."""
-        super().__init__("Conversion Error")
+        super().__init__()
         self.description = description
         self.parameter = kwargs
 
@@ -63,7 +71,7 @@ class CouldNotParseAddress(XKNXException):
 
     def __init__(self, address=None):
         """Initialize CouldNotParseAddress class."""
-        super().__init__("Could not parse address")
+        super().__init__()
         self.address = address
 
     def __str__(self):
@@ -76,7 +84,7 @@ class DeviceIllegalValue(XKNXException):
 
     def __init__(self, value, description):
         """Initialize DeviceIllegalValue class."""
-        super().__init__("Illegal value for device")
+        super().__init__()
         self.value = value
         self.description = description
 
diff --git a/xknx/knxip/cemi_frame.py b/xknx/knxip/cemi_frame.py
index ec925a83..3f023c62 100644
--- a/xknx/knxip/cemi_frame.py
+++ b/xknx/knxip/cemi_frame.py
@@ -105,7 +105,10 @@ class CEMIFrame(KNXIPBody):
 
     def from_knx(self, raw):
         """Parse/deserialize from KNX/IP raw data."""
-        self.code = CEMIMessageCode(raw[0])
+        try:
+            self.code = CEMIMessageCode(raw[0])
+        except ValueError:
+            raise CouldNotParseKNXIP("Could not understand CEMIMessageCode: {0} ".format(raw[0]))
 
         if self.code == CEMIMessageCode.L_DATA_IND or \
                 self.code == CEMIMessageCode.L_Data_REQ or \
@@ -140,7 +143,11 @@ class CEMIFrame(KNXIPBody):
 
         tpci_apci = cemi[9 + addil] * 256 + cemi[10 + addil]
 
-        self.cmd = APCICommand(tpci_apci & 0xFFC0)
+        try:
+            self.cmd = APCICommand(tpci_apci & 0xFFC0)
+        except ValueError:
+            raise CouldNotParseKNXIP(
+                "APCI not supported: {0:#012b}".format(tpci_apci & 0xFFC0))
 
         apdu = cemi[10 + addil:]
         if len(apdu) != self.mpdu_len:


Test patch:
diff --git a/test/core_tests/exceptions_test.py b/test/core_tests/exceptions_test.py
new file mode 100644
index 00000000..41fc1b74
--- /dev/null
+++ b/test/core_tests/exceptions_test.py
@@ -0,0 +1,51 @@
+"""Unit tests for exceptions"""
+import pytest
+
+from xknx.exceptions import (
+    ConversionError, CouldNotParseAddress, CouldNotParseKNXIP,
+    CouldNotParseTelegram, DeviceIllegalValue, XKNXException)
+
+
+@pytest.mark.parametrize(
+    "base,equal,diff",
+    [
+        (
+            ConversionError("desc1"),
+            ConversionError("desc1"),
+            ConversionError("desc2"),
+        ),
+        (
+            CouldNotParseAddress(123),
+            CouldNotParseAddress(123),
+            CouldNotParseAddress(321),
+        ),
+        (
+            CouldNotParseKNXIP("desc1"),
+            CouldNotParseKNXIP("desc1"),
+            CouldNotParseKNXIP("desc2"),
+        ),
+        (
+            CouldNotParseTelegram("desc", arg1=1, arg2=2),
+            CouldNotParseTelegram("desc", arg1=1, arg2=2),
+            CouldNotParseTelegram("desc", arg1=2, arg2=1),
+        ),
+        (
+            DeviceIllegalValue("value1", "desc"),
+            DeviceIllegalValue("value1", "desc"),
+            DeviceIllegalValue("value1", "desc2"),
+        ),
+        (
+            XKNXException("desc1"),
+            XKNXException("desc1"),
+            XKNXException("desc2"),
+        ),
+    ],
+)
+def test_exceptions(base, equal, diff):
+    """Test hashability and repr of exceptions."""
+    assert hash(base) == hash(equal)
+    assert hash(base) != hash(diff)
+    assert base == equal
+    assert base != diff
+    assert repr(base) == repr(equal)
+    assert repr(base) != repr(diff)
diff --git a/test/devices_tests/light_test.py b/test/devices_tests/light_test.py
index b6283672..5de6eddf 100644
--- a/test/devices_tests/light_test.py
+++ b/test/devices_tests/light_test.py
@@ -318,7 +318,7 @@ class TestLight(unittest.TestCase):
         self.assertEqual(xknx.telegrams.qsize(), 1)
         telegram = xknx.telegrams.get_nowait()
         self.assertEqual(telegram,
-                         Telegram(GroupAddress('1/2/5'), payload=DPTArray((0, 15, 23, 24, 25, 26))))
+                         Telegram(GroupAddress('1/2/5'), payload=DPTArray((23, 24, 25, 26, 0, 15))))
         self.assertEqual(light.current_color, ([23, 24, 25], 26))
 
     def test_set_color_rgbw_not_possible(self):
@@ -489,7 +489,7 @@ class TestLight(unittest.TestCase):
                       group_address_color='1/2/4',
                       group_address_rgbw='1/2/5')
         self.assertEqual(light.current_color, (None, None))
-        telegram = Telegram(GroupAddress('1/2/5'), payload=DPTArray((0, 15, 23, 24, 25, 26)))
+        telegram = Telegram(GroupAddress('1/2/5'), payload=DPTArray((23, 24, 25, 26, 0, 15)))
         self.loop.run_until_complete(asyncio.Task(light.process(telegram)))
         self.assertEqual(light.current_color, ([23, 24, 25], 26))
 
diff --git a/test/devices_tests/remote_value_color_rgbw_test.py b/test/devices_tests/remote_value_color_rgbw_test.py
index 2a56d3b0..d352c299 100644
--- a/test/devices_tests/remote_value_color_rgbw_test.py
+++ b/test/devices_tests/remote_value_color_rgbw_test.py
@@ -26,32 +26,32 @@ class TestRemoteValueColorRGBW(unittest.TestCase):
         remote_value = RemoteValueColorRGBW(xknx)
         input_list = [100, 101, 102, 127]
         input_tuple = (100, 101, 102, 127)
-        expected = DPTArray((0x00, 0x0f, 0x64, 0x65, 0x66, 0x7f))
+        expected = DPTArray((0x64, 0x65, 0x66, 0x7f, 0x00, 0x0f))
         self.assertEqual(remote_value.to_knx(input_tuple), expected)
         self.assertEqual(remote_value.to_knx(input_list), expected)
-        self.assertEqual(remote_value.to_knx((15,) + input_tuple), expected)
-        self.assertEqual(remote_value.to_knx([15] + input_list), expected)
-        self.assertEqual(remote_value.to_knx((0, 15) + input_tuple), expected)
-        self.assertEqual(remote_value.to_knx([0, 15] + input_list), expected)
+        self.assertEqual(remote_value.to_knx(input_tuple + (15,)), expected)
+        self.assertEqual(remote_value.to_knx(input_list + [15]), expected)
+        self.assertEqual(remote_value.to_knx(input_tuple + (0, 15)), expected)
+        self.assertEqual(remote_value.to_knx(input_list + [0, 15]), expected)
 
     def test_from_knx(self):
         """Test from_knx function with normal operation."""
         xknx = XKNX(loop=self.loop)
         remote_value = RemoteValueColorRGBW(xknx)
         self.assertEqual(
-            remote_value.from_knx(DPTArray((0x00, 0x00, 0x64, 0x65, 0x66, 0x7f))),
+            remote_value.from_knx(DPTArray((0x64, 0x65, 0x66, 0x7f, 0x00, 0x00))),
             [0, 0, 0, 0])
         self.assertEqual(
-            remote_value.from_knx(DPTArray((0x00, 0x0f, 0x64, 0x65, 0x66, 0x7f))),
+            remote_value.from_knx(DPTArray((0x64, 0x65, 0x66, 0x7f, 0x00, 0x0f))),
             [100, 101, 102, 127])
         self.assertEqual(
-            remote_value.from_knx(DPTArray((0x00, 0x00, 0x64, 0x65, 0x66, 0x7f))),
+            remote_value.from_knx(DPTArray((0x64, 0x65, 0x66, 0x7f, 0x00, 0x00))),
             [100, 101, 102, 127])
         self.assertEqual(
-            remote_value.from_knx(DPTArray((0x00, 0x09, 0xff, 0x65, 0x66, 0xff))),
+            remote_value.from_knx(DPTArray((0xff, 0x65, 0x66, 0xff, 0x00, 0x09))),
             [255, 101, 102, 255])
         self.assertEqual(
-            remote_value.from_knx(DPTArray((0x00, 0x01, 0x64, 0x65, 0x66, 0x7f))),
+            remote_value.from_knx(DPTArray((0x64, 0x65, 0x66, 0x7f, 0x00, 0x01))),
             [255, 101, 102, 127])
 
     def test_to_knx_error(self):
@@ -90,7 +90,7 @@ class TestRemoteValueColorRGBW(unittest.TestCase):
             telegram,
             Telegram(
                 GroupAddress('1/2/3'),
-                payload=DPTArray((0x00, 0x0f, 0x64, 0x65, 0x66, 0x67))))
+                payload=DPTArray((0x64, 0x65, 0x66, 0x67, 0x00, 0x0f))))
         self.loop.run_until_complete(asyncio.Task(remote_value.set((100, 101, 104, 105))))
         self.assertEqual(xknx.telegrams.qsize(), 1)
         telegram = xknx.telegrams.get_nowait()
@@ -98,7 +98,7 @@ class TestRemoteValueColorRGBW(unittest.TestCase):
             telegram,
             Telegram(
                 GroupAddress('1/2/3'),
-                payload=DPTArray((0x00, 0x0f, 0x64, 0x65, 0x68, 0x69))))
+                payload=DPTArray((0x64, 0x65, 0x68, 0x69, 0x00, 0x0f))))
 
     def test_process(self):
         """Test process telegram."""
@@ -108,7 +108,7 @@ class TestRemoteValueColorRGBW(unittest.TestCase):
             group_address=GroupAddress("1/2/3"))
         telegram = Telegram(
             group_address=GroupAddress("1/2/3"),
-            payload=DPTArray((0x00, 0x0f, 0x64, 0x65, 0x66, 0x67)))
+            payload=DPTArray((0x64, 0x65, 0x66, 0x67, 0x00, 0x0f)))
         self.loop.run_until_complete(asyncio.Task(remote_value.process(telegram)))
         self.assertEqual(remote_value.value, [100, 101, 102, 103])
 
diff --git a/test/knxip_tests/cemi_frame_test.py b/test/knxip_tests/cemi_frame_test.py
new file mode 100644
index 00000000..dae3f63c
--- /dev/null
+++ b/test/knxip_tests/cemi_frame_test.py
@@ -0,0 +1,71 @@
+"""Tests for the CEMIFrame object"""
+
+from unittest.mock import MagicMock
+
+from pytest import fixture, raises
+
+from xknx.exceptions import CouldNotParseKNXIP
+from xknx.knx import DPTBinary, PhysicalAddress
+from xknx.knxip.cemi_frame import CEMIFrame
+from xknx.knxip.knxip_enum import APCICommand, CEMIMessageCode
+
+
+def get_data(code, adil, flags, src, dst, mpdu_len, tpci_apci, payload):
+    return [
+        code,
+        adil,  # adil
+        (flags >> 8) & 255,  # flags
+        flags & 255,  # flags
+        (src >> 8) & 255,  # src
+        src & 255,  # src
+        (dst >> 8) & 255,  # dst
+        dst & 255,  # dst
+        mpdu_len,  # mpdu_len
+        (tpci_apci >> 8) & 255,  # tpci_apci
+        tpci_apci & 255,  # tpci_apci
+        *payload,  # payload
+    ]
+
+
+@fixture(name="frame")
+def fixture_frame():
+    """Fixture to get a simple mocked frame"""
+    xknx = MagicMock()
+    return CEMIFrame(xknx)
+
+
+def test_valid_command(frame):
+    """Test for valid frame parsing"""
+    packet_len = frame.from_knx(get_data(0x29, 0, 0, 0, 0, 1, 0, []))
+    assert frame.code == CEMIMessageCode.L_DATA_IND
+    assert frame.cmd == APCICommand.GROUP_READ
+    assert frame.flags == 0
+    assert frame.mpdu_len == 1
+    assert frame.payload == DPTBinary(0)
+    assert frame.src_addr == PhysicalAddress(0)
+    assert frame.dst_addr == PhysicalAddress(0)
+    assert packet_len == 11
+
+
+def test_invalid_tpci_apci(frame):
+    """Test for invalid APCICommand"""
+    with raises(CouldNotParseKNXIP, match=r".*APCI not supported: .*"):
+        frame.from_knx(get_data(0x29, 0, 0, 0, 0, 1, 0xFFC0, []))
+
+
+def test_invalid_apdu_len(frame):
+    """Test for invalid apdu len"""
+    with raises(CouldNotParseKNXIP, match=r".*APDU LEN should be .*"):
+        frame.from_knx(get_data(0x29, 0, 0, 0, 0, 2, 0, []))
+
+
+def test_invalid_invalid_len(frame):
+    """Test for invalid cemi len"""
+    with raises(CouldNotParseKNXIP, match=r".*CEMI too small"):
+        frame.from_knx(get_data(0x29, 0, 0, 0, 0, 2, 0, [])[:5])
+
+
+def test_invalid_invalid_code(frame):
+    """Test for invalid cemi code"""
+    with raises(CouldNotParseKNXIP, match=r".*Could not understand CEMIMessageCode"):
+        frame.from_knx(get_data(0x0, 0, 0, 0, 0, 2, 0, []))