Repository: XKNX/xknx
Pull request #451 (2020-10-06 16:07:13)

Support Python 3.9
Adds CI support for python 3.9

marvin-w: Still waiting for Travis CI support. (No docker image available yet)
marvin-w: Waiting for pyenv release that supports python 3.9.

Patch:
diff --git a/.travis.yml b/.travis.yml
index 1031d4e4..a3753b83 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,5 +1,4 @@
 os: linux
-dist: xenial
 language: python
 
 jobs:
@@ -12,8 +11,8 @@ jobs:
       env: TOXENV=py37
     - python: "3.8"
       env: TOXENV=py38
-    - python: "3.8.5"
-      env: TOXENV=py38
+    - python: "3.9"
+      env: TOXENV=py39
       after_success: coveralls
 
 cache:
diff --git a/changelog.md b/changelog.md
index cbbf8cb2..cec7d858 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,10 +1,32 @@
 # Changelog
 
-## Unreleased changes
+## 0.15.3 Opposite day! 2020-10-29
+
+### Devices
+
+- BinarySensor: added option to invert payloads
+- BinarySensor: `ignore_internal_state` and counter are only applied to GroupValueWrite telegrams, not GroupValueRespond.
+- BinarySensor: if `context_timeout` is set `ignore_internal_state` is set to True.
+- Switch: added option to invert payloads
+
+### Bugfixes
+
+- HA Switch entity: keep state without state_address
+- Cover: fix `set_position` without writable position / auto_stop_if_necessary
+- handle unsupported CEMI Messages without loosing tunnel connection
+
+## 0.15.2 Winter is coming
+
+### Devices
+
+- ClimateMode: Refactor climate modes in operation_mode and controller_mode, also fixes a bug for binary operation modes where the mode would be set to AWAY no matter what value was sent to the bus.
+- Sensor: Add `always_callback` option
+- Switch: Allow resetting switches after x seconds with the new `reset_after` option.
 
 ### Internals
 
-- StateUpdater: Only request one GA at a time.
+- StateUpdater: Only request 3 GAs at a time.
+- RemoteValue: Add support for passive group addresses
 
 ## 0.15.1 bugfix for binary sensors
 
diff --git a/docs/binary_sensor.md b/docs/binary_sensor.md
index 9c9041d3..e38bdccc 100644
--- a/docs/binary_sensor.md
+++ b/docs/binary_sensor.md
@@ -19,14 +19,18 @@ The logic within switches can further handle if a button is pressed once or twic
 binarysensor = BinarySensor(xknx, 'TestInput', group_address_state='1/2/3', device_class='motion')
 ```
 
+## [](#header-2)Interface
+
 - `xknx` is the XKNX object.
 - `name` is the name of the object.
 - `group_address_state` is the KNX group address of the sensor device.
+- `invert` inverts the payload so state "on" is represented by 0 on bus and "off" by 1. Defaults to `False`
 - `sync_state` defines if the value should be actively read from the bus. If `False` no GroupValueRead telegrams will be sent to its group address. Defaults to `True`
-- `ignore_internal_state` allows callback call regardless of the current binary sensor state. Defaults to `True`
-- `context_timeout` time in seconds telegrams should be counted towards the current context to increment the counter. To be used with `ignore_internal_state=True`. Defaults to `None`
+- `ignore_internal_state` allows callback call regardless of the current binary sensor state. Defaults to `False`
+- `context_timeout` time in seconds telegrams should be counted towards the current context to increment the counter. If set `ignore_internal_state` is set `True`. Defaults to `None`
 - `reset_after` may be used to reset the internal state to `OFF` again after given time in sec. Defaults to `None`
-- `device_class` may be used to store the type of sensor, e.g. "motion" for motion detectors.
+- `device_class` may be used to store the type of sensor, e.g. "motion" for motion detectors for Home-Assistant.
+- `device_updated_cb` awaitable callback for each update.
 
 ## [](#header-2)Example
 
diff --git a/docs/climate.md b/docs/climate.md
index c8ab401c..c3f10dd5 100644
--- a/docs/climate.md
+++ b/docs/climate.md
@@ -11,6 +11,88 @@ nav_order: 2
 
 Climate are representations of KNX HVAC/Climate controls.
 
+## [](#header-2)Interface
+
+- `xknx` is the XKNX object.
+- `name` is the name of the object.
+- `group_address_temperature` KNX address of current room temperature.
+- `group_address_target_temperature` KNX address for setting the target temperature if setpoint shift is not supported.
+- `group_address_target_temperature_state` KNX address for reading the target temperature from the KNX bus. Used in for setpoint_shift calculations as base temperature.
+- `group_address_setpoint_shift` KNX address to set setpoint_shift (base temperature deviation).
+- `group_address_setpoint_shift_state` KNX address to read current setpoint_shift.
+- `setpoint_shift_mode` Enum for setpoint_shift payload encoding. Can be *DPT 6.010* or *DPT 9.002*. Default: SetpointShiftMode.DPT6010
+- `setpoint_shift_max` Maximum value for setpoint_shift.
+- `setpoint_shift_min` Minimum value for setpoint_shift.
+- `temperature_step` Set the multiplier for setpoint_shift (*DPT 6*) calculations and the step size for HA UI-elements.
+- `group_address_on_off` KNX address for turning climate device on or off.
+- `group_address_on_off_state` KNX address for reading the on/off state.
+- `on_off_invert` Invert on/off.
+- `max_temp` Maximum value for target temperature.
+- `min_temp` Minimum value for target temperature.
+- `mode` ClimateMode instance for this climate device
+- `group_address_operation_mode` KNX address for operation mode. *DPT 20.102*
+- `group_address_operation_mode_state` KNX address for operation mode status. *DPT 20.102*
+- `group_address_operation_mode_protection` KNX address for switching on/off frost/heat protection mode. *DPT 1*
+- `group_address_operation_mode_night` KNX address for switching on/off night nmode. *DPT 1*
+- `group_address_operation_mode_comfort` KNX address for switching on/off comfort mode. *DPT 1*
+- `group_address_operation_mode_standby` KNX address for switching on/off standby mode. *DPT 1*
+- `group_address_controller_status` KNX address for controller status.
+- `group_address_controller_status_state` KNX address for controller status state.
+- `group_address_controller_mode` KNX address for controller mode. *DPT 20.105*
+- `group_address_controller_mode_state` KNX address for controller mode status. *DPT 20.105*
+- `group_address_heat_cool` KNX address for switching heating / cooling mode. *DPT 1*
+- `group_address_heat_cool_state` KNX address for reading heating / cooling mode. *DPT 1*
+- `operation_modes` Overrides the supported operation modes.
+- `controller_modes` Overrides the supported controller modes.
+- `device_updated_cb` awaitable callback for each update.
+
+**Note:** `group_address_operation_mode_protection` / `group_address_operation_mode_night` / `group_address_operation_mode_comfort` / `group_address_operation_mode_standby` are not necessary if `group_address_operation_mode` was specified. When one of these is set `True`, the others will be set `False`. When one of these is set `Standby`, `Comfort`, `Frost_Protection` and `Night` will be set as supported. If `group_address_operation_mode_standby` is omitted, `Standby` is set when the other 3 are set to `False`.
+If only a subset of operation modes shall be used a list of supported modes may be passed to `operation_modes`.
+
+```python
+climate_mode = ClimateMode(xknx,
+                 'TestClimateMode',
+                 group_address_operation_mode='',
+                 group_address_operation_mode_state='',
+                 group_address_operation_mode_protection=None,
+                 group_address_operation_mode_night=None,
+                 group_address_operation_mode_comfort=None,
+                 group_address_controller_status=None,
+                 group_address_controller_status_state=None,
+                 group_address_controller_mode=None,
+                 group_address_controller_mode_state=None,
+                 operation_modes=None,
+                 controller_modes=None,
+                 device_updated_cb=None)
+
+climate = Climate(
+        xknx,
+        'TestClimate',
+        group_address_temperature='',
+        group_address_target_temperature='',
+        group_address_target_temperature_state='',
+        group_address_setpoint_shift='',
+        group_address_setpoint_shift_state='',
+        temperature_step=0.1,
+        setpoint_shift_max=6,
+        setpoint_shift_min=-6,
+        group_address_on_off='',
+        group_address_on_off_state='',
+        on_off_invert=False,
+        min_temp=18,
+        max_temp=26,
+        mode=climate_mode,
+        device_updated_cb=None)
+
+# Set target temperature to 23 degrees. Works with setpoint_shift too.
+await climate.set_target_temperature(23)
+# Set new setpoint shift value.
+await climate.set_setpoint_shift(1)
+# Reading climate device
+await climate.sync()
+print("Current temperature: ", climate.temperature)
+```
+
 ## [](#header-2)Example
 
 ```python
@@ -58,80 +140,3 @@ groups:
         group_address_operation_mode_comfort: "1/7/10",
       }
 ```
-
-## [](#header-2)Interface
-
-- **group_address_temperature** KNX address of current room temperature.
-- **group_address_target_temperature** KNX address for setting the target temperature if setpoint shift is not supported.
-- **group_address_target_temperature_state** KNX address for reading the target temperature from the KNX bus. Used in for setpoint_shift calculations as base temperature.
-- **group_address_setpoint_shift** KNX address to set setpoint_shift (base temperature deviation).
-- **group_address_setpoint_shift_state** KNX address to read current setpoint_shift.
-- **setpoint_shift_max** Maximum value for setpoint_shift.
-- **setpoint_shift_min** Minimum value for setpoint_shift.
-- **temperature_step** Set the multiplier for setpoint_shift (DPT 6) calculations and the step size for HA UI-elements.
-- **group_address_on_off** KNX address for turning climate device on or off.
-- **group_address_on_off_state** KNX address for reading the on/off state.
-- **on_off_invert** Invert on/off.
-- **max_temp** Maximum value for target temperature.
-- **min_temp** Minimum value for target temperature.
-- **mode** ClimateMode instance for this climate device
-
-* **group_address_operation_mode** KNX address for operation mode. DPT 20.102
-* **group_address_operation_mode_state** KNX address for operation mode status. DPT 20.102
-* **group_address_operation_mode_protection** KNX address for switching on/off frost/heat protection mode. DPT 1
-* **group_address_operation_mode_night** KNX address for switching on/off night nmode. DPT 1
-* **group_address_operation_mode_comfort** KNX address for switching on/off comfort mode. DPT 1
-* **group_address_operation_mode_standby** KNX address for switching on/off standby mode. DPT 1
-* **group_address_controller_status** KNX address for controller status.
-* **group_address_controller_status_state** KNX address for controller status state.
-* **group_address_controller_mode** KNX address for controller mode. DPT 20.105
-* **group_address_controller_mode_state** KNX address for controller mode status. DPT 20.105
-* **group_address_heat_cool** KNX address for switching heating / cooling mode. DPT 1
-* **group_address_heat_cool_state** KNX address for reading heating / cooling mode. DPT 1
-* **operation_modes** Overrides the supported operation modes.
-
-**Note:** `group_address_operation_mode_protection` / `group_address_operation_mode_night` / `group_address_operation_mode_comfort` / `group_address_operation_mode_standby` are not necessary if `group_address_operation_mode` was specified. When one of these is set `True`, the others will be set `False`. When one of these is set "Standby", "Comfort", "Frost_Protection" and "Night" will be set as supported. If `group_address_operation_mode_standby` is omitted, "Standby" is set when the other 3 are set to `False`.
-If only a subset of operation modes shall be used a list of supported modes may be passed to `operation_modes`.
-
-```python
-climate_mode = ClimateMode(xknx,
-                 'TestClimateMode',
-                 group_address_operation_mode='',
-                 group_address_operation_mode_state='',
-                 group_address_operation_mode_protection=None,
-                 group_address_operation_mode_night=None,
-                 group_address_operation_mode_comfort=None,
-                 group_address_controller_status=None,
-                 group_address_controller_status_state=None,
-                 group_address_controller_mode=None,
-                 group_address_controller_mode_state=None,
-                 operation_modes=None,
-                 device_updated_cb=None)
-
-climate = Climate(
-        xknx,
-        'TestClimate',
-        group_address_temperature='',
-        group_address_target_temperature='',
-        group_address_target_temperature_state='',
-        group_address_setpoint_shift='',
-        group_address_setpoint_shift_state='',
-        temperature_step=0.1,
-        setpoint_shift_max=6,
-        setpoint_shift_min=-6,
-        group_address_on_off='',
-        group_address_on_off_state='',
-        on_off_invert=False,
-        min_temp=18,
-        max_temp=26,
-        mode=climate_mode,
-        device_updated_cb=None)
-
-# Set target temperature to 23 degrees. Works with setpoint_shift too.
-await climate.set_target_temperature(23)
-# Set new setpoint shift value.
-await climate.set_setpoint_shift(1)
-# Reading climate device
-await climate.sync()
-print("Current temperature: ", climate.temperature)
-```
diff --git a/docs/configuration.md b/docs/configuration.md
index dc496f19..839a15e4 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -117,7 +117,7 @@ groups:
         travel_time_up: 60,
       }
 
-    # Covers without direct positioning:
+    # Covers without direct positioning and device class "shutter":
     Livingroom.Shutter_3:
       {
         group_address_long: "1/4/9",
@@ -125,6 +125,7 @@ groups:
         group_address_position_feedback: "1/4/11",
         travel_time_down: 50,
         travel_time_up: 60,
+        device_class: "shutter",
       }
 
     # Central Shutters dont have short or position address
diff --git a/docs/cover.md b/docs/cover.md
index 8ecde5f4..beff650b 100644
--- a/docs/cover.md
+++ b/docs/cover.md
@@ -11,35 +11,25 @@ nav_order: 3
 
 Shutters are simple representations of blind/roller cover actuators. With XKNX you can move them up, down, to direct positions or stop them. Internally the class calculates the current position while traveling.
 
-## [](#header-2)Example
-
-```python
-cover = Cover(xknx,
-              'TestCover',
-              group_address_long='1/2/1',
-              group_address_short='1/2/2',
-              group_address_position='1/2/3',
-              group_address_position_state='1/2/4',
-              travel_time_down=50,
-              travel_time_up=60)
-
-# Accessing cover
-await xknx.devices['TestShutter'].set_up()
-```
-
-## [](#header-2)Configuration via **xknx.yaml**
-
-Covers are usually configured via [`xknx.yaml`](/configuration):
-
-```yaml
-groups:
-    cover:
-        Livingroom.Shutter_1: {group_address_long: '1/4/1', group_address_short: '1/4/2', group_address_position_feedback: '1/4/3', group_address_position: '1/4/4', travel_time_down: 50, travel_time_up: 60 }
-```
-
-
 ## [](#header-2)Interface
 
+- `xknx` XKNX object.
+- `name` name of the device.
+- `group_address_long` KNX group address to move the cover to an end position. *DPT 1.001*
+- `group_address_short` KNX group address to move the cover stepwise. *DPT 1.001*
+- `group_address_stop` KNX group address to stop movement. *DPT 1.001*
+- `group_address_position` KNX group address to move to relative position. *DPT 5.001*
+- `group_address_position_state` KNX group address for retrieving the relative position. *DPT 5.001*
+- `group_address_angle` KNX group address to tilt blinds to relative position. *DPT 5.001*
+- `group_address_angle_state` KNX group address to retrieve angle of blinds. *DPT 5.001*
+- `travel_time_down` seconds to reach lower end position. Default: 22
+- `travel_time_up` seconds to reach upper end position. Default: 22
+- `invert_position` invert position (payload for eg. set_up() and relative position). Default: False
+- `invert_angle` invert angle. Default: False
+- `device_class` may be used to store the type of cover, e.g. "shutter" for Home-Assistant (see [cover documentation](https://www.home-assistant.io/integrations/cover/) for details).
+- `device_updated_cb` awaitable callback for each update.
+
+## [](#header-2)Example
 
 ```python
 cover = Cover(xknx,
@@ -53,7 +43,8 @@ cover = Cover(xknx,
               travel_time_down=50,
               travel_time_up=60,
               invert_position=False,
-              invert_angle=False)
+              invert_angle=False,
+              device_class='shutter')
 
 # Moving to up position
 await cover.set_up()
@@ -86,9 +77,12 @@ angle = cover.current_angle()
 is_traveling = cover.is_traveling()
 position_reached = cover.position_reached()
 
-# Helper functions to see if cover is fully closed or fully open
+# Helper functions to see if cover is fully closed or fully open or moving
 is_open = cover.is_open()
 is_closed = cover.is_closed()
+is_traveling = cover.is_traveling()
+is_opening = cover.is_opening()
+is_closing = cover.is_closing()
 
 # Accessing cover via 'do'
 await cover.do('up')
