Repository: XKNX/xknx
Pull request #536 (2020-12-17 20:51:15)

change RemoteValue to ABC and typecheck it
<!--
  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.
-->
typecheck xknx/remote_value/remote_value.py

Annotating its children (eg. RemoteValueClimate etc.) requires to also annotate most of the `dpt` module. I'll save this for later.

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)
- [ ] This change requires a documentation update

## Checklist:

- [ ] I have performed a self-review of my own code
- [ ] The documentation has been adjusted accordingly
- [ ] The changes generate no new warnings
- [ ] Tests have been added that prove the fix is effective or that the feature works
- [ ] The changes are documented in the changelog
- [ ] The Homeassistant plugin has been adjusted in case of new config options


coveralls: 
[![Coverage Status](https://coveralls.io/builds/35804530/badge)](https://coveralls.io/builds/35804530)

Coverage increased (+0.004%) to 94.777% when pulling **47487e28ac3ef8f93b9830601408bedbdd968f9c on farmio:typecheck-remote_value** into **30720c78a10cd84a80dc31df1f74510b042df376 on XKNX:master**.


Patch:
diff --git a/changelog.md b/changelog.md
index 8d91c307..64e46e0b 100644
--- a/changelog.md
+++ b/changelog.md
@@ -5,6 +5,7 @@
 ### Devices
 
 - Sensor: add DPT-3 datatypes "stepwise_dimming", "stepwise_blinds", "startstop_dimming", "startstop_blinds"
+- Light: It is now possible to control lights using individual group addresses for red, green, blue and white
 
 ### HA integration
 
@@ -12,6 +13,10 @@
 
 ### Internals
 
+- Tunnel connections process DisconnectRequest now and closes/reconnects the tunnel when the other side closes gracefully
+- XKNX.connected Event can be used in future to await for a working connection or stop/relaunch tasks if the connection is lost
+- Lower heartbeat rate from every 15sec to every 70 sec and raise ConnectionstateRequest timeout from 1 to 10sec (3/8/1 KNXip Overview §5.8 Timeout Constants)
+- clean up Tunnel class
 - refactored timeout handling in GatewayScanner, RequestResponse and ValueReader.
 - renamed "PhysicalAddress" to "IndividualAddress"
 - Telegram: `group_address` renamed to `destination_address`, to prepare support for other APCI services and add `source_address`
diff --git a/docs/light.md b/docs/light.md
index 4ba00f2b..549d4495 100644
--- a/docs/light.md
+++ b/docs/light.md
@@ -27,6 +27,24 @@ The Light object is either a representation of a binary or dimm actor, LED-contr
 - `group_address_tunable_white_state` KNX group address for the current relative color temperature. *DPT 5.001*
 - `group_address_color_temperature` KNX group address to set absolute color temperature. *DPT 7.600*
 - `group_address_color_temperature_state` KNX group address for the current absolute color temperature. *DPT 7.600*
+
+- `group_address_switch_red` KNX group address to switch the red component. *DPT 1.001*
+- `group_address_switch_red_state` KNX group address for the state of the red component. *DPT 1.001*
+- `group_address_brightness_red` KNX group address to set the brightness of the red component. *DPT 5.001*
+- `group_address_brightness_red_state` KNX group address for the current brightness of the red component. *DPT 5.001*
+- `group_address_switch_green` KNX group address to switch the green component. *DPT 1.001*
+- `group_address_switch_green_state` KNX group address for the state of the green component. *DPT 1.001*
+- `group_address_brightness_green` KNX group address to set the brightness of the green component. *DPT 5.001*
+- `group_address_brightness_green_state` KNX group address for the current brightness of the green component. *DPT 5.001*
+- `group_address_switch_blue` KNX group address to switch the blue component. *DPT 1.001*
+- `group_address_switch_blue_state` KNX group address for the state of the blue component. *DPT 1.001*
+- `group_address_brightness_blue` KNX group address to set the brightness of the blue component. *DPT 5.001*
+- `group_address_brightness_blue_state` KNX group address for the current brightness of the blue component. *DPT 5.001*
+- `group_address_switch_white` KNX group address to switch the white component. *DPT 1.001*
+- `group_address_switch_white_state` KNX group address for the state of the white component. *DPT 1.001*
+- `group_address_brightness_white` KNX group address to set the brightness of the white component. *DPT 5.001*
+- `group_address_brightness_white_state` KNX group address for the current brightness of the white component. *DPT 5.001*
+
 - `min_kelvin` lowest possible color temperature in Kelvin. Default: 2700
 - `max_kelvin` hightest possible color temperature in Kelvin. Default: 6000
 - `device_updated_cb` awaitable callback for each update.
@@ -93,6 +111,60 @@ print(light.current_color_temperature)
 await light.sync()
 ```
 
+## [](#header-2)Example: RGBW light with individual group addresses for red, green, blue and white
+
+```python
+light = Light(xknx,
+              name='TestRGBWLight',
+              group_address_switch_red="1/1/1",
+              group_address_switch_red_state="1/1/2",
+              group_address_brightness_red="1/1/3",
+              group_address_brightness_red_state="1/1/4",
+              group_address_switch_green="1/1/5",
+              group_address_switch_green_state="1/1/6",
+              group_address_brightness_green="1/1/7",
+              group_address_brightness_green_state="1/1/8",
+              group_address_switch_blue="1/1/9",
+              group_address_switch_blue_state="1/1/10",
+              group_address_brightness_blue="1/1/11",
+              group_address_brightness_blue_state="1/1/12",
+              group_address_switch_white="1/1/13",
+              group_address_switch_white_state="1/1/14",
+              group_address_brightness_white="1/1/15",
+              group_address_brightness_white_state="1/1/16")
+
+# Switching light on
+await light.set_on()
+
+# Switching light off
+await light.set_off()
+
+# Set color
+await light.set_color((20, 70,200))
+
+# Set rgbw color
+await light.set_color((20,70,200), 30)
+
+
+# Accessing light via 'do'
+await light.do('on')
+await light.do('off')
+await light.do('brightness:80')
+
+# Accessing state
+print(light.state)
+print(light.supports_brightness)
+print(light.supports_color)
+print(light.supports_rgbw)
+print(light.current_color)
+print(light.supports_tunable_white)
+print(light.supports_color_temperature)
+
+# Requesting current state via KNX GroupValueRead for all _state addresses
+await light.sync()
+
+## [](#header-2)Configuration via **xknx.yaml**
+```
 ## [](#header-2)Configuration via **xknx.yaml**
 
 Lights are usually configured via [`xknx.yaml`](/configuration):
@@ -117,5 +189,28 @@ groups:
         # Light with color temperature in percent
         Living-Room.Light_TW:  {group_address_switch: '1/6/21', group_address_switch_state: '1/6/20', group_address_brightness: '1/6/22', group_address_brightness_state: '1/6/23', group_address_tunable_white: '1/6/24',  group_address_tunable_white_state: '1/6/25'}
 
+        # Light with RGBW color
+        Kitchen.Light_rgbw:
+            {
+                individual_colors:
+                    {
+                        white: {group_address_switch: "1/6/4", group_address_switch_state: "1/6/5", group_address_brightness: "1/6/6", group_address_brightness_state: "1/6/7"},
+                        red: {group_address_switch: "1/6/14", group_address_switch_state: "1/6/15", group_address_brightness: "1/6/16", group_address_brightness_state: "1/6/17"},
+                        green: {group_address_switch: "1/6/24", group_address_switch_state: "1/6/25", group_address_brightness: "1/6/26", group_address_brightness_state: "1/6/27"},
+                        blue: {group_address_switch: "1/6/34", group_address_switch_state: "1/6/35", group_address_brightness: "1/6/36", group_address_brightness_state: "1/6/37"}
+                    }
+            }
+
+        # Light with RGB color and no white
+        Kitchen.Light_rgb:
+            {
+                individual_colors:
+                    {
+                        red: {group_address_switch: "1/6/14", group_address_switch_state: "1/6/15", group_address_brightness: "1/6/16", group_address_brightness_state: "1/6/17"},
+                        green: {group_address_switch: "1/6/24", group_address_switch_state: "1/6/25", group_address_brightness: "1/6/26", group_address_brightness_state: "1/6/27"},
+                        blue: {group_address_switch: "1/6/34", group_address_switch_state: "1/6/35", group_address_brightness: "1/6/36", group_address_brightness_state: "1/6/37"}
+                    }
+                r
+            }
 ```
 
diff --git a/examples/example_tunnel.py b/examples/example_tunnel.py
index 979adde3..86a83c00 100644
--- a/examples/example_tunnel.py
+++ b/examples/example_tunnel.py
@@ -35,7 +35,6 @@ async def main():
         gateway_port=gateway.port,
     )
 
-    await tunnel.connect_udp()
     await tunnel.connect()
 
     await tunnel.send_telegram(
@@ -53,7 +52,6 @@ async def main():
     )
     await asyncio.sleep(2)
 
-    await tunnel.connectionstate()
     await tunnel.disconnect()
 
 
diff --git a/home-assistant-plugin/custom_components/xknx/factory.py b/home-assistant-plugin/custom_components/xknx/factory.py
index 9fe70bf3..cee6b67a 100644
--- a/home-assistant-plugin/custom_components/xknx/factory.py
+++ b/home-assistant-plugin/custom_components/xknx/factory.py
@@ -1,4 +1,6 @@
 """Factory function to initialize KNX devices from config."""
+from typing import Optional, Tuple
+
 from xknx import XKNX
 from xknx.devices import (
     BinarySensor as XknxBinarySensor,
@@ -86,8 +88,30 @@ def _create_cover(knx_module: XKNX, config: ConfigType) -> XknxCover:
     )
 
 
+def _create_light_color(
+    color: str, config: ConfigType
+) -> Tuple[Optional[str], Optional[str], Optional[str], Optional[str]]:
+    """Load color configuration from configuration structure."""
+    if "individual_colors" in config and color in config["individual_colors"]:
+        sub_config = config["individual_colors"][color]
+        group_address_switch = sub_config.get(CONF_ADDRESS)
+        group_address_switch_state = sub_config.get(LightSchema.CONF_STATE_ADDRESS)
+        group_address_brightness = sub_config.get(LightSchema.CONF_BRIGHTNESS_ADDRESS)
+        group_address_brightness_state = sub_config.get(
+            LightSchema.CONF_BRIGHTNESS_STATE_ADDRESS
+        )
+        return (
+            group_address_switch,
+            group_address_switch_state,
+            group_address_brightness,
+            group_address_brightness_state,
+        )
+    return None, None, None, None
+
+
 def _create_light(knx_module: XKNX, config: ConfigType) -> XknxLight:
     """Return a KNX Light device to be used within XKNX."""
+
     group_address_tunable_white = None
     group_address_tunable_white_state = None
     group_address_color_temp = None
@@ -103,10 +127,35 @@ def _create_light(knx_module: XKNX, config: ConfigType) -> XknxLight:
             LightSchema.CONF_COLOR_TEMP_STATE_ADDRESS
         )
 
