Repository: XKNX/xknx

Issue:
XKNXException is an unhashable exception
**Description of problem:**
Seem the XKNXException exception is not hashable causing problems with python traceback module: https://bugs.python.org/issue28603. Oddly that aught to have been solved already.

- [ ] using xknx standalone
- [X] using Home-Assistant knx plugin

**Version information:**
- xknx / Home-Assistant release with the issue:
- last working xknx / Home-Assistant release (if known): 0.98

**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
-->

**Problem-relevant `xknx.yaml` or `configuration.yaml` entries (fill out even if it seems unimportant):**

**Traceback (if applicable):**
Aug 22 08:18:24 hass hass[35975]: TypeError: unhashable type: 'XKNXException'
Aug 22 08:18:24 hass hass[35975]:     _seen.add(exc_value)
Aug 22 08:18:24 hass hass[35975]:   File "/usr/lib/python3.6/traceback.py", line 462, in __init__
Aug 22 08:18:24 hass hass[35975]:     type(value), value, tb, limit=limit).format(chain=chain):
Aug 22 08:18:24 hass hass[35975]:   File "/usr/lib/python3.6/traceback.py", line 100, in print_exception
Aug 22 08:18:24 hass hass[35975]:     traceback.print_exception(ei[0], ei[1], tb, None, sio)
Aug 22 08:18:24 hass hass[35975]:   File "/usr/lib/python3.6/logging/__init__.py", line 533, in formatException
Aug 22 08:18:24 hass hass[35975]:     record.exc_text = self.formatException(record.exc_info)
Aug 22 08:18:24 hass hass[35975]:   File "/usr/lib/python3.6/logging/__init__.py", line 583, in format
Aug 22 08:18:24 hass hass[35975]:     return fmt.format(record)
Aug 22 08:18:24 hass hass[35975]:   File "/usr/lib/python3.6/logging/__init__.py", line 838, in format
Aug 22 08:18:24 hass hass[35975]:     msg = self.format(record)
Aug 22 08:18:24 hass hass[35975]:   File "/usr/lib/python3.6/logging/__init__.py", line 992, in emit

Comments:

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
 


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])