@@ -100,4 +94,12 @@ await cover.do('short_down')
 await cover.sync()
 ```
 
+## [](#header-2)Configuration via **xknx.yaml**
+
+Covers are usually configured via [`xknx.yaml`](/configuration):
 
+```yaml
+groups:
+    cover:
+        Livingroom.Shutter_1: {group_address_long: '1/4/1', group_address_short: '1/4/2', group_address_position_feedback: '1/4/3', group_address_position: '1/4/4', travel_time_down: 50, travel_time_up: 60 }
+```
diff --git a/docs/home_assistant.md b/docs/home_assistant.md
index d7cc5171..2482f75e 100644
--- a/docs/home_assistant.md
+++ b/docs/home_assistant.md
@@ -9,7 +9,7 @@ nav_order: 13
 Home Assistant KNX Integration
 ========================
 
-XKNX is shipped within [Home Assistant (HA)](https://www.home-assistant.io/), the great solution for home automation, in the form of the included [KNX integration](https://www.home-assistant.io/integrations/#search/KNX).
+XKNX is shipped within [Home Assistant (HA)](https://www.home-assistant.io/), the great solution for home automation, in the form of the included [KNX integration](https://www.home-assistant.io/integrations/knx/).
 
 For development and debugging reasons - or to catch up with the newest features - you may still want to use the custom component version of the plugin.
 
@@ -57,25 +57,21 @@ Starting via service is also possible, but you have to change the configuration
 Configuration:
 --------------
 
-The configuration for the manually checked out version works the same as described within [Home Assistant KNX documentation](https://home-assistant.io/integrations/#search/knx) with the difference that the integration and platform is called `xknx` instead of `knx` (which is the HA default KNX integration and platform name).
+The configuration for the manually checked out version works the same as described within [Home Assistant KNX documentation](https://www.home-assistant.io/integrations/knx/) with the difference that the integration and platform is called `xknx` instead of `knx` (which is the HA default KNX integration and platform name).
 
 ```yaml
 xknx:
   tunneling:
     host: '192.168.2.23'