+    (
+        red_switch,
+        red_switch_state,
+        red_brightness,
+        red_brightness_state,
+    ) = _create_light_color(LightSchema.CONF_RED, config)
+    (
+        green_switch,
+        green_switch_state,
+        green_brightness,
+        green_brightness_state,
+    ) = _create_light_color(LightSchema.CONF_GREEN, config)
+    (
+        blue_switch,
+        blue_switch_state,
+        blue_brightness,
+        blue_brightness_state,
+    ) = _create_light_color(LightSchema.CONF_BLUE, config)
+    (
+        white_switch,
+        white_switch_state,
+        white_brightness,
+        white_brightness_state,
+    ) = _create_light_color(LightSchema.CONF_WHITE, config)
+
     return XknxLight(
         knx_module,
         name=config[CONF_NAME],
-        group_address_switch=config[CONF_ADDRESS],
+        group_address_switch=config.get(CONF_ADDRESS),
         group_address_switch_state=config.get(LightSchema.CONF_STATE_ADDRESS),
         group_address_brightness=config.get(LightSchema.CONF_BRIGHTNESS_ADDRESS),
         group_address_brightness_state=config.get(
@@ -120,6 +169,22 @@ def _create_light(knx_module: XKNX, config: ConfigType) -> XknxLight:
         group_address_tunable_white_state=group_address_tunable_white_state,
         group_address_color_temperature=group_address_color_temp,
         group_address_color_temperature_state=group_address_color_temp_state,
+        group_address_switch_red=red_switch,
+        group_address_switch_red_state=red_switch_state,
+        group_address_brightness_red=red_brightness,
+        group_address_brightness_red_state=red_brightness_state,
+        group_address_switch_green=green_switch,
+        group_address_switch_green_state=green_switch_state,
+        group_address_brightness_green=green_brightness,
+        group_address_brightness_green_state=green_brightness_state,
+        group_address_switch_blue=blue_switch,
+        group_address_switch_blue_state=blue_switch_state,
+        group_address_brightness_blue=blue_brightness,
+        group_address_brightness_blue_state=blue_brightness_state,
+        group_address_switch_white=white_switch,
+        group_address_switch_white_state=white_switch_state,
+        group_address_brightness_white=white_brightness,
+        group_address_brightness_white_state=white_brightness_state,
         min_kelvin=config[LightSchema.CONF_MIN_KELVIN],
         max_kelvin=config[LightSchema.CONF_MAX_KELVIN],
     )
diff --git a/home-assistant-plugin/custom_components/xknx/schema.py b/home-assistant-plugin/custom_components/xknx/schema.py
index 54fd3f1c..230f6aca 100644
--- a/home-assistant-plugin/custom_components/xknx/schema.py
+++ b/home-assistant-plugin/custom_components/xknx/schema.py
@@ -139,29 +139,70 @@ class LightSchema:
     DEFAULT_MIN_KELVIN = 2700  # 370 mireds
     DEFAULT_MAX_KELVIN = 6000  # 166 mireds
 
-    SCHEMA = vol.Schema(
+    CONF_INDIVIDUAL_COLORS = "individual_colors"
+    CONF_RED = "red"
+    CONF_GREEN = "green"
+    CONF_BLUE = "blue"
+    CONF_WHITE = "white"
+
+    COLOR_SCHEMA = vol.Schema(
         {
-            vol.Required(CONF_ADDRESS): cv.string,
-            vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+            vol.Optional(CONF_ADDRESS): cv.string,
             vol.Optional(CONF_STATE_ADDRESS): cv.string,
-            vol.Optional(CONF_BRIGHTNESS_ADDRESS): cv.string,
+            vol.Required(CONF_BRIGHTNESS_ADDRESS): cv.string,
             vol.Optional(CONF_BRIGHTNESS_STATE_ADDRESS): cv.string,
-            vol.Optional(CONF_COLOR_ADDRESS): cv.string,
-            vol.Optional(CONF_COLOR_STATE_ADDRESS): cv.string,
-            vol.Optional(CONF_COLOR_TEMP_ADDRESS): cv.string,
-            vol.Optional(CONF_COLOR_TEMP_STATE_ADDRESS): cv.string,
-            vol.Optional(
-                CONF_COLOR_TEMP_MODE, default=DEFAULT_COLOR_TEMP_MODE
-            ): cv.enum(ColorTempModes),
-            vol.Optional(CONF_RGBW_ADDRESS): cv.string,
-            vol.Optional(CONF_RGBW_STATE_ADDRESS): cv.string,
-            vol.Optional(CONF_MIN_KELVIN, default=DEFAULT_MIN_KELVIN): vol.All(
-                vol.Coerce(int), vol.Range(min=1)
+        }
+    )
+
+    SCHEMA = vol.All(
+        vol.Schema(
+            {
+                vol.Optional(CONF_ADDRESS): cv.string,
+                vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+                vol.Optional(CONF_STATE_ADDRESS): cv.string,
+                vol.Optional(CONF_BRIGHTNESS_ADDRESS): cv.string,
+                vol.Optional(CONF_BRIGHTNESS_STATE_ADDRESS): cv.string,
+                vol.Exclusive(CONF_INDIVIDUAL_COLORS, "color"): {
+                    vol.Inclusive(CONF_RED, "colors"): COLOR_SCHEMA,
+                    vol.Inclusive(CONF_GREEN, "colors"): COLOR_SCHEMA,
+                    vol.Inclusive(CONF_BLUE, "colors"): COLOR_SCHEMA,
+                    vol.Optional(CONF_WHITE): COLOR_SCHEMA,
+                },
+                vol.Exclusive(CONF_COLOR_ADDRESS, "color"): cv.string,
+                vol.Optional(CONF_COLOR_STATE_ADDRESS): cv.string,
+                vol.Optional(CONF_COLOR_TEMP_ADDRESS): cv.string,
+                vol.Optional(CONF_COLOR_TEMP_STATE_ADDRESS): cv.string,
+                vol.Optional(
+                    CONF_COLOR_TEMP_MODE, default=DEFAULT_COLOR_TEMP_MODE
+                ): cv.enum(ColorTempModes),
+                vol.Exclusive(CONF_RGBW_ADDRESS, "color"): cv.string,
+                vol.Optional(CONF_RGBW_STATE_ADDRESS): cv.string,
+                vol.Optional(CONF_MIN_KELVIN, default=DEFAULT_MIN_KELVIN): vol.All(
+                    vol.Coerce(int), vol.Range(min=1)
+                ),
+                vol.Optional(CONF_MAX_KELVIN, default=DEFAULT_MAX_KELVIN): vol.All(
+                    vol.Coerce(int), vol.Range(min=1)
+                ),
+            }
+        ),
+        vol.Any(
+            vol.Schema(
+                {
+                    vol.Required(CONF_INDIVIDUAL_COLORS): {
+                        vol.Required(CONF_RED): {vol.Required(CONF_ADDRESS): object},
+                        vol.Required(CONF_GREEN): {vol.Required(CONF_ADDRESS): object},
+                        vol.Required(CONF_BLUE): {vol.Required(CONF_ADDRESS): object},
+                    },
+                },
+                extra=vol.ALLOW_EXTRA,
             ),
-            vol.Optional(CONF_MAX_KELVIN, default=DEFAULT_MAX_KELVIN): vol.All(
-                vol.Coerce(int), vol.Range(min=1)
+            vol.Schema(
+                {
+                    vol.Required(CONF_ADDRESS): object,
+                },
+                extra=vol.ALLOW_EXTRA,
             ),
-        }
+        ),
     )
 
 
diff --git a/setup.cfg b/setup.cfg
index 12ac9c72..6e12973a 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -42,7 +42,7 @@ warn_redundant_casts = true
 warn_unused_configs = true
 
 # add the modules below once we add typing for them so that we fail the build in the future if someone changes something without updating the typings
-[mypy-xknx.core.*,xknx.devices.travelcalculator,xknx.exceptions.*,xknx.io.gateway_scanner,xknx.telegram.*,]
+[mypy-xknx.core.*,xknx.devices.travelcalculator,xknx.exceptions.*,xknx.io.gateway_scanner,xknx.remote_value.remote_value,xknx.telegram.*,]
 strict = true
 ignore_errors = false
 warn_unreachable = true
diff --git a/xknx.yaml b/xknx.yaml
index 18dd1142..a960c046 100644
--- a/xknx.yaml
+++ b/xknx.yaml
@@ -157,6 +157,41 @@ groups:
                 group_address_color: "1/6/7",
                 group_address_color_state: "1/6/8",
             }
+        # Light with color
+        Diningroom.Light_rgb:
+            {
+                individual_colors:
+                    {
+                        white:
+                            {
+                                group_address_switch: "1/6/4",
+                                group_address_switch_state: "1/6/5",
+                                group_address_brightness: "1/6/6",
+                                group_address_brightness_state: "1/6/7",
+                            },
+                        red:
+                            {
+                                group_address_switch: "1/6/14",
+                                group_address_switch_state: "1/6/15",
+                                group_address_brightness: "1/6/16",
+                                group_address_brightness_state: "1/6/17",
+                            },
+                        green:
+                            {
+                                group_address_switch: "1/6/24",
+                                group_address_switch_state: "1/6/25",
+                                group_address_brightness: "1/6/26",
+                                group_address_brightness_state: "1/6/27",
+                            },
+                        blue:
+                            {
+                                group_address_switch: "1/6/34",
+                                group_address_switch_state: "1/6/35",
+                                group_address_brightness: "1/6/36",
+                                group_address_brightness_state: "1/6/37",
+                            }
+                    }
+            }
         # Light with color temperature
         Living-Room.Light_CT:
             {
diff --git a/xknx/config/config_v1.py b/xknx/config/config_v1.py
index be3f36bb..02b9dfd2 100644
--- a/xknx/config/config_v1.py
+++ b/xknx/config/config_v1.py
@@ -23,6 +23,7 @@ from xknx.devices import (
 )
 from xknx.exceptions import XKNXException
 from xknx.io import ConnectionConfig, ConnectionType
+from xknx.telegram import IndividualAddress
 
 logger = logging.getLogger("xknx.log")
 
@@ -51,7 +52,7 @@ class ConfigV1:
         """Parse the general section of xknx.yaml."""
         if "general" in doc:
             if "own_address" in doc["general"]:
-                self.xknx.own_address = doc["general"]["own_address"]
+                self.xknx.own_address = IndividualAddress(doc["general"]["own_address"])
             if "rate_limit" in doc["general"]:
                 self.xknx.rate_limit = doc["general"]["rate_limit"]
             if "multicast_group" in doc["general"]:
diff --git a/xknx/config/schema.py b/xknx/config/schema.py
index 5cc6ef65..c49fd032 100644
--- a/xknx/config/schema.py
+++ b/xknx/config/schema.py
@@ -147,46 +147,99 @@ class LightSchema:
     CONF_RGBW = "rgbw"
     CONF_COLOR_TEMPERATURE = "color_temperature"
     CONF_COLOR = "color"
+    CONF_INDIVIDUAL_COLORS = "individual_colors"
 
     CONF_COLOR_TEMP_MODE = "mode"
     CONF_MIN_KELVIN = "min_kelvin"
     CONF_MAX_KELVIN = "max_kelvin"
 
+    CONF_RED = "red"
+    CONF_GREEN = "green"
+    CONF_BLUE = "blue"
+    CONF_WHITE = "white"
+
     DEFAULT_COLOR_TEMP_MODE = "absolute"
     DEFAULT_MIN_KELVIN = 2700  # 370 mireds
     DEFAULT_MAX_KELVIN = 6000  # 166 mireds
 
-    SCHEMA = BaseDeviceSchema.SCHEMA.extend(
+    COLOR_SCHEMA = vol.Schema(
         {
-            vol.Required(CONF_SWITCH): RemoteValueSchema.SCHEMA.extend(
-                {
-                    vol.Required(CONF_ADDRESS): ensure_group_address,
-                }
-            ),
-            vol.Optional(CONF_BRIGHTNESS): RemoteValueSchema.SCHEMA.extend(
-                {vol.Required(CONF_ADDRESS): ensure_group_address}
-            ),
-            vol.Optional(CONF_RGBW): RemoteValueSchema.SCHEMA.extend(
-                {vol.Required(CONF_ADDRESS): ensure_group_address}
-            ),
-            vol.Optional(CONF_COLOR): RemoteValueSchema.SCHEMA.extend(
+            vol.Optional(CONF_SWITCH): RemoteValueSchema.SCHEMA,
+            vol.Required(CONF_BRIGHTNESS): RemoteValueSchema.SCHEMA.extend(
                 {vol.Required(CONF_ADDRESS): ensure_group_address}
             ),
-            vol.Optional(CONF_COLOR_TEMPERATURE): RemoteValueSchema.SCHEMA.extend(
+        }
+    )
+
+    SCHEMA = vol.All(
+        BaseDeviceSchema.SCHEMA.extend(
+            {
+                vol.Optional(CONF_SWITCH): RemoteValueSchema.SCHEMA.extend(
+                    {vol.Required(CONF_ADDRESS): ensure_group_address}
+                ),
+                vol.Optional(CONF_BRIGHTNESS): RemoteValueSchema.SCHEMA.extend(
+                    {vol.Required(CONF_ADDRESS): ensure_group_address}
+                ),
+                vol.Exclusive(CONF_RGBW, "color"): RemoteValueSchema.SCHEMA.extend(
+                    {vol.Required(CONF_ADDRESS): ensure_group_address}
+                ),
+                vol.Exclusive(CONF_COLOR, "color"): RemoteValueSchema.SCHEMA.extend(
+                    {vol.Required(CONF_ADDRESS): ensure_group_address}
+                ),
+                vol.Exclusive(CONF_INDIVIDUAL_COLORS, "color"): {
+                    vol.Inclusive(CONF_RED, "colors"): COLOR_SCHEMA,
+                    vol.Inclusive(CONF_GREEN, "colors"): COLOR_SCHEMA,
+                    vol.Inclusive(CONF_BLUE, "colors"): COLOR_SCHEMA,
+                    vol.Optional(CONF_WHITE): COLOR_SCHEMA,
+                },
+                vol.Optional(CONF_COLOR_TEMPERATURE): RemoteValueSchema.SCHEMA.extend(
+                    {
+                        vol.Required(CONF_ADDRESS): ensure_group_address,
+                        vol.Optional(
+                            CONF_COLOR_TEMP_MODE, default=DEFAULT_COLOR_TEMP_MODE
+                        ): enum(ColorTempModes),
+                        vol.Optional(
+                            CONF_MIN_KELVIN, default=DEFAULT_MIN_KELVIN
+                        ): vol.All(vol.Coerce(int), vol.Range(min=1)),
+                        vol.Optional(
+                            CONF_MAX_KELVIN, default=DEFAULT_MAX_KELVIN
+                        ): vol.All(vol.Coerce(int), vol.Range(min=1)),
+                    }
+                ),
+            },
+        ),
+        vol.Any(
+            vol.Schema(
                 {
-                    vol.Required(CONF_ADDRESS): ensure_group_address,
-                    vol.Optional(
-                        CONF_COLOR_TEMP_MODE, default=DEFAULT_COLOR_TEMP_MODE
-                    ): enum(ColorTempModes),
-                    vol.Optional(CONF_MIN_KELVIN, default=DEFAULT_MIN_KELVIN): vol.All(
-                        vol.Coerce(int), vol.Range(min=1)
-                    ),
-                    vol.Optional(CONF_MAX_KELVIN, default=DEFAULT_MAX_KELVIN): vol.All(
-                        vol.Coerce(int), vol.Range(min=1)
-                    ),
-                }
+                    vol.Required(CONF_SWITCH): RemoteValueSchema.SCHEMA.extend(
+                        {vol.Required(CONF_ADDRESS): object}
+                    )
+                },
+                extra=vol.ALLOW_EXTRA,
             ),
-        }
+            vol.Schema(
+                {
+                    vol.Required(CONF_INDIVIDUAL_COLORS): {
+                        vol.Required(CONF_RED): {
+                            vol.Required(CONF_SWITCH): {
+                                vol.Required(CONF_ADDRESS): object
+                            }
+                        },
+                        vol.Required(CONF_GREEN): {
+                            vol.Required(CONF_SWITCH): {
+                                vol.Required(CONF_ADDRESS): object
+                            }
+                        },
+                        vol.Required(CONF_BLUE): {
+                            vol.Required(CONF_SWITCH): {
+                                vol.Required(CONF_ADDRESS): object
+                            }
+                        },
+                    },
+                },
+                extra=vol.ALLOW_EXTRA,
+            ),
+        ),
     )
 
 
diff --git a/xknx/devices/light.py b/xknx/devices/light.py
index 34480547..91be195a 100644
--- a/xknx/devices/light.py
+++ b/xknx/devices/light.py
@@ -12,7 +12,7 @@ It provides functionality for
 """
 from enum import Enum
 import logging
-from typing import Optional
+from typing import Optional, Tuple
 
 from xknx.remote_value import (
     RemoteValueColorRGB,
@@ -34,6 +34,57 @@ class ColorTempModes(Enum):
     relative = "DPT-5.001"
 
 
+class _SwitchAndBrightness:
+    def __init__(
+        self,
+        xknx,
+        name,
+        feature_name: str,
+        group_address_switch=None,
+        group_address_switch_state=None,
+        group_address_brightness=None,
+        group_address_brightness_state=None,
+        after_update_cb=None,
+    ):
+        self.switch = RemoteValueSwitch(
+            xknx,
+            group_address_switch,
+            group_address_switch_state,
+            device_name=name,
+            feature_name=feature_name + "_state",
+            after_update_cb=after_update_cb,
+        )
+        self.brightness = RemoteValueScaling(
+            xknx,
+            group_address_brightness,
+            group_address_brightness_state,
+            device_name=name,
+            feature_name=feature_name + "_brightness",
+            after_update_cb=after_update_cb,
+            range_from=0,
+            range_to=255,
+        )
+
+    @property
+    def is_on(self):
+        """Return if light is on."""
+        return self.switch.value
+
+    async def set_on(self):
+        """Switch light on."""
+        if self.switch.initialized:
+            await self.switch.on()
+
+    async def set_off(self):
+        """Switch light off."""
+        if self.switch.initialized:
+            await self.switch.off()
+
+    def __eq__(self, other):
+        """Compare for quality."""
+        return self.__dict__ == other.__dict__
+
+
 # pylint: disable=too-many-public-methods, too-many-instance-attributes
 class Light(Device):
     """Class for managing a light."""
@@ -58,6 +109,22 @@ class Light(Device):
         group_address_tunable_white_state=None,
         group_address_color_temperature=None,
         group_address_color_temperature_state=None,
+        group_address_switch_red=None,
+        group_address_switch_red_state=None,
+        group_address_brightness_red=None,
+        group_address_brightness_red_state=None,
+        group_address_switch_green=None,
+        group_address_switch_green_state=None,
+        group_address_brightness_green=None,
+        group_address_brightness_green_state=None,
+        group_address_switch_blue=None,
+        group_address_switch_blue_state=None,
+        group_address_brightness_blue=None,
+        group_address_brightness_blue_state=None,
+        group_address_switch_white=None,
+        group_address_switch_white_state=None,
+        group_address_brightness_white=None,
+        group_address_brightness_white_state=None,
         min_kelvin=None,
         max_kelvin=None,
         device_updated_cb=None,
@@ -122,6 +189,50 @@ class Light(Device):
             after_update_cb=self.after_update,
         )
 
+        self.red = _SwitchAndBrightness(
+            xknx,
+            self.name,
+            "red",
+            group_address_switch_red,
+            group_address_switch_red_state,
+            group_address_brightness_red,
+            group_address_brightness_red_state,
+            self.after_update,
+        )
+
+        self.green = _SwitchAndBrightness(
+            xknx,
+            self.name,
+            "green",
+            group_address_switch_green,
+            group_address_switch_green_state,
+            group_address_brightness_green,
+            group_address_brightness_green_state,
+            self.after_update,
+        )
+
+        self.blue = _SwitchAndBrightness(
+            xknx,
+            self.name,
+            "blue",
+            group_address_switch_blue,
+            group_address_switch_blue_state,
+            group_address_brightness_blue,
+            group_address_brightness_blue_state,
+            self.after_update,
+        )
+
+        self.white = _SwitchAndBrightness(
+            xknx,
+            self.name,
+            "white",
+            group_address_switch_white,
+            group_address_switch_white_state,
+            group_address_brightness_white,
+            group_address_brightness_white_state,
+            self.after_update,
+        )
+
         self.min_kelvin = min_kelvin
         self.max_kelvin = max_kelvin
 
@@ -135,6 +246,9 @@ class Light(Device):
             self.tunable_white,
             self.color_temperature,
         )
+        for color in (self.red, self.green, self.blue, self.white):
+            yield color.switch
+            yield color.brightness
 
     @property
     def supports_brightness(self):
@@ -144,12 +258,19 @@ class Light(Device):
     @property
     def supports_color(self):
         """Return if light supports color."""
-        return self.color.initialized
+        return self.color.initialized or all(
+            [c.brightness.initialized for c in (self.red, self.green, self.blue)]
+        )
 
     @property
     def supports_rgbw(self):
         """Return if light supports RGBW."""
-        return self.rgbw.initialized
+        return self.rgbw.initialized or all(
+            [
+                c.brightness.initialized
+                for c in (self.red, self.green, self.blue, self.white)
+            ]
+        )
 
     @property
     def supports_tunable_white(self):
@@ -161,6 +282,21 @@ class Light(Device):
         """Return if light supports absolute color temperature."""
         return self.color_temperature.initialized
 
+    @classmethod
+    def read_color_from_config(
+        cls, color, config
+    ) -> Tuple[Optional[str], Optional[str], Optional[str], Optional[str]]:
+        """Load color configuration from configuration structure."""
+        if "individual_colors" in config and color in config["individual_colors"]:
+            sub_config = config["individual_colors"][color]
+            return (
+                sub_config.get("group_address_switch"),
+                sub_config.get("group_address_switch_state"),
+                sub_config.get("group_address_brightness"),
+                sub_config.get("group_address_brightness_state"),
+            )
+        return None, None, None, None
+
     @classmethod
     def from_config(cls, xknx, name, config):
         """Initialize object from configuration structure."""
@@ -183,6 +319,31 @@ class Light(Device):
         min_kelvin = config.get("min_kelvin", Light.DEFAULT_MIN_KELVIN)
         max_kelvin = config.get("max_kelvin", Light.DEFAULT_MAX_KELVIN)
 
+        (
+            red_switch,
+            red_switch_state,
+            red_brightness,
+            red_brightness_state,
+        ) = cls.read_color_from_config("red", config)
+        (
+            green_switch,
+            green_switch_state,
+            green_brightness,
+            green_brightness_state,
+        ) = cls.read_color_from_config("green", config)
+        (
+            blue_switch,
+            blue_switch_state,
+            blue_brightness,
+            blue_brightness_state,
+        ) = cls.read_color_from_config("blue", config)
+        (
+            white_switch,
+            white_switch_state,
+            white_brightness,
+            white_brightness_state,
+        ) = cls.read_color_from_config("white", config)
+
         return cls(
             xknx,
             name,
@@ -198,6 +359,22 @@ class Light(Device):
             group_address_tunable_white_state=group_address_tunable_white_state,
             group_address_color_temperature=group_address_color_temperature,
             group_address_color_temperature_state=group_address_color_temperature_state,
+            group_address_switch_red=red_switch,
+            group_address_switch_red_state=red_switch_state,
+            group_address_brightness_red=red_brightness,
+            group_address_brightness_red_state=red_brightness_state,
+            group_address_switch_green=green_switch,
+            group_address_switch_green_state=green_switch_state,
+            group_address_brightness_green=green_brightness,
+            group_address_brightness_green_state=green_brightness_state,
+            group_address_switch_blue=blue_switch,
+            group_address_switch_blue_state=blue_switch_state,
+            group_address_brightness_blue=blue_brightness,
+            group_address_brightness_blue_state=blue_brightness_state,
+            group_address_switch_white=white_switch,
+            group_address_switch_white_state=white_switch_state,
+            group_address_brightness_white=white_brightness,
+            group_address_brightness_white_state=white_brightness_state,
             min_kelvin=min_kelvin,
             max_kelvin=max_kelvin,
         )
@@ -232,7 +409,51 @@ class Light(Device):
             )
         )
 
-        return '<Light name="{}" ' 'switch="{}"{}{}{}{}{} />'.format(
+        str_red_state = (
+            ""
+            if not self.red.switch.initialized
+            else f' red_state="{self.red.switch.group_addr_str()}"'
+        )
+        str_red_brightness = (
+            ""
+            if not self.red.brightness.initialized
+            else f' red_brightness="{self.red.brightness.group_addr_str()}"'
+        )
+
+        str_green_state = (
+            ""
+            if not self.green.switch.initialized
+            else f' green_state="{self.green.switch.group_addr_str()}"'
+        )
+        str_green_brightness = (
+            ""
+            if not self.green.brightness.initialized
+            else f' green_brightness="{self.green.brightness.group_addr_str()}"'
+        )
+
+        str_blue_state = (
+            ""
+            if not self.blue.switch.initialized
+            else f' blue_state="{self.blue.switch.group_addr_str()}"'
+        )
+        str_blue_brightness = (
+            ""
+            if not self.blue.brightness.initialized
+            else f' blue_brightness="{self.blue.brightness.group_addr_str()}"'
+        )
+
+        str_white_state = (
+            ""
+            if not self.white.switch.initialized
+            else f' white_state="{self.white.switch.group_addr_str()}"'
+        )
+        str_white_brightness = (
+            ""
+            if not self.white.brightness.initialized
+            else f' white_brightness="{self.white.brightness.group_addr_str()}"'
+        )
+
+        return '<Light name="{}" ' 'switch="{}"{}{}{}{}{}{}{}{}{}{}{}{}{} />'.format(
             self.name,
             self.switch.group_addr_str(),
             str_brightness,
@@ -240,20 +461,45 @@ class Light(Device):
             str_rgbw,
             str_tunable_white,
             str_color_temperature,
+            str_red_state,
+            str_red_brightness,
+            str_green_state,
+            str_green_brightness,
+            str_blue_state,
+            str_blue_brightness,
+            str_white_state,
+            str_white_brightness,
         )
 
     @property
     def state(self) -> Optional[bool]:
         """Return the current switch state of the device."""
-        return self.switch.value
+        if self.switch.value is not None:
+            return self.switch.value
+        if any(
+            [
+                c.switch.value is not None
+                for c in (self.red, self.green, self.blue, self.white)
+            ]
+        ):
+            return any(
+                [c.switch.value for c in (self.red, self.green, self.blue, self.white)]
+            )
+        return None
 
     async def set_on(self):
         """Switch light on."""
-        await self.switch.on()
+        if self.switch.initialized:
+            await self.switch.on()
+        for color in (self.red, self.green, self.blue, self.white):
+            await color.set_on()
 
     async def set_off(self):
         """Switch light off."""
-        await self.switch.off()
+        if self.switch.initialized:
+            await self.switch.off()
+        for color in (self.red, self.green, self.blue, self.white):
+            await color.set_off()
 
     @property
     def current_brightness(self):
@@ -275,10 +521,21 @@ class Light(Device):
         If the device supports RGBW, get the current RGB+White values instead.
         """
         if self.supports_rgbw:
-            if not self.rgbw.value:
-                return None, None
-            return self.rgbw.value[:3], self.rgbw.value[3]
-        return self.color.value, None
+            if self.rgbw.initialized:
+                if not self.rgbw.value:
+                    return None, None
+                return self.rgbw.value[:3], self.rgbw.value[3]
+        if self.color.initialized:
+            return self.color.value, None
+        # individual RGB addresses - white will return None when it is not initialized
+        colors = [
+            self.red.brightness.value,
+            self.green.brightness.value,
+            self.blue.brightness.value,
+        ]
+        if None in colors:
+            return None, self.white.brightness.value
+        return colors, self.white.brightness.value
 
     async def set_color(self, color, white=None):
         """
@@ -289,13 +546,36 @@ class Light(Device):
         """
         if white is not None:
             if self.supports_rgbw:
-                await self.rgbw.set(list(color) + [white])
-                return
+                if self.rgbw.initialized:
+                    await self.rgbw.set(list(color) + [white])
+                    return
+                if all(
+                    [
+                        c.brightness.initialized
+                        for c in (self.red, self.green, self.blue, self.white)
+                    ]
+                ):
+                    await self.red.brightness.set(color[0])
+                    await self.green.brightness.set(color[1])
+                    await self.blue.brightness.set(color[2])
+                    await self.white.brightness.set(white)
+                    return
             logger.warning("RGBW not supported for device %s", self.get_name())
         else:
             if self.supports_color:
-                await self.color.set(color)
-                return
+                if self.color.initialized:
+                    await self.color.set(color)
+                    return
+                if all(
+                    [
+                        c.brightness.initialized
+                        for c in (self.red, self.green, self.blue)
+                    ]
+                ):
+                    await self.red.brightness.set(color[0])
+                    await self.green.brightness.set(color[1])
+                    await self.blue.brightness.set(color[2])
+                    return
             logger.warning("Colors not supported for device %s", self.get_name())
 
     @property
diff --git a/xknx/exceptions/exception.py b/xknx/exceptions/exception.py
index f8cf90dd..649c4b19 100644
--- a/xknx/exceptions/exception.py
+++ b/xknx/exceptions/exception.py
@@ -1,5 +1,5 @@
 """Module for XKXN Exceptions."""
-from typing import Any, Dict, Optional, Tuple, Union
+from typing import Any, Optional, Tuple, Union
 
 
 class XKNXException(Exception):
@@ -31,7 +31,7 @@ class CommunicationError(XKNXException):
 class CouldNotParseTelegram(XKNXException):
     """Could not parse telegram error."""
 
-    def __init__(self, description: str, **kwargs: Dict[str, str]) -> None:
+    def __init__(self, description: str, **kwargs: str) -> None:
         """Initialize CouldNotParseTelegram class."""
         super().__init__()
         self.description = description
@@ -78,7 +78,7 @@ class UnsupportedCEMIMessage(XKNXException):
 class ConversionError(XKNXException):
     """Exception class for error while converting one type to another."""
 
-    def __init__(self, description: str, **kwargs: Dict[str, str]) -> None:
+    def __init__(self, description: str, **kwargs: str) -> None:
         """Initialize ConversionError class."""
         super().__init__()
         self.description = description
diff --git a/xknx/io/connectionstate.py b/xknx/io/connectionstate.py
index 403b273a..906a2598 100644
--- a/xknx/io/connectionstate.py
+++ b/xknx/io/connectionstate.py
@@ -1,6 +1,7 @@
 """Abstraction to send ConnectonStateRequest and wait for ConnectionStateResponse."""
 from xknx.knxip import HPAI, ConnectionStateRequest, ConnectionStateResponse, KNXIPFrame
 
+from .const import CONNECTIONSTATE_REQUEST_TIMEOUT
 from .request_response import RequestResponse
 
 
@@ -10,7 +11,12 @@ class ConnectionState(RequestResponse):
     def __init__(self, xknx, udp_client, communication_channel_id):
         """Initialize ConnectionState class."""
         self.udp_client = udp_client
-        super().__init__(xknx, self.udp_client, ConnectionStateResponse)
+        super().__init__(
+            xknx,
+            self.udp_client,
+            ConnectionStateResponse,
+            timeout_in_seconds=CONNECTIONSTATE_REQUEST_TIMEOUT,
+        )
         self.communication_channel_id = communication_channel_id
 
     def create_knxipframe(self) -> KNXIPFrame:
diff --git a/xknx/io/const.py b/xknx/io/const.py
index 7951cad3..fed3c37e 100644
--- a/xknx/io/const.py
+++ b/xknx/io/const.py
@@ -2,3 +2,7 @@
 
 DEFAULT_MCAST_GRP = "224.0.23.12"
 DEFAULT_MCAST_PORT = 3671
+
+CONNECTION_ALIVE_TIME = 120
+CONNECTIONSTATE_REQUEST_TIMEOUT = 10
+HEARTBEAT_RATE = CONNECTION_ALIVE_TIME - (CONNECTIONSTATE_REQUEST_TIMEOUT * 5)
diff --git a/xknx/io/interface.py b/xknx/io/interface.py
new file mode 100644
index 00000000..20227adf
--- /dev/null
+++ b/xknx/io/interface.py
@@ -0,0 +1,26 @@
+"""
+Abstract base for a specific KNX/IP connection (Tunneling or Routing).
+
+* It handles connection and disconnections
+* It starts and stops a udp client
+* It packs Telegrams into KNX Frames and passes them to a udp client
+"""
+from abc import ABC, abstractmethod
+
+from xknx.telegram import Telegram
+
+
+class Interface(ABC):
+    """Abstract base class for KNX/IP connections."""
+
+    @abstractmethod
+    async def connect(self) -> bool:
+        """Connect to KNX bus. Returns True on success."""
+
+    @abstractmethod
+    async def disconnect(self) -> None:
+        """Disconnect from KNX bus."""
+
+    @abstractmethod
+    async def send_telegram(self, telegram: Telegram) -> None:
+        """Send Telegram to KNX bus."""
diff --git a/xknx/io/knxip_interface.py b/xknx/io/knxip_interface.py
index 27fd0ad1..c778d75c 100644
--- a/xknx/io/knxip_interface.py
+++ b/xknx/io/knxip_interface.py
@@ -150,19 +150,19 @@ class KNXIPInterface:
             auto_reconnect=auto_reconnect,
             auto_reconnect_wait=auto_reconnect_wait,
         )
-        await self.interface.start()
+        await self.interface.connect()
 
     async def start_routing(self, local_ip: str) -> None:
         """Start KNX/IP Routing."""
         validate_ip(local_ip, address_name="Local IP address")
         logger.debug("Starting Routing from %s as %s", local_ip, self.xknx.own_address)
         self.interface = Routing(self.xknx, self.telegram_received, local_ip)
-        await self.interface.start()
+        await self.interface.connect()
 
     async def stop(self) -> None:
         """Stop connected interfae (either Tunneling or Routing)."""
         if self.interface is not None:
-            await self.interface.stop()
+            await self.interface.disconnect()
             self.interface = None
 
     def telegram_received(self, telegram):
diff --git a/xknx/io/routing.py b/xknx/io/routing.py
index 3fbd4aa0..6c8ccd3a 100644
--- a/xknx/io/routing.py
+++ b/xknx/io/routing.py
@@ -14,12 +14,13 @@ from xknx.knxip import (
 )
 from xknx.telegram import TelegramDirection
 
+from .interface import Interface
 from .udp_client import UDPClient
 
 logger = logging.getLogger("xknx.log")
 
 
-class Routing:
+class Routing(Interface):
     """Class for handling KNX/IP routing."""
 
     def __init__(self, xknx, telegram_received_callback, local_ip):
@@ -70,10 +71,23 @@ class Routing:
         """Send KNXIPFrame to connected routing device."""
         self.udpclient.send(knxipframe)
 
-    async def start(self):
+    async def connect(self) -> bool:
         """Start routing."""
-        await self.udpclient.connect()
+        try:
+            await self.udpclient.connect()
+        except OSError as ex:
+            logger.debug(
+                "Could not establish connection to KNX/IP network. %s: %s",
+                type(ex).__name__,
+                ex,
+            )
+            # close udp client to prevent open file descriptors
+            await self.udpclient.stop()
+            raise ex
+        self.xknx.connected.set()
+        return True
 
-    async def stop(self):
+    async def disconnect(self):
         """Stop routing."""
         await self.udpclient.stop()
+        self.xknx.connected.clear()
diff --git a/xknx/io/tunnel.py b/xknx/io/tunnel.py
index d81fbb9e..7fa57eaa 100644
--- a/xknx/io/tunnel.py
+++ b/xknx/io/tunnel.py
@@ -9,6 +9,8 @@ import logging
 from xknx.exceptions import CommunicationError, XKNXException
 from xknx.knxip import (
     CEMIMessageCode,
+    DisconnectRequest,
+    DisconnectResponse,
     KNXIPFrame,
     KNXIPServiceType,
     TunnellingAck,
@@ -18,14 +20,16 @@ from xknx.telegram import IndividualAddress, TelegramDirection
 
 from .connect import Connect
 from .connectionstate import ConnectionState
+from .const import HEARTBEAT_RATE
 from .disconnect import Disconnect
+from .interface import Interface
 from .tunnelling import Tunnelling
 from .udp_client import UDPClient
 
 logger = logging.getLogger("xknx.log")
 
 
-class Tunnel:
+class Tunnel(Interface):
     """Class for handling KNX/IP tunnels."""
 
     # pylint: disable=too-many-instance-attributes
@@ -71,77 +75,126 @@ class Tunnel:
         )
 
         self.udp_client.register_callback(
-            self.tunnel_reqest_received, [TunnellingRequest.service_type]
+            self._request_received,
+            [KNXIPServiceType.TUNNELLING_REQUEST, KNXIPServiceType.DISCONNECT_REQUEST],
         )
 
-    def tunnel_reqest_received(self, knxipframe, udp_client):
-        """Handle incoming tunnel request."""
-        # pylint: disable=unused-argument
-        if knxipframe.header.service_type_ident != KNXIPServiceType.TUNNELLING_REQUEST:
-            logger.warning("Service not implemented: %s", knxipframe)
-        else:
-            self.send_ack(
-                knxipframe.body.communication_channel_id,
-                knxipframe.body.sequence_counter,
+    ####################
+    #
+    # CONNECT DISCONNECT
+    #
+    ####################
+
+    async def connect(self):
+        """Connect to a KNX tunneling interface. Returns True on success."""
+        try:
+            await self.udp_client.connect()
+            await self._connect_request()
+        except (OSError, CommunicationError) as ex:
+            logger.debug(
+                "Could not establish connection to KNX/IP interface. %s: %s",
+                type(ex).__name__,
+                ex,
             )
-            # Don't handle invalid cemi frames (None) and only handle incoming L_DATA_IND frames.
-            # Ignore L_DATA_CON confirmation frames. L_DATA_REQ frames should only be outgoing.
-            if (
-                knxipframe.body.cemi is not None
-                and knxipframe.body.cemi.code is CEMIMessageCode.L_DATA_IND
-            ):
-                telegram = knxipframe.body.cemi.telegram
-                telegram.direction = TelegramDirection.INCOMING
-                if self.telegram_received_callback is not None:
-                    self.telegram_received_callback(telegram)
-
-    def send_ack(self, communication_channel_id, sequence_counter):
-        """Send tunnelling ACK after tunnelling request received."""
-        ack = TunnellingAck(
-            self.xknx,
-            communication_channel_id=communication_channel_id,
-            sequence_counter=sequence_counter,
-        )
-        self.udp_client.send(KNXIPFrame.init_from_body(ack))
+            if self.auto_reconnect:
+                self._reconnect_task = asyncio.create_task(self._reconnect())
+                return False
+            # close udp client to prevent open file descriptors
+            await self.udp_client.stop()
+            raise ex
+        else:
+            self._tunnel_established()
+            return True
 
-    async def start(self):
-        """Start tunneling."""
-        await self.connect_udp()
-        await self.connect()
+    def _tunnel_established(self):
+        """Set up interface when the tunnel is ready."""
+        self.sequence_number = 0
+        self.xknx.connected.set()
+        # self._stop_reconnect()
+        self.start_heartbeat()
+
+    def _tunnel_lost(self):
+        """Prepare for reconnection or shutdown when the connection is lost. Callback."""
+        self.xknx.connected.clear()
+        self.stop_heartbeat()
+        if self.auto_reconnect:
+            self._reconnect_task = asyncio.create_task(self._reconnect())
+        else:
+            raise CommunicationError("Tunnel connection closed.")
 
-    async def connect_udp(self):
-        """Connect udp_client."""
-        await self.udp_client.connect()
+    async def _reconnect(self):
+        """Reconnect to tunnel device."""
+        # only send disconnect request if we ever were connected
+        if self.communication_channel is not None:
+            await self._disconnect_request(True)
+            self.communication_channel = None
+        await self.udp_client.stop()
+        await asyncio.sleep(self.auto_reconnect_wait)
+        if await self.connect():
+            logger.info("Successfully reconnected to KNX bus.")
 
-    async def connect(self):
-        """Connect/build tunnel."""
+    def _stop_reconnect(self):
+        """Stop reconnect task if running."""
+        if self._reconnect_task is not None:
+            self._reconnect_task.cancel()
+            self._reconnect_task = None
+
+    async def disconnect(self):
+        """Disconnect tunneling connection."""
+        self.xknx.connected.clear()
+        self.stop_heartbeat()
+        self._stop_reconnect()
+        if self.communication_channel is not None:
+            await self._disconnect_request(False)
+            self.communication_channel = None
+        await self.udp_client.stop()
+
+    ####################
+    #
+    # OUTGOING REQUESTS
+    #
+    ####################
+
+    async def _connect_request(self) -> bool:
+        """Connect to tunnelling server. Return True if succeeded."""
         connect = Connect(self.xknx, self.udp_client)
         await connect.start()
-        if not connect.success:
-            if self.auto_reconnect:
-                logger.warning(
-                    "Could not connect to KNX. Retry in %s seconds.",
-                    self.auto_reconnect_wait,
-                )
-                self._reconnect_task = asyncio.create_task(self.schedule_reconnect())
-                return
-            raise CommunicationError(
-                "Could not establish connection", not self._is_reconnecting
+        if connect.success:
+            self.communication_channel = connect.communication_channel
+            # Use the individual address provided by the tunnelling server
+            self._src_address = IndividualAddress(connect.identifier)
+            logger.debug(
+                "Tunnel established communication_channel=%s, id=%s",
+                connect.communication_channel,
+                connect.identifier,
             )
+            return True
+        raise CommunicationError("ConnectRequest failed")
+
+    async def _connectionstate_request(self):
+        """Return state of tunnel. True if tunnel is in good shape."""
+        conn_state = ConnectionState(
+            self.xknx,
+            self.udp_client,
+            communication_channel_id=self.communication_channel,
+        )
+        await conn_state.start()
+        return conn_state.success
+
+    async def _disconnect_request(self, ignore_error=False):
+        """Disconnect from tunnel device."""
+        disconnect = Disconnect(
+            self.xknx,
+            self.udp_client,
+            communication_channel_id=self.communication_channel,
+        )
+        await disconnect.start()
+        if not disconnect.success and not ignore_error:
+            raise XKNXException("Could not disconnect channel")
         logger.debug(
-            "Tunnel established communication_channel=%s, id=%s",
-            connect.communication_channel,
-            connect.identifier,
+            "Tunnel disconnected (communication_channel: %s)",
+            self.communication_channel,
         )
-        if self._is_reconnecting:
-            logger.info("Successfully reconnected to KNX bus.")
-        self._reconnect_task = None
-        self._is_reconnecting = False
-        self.communication_channel = connect.communication_channel
-        # Use the individual address provided by the tunnelling server
-        self._src_address = IndividualAddress(connect.identifier)
-        self.sequence_number = 0
-        await self.start_heartbeat()
 
     async def send_telegram(self, telegram):
         """
@@ -158,22 +211,25 @@ class Tunnel:
         connection by sending a DISCONNECT_REQUEST frame to the other device’s
         control endpoint.
         """
-        success = await self._send_telegram_impl(telegram)
+        success = await self._tunnelling_request(telegram)
         if not success:
             logger.debug("Sending of telegram failed. Retrying a second time.")
-            success = await self._send_telegram_impl(telegram)
+            success = await self._tunnelling_request(telegram)
             if not success:
                 logger.debug("Resending telegram failed. Reconnecting to tunnel.")
-                await self.reconnect()
-                success = await self._send_telegram_impl(telegram)
+                # TODO: How to test this?
+                if self._reconnect_task is None or self._reconnect_task.done():
+                    self._tunnel_lost()
+                await self.xknx.connected.wait()
+                success = await self._tunnelling_request(telegram)
                 if not success:
                     raise CommunicationError(
-                        "Resending the telegram repeatedly failed.", False
+                        "Resending the telegram repeatedly failed.", True
                     )
-        self.increase_sequence_number()
+        self._increase_sequence_number()
 
-    async def _send_telegram_impl(self, telegram):
-        """Send Telegram to tunnelling device - implementation."""
+    async def _tunnelling_request(self, telegram):
+        """Send Telegram to tunnelling device."""
         tunnelling = Tunnelling(
             self.xknx,
             self.udp_client,
@@ -185,112 +241,96 @@ class Tunnel:
         await tunnelling.start()
         return tunnelling.success
 
-    def increase_sequence_number(self):
+    def _increase_sequence_number(self):
         """Increase sequence number."""
         self.sequence_number += 1
         if self.sequence_number == 256:
             self.sequence_number = 0
 
-    async def connectionstate(self):
-        """Return state of tunnel. True if tunnel is in good shape."""
-        conn_state = ConnectionState(
+    ####################
+    #
+    # INCOMING REQUESTS
+    #
+    ####################
+
+    def _request_received(self, knxipframe, _udp_client):
+        """Handle incoming requests."""
+        # pylint: disable=unused-argument
+        if knxipframe.header.service_type_ident is KNXIPServiceType.TUNNELLING_REQUEST:
+            self._tunnelling_request_received(knxipframe.body)
+        elif (
+            knxipframe.header.service_type_ident is KNXIPServiceType.DISCONNECT_REQUEST
+        ):
+            self._disconnect_request_received(knxipframe.body)
+        else:
+            logger.warning("Service not implemented: %s", knxipframe)
+
+    def _tunnelling_request_received(self, tunneling_request: TunnellingRequest):
+        """Handle incoming tunnel request."""
+        self._send_tunnelling_ack(
+            tunneling_request.communication_channel_id,
+            tunneling_request.sequence_counter,
+        )
+        # Don't handle invalid cemi frames (None) and only handle incoming L_DATA_IND frames.
+        # Ignore L_DATA_CON confirmation frames. L_DATA_REQ frames should only be outgoing.
+        if (
+            tunneling_request.cemi is not None
+            and tunneling_request.cemi.code is CEMIMessageCode.L_DATA_IND
+        ):
+            telegram = tunneling_request.cemi.telegram
+            telegram.direction = TelegramDirection.INCOMING
+            if self.telegram_received_callback is not None:
+                self.telegram_received_callback(telegram)
+
+    def _send_tunnelling_ack(self, communication_channel_id, sequence_counter):
+        """Send tunnelling ACK after tunnelling request received."""
+        ack = TunnellingAck(
             self.xknx,
-            self.udp_client,
-            communication_channel_id=self.communication_channel,
+            communication_channel_id=communication_channel_id,
+            sequence_counter=sequence_counter,
         )
-        await conn_state.start()
-        return conn_state.success
+        self.udp_client.send(KNXIPFrame.init_from_body(ack))
 
-    async def disconnect(self, ignore_error=False):
-        """Disconnect from tunnel device."""
-        # only send disconnect request if we ever were connected
-        if self.communication_channel is None:
-            # close udp client to prevent open file descriptors
-            await self.udp_client.stop()
-            return
-        disconnect = Disconnect(
+    def _disconnect_request_received(self, disconnect_request: DisconnectRequest):
+        """Handle incoming disconnect request."""
+        logger.warning("Received DisconnectRequest from tunnelling sever.")
+        disconnect_response = DisconnectResponse(
             self.xknx,
-            self.udp_client,
             communication_channel_id=self.communication_channel,
         )
-        await disconnect.start()
-        if not disconnect.success and not ignore_error:
-            raise XKNXException("Could not disconnect channel")
-        logger.debug(
-            "Tunnel disconnected (communication_channel: %s)",
-            self.communication_channel,
-        )
-        # close udp client to prevent open file descriptors
-        await self.udp_client.stop()
-
-    async def reconnect(self):
-        """Reconnect to tunnel device."""
-        self._is_reconnecting = True
-        await self.stop_heartbeat()
-        await self.disconnect(True)
-        self.init_udp_client()
-        await self.start()
+        self.udp_client.send(KNXIPFrame.init_from_body(disconnect_response))
+        self.communication_channel = None
+        self._tunnel_lost()
 
-    async def schedule_reconnect(self):
-        """Schedule reconnect to KNX."""
-        await asyncio.sleep(self.auto_reconnect_wait)
-        await self.reconnect()
+    ####################
+    #
+    # HEARTBEAT
+    #
+    ####################
 
-    async def stop_reconnect(self):
-        """Stop reconnect task if running."""
-        if self._reconnect_task is not None:
-            self._reconnect_task.cancel()
-            self._reconnect_task = None
-
-    async def stop(self):
-        """Stop tunneling."""
-        # XXX: set disconnect ignore_error True here. Is there actually anything
-        #      which can happen if disconnect fails? normally this fails because
-        #      we have no connection...
-        # await self.disconnect()
-        await self.stop_heartbeat()
-        await self.stop_reconnect()
-        await self.disconnect(True)
-
-    async def start_heartbeat(self):
+    def start_heartbeat(self):
         """Start heartbeat for monitoring state of tunnel, as suggested by 03.08.02 KNX Core 5.4."""
         self._heartbeat_task = asyncio.create_task(self.do_heartbeat())
 
-    async def stop_heartbeat(self):
+    def stop_heartbeat(self):
         """Stop heartbeat task if running."""
         if self._heartbeat_task is not None:
             self._heartbeat_task.cancel()
             self._heartbeat_task = None
 
     async def do_heartbeat(self):
-        """Heartbeat: Worker 'thread', endless loop for sending heartbeat requests."""
+        """Heartbeat: Worker task, endless loop for sending heartbeat requests."""
         while True:
-            await asyncio.sleep(15)
-            await self.do_heartbeat_impl()
-
-    async def do_heartbeat_impl(self):
-        """Heartbeat: checking connection state and handling result."""
-        connectionsstate = await self.connectionstate()
-        if connectionsstate:
-            await self.do_heartbeat_success()
-        else:
-            await self.do_heartbeat_failed()
+            await asyncio.sleep(HEARTBEAT_RATE)
+            if not await self._connectionstate_request():
+                await self._do_heartbeat_failed()
 
-    async def do_heartbeat_success(self):
-        """Heartbeat: handling success."""
-        self.number_heartbeat_failed = 0
-
-    async def do_heartbeat_failed(self):
+    async def _do_heartbeat_failed(self):
         """Heartbeat: handling error."""
-        self.number_heartbeat_failed = self.number_heartbeat_failed + 1
-        if self.number_heartbeat_failed > 3:
-            if not self._is_reconnecting:
-                logger.warning("Heartbeat to KNX bus failed. Reconnecting.")
-            try:
-                await self.stop_reconnect()
-                await self.reconnect()
-                self.number_heartbeat_failed = 0
-                await self.stop_heartbeat()
-            except CommunicationError as exc:
-                if exc.should_log:
-                    logger.warning(exc)
+        # first heartbeat failed - try 3 more times before disconnecting.
+        for _heartbeats_failed in range(3):
+            if await self._connectionstate_request():
+                return
+        # 3 retries failed
+        logger.warning("Heartbeat to KNX bus failed. Reconnecting.")
+        self._tunnel_lost()
diff --git a/xknx/io/udp_client.py b/xknx/io/udp_client.py
index d9ef021e..aec875a2 100644
--- a/xknx/io/udp_client.py
+++ b/xknx/io/udp_client.py
@@ -60,13 +60,11 @@ class UDPClient:
 
         def error_received(self, exc):
             """Handle errors. Callback for error received."""
-            if hasattr(self, "xknx"):
-                logger.warning("Error received: %s", exc)
+            logger.warning("Error received: %s", exc)
 
         def connection_lost(self, exc):
             """Log error. Callback for connection lost."""
-            if hasattr(self, "xknx") and exc is not None:
-                logger.info("Closing transport.")
+            logger.debug("Closing transport.")
 
     def __init__(
         self,
diff --git a/xknx/remote_value/remote_value.py b/xknx/remote_value/remote_value.py
index 6932e9f6..07603c39 100644
--- a/xknx/remote_value/remote_value.py
+++ b/xknx/remote_value/remote_value.py
@@ -6,55 +6,75 @@ Remote value can be :
 - a group address for reading a KNX value,
 - or a group of both representing the same value.
 """
+from abc import ABC, abstractmethod
 import logging
-from typing import List
+from typing import (
+    TYPE_CHECKING,
+    Any,
+    Awaitable,
+    Callable,
+    Generator,
+    List,
+    Optional,
+    Union,
+)
 
 from xknx.exceptions import CouldNotParseTelegram
 from xknx.telegram import GroupAddress, Telegram
 from xknx.telegram.apci import GroupValueResponse, GroupValueWrite
 
+if TYPE_CHECKING:
+    from xknx.dpt import DPTArray, DPTBinary
+    from xknx.telegram.address import GroupAddressableType
+    from xknx.xknx import XKNX
+
+    AsyncCallback = Callable[[], Awaitable[None]]
+    DPTPayload = Union[DPTArray, DPTBinary]
+
 logger = logging.getLogger("xknx.log")
 
 
-class RemoteValue:
+class RemoteValue(ABC):
     """Class for managing remote knx value."""
 
     # pylint: disable=too-many-instance-attributes
     def __init__(
         self,
-        xknx,
-        group_address=None,
-        group_address_state=None,
-        sync_state=True,
-        device_name=None,
-        feature_name=None,
-        after_update_cb=None,
-        passive_group_addresses: List[str] = None,
+        xknx: "XKNX",
+        group_address: Optional["GroupAddressableType"] = None,
+        group_address_state: Optional["GroupAddressableType"] = None,
+        sync_state: bool = True,
+        device_name: Optional[str] = None,
+        feature_name: Optional[str] = None,
+        after_update_cb: Optional["AsyncCallback"] = None,
+        passive_group_addresses: Optional[List["GroupAddressableType"]] = None,
     ):
         """Initialize RemoteValue class."""
         # pylint: disable=too-many-arguments
         self.xknx = xknx
-        self.passive_group_addresses = RemoteValue.get_passive_group_addresses(
-            passive_group_addresses
-        )
+
         if group_address is not None:
             group_address = GroupAddress(group_address)
         if group_address_state is not None:
             group_address_state = GroupAddress(group_address_state)
+        self.group_address: Optional[GroupAddress] = group_address
+        self.group_address_state: Optional[GroupAddress] = group_address_state
+
+        self.passive_group_addresses: List[
+            GroupAddress
+        ] = RemoteValue.get_passive_group_addresses(passive_group_addresses)
 
-        self.group_address = group_address
-        self.group_address_state = group_address_state
-        self.device_name = "Unknown" if device_name is None else device_name
-        self.feature_name = "Unknown" if feature_name is None else feature_name
+        self.device_name: str = "Unknown" if device_name is None else device_name
+        self.feature_name: str = "Unknown" if feature_name is None else feature_name
         self.after_update_cb = after_update_cb
-        self.payload = None
+        self.payload: Optional["DPTPayload"] = None
 
         if sync_state and self.group_address_state:
             self.xknx.state_updater.register_remote_value(
                 self, tracker_options=sync_state
             )
 
-    def __del__(self):
+    def __del__(self) -> None:
         """Destructor. Removing self from StateUpdater if was registered."""
         try:
             self.xknx.state_updater.unregister_remote_value(self)
@@ -62,7 +82,7 @@ class RemoteValue:
             pass
 
     @property
-    def initialized(self):
+    def initialized(self) -> bool:
         """Evaluate if remote value is initialized with group address."""
         return bool(
             self.group_address_state
@@ -71,19 +91,19 @@ class RemoteValue:
         )
 
     @property
-    def readable(self):
+    def readable(self) -> bool:
         """Evaluate if remote value should be read from bus."""
         return bool(self.group_address_state)
 
     @property
-    def writable(self):
+    def writable(self) -> bool:
         """Evaluate if remote value has a group_address set."""
         return bool(self.group_address)
 
-    def has_group_address(self, group_address):
+    def has_group_address(self, group_address: GroupAddress) -> bool:
         """Test if device has given group address."""
 
-        def _internal_addresses():
+        def _internal_addresses() -> Generator[Optional[GroupAddress], None, None]:
             """Yield all group_addresses."""
             yield self.group_address
             yield self.group_address_state
@@ -91,7 +111,9 @@ class RemoteValue:
 
         return group_address in _internal_addresses()
 
-    def payload_valid(self, payload):
+    @abstractmethod
+    # TODO: typing - remove Optional
+    def payload_valid(self, payload: Optional["DPTPayload"]) -> bool:
         """Test if telegram payload may be parsed - to be implemented in derived class.."""
         # pylint: disable=unused-argument
         logger.warning(
@@ -99,19 +121,19 @@ class RemoteValue:
         )
         return True
 
-    def from_knx(self, payload):
+    @abstractmethod
+    def from_knx(self, payload: "DPTPayload") -> Any:
         """Convert current payload to value - to be implemented in derived class."""
-        # pylint: disable=unused-argument
-        logger.warning("'from_knx()' not implemented for %s", self.__class__.__name__)
 
-    def to_knx(self, value):
+    @abstractmethod
+    def to_knx(self, value: Any) -> "DPTPayload":
         """Convert value to payload - to be implemented in derived class."""
-        # pylint: disable=unused-argument
-        logger.warning("'to_knx()' not implemented for %s", self.__class__.__name__)
 
-    async def process(self, telegram, always_callback=False):
+    async def process(self, telegram: Telegram, always_callback: bool = False) -> bool:
         """Process incoming or outgoing telegram."""
-        if not self.has_group_address(telegram.destination_address):
+        if not isinstance(
+            telegram.destination_address, GroupAddress
+        ) or not self.has_group_address(telegram.destination_address):
             return False
         if not isinstance(
             telegram.payload,
@@ -123,17 +145,17 @@ class RemoteValue:
             raise CouldNotParseTelegram(
                 "payload not a GroupValueWrite or GroupValueResponse",
                 payload=telegram.payload,
-                destination_address=telegram.destination_address,
-                source_address=telegram.source_address,
+                destination_address=str(telegram.destination_address),
+                source_address=str(telegram.source_address),
                 device_name=self.device_name,
                 feature_name=self.feature_name,
             )
         if not self.payload_valid(telegram.payload.value):
             raise CouldNotParseTelegram(
                 "payload invalid",
-                payload=telegram.payload,
-                destination_address=telegram.destination_address,
-                source_address=telegram.source_address,
+                payload=str(telegram.payload),
+                destination_address=str(telegram.destination_address),
+                source_address=str(telegram.source_address),
                 device_name=self.device_name,
                 feature_name=self.feature_name,
             )
@@ -149,23 +171,26 @@ class RemoteValue:
         return True
 
     @property
-    def value(self):
+    def value(self) -> Any:
         """Return current value."""
         if self.payload is None:
             return None
         return self.from_knx(self.payload)
 
-    async def _send(self, payload, response=False):
+    async def _send(self, payload: "DPTPayload", response: bool = False) -> None:
         """Send payload as telegram to KNX bus."""
-        telegram = Telegram(
-            destination_address=self.group_address,
-            payload=(
-                GroupValueResponse(payload) if response else GroupValueWrite(payload)
-            ),
-        )
-        await self.xknx.telegrams.put(telegram)
+        if self.group_address is not None:
+            telegram = Telegram(
+                destination_address=self.group_address,
+                payload=(
+                    GroupValueResponse(payload)
+                    if response
+                    else GroupValueWrite(payload)
+                ),
+            )
+            await self.xknx.telegrams.put(telegram)
 
-    async def set(self, value, response=False):
+    async def set(self, value: Any, response: bool = False) -> None:
         """Set new value."""
         if not self.initialized:
             logger.info(
@@ -188,14 +213,14 @@ class RemoteValue:
         await self._send(payload, response)
         # self.payload is set and after_update_cb() called when the outgoing telegram is processed.
 
-    async def respond(self):
+    async def respond(self) -> None:
         """Send current payload as GroupValueResponse telegram to KNX bus."""
         if self.payload is not None:
             await self._send(self.payload, response=True)
 
     async def read_state(self, wait_for_result: bool = False) -> None:
         """Send GroupValueRead telegram for state address to KNX bus."""
-        if self.readable:
+        if self.group_address_state is not None:
             # pylint: disable=import-outside-toplevel
             # TODO: send a ReadRequset and start a timeout from here instead of ValueReader
             #       cancel timeout form process(); delete ValueReader
@@ -217,11 +242,11 @@ class RemoteValue:
                 await value_reader.send_group_read()
 
     @property
-    def unit_of_measurement(self):
+    def unit_of_measurement(self) -> Optional[str]:
         """Return the unit of measurement."""
         return None
 
-    def group_addr_str(self):
+    def group_addr_str(self) -> str:
         """Return object as readable string."""
         return "{}/{}/{}/{}".format(
             self.group_address.__repr__(),
@@ -230,7 +255,7 @@ class RemoteValue:
             self.value,
         )
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as string representation."""
         return '<{} device_name="{}" feature_name="{}" {}/>'.format(
             self.__class__.__name__,
@@ -239,7 +264,7 @@ class RemoteValue:
             self.group_addr_str(),
         )
 
-    def __eq__(self, other):
+    def __eq__(self, other: Any) -> bool:
         """Equal operator."""
         for key, value in self.__dict__.items():
             if key == "after_update_cb":
@@ -256,7 +281,9 @@ class RemoteValue:
         return True
 
     @staticmethod
-    def get_passive_group_addresses(passive_group_addresses: List[str]) -> List:
+    def get_passive_group_addresses(
+        passive_group_addresses: Optional[List["GroupAddressableType"]],
+    ) -> List[GroupAddress]:
         """Obtain passive state group addresses."""
         if passive_group_addresses is None:
             return []
diff --git a/xknx/telegram/address.py b/xknx/telegram/address.py
index 42516c96..3836421c 100644
--- a/xknx/telegram/address.py
+++ b/xknx/telegram/address.py
@@ -18,8 +18,14 @@ from typing import Optional, Tuple, Union
 
 from xknx.exceptions import CouldNotParseAddress
 
+# TODO: typing - remove need for Optional here
+GroupAddressableType = Optional[Union["GroupAddress", str, int, Tuple[int, int]]]
+IndividualAddressableType = Optional[
+    Union["IndividualAddress", str, int, Tuple[int, int]]
+]
 
-def address_tuple_to_int(address: Tuple) -> int:
+
+def address_tuple_to_int(address: Tuple[int, int]) -> int:
     """
     Convert the tuple `address` to an integer.
 
@@ -80,22 +86,22 @@ class IndividualAddress(BaseAddress):
         r"^(?P<area>\d{1,2})\.(?P<main>\d{1,2})\.(?P<line>\d{1,3})$"
     )
 
-    def __init__(self, address: Union[object, str, int, tuple]) -> None:
+    def __init__(self, address: IndividualAddressableType) -> None:
         """Initialize Address class."""
         super().__init__()
         if isinstance(address, IndividualAddress):
-            self.raw: int = address.raw
+            self.raw = address.raw
         elif isinstance(address, str):
             if address.isdigit():
-                self.raw: int = int(address)  # type: ignore
+                self.raw = int(address)
             else:
-                self.raw: int = self.__string_to_int(address)  # type: ignore
+                self.raw = self.__string_to_int(address)
         elif isinstance(address, tuple) and len(address) == 2:
-            self.raw: int = address_tuple_to_int(address)  # type: ignore
+            self.raw = address_tuple_to_int(address)
         elif isinstance(address, int):
-            self.raw: int = address  # type: ignore
+            self.raw = address
         elif address is None:
-            self.raw: int = 0  # type: ignore
+            self.raw = 0
         else:
             raise CouldNotParseAddress(address)
 
@@ -186,7 +192,7 @@ class GroupAddress(BaseAddress):
 
     def __init__(
         self,
-        address: Union[object, str, tuple, int],
+        address: GroupAddressableType,
         levels: GroupAddressType = GroupAddressType.LONG,
     ) -> None:
         """Initialize Address class."""
diff --git a/xknx/telegram/address_filter.py b/xknx/telegram/address_filter.py
index 7a76776e..20276b4f 100644
--- a/xknx/telegram/address_filter.py
+++ b/xknx/telegram/address_filter.py
@@ -42,7 +42,7 @@ class AddressFilter:
         for part in pattern.split("/"):
             self.level_filters.append(AddressFilter.LevelFilter(part))
         if len(self.level_filters) > 3:
-            raise ConversionError("Too many parts within pattern.", pattern=pattern)  # type: ignore
+            raise ConversionError("Too many parts within pattern.", pattern=pattern)
 
     def match(self, address: Union[str, GroupAddress]) -> bool:
         """Test if provided address matches Addressfilter."""
diff --git a/xknx/telegram/telegram.py b/xknx/telegram/telegram.py
index 3b4280c2..b702c233 100644
--- a/xknx/telegram/telegram.py
+++ b/xknx/telegram/telegram.py
@@ -33,12 +33,10 @@ class Telegram:
 
     def __init__(
         self,
-        destination_address: Union[GroupAddress, IndividualAddress] = GroupAddress(
-            None
-        ),
+        destination_address: Union[GroupAddress, IndividualAddress] = GroupAddress(0),
         direction: TelegramDirection = TelegramDirection.OUTGOING,
         payload: Any = None,
-        source_address: IndividualAddress = IndividualAddress(None),
+        source_address: IndividualAddress = IndividualAddress(0),
     ) -> None:
         """Initialize Telegram class."""
         self.destination_address = destination_address
diff --git a/xknx/xknx.py b/xknx/xknx.py
index d06f68d1..eff9a526 100644
--- a/xknx/xknx.py
+++ b/xknx/xknx.py
@@ -54,6 +54,7 @@ class XKNX:
         self.state_updater = StateUpdater(self)
         self.knxip_interface = None
         self.started = asyncio.Event()
+        self.connected = asyncio.Event()
         self.address_format = address_format
         self.own_address = IndividualAddress(own_address)
         self.rate_limit = rate_limit


Test patch:
diff --git a/requirements/testing.txt b/requirements/testing.txt
index 1f27f36b..5fc7de8a 100644
--- a/requirements/testing.txt
+++ b/requirements/testing.txt
@@ -10,6 +10,6 @@ pylint-strict-informational==0.1
 pytest==6.2.1
 pytest-cov==2.10.1
 pytest-timeout==1.4.2
-setuptools==51.0.0
+setuptools==51.1.0.post20201221
 tox==3.20.1
 mypy==0.790
diff --git a/test/config_tests/config_v1_test.py b/test/config_tests/config_v1_test.py
index 5479f439..be7b10bc 100644
--- a/test/config_tests/config_v1_test.py
+++ b/test/config_tests/config_v1_test.py
@@ -23,6 +23,7 @@ from xknx.devices import (
 )
 from xknx.exceptions import XKNXException
 from xknx.io import ConnectionConfig, ConnectionType
+from xknx.telegram import IndividualAddress
 import yaml
 
 
@@ -54,7 +55,7 @@ class TestConfig(unittest.TestCase):
     #
     def test_config_general(self):
         """Test reading general section from config file."""
-        self.assertEqual(TestConfig.xknx.own_address, "15.15.249")
+        self.assertEqual(TestConfig.xknx.own_address, IndividualAddress("15.15.249"))
         self.assertEqual(TestConfig.xknx.rate_limit, 18)
         self.assertEqual(TestConfig.xknx.multicast_group, "224.1.2.3")
         self.assertEqual(TestConfig.xknx.multicast_port, 1337)
@@ -197,6 +198,38 @@ class TestConfig(unittest.TestCase):
             ),
         )
 
+    def test_config_individual_rgb(self):
+        """Test reading Light with with dimming and color address."""
+        expected = Light(
+            TestConfig.xknx,
+            "Diningroom.Light_rgb",
+            group_address_switch_white="1/6/4",
+            group_address_switch_white_state="1/6/5",
+            group_address_brightness_white="1/6/6",
+            group_address_brightness_white_state="1/6/7",
+            group_address_switch_red="1/6/14",
+            group_address_switch_red_state="1/6/15",
+            group_address_brightness_red="1/6/16",
+            group_address_brightness_red_state="1/6/17",
+            group_address_switch_green="1/6/24",
+            group_address_switch_green_state="1/6/25",
+            group_address_brightness_green="1/6/26",
+            group_address_brightness_green_state="1/6/27",
+            group_address_switch_blue="1/6/34",
+            group_address_switch_blue_state="1/6/35",
+            group_address_brightness_blue="1/6/36",
+            group_address_brightness_blue_state="1/6/37",
+            min_kelvin=Light.DEFAULT_MIN_KELVIN,
+            max_kelvin=Light.DEFAULT_MAX_KELVIN,
+        )
+        result = TestConfig.xknx.devices["Diningroom.Light_rgb"]
+
+        print(f"result: {result} \nexpected: {expected}")
+        self.assertEqual(
+            result,
+            expected,
+        )
+
     def test_config_color_temperature(self):
         """Test reading Light with with dimming and color temperature address."""
         self.assertEqual(
diff --git a/test/config_tests/resources/light/invalid_3.yaml b/test/config_tests/resources/light/invalid_3.yaml
new file mode 100644
index 00000000..3b261fd6
--- /dev/null
+++ b/test/config_tests/resources/light/invalid_3.yaml
@@ -0,0 +1,25 @@
+name: "two or more values in the same group of exclusion 'color' @ .*"
+friendly_name: "Office"
+switch:
+  address: "2/1/5"
+  state_address: "2/4/5"
+  state_update: "expire 60"
+  passive_state_addresses: ["8/8/8"]
+brightness:
+  address: "2/5/5"
+  state_address: "2/5/6"
+  state_update: "expire 60"
+  passive_state_addresses: ["8/8/8"]
+rgbw:
+  address: "2/4/5"
+  state_address: "2/4/6"
+color:
+  address: "2/4/5"
+  state_address: "2/4/6"
+color_temperature:
+  address: "2/5/7"
+  state_address: "2/5/8"
+  state_update: "expire 60"
+  mode: absolute
+  min_kelvin: 2550
+  max_kelvin: 6200
\ No newline at end of file
diff --git a/test/config_tests/resources/light/valid_5.yaml b/test/config_tests/resources/light/valid_5.yaml
new file mode 100644
index 00000000..6e480fff
--- /dev/null
+++ b/test/config_tests/resources/light/valid_5.yaml
@@ -0,0 +1,10 @@
+name: "office"
+friendly_name: "Office"
+switch:
+  address: "2/1/5"
+  state_address: "2/4/5"
+  state_update: "expire 60"
+  passive_state_addresses: ["8/8/8"]
+color:
+  address: "2/4/5"
+  state_address: "2/4/6"
\ No newline at end of file
diff --git a/test/config_tests/resources/light/valid_rgb.yaml b/test/config_tests/resources/light/valid_rgb.yaml
new file mode 100644
index 00000000..d93861d8
--- /dev/null
+++ b/test/config_tests/resources/light/valid_rgb.yaml
@@ -0,0 +1,24 @@
+name: "office"
+friendly_name: "Office"
+individual_colors:
+  red:
+    switch:
+      address: "2/1/5"
+      state_address: "2/4/5"
+    brightness:
+      address: "2/5/7"
+      state_address: "2/5/8"
+  green:
+    switch:
+      address: "2/1/5"
+      state_address: "2/4/5"
+    brightness:
+      address: "2/5/7"
+      state_address: "2/5/8"
+  blue:
+    switch:
+      address: "2/1/5"
+      state_address: "2/4/5"
+    brightness:
+      address: "2/5/7"
+      state_address: "2/5/8"
diff --git a/test/config_tests/resources/light/valid_rgbw.yaml b/test/config_tests/resources/light/valid_rgbw.yaml
new file mode 100644
index 00000000..47d1ad43
--- /dev/null
+++ b/test/config_tests/resources/light/valid_rgbw.yaml
@@ -0,0 +1,31 @@
+name: "office"
+friendly_name: "Office"
+individual_colors:
+  white:
+    switch:
+      address: "2/1/5"
+      state_address: "2/4/5"
+    brightness:
+      address: "2/5/7"
+      state_address: "2/5/8"
+  red:
+    switch:
+      address: "2/1/5"
+      state_address: "2/4/5"
+    brightness:
+      address: "2/5/7"
+      state_address: "2/5/8"
+  green:
+    switch:
+      address: "2/1/5"
+      state_address: "2/4/5"
+    brightness:
+      address: "2/5/7"
+      state_address: "2/5/8"
+  blue:
+    switch:
+      address: "2/1/5"
+      state_address: "2/4/5"
+    brightness:
+      address: "2/5/7"
+      state_address: "2/5/8"
diff --git a/test/config_tests/resources/light/valid_rgbw_no_switch.yaml b/test/config_tests/resources/light/valid_rgbw_no_switch.yaml
new file mode 100644
index 00000000..8b1a7934
--- /dev/null
+++ b/test/config_tests/resources/light/valid_rgbw_no_switch.yaml
@@ -0,0 +1,21 @@
+name: "office"
+friendly_name: "Office"
+switch:
+  address: "2/1/5"
+individual_colors:
+  white:
+    brightness:
+      address: "2/5/7"
+      state_address: "2/5/8"
+  red:
+    brightness:
+      address: "2/5/7"
+      state_address: "2/5/8"
+  green:
+    brightness:
+      address: "2/5/7"
+      state_address: "2/5/8"
+  blue:
+    brightness:
+      address: "2/5/7"
+      state_address: "2/5/8"
diff --git a/test/core_tests/state_updater_test.py b/test/core_tests/state_updater_test.py
index 4f5ac83a..c4cae037 100644
--- a/test/core_tests/state_updater_test.py
+++ b/test/core_tests/state_updater_test.py
@@ -10,6 +10,7 @@ from xknx.remote_value import RemoteValue
 from xknx.telegram import GroupAddress
 
 
+@patch.multiple(RemoteValue, __abstractmethods__=set())
 class TestStateUpdater(unittest.TestCase):
     """Test class for state updater."""
 
@@ -19,6 +20,7 @@ class TestStateUpdater(unittest.TestCase):
         """Set up test class."""
         self.loop = asyncio.new_event_loop()
         asyncio.set_event_loop(self.loop)
+        patch.multiple(RemoteValue, __abstractmethods__=set())
 
     def tearDown(self):
         """Tear down test class."""
diff --git a/test/devices_tests/light_test.py b/test/devices_tests/light_test.py
index 2e54fb9d..19ce18ab 100644
--- a/test/devices_tests/light_test.py
+++ b/test/devices_tests/light_test.py
@@ -65,6 +65,44 @@ class TestLight(unittest.TestCase):
         light = Light(xknx, "Diningroom.Light_1", group_address_switch="1/6/4")
         self.assertFalse(light.supports_color)
 
+    def test_supports_individual_color_true(self):
+        """Test supports_color true."""
+        xknx = XKNX()
+        light = Light(
+            xknx,
+            "Diningroom.Light_1",
+            group_address_switch_red="1/1/1",
+            group_address_switch_red_state="1/1/2",
+            group_address_brightness_red="1/1/3",
+            group_address_brightness_red_state="1/1/4",
+            group_address_switch_green="1/1/5",
+            group_address_switch_green_state="1/1/6",
+            group_address_brightness_green="1/1/7",
+            group_address_brightness_green_state="1/1/8",
+            group_address_switch_blue="1/1/9",
+            group_address_switch_blue_state="1/1/10",
+            group_address_brightness_blue="1/1/11",
+            group_address_brightness_blue_state="1/1/12",
+        )
+        self.assertTrue(light.supports_color)
+
+    def test_supports_individual_color_false(self):
+        """Test supports_color false."""
+        xknx = XKNX()
+        light = Light(
+            xknx,
+            "Diningroom.Light_1",
+            group_address_switch_red="1/1/1",
+            group_address_switch_red_state="1/1/2",
+            group_address_brightness_red="1/1/3",
+            group_address_brightness_red_state="1/1/4",
+            group_address_switch_green="1/1/5",
+            group_address_switch_green_state="1/1/6",
+            group_address_brightness_green="1/1/7",
+            group_address_brightness_green_state="1/1/8",
+        )
+        self.assertFalse(light.supports_color)
+
     #
     # TEST SUPPORT COLOR RGBW
     #
@@ -91,6 +129,52 @@ class TestLight(unittest.TestCase):
         )
         self.assertFalse(light.supports_rgbw)
 
+    def test_supports_individual_rgbw_true(self):
+        """Test supports_rgbw true."""
+        xknx = XKNX()
+        light = Light(
+            xknx,
+            "Diningroom.Light_1",
+            group_address_switch_red="1/1/1",
+            group_address_switch_red_state="1/1/2",
+            group_address_brightness_red="1/1/3",
+            group_address_brightness_red_state="1/1/4",
+            group_address_switch_green="1/1/5",
+            group_address_switch_green_state="1/1/6",
+            group_address_brightness_green="1/1/7",
+            group_address_brightness_green_state="1/1/8",
+            group_address_switch_blue="1/1/9",
+            group_address_switch_blue_state="1/1/10",
+            group_address_brightness_blue="1/1/11",
+            group_address_brightness_blue_state="1/1/12",
+            group_address_switch_white="1/1/13",
+            group_address_switch_white_state="1/1/14",
+            group_address_brightness_white="1/1/15",
+            group_address_brightness_white_state="1/1/16",
+        )
+        self.assertTrue(light.supports_rgbw)
+
+    def test_supports_individual_rgbw_false(self):
+        """Test supports_color false."""
+        xknx = XKNX()
+        light = Light(
+            xknx,
+            "Diningroom.Light_1",
+            group_address_switch_red="1/1/1",
+            group_address_switch_red_state="1/1/2",
+            group_address_brightness_red="1/1/3",
+            group_address_brightness_red_state="1/1/4",
+            group_address_switch_green="1/1/5",
+            group_address_switch_green_state="1/1/6",
+            group_address_brightness_green="1/1/7",
+            group_address_brightness_green_state="1/1/8",
+            group_address_switch_blue="1/1/9",
+            group_address_switch_blue_state="1/1/10",
+            group_address_brightness_blue="1/1/11",
+            group_address_brightness_blue_state="1/1/12",
+        )
+        self.assertFalse(light.supports_rgbw)
+
     #
     # TEST SUPPORT TUNABLE WHITE
     #
@@ -179,6 +263,73 @@ class TestLight(unittest.TestCase):
         self.assertEqual(len(telegrams), 6)
         self.assertListEqual(telegrams, test_telegrams)
 
+    def test_sync_individual_color(self):
+        """Test sync function / sending group reads to KNX bus. Testing with a Light without dimm functionality."""
+        xknx = XKNX()
+        light = Light(
+            xknx,
+            "Diningroom.Light_1",
+            group_address_switch_red="1/1/1",
+            group_address_switch_red_state="1/1/2",
+            group_address_brightness_red="1/1/3",
+            group_address_brightness_red_state="1/1/4",
+            group_address_switch_green="1/1/5",
+            group_address_switch_green_state="1/1/6",
+            group_address_brightness_green="1/1/7",
+            group_address_brightness_green_state="1/1/8",
+            group_address_switch_blue="1/1/9",
+            group_address_switch_blue_state="1/1/10",
+            group_address_brightness_blue="1/1/11",
+            group_address_brightness_blue_state="1/1/12",
+            group_address_switch_white="1/1/13",
+            group_address_switch_white_state="1/1/14",
+            group_address_brightness_white="1/1/15",
+            group_address_brightness_white_state="1/1/16",
+        )
+        self.loop.run_until_complete(light.sync())
+
+        self.assertEqual(xknx.telegrams.qsize(), 8)
+
+        telegrams = [xknx.telegrams.get_nowait() for _ in range(8)]
+
+        test_telegrams = [
+            Telegram(
+                destination_address=GroupAddress("1/1/2"),
+                payload=GroupValueRead(),
+            ),
+            Telegram(
+                destination_address=GroupAddress("1/1/4"),
+                payload=GroupValueRead(),
+            ),
+            Telegram(
+                destination_address=GroupAddress("1/1/6"),
+                payload=GroupValueRead(),
+            ),
+            Telegram(
+                destination_address=GroupAddress("1/1/8"),
+                payload=GroupValueRead(),
+            ),
+            Telegram(
+                destination_address=GroupAddress("1/1/10"),
+                payload=GroupValueRead(),
+            ),
+            Telegram(
+                destination_address=GroupAddress("1/1/12"),
+                payload=GroupValueRead(),
+            ),
+            Telegram(
+                destination_address=GroupAddress("1/1/14"),
+                payload=GroupValueRead(),
+            ),
+            Telegram(
+                destination_address=GroupAddress("1/1/16"),
+                payload=GroupValueRead(),
+            ),
+        ]
+
+        self.assertEqual(len(set(telegrams)), 8)
+        self.assertEqual(set(telegrams), set(test_telegrams))
+
     #
     # SYNC WITH STATE ADDRESS
     #
@@ -256,6 +407,55 @@ class TestLight(unittest.TestCase):
             ),
         )
 
+    def test_set_on_individual_color(self):
+        """Test switching on a Light."""
+        xknx = XKNX()
+        light = Light(
+            xknx,
+            "Diningroom.Light_1",
+            group_address_switch_red="1/1/1",
+            group_address_switch_red_state="1/1/2",
+            group_address_brightness_red="1/1/3",
+            group_address_brightness_red_state="1/1/4",
+            group_address_switch_green="1/1/5",
+            group_address_switch_green_state="1/1/6",
+            group_address_brightness_green="1/1/7",
+            group_address_brightness_green_state="1/1/8",
+            group_address_switch_blue="1/1/9",
+            group_address_switch_blue_state="1/1/10",
+            group_address_brightness_blue="1/1/11",
+            group_address_brightness_blue_state="1/1/12",
+            group_address_switch_white="1/1/13",
+            group_address_switch_white_state="1/1/14",
+            group_address_brightness_white="1/1/15",
+            group_address_brightness_white_state="1/1/16",
+        )
+        self.loop.run_until_complete(light.set_on())
+        self.assertEqual(xknx.telegrams.qsize(), 4)
+
+        telegrams = [xknx.telegrams.get_nowait() for _ in range(4)]
+
+        test_telegrams = [
+            Telegram(
+                destination_address=GroupAddress("1/1/1"),
+                payload=GroupValueWrite(DPTBinary(True)),
+            ),
+            Telegram(
+                destination_address=GroupAddress("1/1/5"),
+                payload=GroupValueWrite(DPTBinary(True)),
+            ),
+            Telegram(
+                destination_address=GroupAddress("1/1/9"),
+                payload=GroupValueWrite(DPTBinary(True)),
+            ),
+            Telegram(
+                destination_address=GroupAddress("1/1/13"),
+                payload=GroupValueWrite(DPTBinary(True)),
+            ),
+        ]
+        self.assertEqual(len(set(telegrams)), 4)
+        self.assertEqual(set(telegrams), set(test_telegrams))
+
     #
     # TEST SET OFF
     #
@@ -279,6 +479,55 @@ class TestLight(unittest.TestCase):
             ),
         )
 
+    def test_set_off_individual_color(self):
+        """Test switching off a Light."""
+        xknx = XKNX()
+        light = Light(
+            xknx,
+            "Diningroom.Light_1",
+            group_address_switch_red="1/1/1",
+            group_address_switch_red_state="1/1/2",
+            group_address_brightness_red="1/1/3",
+            group_address_brightness_red_state="1/1/4",
+            group_address_switch_green="1/1/5",
+            group_address_switch_green_state="1/1/6",
+            group_address_brightness_green="1/1/7",
+            group_address_brightness_green_state="1/1/8",
+            group_address_switch_blue="1/1/9",
+            group_address_switch_blue_state="1/1/10",
+            group_address_brightness_blue="1/1/11",
+            group_address_brightness_blue_state="1/1/12",
+            group_address_switch_white="1/1/13",
+            group_address_switch_white_state="1/1/14",
+            group_address_brightness_white="1/1/15",
+            group_address_brightness_white_state="1/1/16",
+        )
+        self.loop.run_until_complete(light.set_off())
+        self.assertEqual(xknx.telegrams.qsize(), 4)
+
+        telegrams = [xknx.telegrams.get_nowait() for _ in range(4)]
+
+        test_telegrams = [
+            Telegram(
+                destination_address=GroupAddress("1/1/1"),
+                payload=GroupValueWrite(DPTBinary(False)),
+            ),
+            Telegram(
+                destination_address=GroupAddress("1/1/5"),
+                payload=GroupValueWrite(DPTBinary(False)),
+            ),
+            Telegram(
+                destination_address=GroupAddress("1/1/9"),
+                payload=GroupValueWrite(DPTBinary(False)),
+            ),
+            Telegram(
+                destination_address=GroupAddress("1/1/13"),
+                payload=GroupValueWrite(DPTBinary(False)),
+            ),
+        ]
+        self.assertEqual(len(set(telegrams)), 4)
+        self.assertEqual(set(telegrams), set(test_telegrams))
+
     #
     # TEST SET BRIGHTNESS
     #
@@ -351,6 +600,89 @@ class TestLight(unittest.TestCase):
                 "Colors not supported for device %s", "TestLight"
             )
 
+    def test_set_individual_color(self):
+        """Test setting the color of a Light."""
+        xknx = XKNX()
+        light = Light(
+            xknx,
+            "Diningroom.Light_1",
+            group_address_switch_red="1/1/1",
+            group_address_switch_red_state="1/1/2",
+            group_address_brightness_red="1/1/3",
+            group_address_brightness_red_state="1/1/4",
+            group_address_switch_green="1/1/5",
+            group_address_switch_green_state="1/1/6",
+            group_address_brightness_green="1/1/7",
+            group_address_brightness_green_state="1/1/8",
+            group_address_switch_blue="1/1/9",
+            group_address_switch_blue_state="1/1/10",
+            group_address_brightness_blue="1/1/11",
+            group_address_brightness_blue_state="1/1/12",
+        )
+        self.loop.run_until_complete(light.set_color([23, 24, 25]))
+        self.assertEqual(xknx.telegrams.qsize(), 3)
+        telegrams = [xknx.telegrams.get_nowait() for _ in range(3)]
+
+        test_telegrams = [
+            Telegram(
+                destination_address=GroupAddress("1/1/3"),
+                payload=GroupValueWrite(DPTArray(23)),
+            ),
+            Telegram(
+                destination_address=GroupAddress("1/1/7"),
+                payload=GroupValueWrite(DPTArray(24)),
+            ),
+            Telegram(
+                destination_address=GroupAddress("1/1/11"),
+                payload=GroupValueWrite(DPTArray(25)),
+            ),
+        ]
+
+        self.assertEqual(len(set(telegrams)), 3)
+        self.assertEqual(set(telegrams), set(test_telegrams))
+
+        self.loop.run_until_complete(
+            xknx.devices.process(
+                Telegram(
+                    destination_address=GroupAddress("1/1/4"),
+                    payload=GroupValueWrite(DPTArray(23)),
+                )
+            )
+        )
+        self.loop.run_until_complete(
+            xknx.devices.process(
+                Telegram(
+                    destination_address=GroupAddress("1/1/8"),
+                    payload=GroupValueWrite(DPTArray(24)),
+                )
+            )
+        )
+        self.loop.run_until_complete(
+            xknx.devices.process(
+                Telegram(
+                    destination_address=GroupAddress("1/1/12"),
+                    payload=GroupValueWrite(DPTArray(25)),
+                )
+            )
+        )
+        self.assertEqual(light.current_color, ([23, 24, 25], None))
+
+    def test_set_individual_color_not_possible(self):
+        """Test setting the color of a non light without color."""
+        # pylint: disable=invalid-name
+        xknx = XKNX()
+        light = Light(
+            xknx,
+            "TestLight",
+            group_address_switch_red="1/1/1",
+        )
+        with patch("logging.Logger.warning") as mock_warn:
+            self.loop.run_until_complete(light.set_color((23, 24, 25)))
+            self.assertEqual(xknx.telegrams.qsize(), 0)
+            mock_warn.assert_called_with(
+                "Colors not supported for device %s", "TestLight"
+            )
+
     #
     # TEST SET COLOR AS RGBW
     #
@@ -395,6 +727,117 @@ class TestLight(unittest.TestCase):
                 "RGBW not supported for device %s", "TestLight"
             )
 
+    def test_set_individual_color_rgbw(self):
+        """Test setting RGBW value of a Light."""
+        xknx = XKNX()
+        light = Light(
+            xknx,
+            "Diningroom.Light_1",
+            group_address_switch_red="1/1/1",
+            group_address_switch_red_state="1/1/2",
+            group_address_brightness_red="1/1/3",
+            group_address_brightness_red_state="1/1/4",
+            group_address_switch_green="1/1/5",
+            group_address_switch_green_state="1/1/6",
+            group_address_brightness_green="1/1/7",
+            group_address_brightness_green_state="1/1/8",
+            group_address_switch_blue="1/1/9",
+            group_address_switch_blue_state="1/1/10",
+            group_address_brightness_blue="1/1/11",
+            group_address_brightness_blue_state="1/1/12",
+            group_address_switch_white="1/1/13",
+            group_address_switch_white_state="1/1/14",
+            group_address_brightness_white="1/1/15",
+            group_address_brightness_white_state="1/1/16",
+        )
+        self.loop.run_until_complete(light.set_color([23, 24, 25], white=26))
+        self.assertEqual(xknx.telegrams.qsize(), 4)
+        telegrams = [xknx.telegrams.get_nowait() for _ in range(4)]
+
+        test_telegrams = [
+            Telegram(
+                destination_address=GroupAddress("1/1/3"),
+                payload=GroupValueWrite(DPTArray(23)),
+            ),
+            Telegram(
+                destination_address=GroupAddress("1/1/7"),
+                payload=GroupValueWrite(DPTArray(24)),
+            ),
+            Telegram(
+                destination_address=GroupAddress("1/1/11"),
+                payload=GroupValueWrite(DPTArray(25)),
+            ),
+            Telegram(
+                destination_address=GroupAddress("1/1/15"),
+                payload=GroupValueWrite(DPTArray(26)),
+            ),
+        ]
+
+        self.assertEqual(len(set(telegrams)), 4)
+        self.assertEqual(set(telegrams), set(test_telegrams))
+
+        self.loop.run_until_complete(
+            xknx.devices.process(
+                Telegram(
+                    destination_address=GroupAddress("1/1/4"),
+                    payload=GroupValueWrite(DPTArray(23)),
+                )
+            )
+        )
+        self.loop.run_until_complete(
+            xknx.devices.process(
+                Telegram(
+                    destination_address=GroupAddress("1/1/8"),
+                    payload=GroupValueWrite(DPTArray(24)),
+                )
+            )
+        )
+        self.loop.run_until_complete(
+            xknx.devices.process(
+                Telegram(
+                    destination_address=GroupAddress("1/1/12"),
+                    payload=GroupValueWrite(DPTArray(25)),
+                )
+            )
+        )
+        self.loop.run_until_complete(
+            xknx.devices.process(
+                Telegram(
+                    destination_address=GroupAddress("1/1/16"),
+                    payload=GroupValueWrite(DPTArray(26)),
+                )
+            )
+        )
+        self.assertEqual(light.current_color, ([23, 24, 25], 26))
+
+    def test_set_individual_color_rgbw_not_possible(self):
+        """Test setting RGBW value of a non light without color."""
+        # pylint: disable=invalid-name
+        xknx = XKNX()
+        light = Light(
+            xknx,
+            "TestLight",
+            group_address_switch_red="1/1/1",
+            group_address_switch_red_state="1/1/2",
+            group_address_brightness_red="1/1/3",
+            group_address_brightness_red_state="1/1/4",
+            group_address_switch_green="1/1/5",
+            group_address_switch_green_state="1/1/6",
+            group_address_brightness_green="1/1/7",
+            group_address_brightness_green_state="1/1/8",
+            group_address_switch_blue="1/1/9",
+            group_address_switch_blue_state="1/1/10",
+            group_address_brightness_blue="1/1/11",
+            group_address_brightness_blue_state="1/1/12",
+        )
+        with patch("logging.Logger.warning") as mock_warn:
+            self.loop.run_until_complete(light.set_color((23, 24, 25), 26))
+
+            self.assertEqual(xknx.telegrams.qsize(), 0)
+            mock_warn.assert_called_with(
+                "RGBW not supported for device %s", "TestLight"
+            )
+
     #
     # TEST SET TUNABLE WHITE
     #
@@ -500,6 +943,41 @@ class TestLight(unittest.TestCase):
         self.loop.run_until_complete(light.process(telegram))
         self.assertEqual(light.state, False)
 
+    def test_process_color_switch(self):
+        """Test process / reading telegrams from telegram queue. Test if switch position is processed correctly."""
+        xknx = XKNX()
+        light = Light(
+            xknx,
+            "TestLight",
+            group_address_switch_red="1/1/1",
+            group_address_switch_red_state="1/1/2",
+            group_address_brightness_red="1/1/3",
+            group_address_brightness_red_state="1/1/4",
+            group_address_switch_green="1/1/5",
+            group_address_switch_green_state="1/1/6",
+            group_address_brightness_green="1/1/7",
+            group_address_brightness_green_state="1/1/8",
+            group_address_switch_blue="1/1/9",
+            group_address_switch_blue_state="1/1/10",
+            group_address_brightness_blue="1/1/11",
+            group_address_brightness_blue_state="1/1/12",
+        )
+        self.assertEqual(light.state, None)
+
+        telegram = Telegram(
+            destination_address=GroupAddress("1/1/2"),
+            payload=GroupValueWrite(DPTBinary(True)),
+        )
+        self.loop.run_until_complete(light.process(telegram))
+        self.assertEqual(light.state, True)
+
+        telegram = Telegram(
+            destination_address=GroupAddress("1/1/2"),
+            payload=GroupValueWrite(DPTBinary(False)),
+        )
+        self.loop.run_until_complete(light.process(telegram))
+        self.assertEqual(light.state, False)
+
     def test_process_switch_callback(self):
         """Test process / reading telegrams from telegram queue. Test if callback is called."""
         # pylint: disable=no-self-use
@@ -595,6 +1073,46 @@ class TestLight(unittest.TestCase):
         self.loop.run_until_complete(light.process(telegram))
         self.assertEqual(light.current_color, ((23, 24, 25), None))
 
+    def test_process_individual_color(self):
+        """Test process / reading telegrams from telegram queue. Test if color is processed."""
+        xknx = XKNX()
+        light = Light(
+            xknx,
+            "TestLight",
+            group_address_switch_red="1/1/1",
+            group_address_switch_red_state="1/1/2",
+            group_address_brightness_red="1/1/3",
+            group_address_brightness_red_state="1/1/4",
+            group_address_switch_green="1/1/5",
+            group_address_switch_green_state="1/1/6",
+            group_address_brightness_green="1/1/7",
+            group_address_brightness_green_state="1/1/8",
+            group_address_switch_blue="1/1/9",
+            group_address_switch_blue_state="1/1/10",
+            group_address_brightness_blue="1/1/11",
+            group_address_brightness_blue_state="1/1/12",
+        )
+        self.assertEqual(light.current_color, (None, None))
+
+        telegrams = [
+            Telegram(
+                destination_address=GroupAddress("1/1/4"),
+                payload=GroupValueWrite(DPTArray(42)),
+            ),
+            Telegram(
+                destination_address=GroupAddress("1/1/8"),
+                payload=GroupValueWrite(DPTArray(43)),
+            ),
+            Telegram(
+                destination_address=GroupAddress("1/1/12"),
+                payload=GroupValueWrite(DPTArray(44)),
+            ),
+        ]
+
+        for telegram in telegrams:
+            self.loop.run_until_complete(light.process(telegram))
+        self.assertEqual(light.current_color, ([42, 43, 44], None))
+
     def test_process_color_rgbw(self):
         """Test process / reading telegrams from telegram queue. Test if RGBW is processed."""
         xknx = XKNX()
@@ -613,6 +1131,37 @@ class TestLight(unittest.TestCase):
         self.loop.run_until_complete(light.process(telegram))
         self.assertEqual(light.current_color, ([23, 24, 25], 26))
 
+    def test_process_individual_color_rgbw(self):
+        """Test process / reading telegrams from telegram queue. Test if RGBW is processed."""
+        xknx = XKNX()
+        light = Light(
+            xknx,
+            "Diningroom.Light_1",
+            group_address_switch_red="1/1/1",
+            group_address_switch_red_state="1/1/2",
+            group_address_brightness_red="1/1/3",
+            group_address_brightness_red_state="1/1/4",
+            group_address_switch_green="1/1/5",
+            group_address_switch_green_state="1/1/6",
+            group_address_brightness_green="1/1/7",
+            group_address_brightness_green_state="1/1/8",
+            group_address_switch_blue="1/1/9",
+            group_address_switch_blue_state="1/1/10",
+            group_address_brightness_blue="1/1/11",
+            group_address_brightness_blue_state="1/1/12",
+            group_address_switch_white="1/1/13",
+            group_address_switch_white_state="1/1/14",
+            group_address_brightness_white="1/1/15",
+            group_address_brightness_white_state="1/1/16",
+        )
+        self.assertEqual(light.current_color, (None, None))
+        telegram = Telegram(
+            destination_address=GroupAddress("1/1/16"),
+            payload=GroupValueWrite(DPTArray(42)),
+        )
+        self.loop.run_until_complete(light.process(telegram))
+        self.assertEqual(light.current_color, (None, 42))
+
     def test_process_tunable_white(self):
         """Test process / reading telegrams from telegram queue. Test if tunable white is processed."""
         xknx = XKNX()
@@ -786,6 +1335,22 @@ class TestLight(unittest.TestCase):
             group_address_color_temperature_state="1/7/10",
             group_address_rgbw="1/7/11",
             group_address_rgbw_state="1/7/12",
+            group_address_switch_red="1/1/1",
+            group_address_switch_red_state="1/1/2",
+            group_address_brightness_red="1/1/3",
+            group_address_brightness_red_state="1/1/4",
+            group_address_switch_green="1/1/5",
+            group_address_switch_green_state="1/1/6",
+            group_address_brightness_green="1/1/7",
+            group_address_brightness_green_state="1/1/8",
+            group_address_switch_blue="1/1/9",
+            group_address_switch_blue_state="1/1/10",
+            group_address_brightness_blue="1/1/11",
+            group_address_brightness_blue_state="1/1/12",
+            group_address_switch_white="1/1/13",
+            group_address_switch_white_state="1/1/14",
+            group_address_brightness_white="1/1/15",
+            group_address_brightness_white_state="1/1/16",
         )
         self.assertTrue(light.has_group_address(GroupAddress("1/7/1")))
         self.assertTrue(light.has_group_address(GroupAddress("1/7/2")))
@@ -799,4 +1364,22 @@ class TestLight(unittest.TestCase):
         self.assertTrue(light.has_group_address(GroupAddress("1/7/10")))
         self.assertTrue(light.has_group_address(GroupAddress("1/7/11")))
         self.assertTrue(light.has_group_address(GroupAddress("1/7/12")))
+
+        self.assertTrue(light.has_group_address(GroupAddress("1/1/1")))
+        self.assertTrue(light.has_group_address(GroupAddress("1/1/2")))
+        self.assertTrue(light.has_group_address(GroupAddress("1/1/3")))
+        self.assertTrue(light.has_group_address(GroupAddress("1/1/4")))
+        self.assertTrue(light.has_group_address(GroupAddress("1/1/5")))
+        self.assertTrue(light.has_group_address(GroupAddress("1/1/6")))
+        self.assertTrue(light.has_group_address(GroupAddress("1/1/7")))
+        self.assertTrue(light.has_group_address(GroupAddress("1/1/8")))
+        self.assertTrue(light.has_group_address(GroupAddress("1/1/9")))
+        self.assertTrue(light.has_group_address(GroupAddress("1/1/10")))
+        self.assertTrue(light.has_group_address(GroupAddress("1/1/11")))
+        self.assertTrue(light.has_group_address(GroupAddress("1/1/12")))
+        self.assertTrue(light.has_group_address(GroupAddress("1/1/13")))
+        self.assertTrue(light.has_group_address(GroupAddress("1/1/14")))
+        self.assertTrue(light.has_group_address(GroupAddress("1/1/15")))
+        self.assertTrue(light.has_group_address(GroupAddress("1/1/16")))
+
         self.assertFalse(light.has_group_address(GroupAddress("1/7/13")))
diff --git a/test/io_tests/tunnel_test.py b/test/io_tests/tunnel_test.py
index 7f93147c..ab7ba401 100644
--- a/test/io_tests/tunnel_test.py
+++ b/test/io_tests/tunnel_test.py
@@ -32,7 +32,7 @@ class TestTunnelling(unittest.TestCase):
         """Tear down test class."""
         self.loop.close()
 
-    @patch("xknx.io.Tunnel.send_ack")
+    @patch("xknx.io.Tunnel._send_tunnelling_ack")
     def test_tunnel_request_received(self, send_ack_mock):
         """Test Tunnel for calling send_ack on normal frames."""
         # LDataInd GroupValueWrite from 1.1.22 to to 5/1/22 with DPT9 payload 0C 3F
@@ -47,7 +47,7 @@ class TestTunnelling(unittest.TestCase):
         self.tg_received_mock.assert_called_once_with(telegram)
         send_ack_mock.assert_called_once_with(0x02, 0x21)
 
-    @patch("xknx.io.Tunnel.send_ack")
+    @patch("xknx.io.Tunnel._send_tunnelling_ack")
     def test_tunnel_request_received_unsupported_frames(self, send_ack_mock):
         """Test Tunnel sending ACK for unsupported frames."""
         # LDataInd APciPhysAddrRead from 0.0.1 to 0/0/0 broadcast - ETS scan for devices in programming mode
diff --git a/test/remote_value_tests/remote_value_test.py b/test/remote_value_tests/remote_value_test.py
index 4ac9071a..c68a97e1 100644
--- a/test/remote_value_tests/remote_value_test.py
+++ b/test/remote_value_tests/remote_value_test.py
@@ -11,6 +11,7 @@ from xknx.telegram import GroupAddress, Telegram
 from xknx.telegram.apci import GroupValueWrite
 
 
+@patch.multiple(RemoteValue, __abstractmethods__=set())
 class TestRemoteValue(unittest.TestCase):
     """Test class for RemoteValue objects."""
 
@@ -33,26 +34,6 @@ class TestRemoteValue(unittest.TestCase):
                 "'payload_valid()' not implemented for %s", "RemoteValue"
             )
 
-    def test_warn_to_knx(self):
-        """Test for warning if to_knx is not implemented."""
-        xknx = XKNX()
-        remote_value = RemoteValue(xknx)
-        with patch("logging.Logger.warning") as mock_warn:
-            remote_value.to_knx(23)
-            mock_warn.assert_called_with(
-                "'to_knx()' not implemented for %s", "RemoteValue"
-            )
-
-    def test_warn_from_knx(self):
-        """Test for warning if from_knx is not implemented."""
-        xknx = XKNX()
-        remote_value = RemoteValue(xknx)
-        with patch("logging.Logger.warning") as mock_warn:
-            remote_value.from_knx(DPTBinary(0))
-            mock_warn.assert_called_with(
-                "'from_knx()' not implemented for %s", "RemoteValue"
-            )
-
     def test_info_set_uninitialized(self):
         """Test for info if RemoteValue is not initialized."""
         xknx = XKNX()
diff --git a/test/str_test.py b/test/str_test.py
index 2d95776a..335ad9e7 100644
--- a/test/str_test.py
+++ b/test/str_test.py
@@ -1,6 +1,7 @@
 """Unit test for String representations."""
 import asyncio
 import unittest
+from unittest.mock import patch
 
 from xknx import XKNX
 from xknx.devices import (
@@ -72,9 +73,11 @@ class TestStringRepresentations(unittest.TestCase):
         """Tear down test class."""
         self.loop.close()
 
+    @patch.multiple(RemoteValue, __abstractmethods__=set())
     def test_remote_value(self):
         """Test string representation of remote value."""
         xknx = XKNX()
+        # pylint: disable=abstract-class-instantiated
         remote_value = RemoteValue(
             xknx,
             group_address="1/2/3",