Repository: XKNX/xknx
Pull request #1464 (2024-05-25 16:43:00)

Add DPTColorRGB transcoder
<!--
  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.
-->
in favour of an implementation in RemoteValueColorRGB

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)
- [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://app.codecov.io/gh/XKNX/xknx/pull/1464?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 96.63%. Comparing base [(`89a5b37`)](https://app.codecov.io/gh/XKNX/xknx/commit/89a5b3791cb222e56258bcdea5644d83a0f25cb3?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) to head [(`ca07b74`)](https://app.codecov.io/gh/XKNX/xknx/commit/ca07b7455ea016930b886f5b453479c1c078ad20?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX).

<details><summary>Additional details and impacted files</summary>


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

```diff
@@           Coverage Diff           @@
##             main    #1464   +/-   ##
=======================================
  Coverage   96.62%   96.63%           
=======================================
  Files         151      151           
  Lines        9850     9879   +29     
=======================================
+ Hits         9518     9547   +29     
  Misses        332      332           
```


| [Files](https://app.codecov.io/gh/XKNX/xknx/pull/1464?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) | Coverage Δ | |
|---|---|---|
| [xknx/devices/light.py](https://app.codecov.io/gh/XKNX/xknx/pull/1464?src=pr&el=tree&filepath=xknx%2Fdevices%2Flight.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kZXZpY2VzL2xpZ2h0LnB5) | `99.60% <100.00%> (+<0.01%)` | :arrow_up: |
| [xknx/dpt/\_\_init\_\_.py](https://app.codecov.io/gh/XKNX/xknx/pull/1464?src=pr&el=tree&filepath=xknx%2Fdpt%2F__init__.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kcHQvX19pbml0X18ucHk=) | `100.00% <100.00%> (ø)` | |
| [xknx/dpt/dpt.py](https://app.codecov.io/gh/XKNX/xknx/pull/1464?src=pr&el=tree&filepath=xknx%2Fdpt%2Fdpt.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kcHQvZHB0LnB5) | `100.00% <100.00%> (ø)` | |
| [xknx/dpt/dpt\_color.py](https://app.codecov.io/gh/XKNX/xknx/pull/1464?src=pr&el=tree&filepath=xknx%2Fdpt%2Fdpt_color.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9kcHQvZHB0X2NvbG9yLnB5) | `100.00% <100.00%> (ø)` | |
| [xknx/remote\_value/remote\_value\_color\_rgb.py](https://app.codecov.io/gh/XKNX/xknx/pull/1464?src=pr&el=tree&filepath=xknx%2Fremote_value%2Fremote_value_color_rgb.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9yZW1vdGVfdmFsdWUvcmVtb3RlX3ZhbHVlX2NvbG9yX3JnYi5weQ==) | `100.00% <100.00%> (ø)` | |

</details>

Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index 6623333e..45759672 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -11,6 +11,9 @@ nav_order: 2
 ### DPT
 
 - DPTComplex: Common interface for DPT transcoders with multi-value data. Resulting dataclasses can be converted to and from a dict with DPT specific properties to be JSON compatible.
+- Added or refactored DPTs and dataclasses:
+  - 232.600 - DPTColorRGB - RGBColor
+  - 242.600 - DPTColorXYY - XYYColor
 
 # 2.12.2 Fix thread leak 2024-03-05
 
diff --git a/xknx/devices/light.py b/xknx/devices/light.py
index c5bd35db..58f10cda 100644
--- a/xknx/devices/light.py
+++ b/xknx/devices/light.py
@@ -20,7 +20,7 @@ from itertools import chain
 import logging
 from typing import TYPE_CHECKING, Any, Callable, cast
 
-from xknx.dpt.dpt_color import XYYColor
+from xknx.dpt.dpt_color import RGBColor, XYYColor
 from xknx.remote_value import (
     GroupAddressesType,
     RemoteValue,
@@ -467,7 +467,13 @@ class Light(Device):
                 return None, None
             return self.rgbw.value[:3], self.rgbw.value[3]
         if self.color.initialized:
-            return self.color.value, None
+            if self.color.value is None:
+                return None, None
+            return (
+                self.color.value.red,
+                self.color.value.green,
+                self.color.value.blue,
+            ), None
         # individual RGB addresses - white will return None when it is not initialized
         colors = (
             self.red.brightness.value,
@@ -504,7 +510,7 @@ class Light(Device):
         else:
             if self.supports_color:
                 if self.color.initialized:
-                    await self.color.set(color)
+                    await self.color.set(RGBColor(*color))
                     return
                 if all(
                     c.brightness.initialized for c in (self.red, self.green, self.blue)
diff --git a/xknx/dpt/__init__.py b/xknx/dpt/__init__.py
index aa0d3f36..ba7020e7 100644
--- a/xknx/dpt/__init__.py
+++ b/xknx/dpt/__init__.py
@@ -6,7 +6,7 @@ Module for encoding and decoding KNX datatypes.
 """
 
 # flake8: noqa
-from .dpt import DPTBase, DPTNumeric
+from .dpt import DPTBase, DPTNumeric, DPTComplexData, DPTComplex
 from .dpt_1byte_signed import DPTPercentV8, DPTSignedRelativeValue, DPTValue1Count
 from .dpt_1byte_uint import (
     DPTDecimalFactor,
@@ -183,7 +183,7 @@ from .dpt_4byte_int import (
     DPTVolumeLiquidLitre,
     DPTVolumeM3,
 )
-from .dpt_color import DPTColorXYY
+from .dpt_color import DPTColorXYY, DPTColorRGB, RGBColor, XYYColor
 from .dpt_date import DPTDate
 from .dpt_datetime import DPTDateTime
 from .dpt_hvac_mode import DPTControllerStatus, DPTHVACContrMode, DPTHVACMode
diff --git a/xknx/dpt/dpt.py b/xknx/dpt/dpt.py
index 9cf5a59a..70821ce8 100644
--- a/xknx/dpt/dpt.py
+++ b/xknx/dpt/dpt.py
@@ -209,7 +209,7 @@ class DPTComplexData(ABC):
         """Init from a dictionary."""
 
     @abstractmethod
-    def as_dict(self) -> dict[str, str | int | float | bool | None]:
+    def as_dict(self) -> dict[str, Any]:
         """Create a JSON serializable dictionary."""
 
 
diff --git a/xknx/dpt/dpt_color.py b/xknx/dpt/dpt_color.py
index 83583ddc..5476179c 100644
--- a/xknx/dpt/dpt_color.py
+++ b/xknx/dpt/dpt_color.py
@@ -10,6 +10,73 @@ from .dpt import DPTComplex, DPTComplexData
 from .payload import DPTArray, DPTBinary
 
 
+@dataclass
+class RGBColor(DPTComplexData):
+    """
+    Representation of RGB color.
+
+    `red`: int 0..255
+    `green`: int 0..255
+    `blue`: int 0..255
+    """
+
+    red: int
+    green: int
+    blue: int
+
+    @classmethod
+    def from_dict(cls, data: Mapping[str, Any]) -> RGBColor:
+        """Init from a dictionary."""
+        try:
+            red = int(data["red"])
+            green = int(data["green"])
+            blue = int(data["blue"])
+        except KeyError as err:
+            raise ValueError(f"Missing required key: {err}") from err
+        except (ValueError, TypeError) as err:
+            raise ValueError(f"Invalid value for RGB color: {err}") from err
+
+        return cls(red=red, green=green, blue=blue)
+
+    def as_dict(self) -> dict[str, int]:
+        """Create a JSON serializable dictionary."""
+        return {
+            "red": self.red,
+            "green": self.green,
+            "blue": self.blue,
+        }
+
+
+class DPTColorRGB(DPTComplex[RGBColor]):
+    """Abstraction for KNX 3 octet color RGB (DPT 232.600)."""
+
+    data_type = RGBColor
+    payload_type = DPTArray
+    payload_length = 3
+    dpt_main_number = 232
+    dpt_sub_number = 600
+    value_type = "color_rgb"
+
+    @classmethod
+    def from_knx(cls, payload: DPTArray | DPTBinary) -> RGBColor:
+        """Parse/deserialize from KNX/IP raw data."""
+        raw = cls.validate_payload(payload)
+        return RGBColor(
+            red=raw[0],
+            green=raw[1],
+            blue=raw[2],
+        )
+
+    @classmethod
+    def _to_knx(cls, value: RGBColor) -> DPTArray:
+        """Serialize to KNX/IP raw data."""
+        colors: tuple[int, int, int] = (value.red, value.green, value.blue)
+        for color in colors:
+            if not 0 <= color <= 255:
+                raise ValueError(f"Color value out of range 0..255: {value}")
+        return DPTArray(colors)
+
+
 @dataclass
 class XYYColor(DPTComplexData):
     """
@@ -46,7 +113,7 @@ class XYYColor(DPTComplexData):
 
         return cls(color=color, brightness=brightness)
 
-    def as_dict(self) -> dict[str, str | int | float | bool | None]:
+    def as_dict(self) -> dict[str, int | float | None]:
         """Create a JSON serializable dictionary."""
         return {
             "x_axis": self.color[0] if self.color is not None else None,
diff --git a/xknx/remote_value/remote_value_color_rgb.py b/xknx/remote_value/remote_value_color_rgb.py
index ca057b36..645a2ac5 100644
--- a/xknx/remote_value/remote_value_color_rgb.py
+++ b/xknx/remote_value/remote_value_color_rgb.py
@@ -6,11 +6,10 @@ DPT 232.600.
 
 from __future__ import annotations
 
-from collections.abc import Sequence
 from typing import TYPE_CHECKING
 
 from xknx.dpt import DPTArray, DPTBinary
-from xknx.exceptions import ConversionError, CouldNotParseTelegram
+from xknx.dpt.dpt_color import DPTColorRGB, RGBColor
 
 from .remote_value import AsyncCallbackType, GroupAddressesType, RemoteValue
 
@@ -18,7 +17,7 @@ if TYPE_CHECKING:
     from xknx.xknx import XKNX
 
 
-class RemoteValueColorRGB(RemoteValue[tuple[int, int, int]]):
+class RemoteValueColorRGB(RemoteValue[RGBColor]):
     """Abstraction for remote value of KNX DPT 232.600 (DPT_Color_RGB)."""
 
     def __init__(
@@ -42,34 +41,10 @@ class RemoteValueColorRGB(RemoteValue[tuple[int, int, int]]):
             after_update_cb=after_update_cb,
         )
 
-    def to_knx(self, value: Sequence[int]) -> DPTArray:
+    def to_knx(self, value: RGBColor) -> DPTArray:
         """Convert value to payload."""
-        if not isinstance(value, (list, tuple)):
-            raise ConversionError(
-                "Could not serialize RemoteValueColorRGB (wrong type)",
-                value=value,
-                type=type(value),
-            )
-        if len(value) != 3:
-            raise ConversionError(
-                "Could not serialize DPT 232.600 (wrong length)",
-                value=value,
-                type=type(value),
-            )
-        if (
-            any(not isinstance(color, int) for color in value)
-            or any(color < 0 for color in value)
-            or any(color > 255 for color in value)
-        ):
-            raise ConversionError(
-                "Could not serialize DPT 232.600 (wrong bytes)", value=value
-            )
+        return DPTColorRGB.to_knx(value)
 
-        return DPTArray(list(value))
-
-    def from_knx(self, payload: DPTArray | DPTBinary) -> tuple[int, int, int]:
+    def from_knx(self, payload: DPTArray | DPTBinary) -> RGBColor:
         """Convert current payload to value."""
-        if not (isinstance(payload, DPTArray) and len(payload.value) == 3):
-            raise CouldNotParseTelegram("Payload invalid", payload=str(payload))
-
-        return payload.value[0], payload.value[1], payload.value[2]
+        return DPTColorRGB.from_knx(payload)


Test patch:
diff --git a/test/dpt_tests/dpt_color_test.py b/test/dpt_tests/dpt_color_test.py
index 9c8ef8e7..5dd1d004 100644
--- a/test/dpt_tests/dpt_color_test.py
+++ b/test/dpt_tests/dpt_color_test.py
@@ -2,31 +2,172 @@
 
 import pytest
 
-from xknx.dpt.dpt_color import DPTArray, DPTColorXYY, XYYColor
+from xknx.dpt.dpt_color import DPTArray, DPTColorRGB, DPTColorXYY, RGBColor, XYYColor
 from xknx.exceptions import ConversionError, CouldNotParseTelegram
 
 
-class TestXYYColor:
-    """Test XYYColor class."""
+class TestRGBColor:
+    """Test RGBColor class."""
+
+    @pytest.mark.parametrize(
+        ("data", "value"),
+        [
+            ({"red": 0, "green": 128, "blue": 255}, RGBColor(0, 128, 255)),
+            ({"red": 255, "green": 255, "blue": 255}, RGBColor(255, 255, 255)),
+            ({"red": 0, "green": 0, "blue": 0}, RGBColor(0, 0, 0)),
+        ],
+    )
+    def test_dict(self, data, value):
+        """Test from_dict and as_dict methods."""
+        test_value = RGBColor.from_dict(data)
+        assert test_value == value
+        assert value.as_dict() == data
 
     @pytest.mark.parametrize(
         "data",
         [
-            {"x_axis": 0.1, "y_axis": 0.2, "brightness": 128},
-            {"x_axis": 0.5, "y_axis": 0.5, "brightness": 255},
-            {"x_axis": 0.9, "y_axis": 0.8},
-            {"brightness": 50},
+            # incomplete data
             {},
-            {"x_axis": None, "y_axis": None, "brightness": 255},
-            {"x_axis": 0.5, "y_axis": 0.5, "brightness": None},
-            {"x_axis": None, "brightness": 255},
-            {"y_axis": None, "brightness": 255},
+            {"red": 128},
+            {"green": 128},
+            {"blue": 128},
+            {"red": 128, "green": 128},
+            {"red": 128, "blue": 128},
+            {"green": 128, "blue": 128},
+            # invalid data
+            {"red": "a", "green": 128, "blue": 128},
+            {"red": 128, "green": "a", "blue": 128},
+            {"red": 128, "green": 128, "blue": "a"},
+        ],
+    )
+    def test_dict_invalid(self, data):
+        """Test from_dict and as_dict methods."""
+        with pytest.raises(ValueError):
+            RGBColor.from_dict(data)
+
+
+class TestDPTColorRGB:
+    """Test class for KNX RGB-color objects."""
+
+    @pytest.mark.parametrize(
+        ("value", "raw"),
+        [
+            (RGBColor(0, 0, 0), (0x00, 0x00, 0x00)),  # min values
+            (RGBColor(255, 255, 255), (0xFF, 0xFF, 0xFF)),  # max values
+            (RGBColor(128, 128, 128), (0x80, 0x80, 0x80)),  # mid values
+        ],
+    )
+    def test_rgbcolor_value(self, value, raw):
+        """Test DPTColorRGB parsing and streaming."""
+        knx_value = DPTColorRGB.to_knx(value)
+        assert knx_value == DPTArray(raw)
+        assert DPTColorRGB.from_knx(knx_value) == value
+
+    @pytest.mark.parametrize(
+        ("value", "raw"),
+        [
+            ({"red": 128, "green": 128, "blue": 128}, (0x80, 0x80, 0x80)),
+            ({"red": 255, "green": 255, "blue": 255}, (0xFF, 0xFF, 0xFF)),
+        ],
+    )
+    def test_rgbcolor_to_knx_from_dict(self, value, raw):
+        """Test DPTColorRGB parsing from a dict."""
+        knx_value = DPTColorRGB.to_knx(value)
+        assert knx_value == DPTArray(raw)
+
+    @pytest.mark.parametrize(
+        ("red", "green", "blue"),
+        [
+            (-1, 0, 0),
+            (0, -1, 0),
+            (0, 0, -1),
+            (256, 0, 0),
+            (0, 256, 0),
+            (0, 0, 256),
+        ],
+    )
+    def test_rgbcolor_to_knx_limits(self, red, green, blue):
+        """Test initialization of DPTColorRGB with wrong value."""
+        value = RGBColor(red=red, green=green, blue=blue)
+        with pytest.raises(ConversionError):
+            DPTColorRGB.to_knx(value)
+
+    @pytest.mark.parametrize(
+        "value",
+        [
+            None,
+            (0xFF, 0x4E, 0x12),
+            1,
+            ((0x00, 0xFF, 0x4E), 0x12),
+            ((0xFF, 0x4E), (0x12, 0x00)),
+            ((0, 0), "a"),
+            ((0, 0), 0.4),
+            RGBColor(red=0, green=0, blue="a"),
+            RGBColor(red=4, green="a", blue=4),
+            RGBColor(red="a", green=0, blue=1),
+        ],
+    )
+    def test_rgbcolor_wrong_value_to_knx(self, value):
+        """Test DPTColorRGB parsing with wrong value."""
+        with pytest.raises(ConversionError):
+            DPTColorRGB.to_knx(value)
+
+    def test_rgbcolor_wrong_value_from_knx(self):
+        """Test DPTColorRGB parsing with wrong value."""
+        with pytest.raises(CouldNotParseTelegram):
+            DPTColorRGB.from_knx(DPTArray((0xFF, 0x4E, 0x12, 0x23)))
+        with pytest.raises(CouldNotParseTelegram):
+            DPTColorRGB.from_knx(DPTArray((0xFF, 0x4E)))
+
+
+class TestXYYColor:
+    """Test XYYColor class."""
+
+    @pytest.mark.parametrize(
+        ("data", "value"),
+        [
+            (
+                {"x_axis": 0.1, "y_axis": 0.2, "brightness": 128},
+                XYYColor((0.1, 0.2), 128),
+            ),
+            (
+                {"x_axis": 0.5, "y_axis": 0.5, "brightness": 255},
+                XYYColor((0.5, 0.5), 255),
+            ),
+            (
+                {"x_axis": 0.9, "y_axis": 0.8},
+                XYYColor((0.9, 0.8)),
+            ),
+            (
+                {"brightness": 50},
+                XYYColor(None, 50),
+            ),
+            (
+                {},
+                XYYColor(),
+            ),
+            (
+                {"x_axis": None, "y_axis": None, "brightness": 255},
+                XYYColor(None, 255),
+            ),
+            (
+                {"x_axis": 0.5, "y_axis": 0.5, "brightness": None},
+                XYYColor((0.5, 0.5), None),
+            ),
+            (
+                {"x_axis": None, "brightness": 255},
+                XYYColor(None, 255),
+            ),
+            (
+                {"y_axis": None, "brightness": 255},
+                XYYColor(None, 255),
+            ),
         ],
     )
-    def test_dict(self, data):
+    def test_dict(self, data, value):
         """Test from_dict and as_dict methods."""
-        value = XYYColor.from_dict(data)
-        assert value
+        test_value = XYYColor.from_dict(data)
+        assert test_value == value
         # fields default to `None`
         default_dict = {"x_axis": None, "y_axis": None, "brightness": None}
         assert value.as_dict() == default_dict | data
diff --git a/test/remote_value_tests/remote_value_color_rgb_test.py b/test/remote_value_tests/remote_value_color_rgb_test.py
index fe20ac9f..48509fc2 100644
--- a/test/remote_value_tests/remote_value_color_rgb_test.py
+++ b/test/remote_value_tests/remote_value_color_rgb_test.py
@@ -4,6 +4,7 @@ import pytest
 
 from xknx import XKNX
 from xknx.dpt import DPTArray, DPTBinary
+from xknx.dpt.dpt_color import RGBColor
 from xknx.exceptions import ConversionError
 from xknx.remote_value import RemoteValueColorRGB
 from xknx.telegram import GroupAddress, Telegram
@@ -17,42 +18,44 @@ class TestRemoteValueColorRGB:
         """Test to_knx function with normal operation."""
         xknx = XKNX()
         remote_value = RemoteValueColorRGB(xknx)
-        assert remote_value.to_knx((100, 101, 102)) == DPTArray((0x64, 0x65, 0x66))
-        assert remote_value.to_knx([100, 101, 102]) == DPTArray((0x64, 0x65, 0x66))
+        assert remote_value.to_knx(RGBColor(100, 101, 102)) == DPTArray(
+            (0x64, 0x65, 0x66)
+        )
+        assert remote_value.to_knx(RGBColor(100, 101, 102)) == DPTArray(
+            (0x64, 0x65, 0x66)
+        )
 
     def test_from_knx(self):
         """Test from_knx function with normal operation."""
         xknx = XKNX()
         remote_value = RemoteValueColorRGB(xknx)
-        assert remote_value.from_knx(DPTArray((0x64, 0x65, 0x66))) == (100, 101, 102)
+        assert remote_value.from_knx(DPTArray((0x64, 0x65, 0x66))) == RGBColor(
+            100, 101, 102
+        )
 
     def test_to_knx_error(self):
         """Test to_knx function with wrong parametern."""
         xknx = XKNX()
         remote_value = RemoteValueColorRGB(xknx)
         with pytest.raises(ConversionError):
-            remote_value.to_knx((100, 101, 102, 103))
-        with pytest.raises(ConversionError):
-            remote_value.to_knx((100, 101, 256))
-        with pytest.raises(ConversionError):
-            remote_value.to_knx((100, -101, 102))
+            remote_value.to_knx(RGBColor(100, 101, 256))
         with pytest.raises(ConversionError):
-            remote_value.to_knx(("100", 101, 102))
+            remote_value.to_knx(RGBColor(100, -1, 102))
         with pytest.raises(ConversionError):
-            remote_value.to_knx("100, 101, 102")
+            remote_value.to_knx(RGBColor("100", 101, 102))
 
     async def test_set(self):
         """Test setting value."""
         xknx = XKNX()
         remote_value = RemoteValueColorRGB(xknx, group_address=GroupAddress("1/2/3"))
-        await remote_value.set((100, 101, 102))
+        await remote_value.set(RGBColor(100, 101, 102))
         assert xknx.telegrams.qsize() == 1
         telegram = xknx.telegrams.get_nowait()
         assert telegram == Telegram(
             destination_address=GroupAddress("1/2/3"),
             payload=GroupValueWrite(DPTArray((0x64, 0x65, 0x66))),
         )
-        await remote_value.set((100, 101, 104))
+        await remote_value.set(RGBColor(100, 101, 104))
         assert xknx.telegrams.qsize() == 1
         telegram = xknx.telegrams.get_nowait()
         assert telegram == Telegram(
@@ -69,7 +72,7 @@ class TestRemoteValueColorRGB:
             payload=GroupValueWrite(DPTArray((0x64, 0x65, 0x66))),
         )
         await remote_value.process(telegram)
-        assert remote_value.value == (100, 101, 102)
+        assert remote_value.value == RGBColor(100, 101, 102)
 
     async def test_to_process_error(self):
         """Test process erroneous telegram."""