-    port: 3671
-    local_ip: '192.168.2.109'
-
-light:
-  - platform: xknx
-    name: Kitchen-Light-1
-    address: '1/0/9'
-    brightness_address: '1/0/11'
-
-switch:
-  - platform: xknx
-    name: Kitchen.Coffee
-    address: '1/1/6'
+
+  light:
+    - name: Kitchen-Light-1
+      address: '1/0/9'
+      brightness_address: '1/0/11'
+
+  switch:
+    - name: Kitchen.Coffee
+      address: '1/1/6'
 ```
 
 
diff --git a/docs/index.md b/docs/index.md
index cf4fb05f..fc1fedd7 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -52,17 +52,13 @@ async def main():
     await asyncio.sleep(2)
     await xknx.stop()
 
-
-# pylint: disable=invalid-name
-loop = asyncio.get_event_loop()
-loop.run_until_complete(main())
-loop.close()
+asyncio.run(main())
 ```
 
 For more examples please check out the [examples page](https://github.com/XKNX/xknx/tree/master/examples)
 
 # [](#header-1)Getting Help
 
-For questions, feature requests, bugreports wither join the [XKNX chat on Discord](https://discord.gg/EuAQDXU) or write an [email](mailto:xknx@xknx.io).
+For questions, feature requests, bugreports either join the [XKNX chat on Discord](https://discord.gg/EuAQDXU), open an issue at [GitHub](https://github.com/XKNX/xknx) or write an [email](mailto:xknx@xknx.io).
 
 
diff --git a/docs/introduction.md b/docs/introduction.md
index 992539ec..2cb85087 100644
--- a/docs/introduction.md
+++ b/docs/introduction.md
@@ -25,9 +25,7 @@ async def main():
 
     await xknx.stop()
 
-loop = asyncio.get_event_loop()
-loop.run_until_complete(main())
-loop.close()
+asyncio.run(main())
 ```
 
 # [](#header-2)Explanation En Dé­tail:
@@ -57,16 +55,6 @@ Asynchronous start of the XKNX object. `xknx.start()` will connect to a KNX/IP d
 
 Asynchronous stop of the XKNX object. `xknx.stop()` will disconnect from Tunnels - which is important bc most of the devices have a limited amount of channels.
 
-```
-loop = asyncio.get_event_loop()
-loop.run_until_complete(main())
-loop.close()
-```
-
-Boilerplate code, for starting an asynchonous function. See [asyncio](https://www.python.org/dev/peps/pep-3156/) documentation for details.
-
-
-_Important Note:_ If you are using Python 3.5 you have to replace the `await` syntax with the `@asyncio.coroutine` syntax.
 
 
 
diff --git a/docs/light.md b/docs/light.md
index b71d43c4..4ba00f2b 100644
--- a/docs/light.md
+++ b/docs/light.md
@@ -11,56 +11,27 @@ nav_order: 4
 
 The Light object is either a representation of a binary or dimm actor, LED-controller or DALI-gateway.
 
-Expected datapoint types for light functions and their corresponding state addresses:
-- switch: DPT 1.001
-- brightness: DPT 5.001
-- color: DPT 232.600
-- rgbw: DPT 251.600
-- tunable_white: DPT 5.001
-- color_temperature: DPT 7.600
-
-## [](#header-2)Example
-
-```python
-light = Light(xknx,
-              name='TestLight',
-              group_address_switch='1/2/3',
-              group_address_brightness='1/2/5')
-
-# Accessing light
-await xknx.devices['TestLight'].set_on()
-await xknx.devices['TestLight'].set_brightness(23)
-```
-
-## [](#header-2)Configuration via **xknx.yaml**
-
-Lights are usually configured via [`xknx.yaml`](/configuration):
-
-```yaml
-groups:
-    light:
-
-        # Lights with dimming
-        Kitchen.Light_1:     {group_address_switch: '1/6/1', group_address_brightness: '1/6/3'}
-        Diningroom.Light_1:  {group_address_switch: '1/6/4', group_address_brightness: '1/6/6'}
-
-        # Light without dimming
-        Living-Room.Light_1: {group_address_switch: '1/6/7'}
-
-        # Light with extra addresses for states:
-        Office.Light_1:  {group_address_switch: '1/7/4', group_address_switch_state: '1/7/5', group_address_brightness: '1/7/6', group_address_brightness_state: '1/7/7'}
-
-        # Light with color temperature in Kelvin
-        Living-Room.Light_CT:  {group_address_switch: '1/6/11', group_address_switch_state: '1/6/10', group_address_brightness: '1/6/12', group_address_brightness_state: '1/6/13', group_address_color_temperature: '1/6/14',  group_address_color_temperature_state: '1/6/15'}
-
-        # 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'}
-
-```
-
-
 ## [](#header-2)Interface
 
+- `xknx` XKNX object.
+- `name` name of the device.
+- `group_address_switch` KNX group address to switch the light. *DPT 1.001*
+- `group_address_switch_state` KNX group address for the state of the light. *DPT 1.001*
+- `group_address_brightness` KNX group address to set the brightness. *DPT 5.001*
+- `group_address_brightness_state` KNX group address for the current brightness state. *DPT 5.001*
+- `group_address_color` KNX group address to set the RGB color. *DPT 232.600*
+- `group_address_color_state` KNX group address for the current RGB color. *DPT 232.600*
+- `group_address_rgbw` KNX group address to set the RGBW color. *DPT 251.600*
+- `group_address_rgbw_state` KNX group address for the current RGBW color. *DPT 251.600*
+- `group_address_tunable_white` KNX group address to set relative color temperature. *DPT 5.001*
+- `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*
+- `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.
+
+## [](#header-2)Example
 
 ```python
 light = Light(xknx,
@@ -122,4 +93,29 @@ print(light.current_color_temperature)
 await light.sync()
 ```
 
+## [](#header-2)Configuration via **xknx.yaml**
+
+Lights are usually configured via [`xknx.yaml`](/configuration):
+
+```yaml
+groups:
+    light:
+
+        # Lights with dimming
+        Kitchen.Light_1:     {group_address_switch: '1/6/1', group_address_brightness: '1/6/3'}
+        Diningroom.Light_1:  {group_address_switch: '1/6/4', group_address_brightness: '1/6/6'}
+
+        # Light without dimming
+        Living-Room.Light_1: {group_address_switch: '1/6/7'}
+
+        # Light with extra addresses for states:
+        Office.Light_1:  {group_address_switch: '1/7/4', group_address_switch_state: '1/7/5', group_address_brightness: '1/7/6', group_address_brightness_state: '1/7/7'}
+
+        # Light with color temperature in Kelvin
+        Living-Room.Light_CT:  {group_address_switch: '1/6/11', group_address_switch_state: '1/6/10', group_address_brightness: '1/6/12', group_address_brightness_state: '1/6/13', group_address_color_temperature: '1/6/14',  group_address_color_temperature_state: '1/6/15'}
+
+        # 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'}
+
+```
 
diff --git a/docs/sensor.md b/docs/sensor.md
index c741e111..4fa91ba4 100644
--- a/docs/sensor.md
+++ b/docs/sensor.md
@@ -11,24 +11,34 @@ nav_order: 5
 
 Sensors are monitoring temperature, air humidity, pressure etc. from KNX bus.
 
-```python
-    sensor = Sensor(
-        xknx=xknx,
-        name='DiningRoom.Temperature.Sensor',
-        group_address_state='6/2/1',
-        sync_state=True,
-        value_type='temperature'
-    )
-    await sensor.sync()
-    print(sensor)
-```
+## [](#header-2)Interface
 
 * `xknx` is the XKNX object.
 * `name` is the name of the object.
 * `group_address_state` is the KNX group address of the sensor device.
 * `sync_state` defines if the value should be actively read from the bus. If `False` no GroupValueRead telegrams will be sent to its group address. Defaults to `True`
+* `always_callback` defines if a callback/update should always be triggered no matter if the previous and the new state are identical.
 * `value_type` controls how the value should be rendered in a human readable representation. The attribut may have may have the values `percent`, `temperature`, `illuminance`, `speed_ms` or `current`.
+* `device_updated_cb` awaitable callback for each update.
+
+## [](#header-2)Example
+
+```python
+sensor = Sensor(
+    xknx=xknx,
+    name='DiningRoom.Temperature.Sensor',
+    always_callback=False,
+    group_address_state='6/2/1',
+    sync_state=True,
+    value_type='temperature'
+)
+
+await sensor.sync() # Syncs the state. Tries to read the corresponding value from the bus.
 
+sensor.resolve_state() # Returns the value of in a human readable way
+
+sensor.unit_of_measurement() # returns the unit of the value in a human readable way
+```
 
 ## [](#header-2)Configuration via **xknx.yaml**
 
@@ -42,19 +52,6 @@ Sensor objects are usually configured via [`xknx.yaml`](/configuration):
         Some.Other.Value: {group_address_state: '2/0/3'}
 ```
 
-## [](#header-2)Interface
-
-```python
-sensor = Sensor(
-        xknx=xknx,
-        name='DiningRoom.Temperature.Sensor',
-        group_address_state='6/2/1')
 
-await sensor.sync() # Syncs the state. Tries to read the corresponding value from the bus.
-
-sensor.resolve_state() # Returns the value of in a human readable way
-
-sensor.unit_of_measurement() # returns the unit of the value in a human readable way
-```
 
 
diff --git a/docs/switch.md b/docs/switch.md
index df918a3a..46275311 100644
--- a/docs/switch.md
+++ b/docs/switch.md
@@ -11,35 +11,27 @@ nav_order: 6
 
 Switches are simple representations of binary actors. They mainly support switching on and off.
 
-## [](#header-2)Example
-
-```python
-switch = Switch(xknx, 'TestOutlet', group_address='1/2/3')
-
-# Accessing switch
-await xknx.devices['TestOutlet'].set_on()
-```
-
-## [](#header-2)Configuration via **xknx.yaml**
-
-Switches are usually configured via [`xknx.yaml`](/configuration):
+## [](#header-2)Interface
 
-```yaml
-groups:
-    switch:
-        Livingroom.Outlet_1: {group_address: '1/3/1'}
-        Livingroom.Outlet_2: {group_address: '1/3/2'}
-```
+- `xknx` is the XKNX object.
+- `name` is the name of the object.
+- `group_address` is the KNX group address of the switch device. Used for sending.
+- `group_address_state` is the KNX group address of the switch state. Used for updating and reading state.
+- `invert` inverts the payload so state "on" is represented by 0 on bus and "off" by 1. Defaults to `False`
+- `reset_after` may be used to reset the switch to `OFF` again after given time in sec. Defaults to `None`
+- `device_updated_cb` awaitable callback for each update.
 
-## [](#header-2)Interface
 
+## [](#header-2)Example
 
 ```python
 switch = Switch(xknx, 'TestOutlet', group_address='1/2/3')
 
+# Accessing switch via xknx.devices
+await xknx.devices['TestOutlet'].set_on()
+
 # Switching switch on
 await switch.set_on()
-
 # Switching switch off
 await switch.set_off()
 
@@ -50,8 +42,21 @@ await switch.do('off')
 # Accessing state
 print(switch.state)
 
-# Requesting state via KNX GROUP WRITE
-await switch.sync()
+# Requesting state via KNX GroupValueRead
+await switch.sync(wait_for_result=True)
+```
+
+## [](#header-2)Configuration via **xknx.yaml**
+
+Switches are usually configured via [`xknx.yaml`](/configuration):
+
+```yaml
+groups:
+    switch:
+        Livingroom.Outlet_1: {group_address: '1/3/1'}
+        Livingroom.Outlet_2: {group_address: '1/3/2'}
 ```
 
 
+
+
diff --git a/docs/time.md b/docs/time.md
index d7f0dab5..b47605a5 100644
--- a/docs/time.md
+++ b/docs/time.md
@@ -30,6 +30,7 @@ await xknx.devices['TimeTest'].sync()
 * `group_address` is the KNX group address of the sensor device.
 * `broadcast_type` defines the value type that will be sent to the KNX bus. Valid attributes are: 'time', 'date' and 'datetime'. Default: `time`
 * `localtime` If set `True` sync() and GroupValueRead requests always return the current local time. On `False` the set value will be sent. Default: `True`
+* `device_updated_cb` awaitable callback for each update.
 
 
 ## [](#header-2)Configuration via **xknx.yaml**
@@ -52,16 +53,11 @@ from xknx import XKNX
 from xknx.devices import DateTime
 
 async def main():
-    xknx = XKNX()
-    time = DateTime(xknx, 'TimeTest', group_address='1/2/3')
-    print("Sending time to KNX bus every hour")
-    await xknx.start(daemon_mode=True, state_updater=True)
-    await xknx.stop()
-
-# pylint: disable=invalid-name
-loop = asyncio.get_event_loop()
-loop.run_until_complete(main())
-loop.close()
+    async with XKNX(daemon_mode=True) as xknx:
+        time = DateTime(xknx, 'TimeTest', group_address='1/2/3')
+        print("Sending time to KNX bus every hour")
+
+asyncio.run(main())
 ```
 
 ## [](#header-2)Interface
@@ -74,6 +70,7 @@ from xknx.devices import DateTime
 xknx = XKNX()
 time_device = DateTime(xknx, 'TimeTest', group_address='1/2/3', broadcast_type='time')
 
+await xknx.start()
 # Sending Time to KNX bus
 await time_device.broadcast_localtime()
 ```
diff --git a/docs/weather.md b/docs/weather.md
index bf8905e3..b81f8be4 100644
--- a/docs/weather.md
+++ b/docs/weather.md
@@ -15,26 +15,21 @@ The weather device is basically a set of sensors that you can obtain from your w
 
 ```python
 
-    xknx = XKNX()
-    await xknx.start()
-
-    weather = Weather(
-        xknx,
-        'Home',
-        group_address_temperature='7/0/1',
-        group_address_brightness_south='7/0/5',
-        group_address_brightness_east='7/0/4',
-        group_address_brightness_west='7/0/3',
-        group_address_wind_speed='7/0/2',
-        group_address_day_night='7/0/7',
-        group_address_rain_alarm='7/0/0'
-    )
-
-    await weather.sync()
-    await asyncio.sleep(10) # wait for bus responses
-    print(weather)
-
-    await xknx.stop()
+    async with XKNX() as xknx:
+        weather = Weather(
+            xknx,
+            'Home',
+            group_address_temperature='7/0/1',
+            group_address_brightness_south='7/0/5',
+            group_address_brightness_east='7/0/4',
+            group_address_brightness_west='7/0/3',
+            group_address_wind_speed='7/0/2',
+            group_address_day_night='7/0/7',
+            group_address_rain_alarm='7/0/0'
+        )
+
+        await weather.sync(wait_for_result=True)
+        print(weather)
 
 ```
 
@@ -63,6 +58,8 @@ groups:
 
 ## [](#header-2)Interface
 
+- **xknx** is the XKNX object.
+- **name** is the name of the object.
 - **group_address_temperature** KNX address of current outside temperature.
 - **group_address_brightness_south** KNX address for the brightness to south **DPT 9.004**.
 - **group_address_brightness_west** KNX address for the brightness to west **DPT 9.004**.
@@ -76,32 +73,28 @@ groups:
 - **group_address_humidity** KNX address for reading current humidity. **DPT 9.007**
 - **expose_sensors** If true, also exposes all values as sensors to the xknx device list (useful for home assistant). Default: False
 - **sync_state** Periodically sync the state.
+- **device_updated_cb** awaitable callback for each update.
 
 ```python
 
-    xknx = XKNX()
-    await xknx.start()
-
-    weather = Weather(
-        xknx,
-        'Home',
-        group_address_temperature='7/0/1',
-        group_address_brightness_south='7/0/5',
-        group_address_brightness_east='7/0/4',
-        group_address_brightness_west='7/0/3',
-        group_address_wind_speed='7/0/2',
-        group_address_day_night='7/0/7',
-        group_address_rain_alarm='7/0/0'
-    )
-
-    await weather.sync()
-    await asyncio.sleep(10) # wait for bus responses
-
-    print(weather.humidity) # get humidity
-    print(weather.ha_current_state()) # get the current state mapped as a WeatherCondition enum value. (for HA mainly)
-    print(weather.wind_speed) # get the current wind speed in m/s
-
-    await xknx.stop()
+    async with XKNX() as xknx:
+        weather = Weather(
+            xknx,
+            'Home',
+            group_address_temperature='7/0/1',
+            group_address_brightness_south='7/0/5',
+            group_address_brightness_east='7/0/4',
+            group_address_brightness_west='7/0/3',
+            group_address_wind_speed='7/0/2',
+            group_address_day_night='7/0/7',
+            group_address_rain_alarm='7/0/0'
+        )
+
+        await weather.sync(wait_for_result=True)
+
+        print(weather.humidity) # get humidity
+        print(weather.ha_current_state()) # get the current state mapped as a WeatherCondition enum value. (for HA mainly)
+        print(weather.wind_speed) # get the current wind speed in m/s
 
 ```
 
diff --git a/docs/xknx.md b/docs/xknx.md
index 20bbc4c1..57427120 100644
--- a/docs/xknx.md
+++ b/docs/xknx.md
@@ -71,25 +71,7 @@ Will disconnect from tunneling devices and stop the different queues.
 
 # [](#header-2)Using XKNX as an asynchronous context manager
 
-Except of writing:
-
-```python
-import asyncio
-
-async def main():
-    xknx = XKNX()
-    await xknx.start()
-    switch = Switch(xknx,
-                name='TestSwitch',
-                group_address='1/1/11')
-
-    await switch.set_on()
-    await xknx.stop()
-
-asyncio.run(main())
-```
-
-you can now use the following syntax:
+You can also use an asynchronous context manager instead of calling `xknx.start()` and `xknx.stop()`:
 
 ```python
 import asyncio
@@ -107,7 +89,7 @@ asyncio.run(main())
 
 # [](#header-2)Devices
 
-The XKNX may keep all devices in a local storage named `devices`. All devices may be accessed by their name: `xknx.devices['NameOfDevice']`. If XKNX receives an update via KNX GROUP WRITE the device is updated automatically.
+XKNX keeps all initialized devices in a local storage named `devices`. All devices may be accessed by their name: `xknx.devices['NameOfDevice']`. When an update via KNX GroupValueWrite or GroupValueResponse was received devices will be updated accordingly.
 
 Example:
 
@@ -123,7 +105,7 @@ await xknx.devices['TestSwitch'].set_off()
 
 # [](#header-2)Callbacks
 
-The `telegram_received_cb` will be called for each KNX telegram received by the XKNX daemon. Example:
+An awaitable `telegram_received_cb` will be called for each KNX telegram received by the XKNX daemon. Example:
 
 ```python
 import asyncio
@@ -133,14 +115,11 @@ async def telegram_received_cb(telegram):
     print("Telegram received: {0}".format(telegram))
 
 async def main():
-    xknx = XKNX(telegram_received_cb=telegram_received_cb)
-    await xknx.start(daemon_mode=True)
+    xknx = XKNX(telegram_received_cb=telegram_received_cb, daemon_mode=True)
+    await xknx.start()
     await xknx.stop()
 
-# pylint: disable=invalid-name
-loop = asyncio.get_event_loop()
-loop.run_until_complete(main())
-loop.close()
+asyncio.run(main())
 ```
 
 For all devices stored in the `devices` storage (see [above](#devices)) a callback for each update may be defined:
@@ -156,19 +135,15 @@ async def device_updated_cb(device):
 
 
 async def main():
-    xknx = XKNX(device_updated_cb=device_updated_cb)
+    xknx = XKNX(device_updated_cb=device_updated_cb, daemon_mode=True)
     switch = Switch(xknx,
                     name='TestSwitch',
                     group_address='1/1/11')
 
-    await xknx.start(daemon_mode=True)
-
+    await xknx.start()
     await xknx.stop()
 
-# pylint: disable=invalid-name
-loop = asyncio.get_event_loop()
-loop.run_until_complete(main())
-loop.close()
+asyncio.run(main())
 ```
 
 
diff --git a/home-assistant-plugin/custom_components/xknx/climate.py b/home-assistant-plugin/custom_components/xknx/climate.py
index 1960627a..5d530faf 100644
--- a/home-assistant-plugin/custom_components/xknx/climate.py
+++ b/home-assistant-plugin/custom_components/xknx/climate.py
@@ -2,7 +2,7 @@
 from typing import List, Optional
 
 from xknx.devices import Climate as XknxClimate
-from xknx.dpt import HVACOperationMode
+from xknx.dpt import HVACControllerMode, HVACOperationMode
 
 from homeassistant.components.climate import ClimateEntity
 from homeassistant.components.climate.const import (
@@ -14,10 +14,10 @@ from homeassistant.components.climate.const import (
 )
 from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS
 
-from .const import DOMAIN, OPERATION_MODES, PRESET_MODES
+from .const import CONTROLLER_MODES, DOMAIN, PRESET_MODES
 from .knx_entity import KnxEntity
 
-OPERATION_MODES_INV = dict(reversed(item) for item in OPERATION_MODES.items())
+CONTROLLER_MODES_INV = dict(reversed(item) for item in CONTROLLER_MODES.items())
 PRESET_MODES_INV = dict(reversed(item) for item in PRESET_MODES.items())
 
 
@@ -92,27 +92,27 @@ class KNXClimate(KnxEntity, ClimateEntity):
         """Return current operation ie. heat, cool, idle."""
         if self._device.supports_on_off and not self._device.is_on:
             return HVAC_MODE_OFF
-        if self._device.mode.supports_operation_mode:
-            return OPERATION_MODES.get(
-                self._device.mode.operation_mode.value, HVAC_MODE_HEAT
+        if self._device.mode.supports_controller_mode:
+            return CONTROLLER_MODES.get(
+                self._device.mode.controller_mode.value, HVAC_MODE_HEAT
             )
         # default to "heat"
         return HVAC_MODE_HEAT
 
     @property
     def hvac_modes(self) -> Optional[List[str]]:
-        """Return the list of available operation modes."""
-        _operations = [
-            OPERATION_MODES.get(operation_mode.value)
-            for operation_mode in self._device.mode.operation_modes
+        """Return the list of available operation/controller modes."""
+        _controller_modes = [
+            CONTROLLER_MODES.get(controller_mode.value)
+            for controller_mode in self._device.mode.controller_modes
         ]
 
         if self._device.supports_on_off:
-            if not _operations:
-                _operations.append(HVAC_MODE_HEAT)
-            _operations.append(HVAC_MODE_OFF)
+            if not _controller_modes:
+                _controller_modes.append(HVAC_MODE_HEAT)
+            _controller_modes.append(HVAC_MODE_OFF)
 
-        _modes = list(set(filter(None, _operations)))
+        _modes = list(set(filter(None, _controller_modes)))
         # default to ["heat"]
         return _modes if _modes else [HVAC_MODE_HEAT]
 
@@ -123,11 +123,11 @@ class KNXClimate(KnxEntity, ClimateEntity):
         else:
             if self._device.supports_on_off and not self._device.is_on:
                 await self._device.turn_on()
-            if self._device.mode.supports_operation_mode:
-                knx_operation_mode = HVACOperationMode(
-                    OPERATION_MODES_INV.get(hvac_mode)
+            if self._device.mode.supports_controller_mode:
+                knx_controller_mode = HVACControllerMode(
+                    CONTROLLER_MODES_INV.get(hvac_mode)
                 )
-                await self._device.mode.set_operation_mode(knx_operation_mode)
+                await self._device.mode.set_controller_mode(knx_controller_mode)
         self.async_write_ha_state()
 
     @property
diff --git a/home-assistant-plugin/custom_components/xknx/const.py b/home-assistant-plugin/custom_components/xknx/const.py
index b5bdd447..55f74cc8 100644
--- a/home-assistant-plugin/custom_components/xknx/const.py
+++ b/home-assistant-plugin/custom_components/xknx/const.py
@@ -11,11 +11,13 @@ from homeassistant.components.climate.const import (
     PRESET_AWAY,
     PRESET_COMFORT,
     PRESET_ECO,
+    PRESET_NONE,
     PRESET_SLEEP,
 )
 
 DOMAIN = "xknx"
 
+CONF_INVERT = "invert"
 CONF_STATE_ADDRESS = "state_address"
 CONF_SYNC_STATE = "sync_state"
 CONF_RESET_AFTER = "reset_after"
@@ -42,8 +44,8 @@ class SupportedPlatforms(Enum):
     weather = "weather"
 
 
-# Map KNX operation modes to HA modes. This list might not be complete.
-OPERATION_MODES = {
+# Map KNX controller modes to HA modes. This list might not be complete.
+CONTROLLER_MODES = {
     # Map DPT 20.105 HVAC control modes
     "Auto": HVAC_MODE_AUTO,
     "Heat": HVAC_MODE_HEAT,
@@ -55,6 +57,7 @@ OPERATION_MODES = {
 
 PRESET_MODES = {
     # Map DPT 20.102 HVAC operating modes to HA presets
+    "Auto": PRESET_NONE,
     "Frost Protection": PRESET_ECO,
     "Night": PRESET_SLEEP,
     "Standby": PRESET_AWAY,
diff --git a/home-assistant-plugin/custom_components/xknx/cover.py b/home-assistant-plugin/custom_components/xknx/cover.py
index c677b12c..b88b1cfe 100644
--- a/home-assistant-plugin/custom_components/xknx/cover.py
+++ b/home-assistant-plugin/custom_components/xknx/cover.py
@@ -5,6 +5,7 @@ from homeassistant.components.cover import (
     ATTR_POSITION,
     ATTR_TILT_POSITION,
     DEVICE_CLASS_BLIND,
+    DEVICE_CLASSES,
     SUPPORT_CLOSE,
     SUPPORT_OPEN,
     SUPPORT_SET_POSITION,
@@ -47,6 +48,8 @@ class KNXCover(KnxEntity, CoverEntity):
     @property
     def device_class(self):
         """Return the class of this device, from component DEVICE_CLASSES."""
+        if self._device.device_class in DEVICE_CLASSES:
+            return self._device.device_class
         if self._device.supports_angle:
             return DEVICE_CLASS_BLIND
         return None
diff --git a/home-assistant-plugin/custom_components/xknx/factory.py b/home-assistant-plugin/custom_components/xknx/factory.py
index 9e721817..608ff40d 100644
--- a/home-assistant-plugin/custom_components/xknx/factory.py
+++ b/home-assistant-plugin/custom_components/xknx/factory.py
@@ -82,6 +82,7 @@ def _create_cover(knx_module: XKNX, config: ConfigType) -> XknxCover:
         travel_time_up=config[CoverSchema.CONF_TRAVELLING_TIME_UP],
         invert_position=config[CoverSchema.CONF_INVERT_POSITION],
         invert_angle=config[CoverSchema.CONF_INVERT_ANGLE],
+        device_class=config.get(CONF_DEVICE_CLASS),
     )
 
 
@@ -164,6 +165,7 @@ def _create_climate(knx_module: XKNX, config: ConfigType) -> XknxClimate:
             ClimateSchema.CONF_HEAT_COOL_STATE_ADDRESS
         ),
         operation_modes=config.get(ClimateSchema.CONF_OPERATION_MODES),
+        controller_modes=config.get(ClimateSchema.CONF_CONTROLLER_MODES),
     )
 
     return XknxClimate(
@@ -202,6 +204,7 @@ def _create_switch(knx_module: XKNX, config: ConfigType) -> XknxSwitch:
         name=config[CONF_NAME],
         group_address=config[CONF_ADDRESS],
         group_address_state=config.get(SwitchSchema.CONF_STATE_ADDRESS),
+        invert=config.get(SwitchSchema.CONF_INVERT),
         reset_after=config.get(SwitchSchema.CONF_RESET_AFTER),
     )
 
@@ -213,6 +216,7 @@ def _create_sensor(knx_module: XKNX, config: ConfigType) -> XknxSensor:
         name=config[CONF_NAME],
         group_address_state=config[SensorSchema.CONF_STATE_ADDRESS],
         sync_state=config[SensorSchema.CONF_SYNC_STATE],
+        always_callback=config[SensorSchema.CONF_ALWAYS_CALLBACK],
         value_type=config[CONF_TYPE],
     )
 
@@ -244,6 +248,7 @@ def _create_binary_sensor(knx_module: XKNX, config: ConfigType) -> XknxBinarySen
         knx_module,
         name=device_name,
         group_address_state=config[BinarySensorSchema.CONF_STATE_ADDRESS],
+        invert=config.get(BinarySensorSchema.CONF_INVERT),
         sync_state=config[BinarySensorSchema.CONF_SYNC_STATE],
         device_class=config.get(CONF_DEVICE_CLASS),
         ignore_internal_state=config[BinarySensorSchema.CONF_IGNORE_INTERNAL_STATE],
diff --git a/home-assistant-plugin/custom_components/xknx/schema.py b/home-assistant-plugin/custom_components/xknx/schema.py
index 89a6fac7..8affa81d 100644
--- a/home-assistant-plugin/custom_components/xknx/schema.py
+++ b/home-assistant-plugin/custom_components/xknx/schema.py
@@ -15,10 +15,11 @@ from homeassistant.const import (
 import homeassistant.helpers.config_validation as cv
 
 from .const import (
+    CONF_INVERT,
     CONF_RESET_AFTER,
     CONF_STATE_ADDRESS,
     CONF_SYNC_STATE,
-    OPERATION_MODES,
+    CONTROLLER_MODES,
     PRESET_MODES,
     ColorTempModes,
 )
@@ -76,6 +77,7 @@ class CoverSchema:
             ): cv.positive_int,
             vol.Optional(CONF_INVERT_POSITION, default=False): cv.boolean,
             vol.Optional(CONF_INVERT_ANGLE, default=False): cv.boolean,
+            vol.Optional(CONF_DEVICE_CLASS): cv.string,
         }
     )
 
@@ -85,6 +87,7 @@ class BinarySensorSchema:
 
     CONF_STATE_ADDRESS = CONF_STATE_ADDRESS
     CONF_SYNC_STATE = CONF_SYNC_STATE
+    CONF_INVERT = CONF_INVERT
     CONF_IGNORE_INTERNAL_STATE = "ignore_internal_state"
     CONF_CONTEXT_TIMEOUT = "context_timeout"
     CONF_RESET_AFTER = CONF_RESET_AFTER
@@ -108,6 +111,7 @@ class BinarySensorSchema:
                     vol.Coerce(float), vol.Range(min=0, max=10)
                 ),
                 vol.Optional(CONF_DEVICE_CLASS): cv.string,
+                vol.Optional(CONF_INVERT): cv.boolean,
                 vol.Optional(CONF_RESET_AFTER): cv.positive_int,
             }
         ),
@@ -188,6 +192,7 @@ class ClimateSchema:
     CONF_OPERATION_MODE_COMFORT_ADDRESS = "operation_mode_comfort_address"
     CONF_OPERATION_MODE_STANDBY_ADDRESS = "operation_mode_standby_address"
     CONF_OPERATION_MODES = "operation_modes"
+    CONF_CONTROLLER_MODES = "controller_modes"
     CONF_ON_OFF_ADDRESS = "on_off_address"
     CONF_ON_OFF_STATE_ADDRESS = "on_off_state_address"
     CONF_ON_OFF_INVERT = "on_off_invert"
@@ -241,7 +246,10 @@ class ClimateSchema:
                     CONF_ON_OFF_INVERT, default=DEFAULT_ON_OFF_INVERT
                 ): cv.boolean,
                 vol.Optional(CONF_OPERATION_MODES): vol.All(
-                    cv.ensure_list, [vol.In({**OPERATION_MODES, **PRESET_MODES})]
+                    cv.ensure_list, [vol.In({**PRESET_MODES})]
+                ),
+                vol.Optional(CONF_CONTROLLER_MODES): vol.All(
+                    cv.ensure_list, [vol.In({**CONTROLLER_MODES})]
                 ),
                 vol.Optional(CONF_MIN_TEMP): vol.Coerce(float),
                 vol.Optional(CONF_MAX_TEMP): vol.Coerce(float),
@@ -253,6 +261,7 @@ class ClimateSchema:
 class SwitchSchema:
     """Voluptuous schema for KNX switches."""
 
+    CONF_INVERT = CONF_INVERT
     CONF_STATE_ADDRESS = CONF_STATE_ADDRESS
     CONF_RESET_AFTER = CONF_RESET_AFTER
 
@@ -262,7 +271,8 @@ class SwitchSchema:
             vol.Required(CONF_ADDRESS): cv.string,
             vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
             vol.Optional(CONF_STATE_ADDRESS): cv.string,
-            vol.Optional(CONF_RESET_AFTER): cv.positive_float,
+            vol.Optional(CONF_INVERT): cv.boolean,
+            vol.Optional(CONF_RESET_AFTER): vol.All(vol.Coerce(int), vol.Range(min=0)),
         }
     )
 
@@ -302,6 +312,7 @@ class NotifySchema:
 class SensorSchema:
     """Voluptuous schema for KNX sensors."""
 
+    CONF_ALWAYS_CALLBACK = "always_callback"
     CONF_STATE_ADDRESS = CONF_STATE_ADDRESS
     CONF_SYNC_STATE = CONF_SYNC_STATE
     DEFAULT_NAME = "KNX Sensor"
@@ -314,6 +325,7 @@ class SensorSchema:
                 cv.boolean,
                 cv.string,
             ),
+            vol.Optional(CONF_ALWAYS_CALLBACK, default=False): cv.boolean,
             vol.Required(CONF_STATE_ADDRESS): cv.string,
             vol.Required(CONF_TYPE): vol.Any(int, float, str),
         }
diff --git a/setup.cfg b/setup.cfg
index 64811765..e9db9b5e 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -33,7 +33,7 @@ forced_separate = test
 combine_as_imports = true
 
 [mypy]
-python_version = 3.7
+python_version = 3.8
 ignore_errors = true
 follow_imports = silent
 ignore_missing_imports = true
@@ -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.telegram.*]
+[mypy-xknx.telegram.*,xknx.exceptions.*]
 strict = true
 ignore_errors = false
 warn_unreachable = true
diff --git a/setup.py b/setup.py
index 9df165a3..68d8549a 100644
--- a/setup.py
+++ b/setup.py
@@ -35,6 +35,7 @@ setup(
         "License :: OSI Approved :: MIT License",
         "Programming Language :: Python :: 3.7",
         "Programming Language :: Python :: 3.8",
+        "Programming Language :: Python :: 3.9",
     ],
     packages=find_packages(),
     install_requires=REQUIRES,
diff --git a/tox.ini b/tox.ini
index d09b2bab..2f27056a 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = py37, py38, typing, pylint, lint
+envlist = py37, py38, py39, typing, pylint, lint
 skip_missing_interpreters = True
 
 [testenv]
diff --git a/xknx.yaml b/xknx.yaml
index 984c2b06..ed119385 100644
--- a/xknx.yaml
+++ b/xknx.yaml
@@ -101,7 +101,7 @@ groups:
                 travel_time_up: 60,
             }
 
-        # Covers without direct positioning:
+        # Covers without direct positioning and device class "shutter":
         Livingroom.Shutter_3:
             {
                 group_address_long: "1/4/9",
@@ -109,6 +109,7 @@ groups:
                 group_address_position_state: "1/4/11",
                 travel_time_down: 50,
                 travel_time_up: 60,
+                device_class: "shutter",
             }
 
         # Central Shutters dont have short or position address
diff --git a/xknx/__version__.py b/xknx/__version__.py
index 6226fc53..66489472 100644
--- a/xknx/__version__.py
+++ b/xknx/__version__.py
@@ -1,3 +1,3 @@
 """XKNX version."""
 
-__version__ = "0.15.1"
+__version__ = "0.15.3"
diff --git a/xknx/core/state_updater.py b/xknx/core/state_updater.py
index 268730df..db7df306 100644
--- a/xknx/core/state_updater.py
+++ b/xknx/core/state_updater.py
@@ -14,12 +14,12 @@ logger = logging.getLogger("xknx.state_updater")
 class StateUpdater:
     """Class for keeping the states of RemoteValues up to date."""
 
-    def __init__(self, xknx):
+    def __init__(self, xknx, parallel_reads: int = 3):
         """Initialize StateUpdater class."""
         self.xknx = xknx
         self.started = False
         self._workers = {}
-        self._one_by_one = asyncio.Lock()
+        self._semaphore = asyncio.Semaphore(value=parallel_reads)
 
     def register_remote_value(self, remote_value: RemoteValue, tracker_options=True):
         """Register a RemoteValue to initialize its state and/or track for expiration."""
@@ -77,7 +77,7 @@ class StateUpdater:
 
         async def read_state_mutex():
             """Schedule to read the state from the KNX bus - one at a time."""
-            async with self._one_by_one:
+            async with self._semaphore:
                 # wait until there is nothing else to send to the bus
                 await self.xknx.telegram_queue.outgoing_queue.join()
                 logger.debug(
diff --git a/xknx/devices/binary_sensor.py b/xknx/devices/binary_sensor.py
index f1909de0..2880ca20 100644
--- a/xknx/devices/binary_sensor.py
+++ b/xknx/devices/binary_sensor.py
@@ -25,10 +25,11 @@ class BinarySensor(Device):
     def __init__(
         self,
         xknx,
-        name,
+        name: str,
         group_address_state=None,
+        invert: Optional[bool] = False,
         sync_state: bool = True,
-        ignore_internal_state: bool = True,
+        ignore_internal_state: bool = False,
         device_class: str = None,
         reset_after: Optional[float] = None,
         actions: List[Action] = None,
@@ -43,7 +44,7 @@ class BinarySensor(Device):
 
         self.actions = actions
         self.device_class = device_class
-        self.ignore_internal_state = ignore_internal_state
+        self.ignore_internal_state = ignore_internal_state or bool(context_timeout)
         self.reset_after = reset_after
         self.state = None
 
@@ -57,6 +58,7 @@ class BinarySensor(Device):
         self.remote_value = RemoteValueSwitch(
             xknx,
             group_address_state=group_address_state,
+            invert=invert,
             sync_state=sync_state,
             device_name=self.name,
             # after_update called internally
@@ -79,11 +81,12 @@ class BinarySensor(Device):
     def from_config(cls, xknx, name, config):
         """Initialize object from configuration structure."""
         group_address_state = config.get("group_address_state")
+        invert = config.get("invert")
         context_timeout = config.get("context_timeout")
         reset_after = config.get("reset_after")
         sync_state = config.get("sync_state", True)
         device_class = config.get("device_class")
-        ignore_internal_state = config.get("ignore_internal_state", True)
+        ignore_internal_state = config.get("ignore_internal_state", False)
         actions = []
         if "actions" in config:
             for action in config["actions"]:
@@ -94,6 +97,7 @@ class BinarySensor(Device):
             xknx,
             name,
             group_address_state=group_address_state,
+            invert=invert,
             sync_state=sync_state,
             ignore_internal_state=ignore_internal_state,
             context_timeout=context_timeout,
@@ -175,13 +179,19 @@ class BinarySensor(Device):
     async def process_group_write(self, telegram):
         """Process incoming and outgoing GROUP WRITE telegram."""
         if await self.remote_value.process(telegram, always_callback=True):
-
-            if self.reset_after is not None and self.state:
-                if self._reset_task:
-                    self._reset_task.cancel()
-                self._reset_task = asyncio.create_task(
-                    self._reset_state(self.reset_after)
-                )
+            self._process_reset_after()
+
+    async def process_group_response(self, telegram):
+        """Process incoming GroupValueResponse telegrams."""
+        if await self.remote_value.process(telegram, always_callback=False):
+            self._process_reset_after()
+
+    def _process_reset_after(self):
+        """Create Task for resetting state if 'reset_after' is configured."""
+        if self.reset_after is not None and self.state:
+            if self._reset_task:
+                self._reset_task.cancel()
+            self._reset_task = asyncio.create_task(self._reset_state(self.reset_after))
 
     async def _reset_state(self, wait_seconds: float):
         await asyncio.sleep(wait_seconds)
diff --git a/xknx/devices/climate_mode.py b/xknx/devices/climate_mode.py
index 23b9d959..86062ce2 100644
--- a/xknx/devices/climate_mode.py
+++ b/xknx/devices/climate_mode.py
@@ -1,9 +1,12 @@
 """
-Module for managing the climate mode.
+Module for managing operation and controller modes.
 
-Climate modes can be 'auto', 'comfort', 'standby', 'economy' or 'protection'.
+Operation modes can be 'auto', 'comfort', 'standby', 'economy', 'protection' and use either a binary DPT or DPT 20.102.
+Controller modes use DPT 20.105.
 """
-from xknx.dpt import HVACOperationMode
+from itertools import chain
+
+from xknx.dpt import HVACControllerMode, HVACOperationMode
 from xknx.exceptions import DeviceIllegalValue
 from xknx.remote_value import (
     RemoteValueBinaryHeatCool,
@@ -36,6 +39,7 @@ class ClimateMode(Device):
         group_address_heat_cool=None,
         group_address_heat_cool_state=None,
         operation_modes=None,
+        controller_modes=None,
         device_updated_cb=None,
     ):
         """Initialize ClimateMode class."""
@@ -120,35 +124,48 @@ class ClimateMode(Device):
             sync_state=True,
             device_name=name,
             feature_name="Heat/Cool",
-            operation_mode=HVACOperationMode.HEAT,
+            controller_mode=HVACControllerMode.HEAT,
             after_update_cb=None,
         )
 
         self.operation_mode = HVACOperationMode.STANDBY
+        self.controller_mode = HVACControllerMode.HEAT
 
-        self.operation_modes_ = []
+        self._operation_modes = []
         if operation_modes is None:
-            self.operation_modes_ = self.gather_operation_modes()
+            self._operation_modes = self.gather_operation_modes()
         else:
             for mode in operation_modes:
                 if isinstance(mode, str):
-                    self.operation_modes_.append(HVACOperationMode(mode))
+                    self._operation_modes.append(HVACOperationMode(mode))
                 elif isinstance(mode, HVACOperationMode):
-                    self.operation_modes_.append(mode)
-
-        self.supports_operation_mode = (
-            group_address_operation_mode is not None
-            or group_address_operation_mode_state is not None
-            or group_address_operation_mode_protection is not None
-            or group_address_operation_mode_night is not None
-            or group_address_operation_mode_comfort is not None
-            or group_address_operation_mode_standby is not None
-            or group_address_controller_status is not None
-            or group_address_controller_status_state is not None
-            or group_address_controller_mode is not None
-            or group_address_controller_mode_state is not None
-            or group_address_heat_cool is not None
-            or group_address_heat_cool_state is not None
+                    self._operation_modes.append(mode)
+
+        self._controller_modes = []
+        if controller_modes is None:
+            self._controller_modes = self.gather_controller_modes()
+        else:
+            for mode in controller_modes:
+                if isinstance(mode, str):
+                    self._controller_modes.append(HVACControllerMode(mode))
+                elif isinstance(mode, HVACControllerMode):
+                    self._controller_modes.append(mode)
+
+        self.supports_operation_mode = any(
+            operation_mode.initialized
+            for operation_mode in self._iter_byte_operation_modes()
+        ) or any(
+            operation_mode.initialized
+            for operation_mode in self._iter_binary_operation_modes()
+        )
+        self.supports_controller_mode = any(
+            operation_mode.initialized
+            for operation_mode in self._iter_controller_remote_values()
+        )
+
+        self._use_binary_operation_modes = any(
+            operation_mode.initialized
+            for operation_mode in self._iter_binary_operation_modes()
         )
 
     @classmethod
@@ -201,15 +218,33 @@ class ClimateMode(Device):
 
     def _iter_remote_values(self):
         """Iterate climate mode RemoteValue classes."""
+        return chain(
+            self._iter_byte_operation_modes(),
+            self._iter_controller_remote_values(),
+            self._iter_binary_operation_modes(),
+        )
+
+    def _iter_byte_operation_modes(self):
+        """Iterate normal DPT 20.102 operation mode remote values."""
         yield from (
-            self.remote_value_controller_mode,
-            self.remote_value_controller_status,
             self.remote_value_operation_mode,
+            self.remote_value_controller_status,
+        )
+
+    def _iter_controller_remote_values(self):
+        """Iterate DPT 20.105 controller remote values."""
+        yield from (
+            self.remote_value_controller_mode,
+            self.remote_value_heat_cool,
+        )
+
+    def _iter_binary_operation_modes(self):
+        """Iterate DPT 1 binary operation modes."""
+        yield from (
             self.remote_value_operation_mode_comfort,
             self.remote_value_operation_mode_night,
             self.remote_value_operation_mode_protection,
             self.remote_value_operation_mode_standby,
-            self.remote_value_heat_cool,
         )
 
     async def _set_internal_operation_mode(self, operation_mode):
@@ -218,56 +253,97 @@ class ClimateMode(Device):
             self.operation_mode = operation_mode
             await self.after_update()
 
+    async def _set_internal_controller_mode(self, controller_mode):
+        """Set internal value of controller mode. Call hooks if controller mode was changed."""
+        if controller_mode != self.controller_mode:
+            self.controller_mode = controller_mode
+            await self.after_update()
+
     async def set_operation_mode(self, operation_mode):
         """Set the operation mode of a thermostat. Send new operation_mode to BUS and update internal state."""
         if (
             not self.supports_operation_mode
-            or operation_mode not in self.operation_modes_
+            or operation_mode not in self._operation_modes
         ):
-            raise DeviceIllegalValue("operation mode not supported", operation_mode)
+            raise DeviceIllegalValue(
+                "operation (preset) mode not supported", operation_mode
+            )
 
-        for rv in self._iter_remote_values():
+        for rv in chain(
+            self._iter_byte_operation_modes(), self._iter_binary_operation_modes()
+        ):
             if rv.writable and operation_mode in rv.supported_operation_modes():
                 await rv.set(operation_mode)
 
         await self._set_internal_operation_mode(operation_mode)
 
+    async def set_controller_mode(self, controller_mode):
+        """Set the controller mode of a thermostat. Send new controller mode to the bus and update internal state."""
+        if (
+            not self.supports_controller_mode
+            or controller_mode not in self._controller_modes
+        ):
+            raise DeviceIllegalValue(
+                "controller (HVAC) mode not supported", controller_mode
+            )
+
+        for rv in self._iter_controller_remote_values():
+            if rv.writable and controller_mode in rv.supported_operation_modes():
+                await rv.set(controller_mode)
+
+        await self._set_internal_controller_mode(controller_mode)
+
     @property
     def operation_modes(self):
         """Return all configured operation modes."""
         if not self.supports_operation_mode:
             return []
-        return self.operation_modes_
+        return self._operation_modes
+
+    @property
+    def controller_modes(self):
+        """Return all configured controller modes."""
+        if not self.supports_controller_mode:
+            return []
+        return self._controller_modes
 
     def gather_operation_modes(self):
         """Gather operation modes from RemoteValues."""
         operation_modes = []
-        for rv in self._iter_remote_values():
+        for rv in chain(
+            self._iter_binary_operation_modes(), self._iter_byte_operation_modes()
+        ):
             if rv.writable:
                 operation_modes.extend(rv.supported_operation_modes())
+
+        # remove duplicates
+        return list(set(operation_modes))
+
+    def gather_controller_modes(self):
+        """Gather controller modes from RemoteValues."""
+        operation_modes = []
+        for rv in self._iter_controller_remote_values():
+            if rv.writable:
+                operation_modes.extend(rv.supported_operation_modes())
+
         # remove duplicates
         return list(set(operation_modes))
 
     async def process_group_write(self, telegram):
         """Process incoming and outgoing GROUP WRITE telegram."""
         if self.supports_operation_mode:
-            processed = False
             for rv in self._iter_remote_values():
                 if await rv.process(telegram):
-                    # don't set when binary climate mode rv is False
+                    #  ignore inactive RemoteValueBinaryOperationMode
                     if rv.value:
                         await self._set_internal_operation_mode(rv.value)
                         return
-                    processed = True
-            # if no operation mode has been set and all binary operation modes are False
-            if processed:
-                await self._set_internal_operation_mode(HVACOperationMode.STANDBY)
 
-    async def sync(self, wait_for_result=False):
-        """Read states of device from KNX bus."""
-        if self.supports_operation_mode:
-            for rv in self._iter_remote_values():
-                await rv.read_state(wait_for_result=wait_for_result)
+        if self.supports_controller_mode:
+            for rv in self._iter_controller_remote_values():
+                if await rv.process(telegram):
+                    await self._set_internal_controller_mode(rv.value)
+                    return
 
     def __str__(self):
         """Return object as readable string."""
diff --git a/xknx/devices/cover.py b/xknx/devices/cover.py
index 4ff53c1a..c6a2f3ec 100644
--- a/xknx/devices/cover.py
+++ b/xknx/devices/cover.py
@@ -49,6 +49,7 @@ class Cover(Device):
         invert_position=False,
         invert_angle=False,
         device_updated_cb=None,
+        device_class=None,
     ):
         """Initialize Cover class."""
         # pylint: disable=too-many-arguments
@@ -110,6 +111,8 @@ class Cover(Device):
 
         self.travelcalculator = TravelCalculator(travel_time_down, travel_time_up)
 
+        self.device_class = device_class
+
     def _iter_remote_values(self):
         """Iterate the devices RemoteValue classes."""
         yield from (self.updown, self.step, self.stop_, self.position, self.angle)
@@ -128,6 +131,7 @@ class Cover(Device):
         travel_time_up = config.get("travel_time_up", cls.DEFAULT_TRAVEL_TIME_UP)
         invert_position = config.get("invert_position", False)
         invert_angle = config.get("invert_angle", False)
+        device_class = config.get("device_class")
 
         return cls(
             xknx,
@@ -143,6 +147,7 @@ class Cover(Device):
             travel_time_up=travel_time_up,
             invert_position=invert_position,
             invert_angle=invert_angle,
+            device_class=device_class,
         )
 
     def __str__(self):
@@ -270,13 +275,20 @@ class Cover(Device):
 
     async def process_group_write(self, telegram):
         """Process incoming and outgoing GROUP WRITE telegram."""
+        # call after_update to account for travelcalculator changes
         if await self.updown.process(telegram):
-            if self.updown.value == RemoteValueUpDown.Direction.UP:
+            if (
+                not self.is_opening()
+                and self.updown.value == RemoteValueUpDown.Direction.UP
+            ):
                 self.travelcalculator.start_travel_up()
-            else:
+                await self.after_update()
+            elif (
+                not self.is_closing()
+                and self.updown.value == RemoteValueUpDown.Direction.DOWN
+            ):
                 self.travelcalculator.start_travel_down()
-            # call after_update to account for travelcalculator changes
-            await self.after_update()
+                await self.after_update()
         # stop from bus
         if await self.stop_.process(telegram) or await self.step.process(telegram):
             if self.is_traveling():
diff --git a/xknx/devices/sensor.py b/xknx/devices/sensor.py
index 1ac1471e..de62ec95 100644
--- a/xknx/devices/sensor.py
+++ b/xknx/devices/sensor.py
@@ -19,14 +19,15 @@ class Sensor(Device):
         xknx,
         name,
         group_address_state=None,
-        sync_state=True,
-        value_type=None,
+        sync_state: bool = True,
+        always_callback: bool = False,
+        value_type: str = None,
         device_updated_cb=None,
     ):
         """Initialize Sensor class."""
         # pylint: disable=too-many-arguments
         super().__init__(xknx, name, device_updated_cb)
-
+        self.always_callback = always_callback
         self.sensor_value = RemoteValueSensor(
             xknx,
             group_address_state=group_address_state,
@@ -45,6 +46,7 @@ class Sensor(Device):
         """Initialize object from configuration structure."""
         group_address_state = config.get("group_address_state")
         sync_state = config.get("sync_state", True)
+        always_callback = config.get("always_callback", False)
         value_type = config.get("value_type")
 
         return cls(
@@ -52,11 +54,16 @@ class Sensor(Device):
             name,
             group_address_state=group_address_state,
             sync_state=sync_state,
+            always_callback=always_callback,
             value_type=value_type,
         )
 
     async def process_group_write(self, telegram):
         """Process incoming and outgoing GROUP WRITE telegram."""
+        await self.sensor_value.process(telegram, always_callback=self.always_callback)
+
+    async def process_group_response(self, telegram):
+        """Process incoming GroupValueResponse telegrams."""
         await self.sensor_value.process(telegram)
 
     def unit_of_measurement(self):
diff --git a/xknx/devices/switch.py b/xknx/devices/switch.py
index 0a9dab20..eb46180b 100644
--- a/xknx/devices/switch.py
+++ b/xknx/devices/switch.py
@@ -8,6 +8,7 @@ It provides functionality for
 """
 import asyncio
 import logging
+from typing import Optional
 
 from xknx.remote_value import RemoteValueSwitch
 
@@ -22,10 +23,11 @@ class Switch(Device):
     def __init__(
         self,
         xknx,
-        name,
-        reset_after=None,
+        name: str,
         group_address=None,
         group_address_state=None,
+        invert: Optional[bool] = False,
+        reset_after: Optional[float] = None,
         device_updated_cb=None,
     ):
         """Initialize Switch class."""
@@ -40,8 +42,8 @@ class Switch(Device):
             xknx,
             group_address,
             group_address_state,
+            invert=invert,
             device_name=self.name,
-            after_update_cb=self.after_update,
         )
 
     def _iter_remote_values(self):
@@ -58,12 +60,16 @@ class Switch(Device):
         """Initialize object from configuration structure."""
         group_address = config.get("group_address")
         group_address_state = config.get("group_address_state")
+        invert = config.get("invert")
+        reset_after = config.get("reset_after")
 
         return cls(
             xknx,
             name,
             group_address=group_address,
             group_address_state=group_address_state,
+            invert=invert,
+            reset_after=reset_after,
         )
 
     async def set_on(self):
@@ -95,6 +101,7 @@ class Switch(Device):
                 self._reset_task = asyncio.create_task(
                     self._reset_state(self.reset_after)
                 )
+            await self.after_update()
 
     async def _reset_state(self, wait_seconds: float):
         await asyncio.sleep(wait_seconds)
diff --git a/xknx/dpt/__init__.py b/xknx/dpt/__init__.py
index 8dc38991..b5bfff82 100644
--- a/xknx/dpt/__init__.py
+++ b/xknx/dpt/__init__.py
@@ -164,6 +164,7 @@ from .dpt_hvac_mode import (
     DPTControllerStatus,
     DPTHVACContrMode,
     DPTHVACMode,
+    HVACControllerMode,
     HVACOperationMode,
 )
 from .dpt_scaling import DPTAngle, DPTScaling
diff --git a/xknx/dpt/dpt_hvac_mode.py b/xknx/dpt/dpt_hvac_mode.py
index c23c16c4..1b0052c9 100644
--- a/xknx/dpt/dpt_hvac_mode.py
+++ b/xknx/dpt/dpt_hvac_mode.py
@@ -15,6 +15,12 @@ class HVACOperationMode(Enum):
     STANDBY = "Standby"
     NIGHT = "Night"
     FROST_PROTECTION = "Frost Protection"
+
+
+class HVACControllerMode(Enum):
+    """Enum for the different KNX HVAC controller modes."""
+
+    AUTO = "Auto"
     HEAT = "Heat"
     MORNING_WARMUP = "Morning Warmup"
     COOL = "Cool"
@@ -62,20 +68,20 @@ class DPTHVACContrMode(_DPTClimateMode):
     """
 
     SUPPORTED_MODES = {
-        0: HVACOperationMode.AUTO,
-        1: HVACOperationMode.HEAT,
-        2: HVACOperationMode.MORNING_WARMUP,
-        3: HVACOperationMode.COOL,
-        4: HVACOperationMode.NIGHT_PURGE,
-        5: HVACOperationMode.PRECOOL,
-        6: HVACOperationMode.OFF,
-        7: HVACOperationMode.TEST,
-        8: HVACOperationMode.EMERGENCY_HEAT,
-        9: HVACOperationMode.FAN_ONLY,
-        10: HVACOperationMode.FREE_COOL,
-        11: HVACOperationMode.ICE,
-        14: HVACOperationMode.DRY,
-        20: HVACOperationMode.NODEM,
+        0: HVACControllerMode.AUTO,
+        1: HVACControllerMode.HEAT,
+        2: HVACControllerMode.MORNING_WARMUP,
+        3: HVACControllerMode.COOL,
+        4: HVACControllerMode.NIGHT_PURGE,
+        5: HVACControllerMode.PRECOOL,
+        6: HVACControllerMode.OFF,
+        7: HVACControllerMode.TEST,
+        8: HVACControllerMode.EMERGENCY_HEAT,
+        9: HVACControllerMode.FAN_ONLY,
+        10: HVACControllerMode.FREE_COOL,
+        11: HVACControllerMode.ICE,
+        14: HVACControllerMode.DRY,
+        20: HVACControllerMode.NODEM,
     }
 
 
diff --git a/xknx/exceptions/exception.py b/xknx/exceptions/exception.py
index 9f09c05b..f8cf90dd 100644
--- a/xknx/exceptions/exception.py
+++ b/xknx/exceptions/exception.py
@@ -1,18 +1,19 @@
 """Module for XKXN Exceptions."""
+from typing import Any, Dict, Optional, Tuple, Union
 
 
 class XKNXException(Exception):
     """Default XKNX Exception."""
 
-    def __eq__(self, other):
+    def __eq__(self, other: Optional[object]) -> bool:
         """Equal operator."""
         return repr(self) == repr(other)
 
-    def __hash__(self):
+    def __hash__(self) -> int:
         """Hash function."""
         return hash(str(self))
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         """Representation of object."""
         return str(self)
 
@@ -20,7 +21,7 @@ class XKNXException(Exception):
 class CommunicationError(XKNXException):
     """Unable to communicate with KNX Bus."""
 
-    def __init__(self, message, should_log=True) -> None:
+    def __init__(self, message: str, should_log: bool = True) -> None:
         """Instantiate exception."""
         super().__init__(message)
 
@@ -30,18 +31,18 @@ class CommunicationError(XKNXException):
 class CouldNotParseTelegram(XKNXException):
     """Could not parse telegram error."""
 
-    def __init__(self, description, **kwargs) -> None:
+    def __init__(self, description: str, **kwargs: Dict[str, str]) -> None:
         """Initialize CouldNotParseTelegram class."""
         super().__init__()
         self.description = description
         self.parameter = kwargs
 
-    def _format_parameter(self):
+    def _format_parameter(self) -> str:
         return " ".join(
             [f'{key}="{value}"' for (key, value) in sorted(self.parameter.items())]
         )
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return '<CouldNotParseTelegram description="{}" {}/>'.format(
             self.description, self._format_parameter()
@@ -51,12 +52,12 @@ class CouldNotParseTelegram(XKNXException):
 class CouldNotParseKNXIP(XKNXException):
     """Exception class for wrong KNXIP data."""
 
-    def __init__(self, description="") -> None:
+    def __init__(self, description: str = "") -> None:
         """Initialize CouldNotParseKNXIP class."""
         super().__init__()
         self.description = description
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return f'<CouldNotParseKNXIP description="{self.description}" />'
 
@@ -64,12 +65,12 @@ class CouldNotParseKNXIP(XKNXException):
 class UnsupportedCEMIMessage(XKNXException):
     """Exception class for unsupported CEMI Messages."""
 
-    def __init__(self, description="") -> None:
+    def __init__(self, description: str = "") -> None:
         """Initialize UnsupportedCEMIMessage class."""
         super().__init__()
         self.description = description
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return f'<UnsupportedCEMIMessage description="{self.description}" />'
 
@@ -77,18 +78,18 @@ class UnsupportedCEMIMessage(XKNXException):
 class ConversionError(XKNXException):
     """Exception class for error while converting one type to another."""
 
-    def __init__(self, description, **kwargs) -> None:
+    def __init__(self, description: str, **kwargs: Dict[str, str]) -> None:
         """Initialize ConversionError class."""
         super().__init__()
         self.description = description
         self.parameter = kwargs
 
-    def _format_parameter(self):
+    def _format_parameter(self) -> str:
         return " ".join(
             [f'{key}="{value}"' for (key, value) in sorted(self.parameter.items())]
         )
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return f'<ConversionError description="{self.description}" {self._format_parameter()}/>'
 
@@ -96,12 +97,14 @@ class ConversionError(XKNXException):
 class CouldNotParseAddress(XKNXException):
     """Exception class for wrong address format."""
 
-    def __init__(self, address=None) -> None:
+    def __init__(
+        self, address: Union[object, str, Tuple[Any, ...], int, None] = None
+    ) -> None:
         """Initialize CouldNotParseAddress class."""
         super().__init__()
         self.address = address
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return f'<CouldNotParseAddress address="{self.address}" />'
 
@@ -109,13 +112,13 @@ class CouldNotParseAddress(XKNXException):
 class DeviceIllegalValue(XKNXException):
     """Exception class for setting a value of a device with an illegal value."""
 
-    def __init__(self, value, description) -> None:
+    def __init__(self, value: Any, description: str) -> None:
         """Initialize DeviceIllegalValue class."""
         super().__init__()
         self.value = value
         self.description = description
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return '<DeviceIllegalValue description="{}" value="{}" />'.format(
             self.value, self.description
diff --git a/xknx/io/knxip_interface.py b/xknx/io/knxip_interface.py
index b88bbe0e..27fd0ad1 100644
--- a/xknx/io/knxip_interface.py
+++ b/xknx/io/knxip_interface.py
@@ -152,14 +152,14 @@ class KNXIPInterface:
         )
         await self.interface.start()
 
-    async def start_routing(self, local_ip):
+    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", local_ip)
+        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()
 
-    async def stop(self):
+    async def stop(self) -> None:
         """Stop connected interfae (either Tunneling or Routing)."""
         if self.interface is not None:
             await self.interface.stop()
diff --git a/xknx/io/udp_client.py b/xknx/io/udp_client.py
index 3abe7df3..1bed0091 100644
--- a/xknx/io/udp_client.py
+++ b/xknx/io/udp_client.py
@@ -78,7 +78,7 @@ class UDPClient:
         self.transport = None
         self.callbacks = []
 
-    def data_received_callback(self, raw):
+    def data_received_callback(self, raw: bytes):
         """Parse and process KNXIP frame. Callback for having received an UDP packet."""
         if raw:
             try:
@@ -89,7 +89,7 @@ class UDPClient:
             except CouldNotParseKNXIP as couldnotparseknxip:
                 logger.exception(couldnotparseknxip)
 
-    def handle_knxipframe(self, knxipframe):
+    def handle_knxipframe(self, knxipframe: KNXIPFrame) -> None:
         """Handle KNXIP Frame and call all callbacks which watch for the service type ident."""
         handled = False
         for callback in self.callbacks:
diff --git a/xknx/knxip/cemi_frame.py b/xknx/knxip/cemi_frame.py
index aa8fbbb6..29065cff 100644
--- a/xknx/knxip/cemi_frame.py
+++ b/xknx/knxip/cemi_frame.py
@@ -134,7 +134,9 @@ class CEMIFrame:
             self.code = CEMIMessageCode(raw[0])
         except ValueError:
             raise UnsupportedCEMIMessage(
-                "CEMIMessageCode not implemented: {} ".format(raw[0])
+                "CEMIMessageCode not implemented: {} in CEMI: {}".format(
+                    raw[0], raw.hex()
+                )
             )
 
         if self.code not in (
@@ -143,7 +145,9 @@ class CEMIFrame:
             CEMIMessageCode.L_DATA_CON,
         ):
             raise UnsupportedCEMIMessage(
-                "Could not handle CEMIMessageCode: {} / {}".format(self.code, raw[0])
+                "Could not handle CEMIMessageCode: {} / {} in CEMI: {}".format(
+                    self.code, raw[0], raw.hex()
+                )
             )
 
         return self.from_knx_data_link_layer(raw)
@@ -153,7 +157,7 @@ class CEMIFrame:
         if len(cemi) < 11:
             # eg. ETS Line-Scan issues L_DATA_IND with length 10
             raise UnsupportedCEMIMessage(
-                "CEMI too small. Length: {}; CEMI: {}".format(len(cemi), cemi)
+                "CEMI too small. Length: {}; CEMI: {}".format(len(cemi), cemi.hex())
             )
 
         # AddIL (Additional Info Length), as specified within
@@ -183,13 +187,17 @@ class CEMIFrame:
             self.cmd = APCICommand(tpci_apci & 0xFFC0)
         except ValueError:
             raise UnsupportedCEMIMessage(
-                "APCI not supported: {:#012b}".format(tpci_apci & 0xFFC0)
+                "APCI not supported: {:#012b} in CEMI: {}".format(
+                    tpci_apci & 0xFFC0, cemi.hex()
+                )
             )
 
         apdu = cemi[10 + addil :]
         if len(apdu) != self.mpdu_len:
             raise CouldNotParseKNXIP(
-                "APDU LEN should be {} but is {}".format(self.mpdu_len, len(apdu))
+                "APDU LEN should be {} but is {} in CEMI: {}".format(
+                    self.mpdu_len, len(apdu), cemi.hex()
+                )
             )
 
         if len(apdu) == 1:
diff --git a/xknx/knxip/tunnelling_request.py b/xknx/knxip/tunnelling_request.py
index 953765d1..83133a1a 100644
--- a/xknx/knxip/tunnelling_request.py
+++ b/xknx/knxip/tunnelling_request.py
@@ -62,10 +62,10 @@ class TunnellingRequest(KNXIPBody):
         try:
             pos += self.cemi.from_knx(raw[pos:])
         except UnsupportedCEMIMessage as unsupported_cemi_err:
-            logger.warning("CEMI not supported: %s", unsupported_cemi_err)
+            logger.debug("CEMI not supported: %s", unsupported_cemi_err)
             # Set cemi to None - this is checked in Tunnel() to send Ack even for unsupported CEMI messages.
             self.cemi = None
-            pos += len(raw)
+            return len(raw)
         return pos
 
     def to_knx(self):
diff --git a/xknx/remote_value/remote_value_climate_mode.py b/xknx/remote_value/remote_value_climate_mode.py
index 751615b1..6b9868c3 100644
--- a/xknx/remote_value/remote_value_climate_mode.py
+++ b/xknx/remote_value/remote_value_climate_mode.py
@@ -16,6 +16,7 @@ from xknx.dpt import (
 )
 from xknx.exceptions import ConversionError, CouldNotParseTelegram
 
+from ..dpt.dpt_hvac_mode import HVACControllerMode
 from .remote_value import RemoteValue
 
 
@@ -169,25 +170,79 @@ class RemoteValueBinaryOperationMode(_RemoteValueBinaryClimateMode):
         )
 
 
-class RemoteValueBinaryHeatCool(_RemoteValueBinaryClimateMode):
+class RemoteValueBinaryHeatCool(RemoteValue):
     """Abstraction for remote value of heat/cool controller mode."""
 
+    def __init__(
+        self,
+        xknx,
+        group_address=None,
+        group_address_state=None,
+        sync_state=True,
+        device_name=None,
+        feature_name="Controller Mode Heat/Cool",
+        after_update_cb=None,
+        controller_mode=None,
+    ):
+        """Initialize remote value of KNX DPT 1 representing a climate controller mode."""
+        # pylint: disable=too-many-arguments
+        if not isinstance(controller_mode, HVACControllerMode):
+            raise ConversionError(
+                "Invalid controller mode type",
+                controller_mode=controller_mode,
+                device_name=device_name,
+                feature_name=feature_name,
+            )
+        if controller_mode not in self.supported_operation_modes():
+            raise ConversionError(
+                "Controller mode not supported for binary mode object",
+                controller_mode=controller_mode,
+                device_name=device_name,
+                feature_name=feature_name,
+            )
+        self.controller_mode = controller_mode
+        super().__init__(
+            xknx,
+            group_address=group_address,
+            group_address_state=group_address_state,
+            sync_state=True,
+            device_name=device_name,
+            feature_name=feature_name,
+            after_update_cb=after_update_cb,
+        )
+
+    def payload_valid(self, payload):
+        """Test if telegram payload may be parsed."""
+        return isinstance(payload, DPTBinary)
+
     @staticmethod
     def supported_operation_modes():
         """Return a list of the configured operation mode."""
-        return [HVACOperationMode.HEAT, HVACOperationMode.COOL]
+        return [HVACControllerMode.HEAT, HVACControllerMode.COOL]
+
+    def to_knx(self, value):
+        """Convert value to payload."""
+        if isinstance(value, HVACControllerMode):
+            # foreign operation modes will set the RemoteValue to False
+            return DPTBinary(value == self.controller_mode)
+        raise ConversionError(
+            "value invalid",
+            value=value,
+            device_name=self.device_name,
+            feature_name=self.feature_name,
+        )
 
     def from_knx(self, payload):
         """Convert current payload to value."""
         if payload == DPTBinary(1):
-            return self.operation_mode
+            return self.controller_mode
         if payload == DPTBinary(0):
             # return the other operation mode
             return next(
                 (
                     _op
                     for _op in self.supported_operation_modes()
-                    if _op is not self.operation_mode
+                    if _op is not self.controller_mode
                 ),
                 None,
             )
diff --git a/xknx/remote_value/remote_value_switch.py b/xknx/remote_value/remote_value_switch.py
index 66cfac28..18fbfcee 100644
--- a/xknx/remote_value/remote_value_switch.py
+++ b/xknx/remote_value/remote_value_switch.py
@@ -3,7 +3,7 @@ Module for managing an DPT Switch remote value.
 
 DPT 1.001.
 """
-from typing import List
+from typing import List, Optional
 
 from xknx.dpt import DPTBinary
 from xknx.exceptions import ConversionError, CouldNotParseTelegram
@@ -19,11 +19,11 @@ class RemoteValueSwitch(RemoteValue):
         xknx,
         group_address=None,
         group_address_state=None,
-        sync_state=True,
-        device_name=None,
-        feature_name="State",
+        sync_state: bool = True,
+        device_name: str = None,
+        feature_name: str = "State",
         after_update_cb=None,
-        invert=False,
+        invert: Optional[bool] = False,
         passive_group_addresses: List[str] = None,
     ):
         """Initialize remote value of KNX DPT 1.001."""
@@ -38,7 +38,7 @@ class RemoteValueSwitch(RemoteValue):
             after_update_cb=after_update_cb,
             passive_group_addresses=passive_group_addresses,
         )
-        self.invert = invert
+        self.invert = bool(invert)
 
     def payload_valid(self, payload):
         """Test if telegram payload may be parsed."""
diff --git a/xknx/telegram/address.py b/xknx/telegram/address.py
index cfce86d8..b70f6418 100644
--- a/xknx/telegram/address.py
+++ b/xknx/telegram/address.py
@@ -147,9 +147,13 @@ class PhysicalAddress(BaseAddress):
         """Return `True` if this address is a valid line address."""
         return not self.is_device
 
+    def __str__(self) -> str:
+        """Return object as in KNX notation (e.g. '1.2.3')."""
+        return f"{self.area}.{self.main}.{self.line}"
+
     def __repr__(self) -> str:
         """Return this object as parsable string."""
-        return 'PhysicalAddress("{0.area}.{0.main}.{0.line}")'.format(self)
+        return f'PhysicalAddress("{self}")'
 
 
 class GroupAddressType(Enum):
diff --git a/xknx/telegram/address_filter.py b/xknx/telegram/address_filter.py
index 20276b4f..7a76776e 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)
+            raise ConversionError("Too many parts within pattern.", pattern=pattern)  # type: ignore
 
     def match(self, address: Union[str, GroupAddress]) -> bool:
         """Test if provided address matches Addressfilter."""


Test patch:
diff --git a/requirements/testing.txt b/requirements/testing.txt
index d26401a2..9d7b5606 100644
--- a/requirements/testing.txt
+++ b/requirements/testing.txt
@@ -1,15 +1,15 @@
 -r production.txt
-pre-commit==2.7.1
-isort==5.6.1
+pre-commit==2.8.2
+isort==5.6.4
 coveralls==2.1.2
 flake8==3.8.4
 flake8-isort==4.0.0
 pydocstyle==5.1.1
 pylint==2.6.0
 pylint-strict-informational==0.1
-pytest==6.1.1
+pytest==6.1.2
 pytest-cov==2.10.1
 pytest-timeout==1.4.2
-setuptools==50.3.0
-tox==3.20.0
-mypy==0.782
+setuptools==50.3.2
+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 6c12b6c4..25b1b563 100644
--- a/test/config_tests/config_v1_test.py
+++ b/test/config_tests/config_v1_test.py
@@ -268,6 +268,22 @@ class TestConfig(unittest.TestCase):
             ),
         )
 
+    def test_config_cover_device_class(self):
+        """Test reading cover with device_class from config file."""
+        self.assertEqual(
+            TestConfig.xknx.devices["Livingroom.Shutter_3"],
+            Cover(
+                TestConfig.xknx,
+                "Livingroom.Shutter_3",
+                group_address_long="1/4/9",
+                group_address_short="1/4/10",
+                group_address_position_state="1/4/11",
+                travel_time_down=50,
+                travel_time_up=60,
+                device_class="shutter",
+            ),
+        )
+
     def test_config_cover_venetian(self):
         """Test reading Cover with angle from config file."""
         self.assertEqual(
diff --git a/test/devices_tests/binary_sensor_test.py b/test/devices_tests/binary_sensor_test.py
index c7bad9c8..471be8fe 100644
--- a/test/devices_tests/binary_sensor_test.py
+++ b/test/devices_tests/binary_sensor_test.py
@@ -7,7 +7,7 @@ from xknx import XKNX
 from xknx.devices import Action, BinarySensor, Switch
 from xknx.dpt import DPTArray, DPTBinary
 from xknx.exceptions import CouldNotParseTelegram
-from xknx.telegram import GroupAddress, Telegram
+from xknx.telegram import GroupAddress, Telegram, TelegramType
 
 
 class AsyncMock(MagicMock):
@@ -59,20 +59,62 @@ class TestBinarySensor(unittest.TestCase):
         self.loop.run_until_complete(binaryinput2.process(telegram_off2))
         self.assertEqual(binaryinput2.state, False)
 
+    def test_process_invert(self):
+        """Test process / reading telegrams from telegram queue."""
+        xknx = XKNX()
+        bs_invert = BinarySensor(xknx, "TestInput", "1/2/3", invert=True)
+
+        self.assertEqual(bs_invert.state, None)
+
+        telegram_on = Telegram(
+            group_address=GroupAddress("1/2/3"), payload=DPTBinary(0)
+        )
+        self.loop.run_until_complete(bs_invert.process(telegram_on))
+
+        self.assertEqual(bs_invert.state, True)
+
+        telegram_off = Telegram(
+            group_address=GroupAddress("1/2/3"), payload=DPTBinary(1)
+        )
+        self.loop.run_until_complete(bs_invert.process(telegram_off))
+        self.assertEqual(bs_invert.state, False)
+
     def test_process_reset_after(self):
         """Test process / reading telegrams from telegram queue."""
         xknx = XKNX()
         reset_after_sec = 0.001
+        async_after_update_callback = AsyncMock()
         binaryinput = BinarySensor(
-            xknx, "TestInput", "1/2/3", reset_after=reset_after_sec
+            xknx,
+            "TestInput",
+            "1/2/3",
+            reset_after=reset_after_sec,
+            device_updated_cb=async_after_update_callback,
         )
         telegram_on = Telegram(
             group_address=GroupAddress("1/2/3"), payload=DPTBinary(1)
         )
 
         self.loop.run_until_complete(binaryinput.process(telegram_on))
-        self.loop.run_until_complete(asyncio.sleep(reset_after_sec * 2))
-        self.assertEqual(binaryinput.state, False)
+        self.assertTrue(binaryinput.state)
+        self.loop.run_until_complete(asyncio.sleep(reset_after_sec * 1.1))
+        self.assertFalse(binaryinput.state)
+        # once for 'on' and once for 'off'
+        self.assertEqual(async_after_update_callback.call_count, 2)
+
+        async_after_update_callback.reset_mock()
+        # multiple telegrams during reset_after time period shall reset timer
+        self.loop.run_until_complete(binaryinput.process(telegram_on))
+        async_after_update_callback.assert_called_once()
+        self.loop.run_until_complete(binaryinput.process(telegram_on))
+        self.loop.run_until_complete(binaryinput.process(telegram_on))
+        # second and third telegram resets timer but doesn't run callback
+        async_after_update_callback.assert_called_once()
+        self.assertTrue(binaryinput.state)
+        self.loop.run_until_complete(asyncio.sleep(reset_after_sec * 1.1))
+        self.assertFalse(binaryinput.state)
+        # once for 'on' and once for 'off'
+        self.assertEqual(async_after_update_callback.call_count, 2)
 
     def test_process_action(self):
         """Test process / reading telegrams from telegram queue. Test if action is executed."""
@@ -196,12 +238,7 @@ class TestBinarySensor(unittest.TestCase):
         switch = BinarySensor(
             xknx, "TestInput", group_address_state="1/2/3", ignore_internal_state=False
         )
-
-        after_update_callback = Mock()
-
-        async def async_after_update_callback(device):
-            """Async callback."""
-            after_update_callback(device)
+        async_after_update_callback = AsyncMock()
 
         switch.register_device_updated_cb(async_after_update_callback)
 
@@ -209,12 +246,12 @@ class TestBinarySensor(unittest.TestCase):
         self.loop.run_until_complete(switch.process(telegram))
         # no _context_task started because ignore_internal_state is False
         self.assertIsNone(switch._context_task)
-        after_update_callback.assert_called_once_with(switch)
+        async_after_update_callback.assert_called_once_with(switch)
 
-        after_update_callback.reset_mock()
+        async_after_update_callback.reset_mock()
         # send same telegram again
         self.loop.run_until_complete(switch.process(telegram))
-        after_update_callback.assert_not_called()
+        async_after_update_callback.assert_not_called()
 
     def test_process_callback_ignore_internal_state(self):
         """Test after_update_callback after state of switch was changed."""
@@ -227,12 +264,7 @@ class TestBinarySensor(unittest.TestCase):
             ignore_internal_state=True,
             context_timeout=0.001,
         )
-
-        after_update_callback = Mock()
-
-        async def async_after_update_callback(device):
-            """Async callback."""
-            after_update_callback(device)
+        async_after_update_callback = AsyncMock()
 
         switch.register_device_updated_cb(async_after_update_callback)
 
@@ -240,25 +272,25 @@ class TestBinarySensor(unittest.TestCase):
         self.assertEqual(switch.counter, 0)
 
         self.loop.run_until_complete(switch.process(telegram))
-        after_update_callback.assert_not_called()
+        async_after_update_callback.assert_not_called()
         self.assertEqual(switch.counter, 1)
         self.loop.run_until_complete(switch._context_task)
-        after_update_callback.assert_called_with(switch)
+        async_after_update_callback.assert_called_with(switch)
         # once with counter 1 and once with counter 0
-        self.assertEqual(after_update_callback.call_count, 2)
+        self.assertEqual(async_after_update_callback.call_count, 2)
 
-        after_update_callback.reset_mock()
+        async_after_update_callback.reset_mock()
         # send same telegram again
         self.loop.run_until_complete(switch.process(telegram))
         self.assertEqual(switch.counter, 1)
         self.loop.run_until_complete(switch.process(telegram))
         self.assertEqual(switch.counter, 2)
-        after_update_callback.assert_not_called()
+        async_after_update_callback.assert_not_called()
 
         self.loop.run_until_complete(switch._context_task)
-        after_update_callback.assert_called_with(switch)
+        async_after_update_callback.assert_called_with(switch)
         # once with counter 2 and once with counter 0
-        self.assertEqual(after_update_callback.call_count, 2)
+        self.assertEqual(async_after_update_callback.call_count, 2)
         self.assertEqual(switch.counter, 0)
 
     def test_process_callback_ignore_internal_state_no_counter(self):
@@ -272,12 +304,7 @@ class TestBinarySensor(unittest.TestCase):
             ignore_internal_state=True,
             context_timeout=0,
         )
-
-        after_update_callback = Mock()
-
-        async def async_after_update_callback(device):
-            """Async callback."""
-            after_update_callback(device)
+        async_after_update_callback = AsyncMock()
 
         switch.register_device_updated_cb(async_after_update_callback)
 
@@ -285,12 +312,49 @@ class TestBinarySensor(unittest.TestCase):
         self.loop.run_until_complete(switch.process(telegram))
         # no _context_task started because context_timeout is False
         self.assertIsNone(switch._context_task)
-        after_update_callback.assert_called_once_with(switch)
+        async_after_update_callback.assert_called_once_with(switch)
 
-        after_update_callback.reset_mock()
+        async_after_update_callback.reset_mock()
         # send same telegram again
         self.loop.run_until_complete(switch.process(telegram))
-        after_update_callback.assert_called_once_with(switch)
+        async_after_update_callback.assert_called_once_with(switch)
+
+    def test_process_group_value_response(self):
+        """Test precess of GroupValueResponse telegrams."""
+        # pylint: disable=protected-access
+        xknx = XKNX()
+        switch = BinarySensor(
+            xknx,
+            "TestInput",
+            group_address_state="1/2/3",
+            ignore_internal_state=True,
+        )
+        async_after_update_callback = AsyncMock()
+
+        switch.register_device_updated_cb(async_after_update_callback)
+
+        write_telegram = Telegram(
+            group_address=GroupAddress("1/2/3"), payload=DPTBinary(1)
+        )
+        response_telegram = Telegram(
+            group_address=GroupAddress("1/2/3"),
+            payload=DPTBinary(1),
+            telegramtype=TelegramType.GROUP_RESPONSE,
+        )
+        self.assertIsNone(switch.state)
+        # initial GroupValueResponse changes state and runs callback
+        self.loop.run_until_complete(switch.process(response_telegram))
+        self.assertTrue(switch.state)
+        async_after_update_callback.assert_called_once_with(switch)
+        # GroupValueWrite with same payload runs callback because of `ignore_internal_state`
+        async_after_update_callback.reset_mock()
+        self.loop.run_until_complete(switch.process(write_telegram))
+        self.assertTrue(switch.state)
+        async_after_update_callback.assert_called_once_with(switch)
+        # GroupValueResponse should not run callback when state has not changed
+        async_after_update_callback.reset_mock()
+        self.loop.run_until_complete(switch.process(response_telegram))
+        async_after_update_callback.assert_not_called()
 
     #
     # TEST COUNTER
diff --git a/test/devices_tests/climate_test.py b/test/devices_tests/climate_test.py
index 5435cb15..d9fb3e30 100644
--- a/test/devices_tests/climate_test.py
+++ b/test/devices_tests/climate_test.py
@@ -17,6 +17,7 @@ from xknx.dpt import (
     DPTValue1Count,
     HVACOperationMode,
 )
+from xknx.dpt.dpt_hvac_mode import HVACControllerMode
 from xknx.exceptions import CouldNotParseTelegram, DeviceIllegalValue
 from xknx.telegram import GroupAddress, Telegram, TelegramType
 
@@ -308,9 +309,9 @@ class TestClimate(unittest.TestCase):
             xknx, "TestClimate", group_address_controller_mode="1/2/4"
         )
 
-        for _, operation_mode in DPTHVACContrMode.SUPPORTED_MODES.items():
+        for _, controller_mode in DPTHVACContrMode.SUPPORTED_MODES.items():
             self.loop.run_until_complete(
-                climate_mode.set_operation_mode(operation_mode)
+                climate_mode.set_controller_mode(controller_mode)
             )
 
             self.assertEqual(xknx.telegrams.qsize(), 1)
@@ -319,7 +320,7 @@ class TestClimate(unittest.TestCase):
                 telegram,
                 Telegram(
                     GroupAddress("1/2/4"),
-                    payload=DPTArray(DPTHVACContrMode.to_knx(operation_mode)),
+                    payload=DPTArray(DPTHVACContrMode.to_knx(controller_mode)),
                 ),
             )
 
@@ -332,6 +333,15 @@ class TestClimate(unittest.TestCase):
                 climate_mode.set_operation_mode(HVACOperationMode.AUTO)
             )
 
+    def test_set_controller_mode_not_supported(self):
+        """Test set_controller_mode but not supported."""
+        xknx = XKNX()
+        climate_mode = ClimateMode(xknx, "TestClimate")
+        with self.assertRaises(DeviceIllegalValue):
+            self.loop.run_until_complete(
+                climate_mode.set_controller_mode(HVACControllerMode.HEAT)
+            )
+
     def test_set_operation_mode_with_controller_status(self):
         """Test set_operation_mode with controller status adddressedefined."""
         xknx = XKNX()
@@ -387,7 +397,7 @@ class TestClimate(unittest.TestCase):
         self.assertSetEqual(set(telegrams), set(test_telegrams))
 
     def test_set_heat_cool_binary(self):
-        """Test set_operation_mode with binary heat/cool group adddresses defined."""
+        """Test set_operation_mode with binary heat/cool group addresses defined."""
         xknx = XKNX()
         climate_mode = ClimateMode(
             xknx,
@@ -397,7 +407,7 @@ class TestClimate(unittest.TestCase):
         )
 
         self.loop.run_until_complete(
-            climate_mode.set_operation_mode(HVACOperationMode.HEAT)
+            climate_mode.set_controller_mode(HVACControllerMode.HEAT)
         )
         self.assertEqual(xknx.telegrams.qsize(), 1)
         telegram = xknx.telegrams.get_nowait()
@@ -405,7 +415,7 @@ class TestClimate(unittest.TestCase):
             telegram, Telegram(GroupAddress("1/2/14"), payload=DPTBinary(True))
         )
         self.loop.run_until_complete(
-            climate_mode.set_operation_mode(HVACOperationMode.COOL)
+            climate_mode.set_controller_mode(HVACControllerMode.COOL)
         )
         self.assertEqual(xknx.telegrams.qsize(), 1)
         telegram = xknx.telegrams.get_nowait()
@@ -1012,70 +1022,17 @@ class TestClimate(unittest.TestCase):
             self.loop.run_until_complete(climate_mode.process(telegram))
             self.assertEqual(climate_mode.operation_mode, operation_mode)
 
-    def test_process_binary_operation_mode(self):
-        """Test process / reading telegrams from telegram queue. Test binary operation mode is processed correctly."""
-        xknx = XKNX()
-        climate_mode = ClimateMode(
-            xknx,
-            "TestClimate",
-            group_address_operation_mode_protection="1/2/5",
-            group_address_operation_mode_night="1/2/6",
-            group_address_operation_mode_comfort="1/2/7",
-        )
-        # Note: disabling of inactive binary modes should be done by the
-        #       KNX actuator when a new mode is set on the bus.
-        self.assertIsNone(climate_mode.remote_value_operation_mode_protection.payload)
-        self.assertIsNone(climate_mode.remote_value_operation_mode_night.payload)
-        self.assertIsNone(climate_mode.remote_value_operation_mode_comfort.payload)
-        # Protection
-        telegram = Telegram(GroupAddress("1/2/5"))
-        telegram.payload = DPTBinary(True)
-        self.loop.run_until_complete(climate_mode.process(telegram))
-        self.assertEqual(
-            climate_mode.operation_mode, HVACOperationMode.FROST_PROTECTION
-        )
-        # disable Protection -> Standby
-        telegram = Telegram(GroupAddress("1/2/5"))
-        telegram.payload = DPTBinary(False)
-        self.loop.run_until_complete(climate_mode.process(telegram))
-        self.assertEqual(climate_mode.operation_mode, HVACOperationMode.STANDBY)
-        # Night
-        telegram = Telegram(GroupAddress("1/2/6"))
-        telegram.payload = DPTBinary(True)
-        self.loop.run_until_complete(climate_mode.process(telegram))
-        self.assertEqual(climate_mode.operation_mode, HVACOperationMode.NIGHT)
-        # disable Night -> Standby
-        telegram = Telegram(GroupAddress("1/2/6"))
-        telegram.payload = DPTBinary(False)
-        self.loop.run_until_complete(climate_mode.process(telegram))
-        self.assertEqual(climate_mode.operation_mode, HVACOperationMode.STANDBY)
-        # Comfort
-        telegram = Telegram(GroupAddress("1/2/7"))
-        telegram.payload = DPTBinary(True)
-        self.loop.run_until_complete(climate_mode.process(telegram))
-        self.assertEqual(climate_mode.operation_mode, HVACOperationMode.COMFORT)
-        # process unused group address -> no reaction
-        telegram = Telegram(GroupAddress("0/0/1"))
-        telegram.payload = DPTBinary(True)
-        self.loop.run_until_complete(climate_mode.process(telegram))
-        self.assertEqual(climate_mode.operation_mode, HVACOperationMode.COMFORT)
-        # disable Comfort -> Standby
-        telegram = Telegram(GroupAddress("1/2/7"))
-        telegram.payload = DPTBinary(False)
-        self.loop.run_until_complete(climate_mode.process(telegram))
-        self.assertEqual(climate_mode.operation_mode, HVACOperationMode.STANDBY)
-
     def test_process_controller_mode(self):
         """Test process / reading telegrams from telegram queue. Test if DPT20.105 controller mode is set correctly."""
         xknx = XKNX()
         climate_mode = ClimateMode(
             xknx, "TestClimate", group_address_controller_mode="1/2/5"
         )
-        for _, operation_mode in DPTHVACContrMode.SUPPORTED_MODES.items():
+        for _, controller_mode in DPTHVACContrMode.SUPPORTED_MODES.items():
             telegram = Telegram(GroupAddress("1/2/5"))
-            telegram.payload = DPTArray(DPTHVACContrMode.to_knx(operation_mode))
+            telegram.payload = DPTArray(DPTHVACContrMode.to_knx(controller_mode))
             self.loop.run_until_complete(climate_mode.process(telegram))
-            self.assertEqual(climate_mode.operation_mode, operation_mode)
+            self.assertEqual(climate_mode.controller_mode, controller_mode)
 
     def test_process_controller_status_wrong_payload(self):
         """Test process wrong telegram for controller status (wrong payload type)."""
@@ -1161,12 +1118,12 @@ class TestClimate(unittest.TestCase):
         telegram = Telegram(GroupAddress("1/2/14"))
         telegram.payload = DPTBinary(False)
         self.loop.run_until_complete(climate_mode.process(telegram))
-        self.assertEqual(climate_mode.operation_mode, HVACOperationMode.COOL)
+        self.assertEqual(climate_mode.controller_mode, HVACControllerMode.COOL)
 
         telegram = Telegram(GroupAddress("1/2/14"))
         telegram.payload = DPTBinary(True)
         self.loop.run_until_complete(climate_mode.process(telegram))
-        self.assertEqual(climate_mode.operation_mode, HVACOperationMode.HEAT)
+        self.assertEqual(climate_mode.controller_mode, HVACControllerMode.HEAT)
 
     #
     # SUPPORTED OPERATION MODES
@@ -1258,8 +1215,8 @@ class TestClimate(unittest.TestCase):
             group_address_heat_cool_state="1/2/15",
         )
         self.assertEqual(
-            set(climate_mode.operation_modes),
-            {HVACOperationMode.HEAT, HVACOperationMode.COOL},
+            set(climate_mode.controller_modes),
+            {HVACControllerMode.HEAT, HVACControllerMode.COOL},
         )
 
     def test_custom_supported_operation_modes(self):
@@ -1287,6 +1244,35 @@ class TestClimate(unittest.TestCase):
         )
         self.assertEqual(climate_mode.operation_modes, modes)
 
+    def test_custom_supported_controller_modes_as_str(self):
+        """Test get_supported_operation_modes with custom mode as str list."""
+        str_modes = ["Heat", "Cool", HVACControllerMode.NODEM]
+        modes = [
+            HVACControllerMode.HEAT,
+            HVACControllerMode.COOL,
+            HVACControllerMode.NODEM,
+        ]
+        xknx = XKNX()
+        climate_mode = ClimateMode(
+            xknx,
+            "TestClimate",
+            group_address_controller_mode="1/2/7",
+            controller_modes=str_modes,
+        )
+        self.assertEqual(climate_mode.controller_modes, modes)
+
+    def test_custom_supported_controller_modes_when_controller_mode_unsupported(self):
+        """Test get_supported_operation_modes with custom mode as str list."""
+        str_modes = ["Heat", "Cool"]
+        modes = []
+        xknx = XKNX()
+        climate_mode = ClimateMode(
+            xknx,
+            "TestClimate",
+            controller_modes=str_modes,
+        )
+        self.assertEqual(climate_mode.controller_modes, modes)
+
     def test_process_power_status(self):
         """Test process / reading telegrams from telegram queue. Test if DPT20.105 controller mode is set correctly."""
         xknx = XKNX()
diff --git a/test/devices_tests/cover_test.py b/test/devices_tests/cover_test.py
index ac00f704..0865454c 100644
--- a/test/devices_tests/cover_test.py
+++ b/test/devices_tests/cover_test.py
@@ -397,6 +397,10 @@ class TestCover(unittest.TestCase):
             telegram, Telegram(GroupAddress("1/2/1"), payload=DPTBinary(0))
         )
         self.assertEqual(cover.travelcalculator.travel_to_position, 50)
+        self.assertTrue(cover.is_opening())
+        # process the outgoing telegram to make sure it doesn't overwrite the target position
+        self.loop.run_until_complete(cover.process(telegram))
+        self.assertEqual(cover.travelcalculator.travel_to_position, 50)
 
     def test_position_without_position_address_down(self):
         """Test moving cover down - with no absolute positioning supported."""
@@ -416,6 +420,10 @@ class TestCover(unittest.TestCase):
             telegram, Telegram(GroupAddress("1/2/1"), payload=DPTBinary(1))
         )
         self.assertEqual(cover.travelcalculator.travel_to_position, 80)
+        self.assertTrue(cover.is_closing())
+        # process the outgoing telegram to make sure it doesn't overwrite the target position
+        self.loop.run_until_complete(cover.process(telegram))
+        self.assertEqual(cover.travelcalculator.travel_to_position, 80)
 
     def test_position_without_position_address_uninitialized_up(self):
         """Test moving uninitialized cover to absolute position - with no absolute positioning supported."""
@@ -461,7 +469,6 @@ class TestCover(unittest.TestCase):
             )
 
         self.loop.run_until_complete(cover.set_position(100))
-        print(cover.travelcalculator.position_closed)
         self.assertEqual(xknx.telegrams.qsize(), 1)
         telegram = xknx.telegrams.get_nowait()
         self.assertEqual(
diff --git a/test/devices_tests/sensor_test.py b/test/devices_tests/sensor_test.py
index 2043eb3c..f7a976e2 100644
--- a/test/devices_tests/sensor_test.py
+++ b/test/devices_tests/sensor_test.py
@@ -47,6 +47,60 @@ class TestSensor(unittest.TestCase):
         self.assertEqual(sensor.unit_of_measurement(), "K")
         self.assertEqual(sensor.ha_device_class(), None)
 
+    def test_always_callback_sensor(self):
+        """Test always callback sensor."""
+        xknx = XKNX()
+        sensor = Sensor(
+            xknx,
+            "TestSensor",
+            group_address_state="1/2/3",
+            always_callback=False,
+            value_type="volume_liquid_litre",
+        )
+
+        after_update_callback = Mock()
+
+        async def async_after_update_callback(device):
+            """Async callback."""
+            after_update_callback(device)
+
+        sensor.register_device_updated_cb(async_after_update_callback)
+
+        payload = DPTArray(
+            (
+                0x00,
+                0x00,
+                0x01,
+                0x00,
+            )
+        )
+
+        #  set initial payload of sensor
+        sensor.sensor_value.payload = payload
+
+        telegram = Telegram(group_address=GroupAddress("1/2/3"), payload=payload)
+        response_telegram = Telegram(
+            group_address=GroupAddress("1/2/3"),
+            payload=payload,
+            telegramtype=TelegramType.GROUP_RESPONSE,
+        )
+
+        # verify not called when always_callback is False
+        self.loop.run_until_complete(sensor.process(telegram))
+        after_update_callback.assert_not_called()
+        after_update_callback.reset_mock()
+
+        sensor.always_callback = True
+
+        # verify called when always_callback is True
+        self.loop.run_until_complete(sensor.process(telegram))
+        after_update_callback.assert_called_once()
+        after_update_callback.reset_mock()
+
+        # verify not called when processing read responses
+        self.loop.run_until_complete(sensor.process(response_telegram))
+        after_update_callback.assert_not_called()
+
     def test_str_acceleration(self):
         """Test resolve state with acceleration sensor."""
         xknx = XKNX()
diff --git a/test/devices_tests/switch_test.py b/test/devices_tests/switch_test.py
index a9520431..55fe603d 100644
--- a/test/devices_tests/switch_test.py
+++ b/test/devices_tests/switch_test.py
@@ -59,21 +59,36 @@ class TestSwitch(unittest.TestCase):
         """Test process / reading telegrams from telegram queue. Test if device was updated."""
         xknx = XKNX()
         switch = Switch(xknx, "TestOutlet", group_address="1/2/3")
-
         self.assertEqual(switch.state, False)
 
         telegram_on = Telegram(
             group_address=GroupAddress("1/2/3"), payload=DPTBinary(1)
         )
         self.loop.run_until_complete(switch.process(telegram_on))
-
         self.assertEqual(switch.state, True)
 
         telegram_off = Telegram(
             group_address=GroupAddress("1/2/3"), payload=DPTBinary(0)
         )
         self.loop.run_until_complete(switch.process(telegram_off))
+        self.assertEqual(switch.state, False)
 
+    def test_process_invert(self):
+        """Test process / reading telegrams from telegram queue with inverted switch."""
+        xknx = XKNX()
+        switch = Switch(xknx, "TestOutlet", group_address="1/2/3", invert=True)
+        self.assertEqual(switch.state, False)
+
+        telegram_on = Telegram(
+            group_address=GroupAddress("1/2/3"), payload=DPTBinary(0)
+        )
+        self.loop.run_until_complete(switch.process(telegram_on))
+        self.assertEqual(switch.state, True)
+
+        telegram_off = Telegram(
+            group_address=GroupAddress("1/2/3"), payload=DPTBinary(1)
+        )
+        self.loop.run_until_complete(switch.process(telegram_off))
         self.assertEqual(switch.state, False)
 
     def test_process_reset_after(self):
@@ -165,6 +180,28 @@ class TestSwitch(unittest.TestCase):
             telegram, Telegram(GroupAddress("1/2/3"), payload=DPTBinary(0))
         )
 
+    #
+    # TEST SET INVERT
+    #
+    def test_set_invert(self):
+        """Test switching on/off inverted switch."""
+        xknx = XKNX()
+        switch = Switch(xknx, "TestOutlet", group_address="1/2/3", invert=True)
+
+        self.loop.run_until_complete(switch.set_on())
+        self.assertEqual(xknx.telegrams.qsize(), 1)
+        telegram = xknx.telegrams.get_nowait()
+        self.assertEqual(
+            telegram, Telegram(GroupAddress("1/2/3"), payload=DPTBinary(0))
+        )
+
+        self.loop.run_until_complete(switch.set_off())
+        self.assertEqual(xknx.telegrams.qsize(), 1)
+        telegram = xknx.telegrams.get_nowait()
+        self.assertEqual(
+            telegram, Telegram(GroupAddress("1/2/3"), payload=DPTBinary(1))
+        )
+
     #
     # TEST DO
     #
diff --git a/test/io_tests/tunnel_test.py b/test/io_tests/tunnel_test.py
new file mode 100644
index 00000000..7f93147c
--- /dev/null
+++ b/test/io_tests/tunnel_test.py
@@ -0,0 +1,70 @@
+"""Unit test for KNX/IP Tunnelling Request/Response."""
+import asyncio
+import unittest
+from unittest.mock import Mock, patch
+
+from xknx import XKNX
+from xknx.io import Tunnel
+from xknx.knxip import CEMIFrame
+from xknx.telegram import TelegramDirection
+
+
+class TestTunnelling(unittest.TestCase):
+    """Test class for xknx/io/Tunnelling objects."""
+
+    def setUp(self):
+        """Set up test class."""
+        self.loop = asyncio.new_event_loop()
+        asyncio.set_event_loop(self.loop)
+        self.xknx = XKNX()
+        self.tg_received_mock = Mock()
+        self.tunnel = Tunnel(
+            self.xknx,
+            local_ip="192.168.1.1",
+            gateway_ip="192.168.1.2",
+            gateway_port=3671,
+            telegram_received_callback=self.tg_received_mock,
+            auto_reconnect=False,
+            auto_reconnect_wait=3,
+        )
+
+    def tearDown(self):
+        """Tear down test class."""
+        self.loop.close()
+
+    @patch("xknx.io.Tunnel.send_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
+        # communication_channel_id: 0x02   sequence_counter: 0x21
+        raw = bytes.fromhex("0610 0420 0017 04 02 21 00 2900bcd011162916030080 0c 3f")
+        _cemi = CEMIFrame(self.xknx)
+        _cemi.from_knx(raw[10:])
+        telegram = _cemi.telegram
+        telegram.direction = TelegramDirection.INCOMING
+
+        self.tunnel.udp_client.data_received_callback(raw)
+        self.tg_received_mock.assert_called_once_with(telegram)
+        send_ack_mock.assert_called_once_with(0x02, 0x21)
+
+    @patch("xknx.io.Tunnel.send_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
+        # <UnsupportedCEMIMessage description="APCI not supported: 0b0100000000 in CEMI: 2900b0d000010000010100" />
+        # communication_channel_id: 0x02   sequence_counter: 0x4f
+        raw = bytes.fromhex("0610 0420 0015 04 02 4f 00 2900b0d000010000010100")
+
+        self.tunnel.udp_client.data_received_callback(raw)
+        self.tg_received_mock.assert_not_called()
+        send_ack_mock.assert_called_once_with(0x02, 0x4F)
+        send_ack_mock.reset_mock()
+
+        # LDataInd T_Connect from 1.0.250 to 1.0.255 (xknx tunnel endpoint) - ETS Line-Scan
+        # <UnsupportedCEMIMessage description="CEMI too small. Length: 10; CEMI: 2900b06010fa10ff0080" />
+        # communication_channel_id: 0x02   sequence_counter: 0x81
+        raw = bytes.fromhex("0610 0420 0014 04 02 81 00 2900b06010fa10ff0080")
+
+        self.tunnel.udp_client.data_received_callback(raw)
+        self.tg_received_mock.assert_not_called()
+        send_ack_mock.assert_called_once_with(0x02, 0x81)
diff --git a/test/knxip_tests/cemi_frame_test.py b/test/knxip_tests/cemi_frame_test.py
index 0b1fa670..b911f8fb 100644
--- a/test/knxip_tests/cemi_frame_test.py
+++ b/test/knxip_tests/cemi_frame_test.py
@@ -11,20 +11,22 @@ from xknx.telegram import PhysicalAddress, Telegram
 
 
 def get_data(code, adil, flags, src, dst, mpdu_len, tpci_apci, payload):
-    return [
-        code,
-        adil,  # adil
-        (flags >> 8) & 255,  # flags
-        flags & 255,  # flags
-        (src >> 8) & 255,  # src
-        src & 255,  # src
-        (dst >> 8) & 255,  # dst
-        dst & 255,  # dst
-        mpdu_len,  # mpdu_len
-        (tpci_apci >> 8) & 255,  # tpci_apci
-        tpci_apci & 255,  # tpci_apci
-        *payload,  # payload
-    ]
+    return bytes(
+        [
+            code,
+            adil,  # adil
+            (flags >> 8) & 255,  # flags
+            flags & 255,  # flags
+            (src >> 8) & 255,  # src
+            src & 255,  # src
+            (dst >> 8) & 255,  # dst
+            dst & 255,  # dst
+            mpdu_len,  # mpdu_len
+            (tpci_apci >> 8) & 255,  # tpci_apci
+            tpci_apci & 255,  # tpci_apci
+            *payload,  # payload
+        ]
+    )
 
 
 @fixture(name="frame")
@@ -127,7 +129,9 @@ def test_from_knx_with_not_handleable_cemi(frame):
 def test_from_knx_with_not_implemented_cemi(frame):
     """Test for having not implemented CEMI set"""
     with raises(UnsupportedCEMIMessage, match=r".*Could not handle CEMIMessageCode:.*"):
-        frame.from_knx(get_data(CEMIMessageCode.L_BUSMON_IND, 0, 0, 0, 0, 2, 0, []))
+        frame.from_knx(
+            get_data(CEMIMessageCode.L_BUSMON_IND.value, 0, 0, 0, 0, 2, 0, [])
+        )
 
 
 def test_invalid_telegram(frame):
diff --git a/test/knxip_tests/routing_indication_test.py b/test/knxip_tests/routing_indication_test.py
index e4631de1..e9939b58 100644
--- a/test/knxip_tests/routing_indication_test.py
+++ b/test/knxip_tests/routing_indication_test.py
@@ -25,26 +25,7 @@ class Test_KNXIP(unittest.TestCase):
 
     def test_from_knx(self):
         """Test parsing and streaming CEMIFrame KNX/IP packet (payload=0xf0)."""
-        raw = (
-            0x06,
-            0x10,
-            0x05,
-            0x30,
-            0x00,
-            0x12,
-            0x29,
-            0x00,
-            0xBC,
-            0xD0,
-            0x12,
-            0x02,
-            0x01,
-            0x51,
-            0x02,
-            0x00,
-            0x40,
-            0xF0,
-        )
+        raw = bytes.fromhex("0610053000122900BCD012020151020040F0")
         xknx = XKNX()
         knxipframe = KNXIPFrame(xknx)
         knxipframe.from_knx(raw)
@@ -60,26 +41,7 @@ class Test_KNXIP(unittest.TestCase):
 
     def test_from_knx_to_knx(self):
         """Test parsing and streaming CEMIFrame KNX/IP."""
-        raw = (
-            0x06,
-            0x10,
-            0x05,
-            0x30,
-            0x00,
-            0x12,
-            0x29,
-            0x00,
-            0xBC,
-            0xD0,
-            0x12,
-            0x02,
-            0x01,
-            0x51,
-            0x02,
-            0x00,
-            0x40,
-            0xF0,
-        )
+        raw = bytes.fromhex("0610053000122900BCD012020151020040F0")
         xknx = XKNX()
         knxipframe = KNXIPFrame(xknx)
         knxipframe.from_knx(raw)
@@ -107,28 +69,7 @@ class Test_KNXIP(unittest.TestCase):
         knxipframe.body.cemi.set_hops(5)
         knxipframe.normalize()
 
-        raw = (
-            0x06,
-            0x10,
-            0x05,
-            0x30,
-            0x00,
-            0x14,
-            0x29,
-            0x00,
-            0xBC,
-            0xD0,
-            0x12,
-            0x02,
-            0x01,
-            0x51,
-            0x04,
-            0x00,
-            0x80,
-            13,
-            23,
-            42,
-        )
+        raw = bytes.fromhex("0610053000142900BCD012020151040080 0d 17 2a")
 
         self.assertEqual(knxipframe.header.to_knx(), list(raw[0:6]))
         self.assertEqual(knxipframe.body.to_knx(), list(raw[6:]))
@@ -136,26 +77,7 @@ class Test_KNXIP(unittest.TestCase):
 
     def test_telegram_get(self):
         """Test parsing and streaming CEMIFrame KNX/IP packet, group read."""
-        raw = (
-            0x06,
-            0x10,
-            0x05,
-            0x30,
-            0x00,
-            0x12,
-            0x29,
-            0x00,
-            0xBC,
-            0xD0,
-            0x12,
-            0x02,
-            0x01,
-            0x51,
-            0x02,
-            0x00,
-            0x40,
-            0xF0,
-        )
+        raw = bytes.fromhex("0610053000122900BCD012020151020040F0")
         xknx = XKNX()
         knxipframe = KNXIPFrame(xknx)
         knxipframe.from_knx(raw)
@@ -178,25 +100,7 @@ class Test_KNXIP(unittest.TestCase):
     def test_EndTOEnd_group_write_binary_on(self):
         """Test parsing and streaming CEMIFrame KNX/IP packet, switch on light in my kitchen."""
         # Switch on Kitchen-L1
-        raw = (
-            0x06,
-            0x10,
-            0x05,
-            0x30,
-            0x00,
-            0x11,
-            0x29,
-            0x00,
-            0xBC,
-            0xD0,
-            0xFF,
-            0xF9,
-            0x01,
-            0x49,
-            0x01,
-            0x00,
-            0x81,
-        )
+        raw = bytes.fromhex("0610053000112900BCD0FFF90149010081")
         xknx = XKNX()
         knxipframe = KNXIPFrame(xknx)
         knxipframe.from_knx(raw)
@@ -217,25 +121,7 @@ class Test_KNXIP(unittest.TestCase):
     def test_EndTOEnd_group_write_binary_off(self):
         """Test parsing and streaming CEMIFrame KNX/IP packet, switch off light in my kitchen."""
         # Switch off Kitchen-L1
-        raw = (
-            0x06,
-            0x10,
-            0x05,
-            0x30,
-            0x00,
-            0x11,
-            0x29,
-            0x00,
-            0xBC,
-            0xD0,
-            0xFF,
-            0xF9,
-            0x01,
-            0x49,
-            0x01,
-            0x00,
-            0x80,
-        )
+        raw = bytes.fromhex("0610053000112900BCD0FFF90149010080")
         xknx = XKNX()
         knxipframe = KNXIPFrame(xknx)
         knxipframe.from_knx(raw)
@@ -256,26 +142,7 @@ class Test_KNXIP(unittest.TestCase):
     def test_EndTOEnd_group_write_1byte(self):
         """Test parsing and streaming CEMIFrame KNX/IP packet, dimm light in my kitchen."""
         # Dimm Kitchen L1 to 0x65
-        raw = (
-            0x06,
-            0x10,
-            0x05,
-            0x30,
-            0x00,
-            0x12,
-            0x29,
-            0x00,
-            0xBC,
-            0xD0,
-            0xFF,
-            0xF9,
-            0x01,
-            0x4B,
-            0x02,
-            0x00,
-            0x80,
-            0x65,
-        )
+        raw = bytes.fromhex("0610053000122900BCD0FFF9014B02008065")
         xknx = XKNX()
         knxipframe = KNXIPFrame(xknx)
         knxipframe.from_knx(raw)
@@ -298,27 +165,7 @@ class Test_KNXIP(unittest.TestCase):
     def test_EndTOEnd_group_write_2bytes(self):
         """Test parsing and streaming CEMIFrame KNX/IP packet, setting value of thermostat."""
         # Incoming Temperature from thermostat
-        raw = (
-            0x06,
-            0x10,
-            0x05,
-            0x30,
-            0x00,
-            0x13,
-            0x29,
-            0x00,
-            0xBC,
-            0xD0,
-            0x14,
-            0x02,
-            0x08,
-            0x01,
-            0x03,
-            0x00,
-            0x80,
-            0x07,
-            0xC1,
-        )
+        raw = bytes.fromhex("0610053000132900BCD01402080103008007C1")
         xknx = XKNX()
         knxipframe = KNXIPFrame(xknx)
         knxipframe.from_knx(raw)
@@ -344,25 +191,7 @@ class Test_KNXIP(unittest.TestCase):
     def test_EndTOEnd_group_read(self):
         """Test parsing and streaming CEMIFrame KNX/IP packet, group read."""
         # State request
-        raw = (
-            0x06,
-            0x10,
-            0x05,
-            0x30,
-            0x00,
-            0x11,
-            0x29,
-            0x00,
-            0xBC,
-            0xD0,
-            0xFF,
-            0xF9,
-            0x01,
-            0xB8,
-            0x01,
-            0x00,
-            0x00,
-        )
+        raw = bytes.fromhex("0610053000112900BCD0FFF901B8010000")
         xknx = XKNX()
         knxipframe = KNXIPFrame(xknx)
         knxipframe.from_knx(raw)
@@ -385,25 +214,7 @@ class Test_KNXIP(unittest.TestCase):
     def test_EndTOEnd_group_response(self):
         """Test parsing and streaming CEMIFrame KNX/IP packet, group response."""
         # Incoming state
-        raw = (
-            0x06,
-            0x10,
-            0x05,
-            0x30,
-            0x00,
-            0x11,
-            0x29,
-            0x00,
-            0xBC,
-            0xD0,
-            0x13,
-            0x01,
-            0x01,
-            0x88,
-            0x01,
-            0x00,
-            0x41,
-        )
+        raw = bytes.fromhex("0610053000112900BCD013010188010041")
         xknx = XKNX()
         knxipframe = KNXIPFrame(xknx)
         knxipframe.from_knx(raw)
@@ -439,25 +250,8 @@ class Test_KNXIP(unittest.TestCase):
         knxipframe.body.cemi.set_hops(5)
         knxipframe.normalize()
 
-        raw = (
-            0x06,
-            0x10,
-            0x05,
-            0x30,
-            0x00,
-            0x11,
-            0x29,
-            0x00,
-            0xBC,
-            0xD0,
-            0x13,
-            0x01,
-            0x01,
-            0x51,
-            0x01,
-            0x00,
-            0xBF,
-        )
+        raw = bytes.fromhex("0610053000112900BCD0130101510100BF")
+
         self.assertEqual(knxipframe.to_knx(), list(raw))
 
         knxipframe2 = KNXIPFrame(xknx)
@@ -469,5 +263,5 @@ class Test_KNXIP(unittest.TestCase):
         """Test parsing and streaming CEMIFrame KNX/IP packet with unsupported CEMICode."""
         xknx = XKNX()
         ri = RoutingIndication(xknx)
-
-        self.assertEqual(11, ri.from_knx([43, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0]))
+        raw = bytes([43, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0])
+        self.assertEqual(11, ri.from_knx(raw))
diff --git a/test/remote_value_tests/remote_value_climate_mode_test.py b/test/remote_value_tests/remote_value_climate_mode_test.py
index 57fdae6c..923130ac 100644
--- a/test/remote_value_tests/remote_value_climate_mode_test.py
+++ b/test/remote_value_tests/remote_value_climate_mode_test.py
@@ -4,6 +4,7 @@ import unittest
 
 from xknx import XKNX
 from xknx.dpt import DPTArray, DPTBinary, HVACOperationMode
+from xknx.dpt.dpt_hvac_mode import HVACControllerMode
 from xknx.exceptions import ConversionError, CouldNotParseTelegram
 from xknx.remote_value import (
     RemoteValueBinaryHeatCool,
@@ -60,10 +61,19 @@ class TestRemoteValueDptValue1Ucount(unittest.TestCase):
         """Test to_knx function with normal operation."""
         xknx = XKNX()
         remote_value = RemoteValueBinaryHeatCool(
-            xknx, operation_mode=HVACOperationMode.HEAT
+            xknx, controller_mode=HVACControllerMode.HEAT
         )
-        self.assertEqual(remote_value.to_knx(HVACOperationMode.HEAT), DPTBinary(True))
-        self.assertEqual(remote_value.to_knx(HVACOperationMode.COOL), DPTBinary(False))
+        self.assertEqual(remote_value.to_knx(HVACControllerMode.HEAT), DPTBinary(True))
+        self.assertEqual(remote_value.to_knx(HVACControllerMode.COOL), DPTBinary(False))
+
+    def test_to_knx_heat_cool_error(self):
+        """Test to_knx function with wrong controller mode."""
+        xknx = XKNX()
+        remote_value = RemoteValueBinaryHeatCool(
+            xknx, controller_mode=HVACControllerMode.HEAT
+        )
+        with self.assertRaises(ConversionError):
+            remote_value.to_knx(HVACOperationMode.STANDBY)
 
     def test_from_knx_operation_mode(self):
         """Test from_knx function with normal operation."""
@@ -79,7 +89,7 @@ class TestRemoteValueDptValue1Ucount(unittest.TestCase):
         """Test from_knx function with invalid payload."""
         xknx = XKNX()
         remote_value = RemoteValueBinaryHeatCool(
-            xknx, operation_mode=HVACOperationMode.HEAT
+            xknx, controller_mode=HVACControllerMode.HEAT
         )
         with self.assertRaises(CouldNotParseTelegram):
             remote_value.from_knx(DPTArray((0x9, 0xF)))
@@ -105,24 +115,26 @@ class TestRemoteValueDptValue1Ucount(unittest.TestCase):
         """Test from_knx function with normal operation."""
         xknx = XKNX()
         remote_value = RemoteValueBinaryHeatCool(
-            xknx, operation_mode=HVACOperationMode.HEAT
+            xknx, controller_mode=HVACControllerMode.HEAT
+        )
+        self.assertEqual(
+            remote_value.from_knx(DPTBinary(True)), HVACControllerMode.HEAT
         )
-        self.assertEqual(remote_value.from_knx(DPTBinary(True)), HVACOperationMode.HEAT)
         self.assertEqual(
-            remote_value.from_knx(DPTBinary(False)), HVACOperationMode.COOL
+            remote_value.from_knx(DPTBinary(False)), HVACControllerMode.COOL
         )
 
     def test_from_knx_unsupported_operation_mode(self):
         """Test from_knx function with unsupported operation."""
         xknx = XKNX()
         with self.assertRaises(ConversionError):
-            RemoteValueBinaryHeatCool(xknx, operation_mode=HVACOperationMode.NODEM)
+            RemoteValueBinaryHeatCool(xknx, controller_mode=HVACControllerMode.NODEM)
 
     def test_from_knx_unknown_operation_mode(self):
         """Test from_knx function with unsupported operation."""
         xknx = XKNX()
         with self.assertRaises(ConversionError):
-            RemoteValueBinaryHeatCool(xknx, operation_mode=None)
+            RemoteValueBinaryHeatCool(xknx, controller_mode=None)
 
     def test_supported_operation_modes_not_implemented(self):
         """Test from_knx function with unsupported operation."""
@@ -257,7 +269,7 @@ class TestRemoteValueDptValue1Ucount(unittest.TestCase):
         remote_value = RemoteValueBinaryHeatCool(
             xknx,
             group_address=GroupAddress("1/2/3"),
-            operation_mode=HVACOperationMode.COOL,
+            controller_mode=HVACControllerMode.COOL,
         )
         with self.assertRaises(CouldNotParseTelegram):
             telegram = Telegram(
diff --git a/test/str_test.py b/test/str_test.py
index a1b3260b..09b4a7ff 100644
--- a/test/str_test.py
+++ b/test/str_test.py
@@ -510,7 +510,7 @@ class TestStringRepresentations(unittest.TestCase):
             "<DIBDeviceInformation \n"
             '\tknx_medium="KNXMedium.TP1" \n'
             '\tprogramming_mode="False" \n'
-            '\tindividual_address="PhysicalAddress("1.1.0")" \n'
+            '\tindividual_address="1.1.0" \n'
             '\tinstallation_number="2" \n'
             '\tproject_number="564" \n'
             '\tserial_number="13:37:13:37:13:37" \n'