Repository: XKNX/xknx
Pull request #613 (2021-02-23 10:01:46)

Use correct move_short direction when cover stopping
<!--
  You are awesome! Thanks for contributing to our project!
  Please, DO NOT DELETE ANY TEXT from this template!.
-->
## Description

So far when stopping a cover, xknx always triggered a move_short in a fixed direction, regardless of whether we are moving up or down. Same for stopping a cover tilt.

Now the cover travel direction is taken into account.

Fixes # (issue)

## Type of change
<!--
Please tick the applicable options.
NOTE: Ticking multiple options most likely indicates
that your change is to big and it is suggested to split it into several smaller PRs.
-->

- [X] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update

## Checklist:

- [X] I have performed a self-review of my own code
- [ ] The documentation has been adjusted accordingly
- [X] The changes generate no new warnings
- [ ] Tests have been added that prove the fix is effective or that the feature works
- [ ] The changes are documented in the changelog
- [ ] The Homeassistant plugin has been adjusted in case of new config options



Patch:
diff --git a/home-assistant-plugin/custom_components/xknx/__init__.py b/home-assistant-plugin/custom_components/xknx/__init__.py
index 38e848bf..7c592c63 100644
--- a/home-assistant-plugin/custom_components/xknx/__init__.py
+++ b/home-assistant-plugin/custom_components/xknx/__init__.py
@@ -106,31 +106,31 @@ CONFIG_SCHEMA = vol.Schema(
                     vol.Optional(CONF_XKNX_EXPOSE): vol.All(
                         cv.ensure_list, [ExposeSchema.SCHEMA]
                     ),
-                    vol.Optional(SupportedPlatforms.cover.value): vol.All(
+                    vol.Optional(SupportedPlatforms.COVER.value): vol.All(
                         cv.ensure_list, [CoverSchema.SCHEMA]
                     ),
-                    vol.Optional(SupportedPlatforms.binary_sensor.value): vol.All(
+                    vol.Optional(SupportedPlatforms.BINARY_sensor.value): vol.All(
                         cv.ensure_list, [BinarySensorSchema.SCHEMA]
                     ),
-                    vol.Optional(SupportedPlatforms.light.value): vol.All(
+                    vol.Optional(SupportedPlatforms.LIGHT.value): vol.All(
                         cv.ensure_list, [LightSchema.SCHEMA]
                     ),
-                    vol.Optional(SupportedPlatforms.climate.value): vol.All(
+                    vol.Optional(SupportedPlatforms.CLIMATE.value): vol.All(
                         cv.ensure_list, [ClimateSchema.SCHEMA]
                     ),
-                    vol.Optional(SupportedPlatforms.notify.value): vol.All(
+                    vol.Optional(SupportedPlatforms.NOTIFY.value): vol.All(
                         cv.ensure_list, [NotifySchema.SCHEMA]
                     ),
-                    vol.Optional(SupportedPlatforms.switch.value): vol.All(
+                    vol.Optional(SupportedPlatforms.SWITCH.value): vol.All(
                         cv.ensure_list, [SwitchSchema.SCHEMA]
                     ),
-                    vol.Optional(SupportedPlatforms.sensor.value): vol.All(
+                    vol.Optional(SupportedPlatforms.SENSOR.value): vol.All(
                         cv.ensure_list, [SensorSchema.SCHEMA]
                     ),
-                    vol.Optional(SupportedPlatforms.scene.value): vol.All(
+                    vol.Optional(SupportedPlatforms.SCENE.value): vol.All(
                         cv.ensure_list, [SceneSchema.SCHEMA]
                     ),
-                    vol.Optional(SupportedPlatforms.weather.value): vol.All(
+                    vol.Optional(SupportedPlatforms.WEATHER.value): vol.All(
                         cv.ensure_list, [WeatherSchema.SCHEMA]
                     ),
                     vol.Optional(SupportedPlatforms.fan.value): vol.All(
diff --git a/home-assistant-plugin/custom_components/xknx/const.py b/home-assistant-plugin/custom_components/xknx/const.py
index d9605ffc..a6926eeb 100644
--- a/home-assistant-plugin/custom_components/xknx/const.py
+++ b/home-assistant-plugin/custom_components/xknx/const.py
@@ -26,23 +26,23 @@ CONF_RESET_AFTER = "reset_after"
 class ColorTempModes(Enum):
     """Color temperature modes for config validation."""
 
-    absolute = "DPT-7.600"
-    relative = "DPT-5.001"
+    ABSOLUTE = "DPT-7.600"
+    RELATIVE = "DPT-5.001"
 
 
 class SupportedPlatforms(Enum):
     """Supported platforms."""
 
-    binary_sensor = "binary_sensor"
-    climate = "climate"
-    cover = "cover"
-    fan = "fan"
-    light = "light"
-    notify = "notify"
-    scene = "scene"
-    sensor = "sensor"
-    switch = "switch"
-    weather = "weather"
+    BINARY_SENSOR = "binary_sensor"
+    CLIMATE = "climate"
+    COVER = "cover"
+    FAN = "fan"
+    LIGHT = "light"
+    NOTIFY = "notify"
+    SCENE = "scene"
+    SENSOR = "sensor"
+    SWITCH = "switch"
+    WEATHER = "weather"
 
 
 # Map KNX controller modes to HA modes. This list might not be complete.
diff --git a/home-assistant-plugin/custom_components/xknx/factory.py b/home-assistant-plugin/custom_components/xknx/factory.py
index 4f50da4d..e8460946 100644
--- a/home-assistant-plugin/custom_components/xknx/factory.py
+++ b/home-assistant-plugin/custom_components/xknx/factory.py
@@ -40,34 +40,34 @@ def create_knx_device(
     config: ConfigType,
 ) -> XknxDevice:
     """Return the requested XKNX device."""
-    if platform is SupportedPlatforms.light:
+    if platform is SupportedPlatforms.LIGHT:
         return _create_light(knx_module, config)
 
-    if platform is SupportedPlatforms.cover:
+    if platform is SupportedPlatforms.COVER:
         return _create_cover(knx_module, config)
 
-    if platform is SupportedPlatforms.climate:
+    if platform is SupportedPlatforms.CLIMATE:
         return _create_climate(knx_module, config)
 
-    if platform is SupportedPlatforms.switch:
+    if platform is SupportedPlatforms.SWITCH:
         return _create_switch(knx_module, config)
 
-    if platform is SupportedPlatforms.sensor:
+    if platform is SupportedPlatforms.SENSOR:
         return _create_sensor(knx_module, config)
 
-    if platform is SupportedPlatforms.notify:
+    if platform is SupportedPlatforms.NOTIFY:
         return _create_notify(knx_module, config)
 
-    if platform is SupportedPlatforms.scene:
+    if platform is SupportedPlatforms.SCENE:
         return _create_scene(knx_module, config)
 
-    if platform is SupportedPlatforms.binary_sensor:
+    if platform is SupportedPlatforms.BINARY_sensor:
         return _create_binary_sensor(knx_module, config)
 
-    if platform is SupportedPlatforms.weather:
+    if platform is SupportedPlatforms.WEATHER:
         return _create_weather(knx_module, config)
 
-    if platform is SupportedPlatforms.fan:
+    if platform is SupportedPlatforms.FAN:
         return _create_fan(knx_module, config)
 
 
@@ -121,12 +121,12 @@ def _create_light(knx_module: XKNX, config: ConfigType) -> XknxLight:
     group_address_tunable_white_state = None
     group_address_color_temp = None
     group_address_color_temp_state = None
-    if config[LightSchema.CONF_COLOR_TEMP_MODE] == ColorTempModes.absolute:
+    if config[LightSchema.CONF_COLOR_TEMP_MODE] == ColorTempModes.ABSOLUTE:
         group_address_color_temp = config.get(LightSchema.CONF_COLOR_TEMP_ADDRESS)
         group_address_color_temp_state = config.get(
             LightSchema.CONF_COLOR_TEMP_STATE_ADDRESS
         )
-    elif config[LightSchema.CONF_COLOR_TEMP_MODE] == ColorTempModes.relative:
+    elif config[LightSchema.CONF_COLOR_TEMP_MODE] == ColorTempModes.RELATIVE:
         group_address_tunable_white = config.get(LightSchema.CONF_COLOR_TEMP_ADDRESS)
         group_address_tunable_white_state = config.get(
             LightSchema.CONF_COLOR_TEMP_STATE_ADDRESS
diff --git a/home-assistant-plugin/custom_components/xknx/fan.py b/home-assistant-plugin/custom_components/xknx/fan.py
index 6aa4f722..43d1cd7d 100644
--- a/home-assistant-plugin/custom_components/xknx/fan.py
+++ b/home-assistant-plugin/custom_components/xknx/fan.py
@@ -34,14 +34,14 @@ class KNXFan(KnxEntity, FanEntity):
         """Initialize of KNX fan."""
         super().__init__(device)
 
-        if self._device.mode == FanSpeedMode.Step:
+        if self._device.mode == FanSpeedMode.STEP:
             self._step_range = (1, device.max_step)
         else:
             self._step_range = None
 
     async def async_set_percentage(self, percentage: int) -> None:
         """Set the speed of the fan, as a percentage."""
-        if self._device.mode == FanSpeedMode.Step:
+        if self._device.mode == FanSpeedMode.STEP:
             step = math.ceil(percentage_to_ranged_value(self._step_range, percentage))
             await self._device.set_speed(step)
         else:
@@ -63,7 +63,7 @@ class KNXFan(KnxEntity, FanEntity):
         if self._device.current_speed is None:
             return None
 
-        if self._device.mode == FanSpeedMode.Step:
+        if self._device.mode == FanSpeedMode.STEP:
             return ranged_value_to_percentage(
                 self._step_range, self._device.current_speed
             )
diff --git a/xknx/devices/cover.py b/xknx/devices/cover.py
index c250ef7c..ae318815 100644
--- a/xknx/devices/cover.py
+++ b/xknx/devices/cover.py
@@ -18,7 +18,7 @@ from xknx.remote_value import (
 )
 
 from .device import Device, DeviceCallbackType
-from .travelcalculator import TravelCalculator
+from .travelcalculator import TravelCalculator, TravelStatus
 
 if TYPE_CHECKING:
     from xknx.remote_value import RemoteValue
@@ -125,6 +125,7 @@ class Cover(Device):
         self.travel_time_up = travel_time_up
 
         self.travelcalculator = TravelCalculator(travel_time_down, travel_time_up)
+        self.travel_direction_tilt: Optional[TravelStatus] = None
 
         self.device_class = device_class
 
@@ -198,12 +199,14 @@ class Cover(Device):
         """Move cover down."""
         await self.updown.down()
         self.travelcalculator.start_travel_down()
+        self.travel_direction_tilt = None
         await self.after_update()
 
     async def set_up(self) -> None:
         """Move cover up."""
         await self.updown.up()
         self.travelcalculator.start_travel_up()
+        self.travel_direction_tilt = None
         await self.after_update()
 
     async def set_short_down(self) -> None:
@@ -219,11 +222,21 @@ class Cover(Device):
         if self.stop_.writable:
             await self.stop_.on()
         elif self.step.writable:
-            await self.step.increase()
+            if (
+                self.travel_direction_tilt == TravelStatus.DIRECTION_UP
+                or self.travelcalculator.travel_direction == TravelStatus.DIRECTION_UP
+            ):
+                await self.step.decrease()
+            elif (
+                self.travel_direction_tilt == TravelStatus.DIRECTION_DOWN
+                or self.travelcalculator.travel_direction == TravelStatus.DIRECTION_DOWN
+            ):
+                await self.step.increase()
         else:
             logger.warning("Stop not supported for device %s", self.get_name())
             return
         self.travelcalculator.stop()
+        self.travel_direction_tilt = None
         await self.after_update()
 
     async def set_position(self, position: int) -> None:
@@ -271,14 +284,22 @@ class Cover(Device):
         if not self.supports_angle:
             logger.warning("Angle not supported for device %s", self.get_name())
             return
+
+        current_angle = self.current_angle()
+        self.travel_direction_tilt = (
+            TravelStatus.DIRECTION_DOWN
+            if current_angle is not None and angle >= current_angle
+            else TravelStatus.DIRECTION_UP
+        )
+
         await self.angle.set(angle)
 
     async def auto_stop_if_necessary(self) -> None:
         """Do auto stop if necessary."""
         # If device does not support auto_positioning,
-        # we have to stop the device when position is reached.
+        # we have to stop the device when position is reached,
         # unless device was traveling to fully open
-        # or fully closed state
+        # or fully closed state.
         if (
             self.supports_stop
             and not self.position_target.writable
diff --git a/xknx/devices/fan.py b/xknx/devices/fan.py
index da9b44d9..c30ab2da 100644
--- a/xknx/devices/fan.py
+++ b/xknx/devices/fan.py
@@ -30,8 +30,8 @@ logger = logging.getLogger("xknx.log")
 class FanSpeedMode(Enum):
     """Enum for setting the fan speed mode."""
 
-    Percent = 1
-    Step = 2
+    PERCENT = 1
+    STEP = 2
 
 
 class Fan(Device):
@@ -56,10 +56,10 @@ class Fan(Device):
         super().__init__(xknx, name, device_updated_cb)
 
         self.speed: Union[RemoteValueDptValue1Ucount, RemoteValueScaling]
-        self.mode = FanSpeedMode.Step if max_step is not None else FanSpeedMode.Percent
+        self.mode = FanSpeedMode.STEP if max_step is not None else FanSpeedMode.PERCENT
         self.max_step = max_step
 
-        if self.mode == FanSpeedMode.Step:
+        if self.mode == FanSpeedMode.STEP:
             self.speed = RemoteValueDptValue1Ucount(
                 xknx,
                 group_address_speed,
diff --git a/xknx/devices/light.py b/xknx/devices/light.py
index 4daeb1b3..172bc33c 100644
--- a/xknx/devices/light.py
+++ b/xknx/devices/light.py
@@ -47,8 +47,8 @@ logger = logging.getLogger("xknx.log")
 class ColorTempModes(Enum):
     """Color temperature modes for config validation."""
 
-    absolute = "DPT-7.600"
-    relative = "DPT-5.001"
+    ABSOLUTE = "DPT-7.600"
+    RELATIVE = "DPT-5.001"
 
 
 class _SwitchAndBrightness:
@@ -261,10 +261,14 @@ class Light(Device):
         yield self.rgbw
         yield self.tunable_white
         yield self.color_temperature
-        for color in (self.red, self.green, self.blue, self.white):
+        for color in self._iter_individual_colors():
             yield color.switch
             yield color.brightness
 
+    def _iter_individual_colors(self) -> Iterator[_SwitchAndBrightness]:
+        """Iterate the devices individual colors."""
+        yield from (self.red, self.green, self.blue, self.white)
+
     @property
     def supports_brightness(self) -> bool:
         """Return if light supports brightness."""
@@ -274,17 +278,14 @@ class Light(Device):
     def supports_color(self) -> bool:
         """Return if light supports color."""
         return self.color.initialized or all(
-            [c.brightness.initialized for c in (self.red, self.green, self.blue)]
+            c.brightness.initialized for c in (self.red, self.green, self.blue)
         )
 
     @property
     def supports_rgbw(self) -> bool:
         """Return if light supports RGBW."""
         return self.rgbw.initialized or all(
-            [
-                c.brightness.initialized
-                for c in (self.red, self.green, self.blue, self.white)
-            ]
+            c.brightness.initialized for c in self._iter_individual_colors()
         )
 
     @property
@@ -496,29 +497,22 @@ class Light(Device):
         """Return the current switch state of the device."""
         if self.switch.value is not None:
             return self.switch.value  # type: ignore
-        if any(
-            [
-                c.switch.value is not None
-                for c in (self.red, self.green, self.blue, self.white)
-            ]
-        ):
-            return any(
-                [c.switch.value for c in (self.red, self.green, self.blue, self.white)]
-            )
+        if any(c.switch.value is not None for c in self._iter_individual_colors()):
+            return any(c.switch.value for c in self._iter_individual_colors())
         return None
 
     async def set_on(self) -> None:
         """Switch light on."""
         if self.switch.initialized:
             await self.switch.on()
-        for color in (self.red, self.green, self.blue, self.white):
+        for color in self._iter_individual_colors():
             await color.set_on()
 
     async def set_off(self) -> None:
         """Switch light off."""
         if self.switch.initialized:
             await self.switch.off()
-        for color in (self.red, self.green, self.blue, self.white):
+        for color in self._iter_individual_colors():
             await color.set_off()
 
     @property
@@ -570,10 +564,7 @@ class Light(Device):
                     await self.rgbw.set(list(color) + [white])
                     return
                 if all(
-                    [
-                        c.brightness.initialized
-                        for c in (self.red, self.green, self.blue, self.white)
-                    ]
+                    c.brightness.initialized for c in self._iter_individual_colors()
                 ):
                     await self.red.brightness.set(color[0])
                     await self.green.brightness.set(color[1])
@@ -587,10 +578,7 @@ class Light(Device):
                     await self.color.set(color)
                     return
                 if all(
-                    [
-                        c.brightness.initialized
-                        for c in (self.red, self.green, self.blue)
-                    ]
+                    c.brightness.initialized for c in (self.red, self.green, self.blue)
                 ):
                     await self.red.brightness.set(color[0])
                     await self.green.brightness.set(color[1])
diff --git a/xknx/devices/weather.py b/xknx/devices/weather.py
index 3185f861..8da041b0 100644
--- a/xknx/devices/weather.py
+++ b/xknx/devices/weather.py
@@ -30,45 +30,45 @@ if TYPE_CHECKING:
 class WeatherCondition(Enum):
     """Home assistant weather conditions (partially)."""
 
-    clear_night = "clear-night"
-    cloudy = "cloudy"
-    lightning = "lightning"
-    lightning_rainy = "lightning-rainy"
-    partly_cloudy = "partly-cloudy"
-    pouring = "pouring"
-    rainy = "rainy"
-    snowy = "snowy"
-    snowy_rainy = "snowy-rainy"
-    sunny = "sunny"
-    windy = "windy"
-    windy_variant = "windy_variant"
-    exceptional = "exceptional"
+    CLEAR_NIGHT = "clear-night"
+    CLOUDY = "cloudy"
+    LIGHTNING = "lightning"
+    LIGHTNING_RAINY = "lightning-rainy"
+    PARTLY_CLOUDY = "partly-cloudy"
+    POURING = "pouring"
+    RAINY = "rainy"
+    SNOWY = "snowy"
+    SNOWY_RAINY = "snowy-rainy"
+    SUNNY = "sunny"
+    WINDY = "windy"
+    WINDY_VARIANT = "windy_variant"
+    EXCEPTIONAL = "exceptional"
 
 
 class Season(Enum):
     """Seasonal mapping for illuminance."""
 
-    winter = 0
-    summer = 1
+    WINTER = 0
+    SUMMER = 1
 
 
 YEAR = 2000  # dummy leap year to allow input X-02-29 (leap day)
 # Map year to winter and summer in order to be able to have seasonal illuminance checks
 # Sun during summer is stronger than in winter
 SEASONS = [
-    (Season.winter, (date(YEAR, 1, 1), date(YEAR, 4, 20))),
-    (Season.summer, (date(YEAR, 4, 21), date(YEAR, 10, 1))),
-    (Season.winter, (date(YEAR, 10, 2), date(YEAR, 12, 31))),
+    (Season.WINTER, (date(YEAR, 1, 1), date(YEAR, 4, 20))),
+    (Season.SUMMER, (date(YEAR, 4, 21), date(YEAR, 10, 1))),
+    (Season.WINTER, (date(YEAR, 10, 2), date(YEAR, 12, 31))),
 ]
 
 # Define current condition
 ILLUMINANCE_MAPPING: Tuple[
     Tuple[Season, Callable[[float], bool], WeatherCondition], ...
 ] = (
-    (Season.summer, lambda lx: 2000 <= lx <= 20000, WeatherCondition.cloudy),
-    (Season.summer, lambda lx: lx > 20000, WeatherCondition.sunny),
-    (Season.winter, lambda lx: 999 <= lx <= 4500, WeatherCondition.cloudy),
-    (Season.winter, lambda lx: lx > 4500, WeatherCondition.sunny),
+    (Season.SUMMER, lambda lx: 2000 <= lx <= 20000, WeatherCondition.CLOUDY),
+    (Season.SUMMER, lambda lx: lx > 20000, WeatherCondition.SUNNY),
+    (Season.WINTER, lambda lx: 999 <= lx <= 4500, WeatherCondition.CLOUDY),
+    (Season.WINTER, lambda lx: lx > 4500, WeatherCondition.SUNNY),
 )
 
 
@@ -419,16 +419,16 @@ class Weather(Device):
             )
 
         if self.wind_alarm and self.rain_alarm:
-            return WeatherCondition.lightning_rainy
+            return WeatherCondition.LIGHTNING_RAINY
 
         if self.frost_alarm and self.rain_alarm:
-            return WeatherCondition.snowy_rainy
+            return WeatherCondition.SNOWY_RAINY
 
         if self.rain_alarm:
-            return WeatherCondition.rainy
+            return WeatherCondition.RAINY
 
         if self.wind_alarm:
-            return WeatherCondition.windy
+            return WeatherCondition.WINDY
 
         current_season: Season = _get_season(current_date)
         _season: Season
@@ -439,9 +439,9 @@ class Weather(Device):
                 return result
 
         if self.day_night is False:
-            return WeatherCondition.clear_night
+            return WeatherCondition.CLEAR_NIGHT
 
-        return WeatherCondition.exceptional
+        return WeatherCondition.EXCEPTIONAL
 
     @classmethod
     def from_config(cls, xknx: "XKNX", name: str, config: Any) -> "Weather":
diff --git a/xknx/io/tunnelling.py b/xknx/io/tunnelling.py
index 8deb18c0..7d04f1ce 100644
--- a/xknx/io/tunnelling.py
+++ b/xknx/io/tunnelling.py
@@ -47,7 +47,7 @@ class Tunnelling(RequestResponse):
         cemi = CEMIFrame.init_from_telegram(
             self.xknx,
             telegram=self.telegram,
-            code=CEMIMessageCode.L_Data_REQ,
+            code=CEMIMessageCode.L_DATA_REQ,
             src_addr=self.src_address,
         )
         tunnelling_request = TunnellingRequest(
diff --git a/xknx/knxip/cemi_frame.py b/xknx/knxip/cemi_frame.py
index 068c41be..82aa19ea 100644
--- a/xknx/knxip/cemi_frame.py
+++ b/xknx/knxip/cemi_frame.py
@@ -121,7 +121,7 @@ class CEMIFrame:
 
         if self.code not in (
             CEMIMessageCode.L_DATA_IND,
-            CEMIMessageCode.L_Data_REQ,
+            CEMIMessageCode.L_DATA_REQ,
             CEMIMessageCode.L_DATA_CON,
         ):
             raise UnsupportedCEMIMessage(
@@ -133,7 +133,7 @@ class CEMIFrame:
         return self.from_knx_data_link_layer(raw)
 
     def from_knx_data_link_layer(self, cemi: bytes) -> int:
-        """Parse L_DATA_IND, CEMIMessageCode.L_Data_REQ, CEMIMessageCode.L_DATA_CON."""
+        """Parse L_DATA_IND, CEMIMessageCode.L_DATA_REQ, CEMIMessageCode.L_DATA_CON."""
         if len(cemi) < 11:
             # eg. ETS Line-Scan issues L_DATA_IND with length 10
             raise UnsupportedCEMIMessage(
diff --git a/xknx/knxip/knxip_enum.py b/xknx/knxip/knxip_enum.py
index d45fb5e6..a6dd98b3 100644
--- a/xknx/knxip/knxip_enum.py
+++ b/xknx/knxip/knxip_enum.py
@@ -57,7 +57,7 @@ class CEMIMessageCode(Enum):
 
     # FROM NETWORK LAYER TO DATA LINK LAYER
     L_RAW_REQ = 0x10
-    L_Data_REQ = 0x11  # Data Service.
+    L_DATA_REQ = 0x11  # Data Service.
     # Primitive used for transmitting a data frame
     L_POLL_DATA_REQ = 0x13  # Poll Data Service
 
diff --git a/xknx/knxip/tunnelling_request.py b/xknx/knxip/tunnelling_request.py
index 4c1755de..12848ce8 100644
--- a/xknx/knxip/tunnelling_request.py
+++ b/xknx/knxip/tunnelling_request.py
@@ -42,7 +42,7 @@ class TunnellingRequest(KNXIPBody):
         self.cemi: Optional[CEMIFrame] = (
             cemi
             if cemi is not None
-            else CEMIFrame(xknx, code=CEMIMessageCode.L_Data_REQ)
+            else CEMIFrame(xknx, code=CEMIMessageCode.L_DATA_REQ)
         )
 
     def calculated_length(self) -> int:
diff --git a/xknx/remote_value/remote_value.py b/xknx/remote_value/remote_value.py
index 9bcda02d..e6d1ba87 100644
--- a/xknx/remote_value/remote_value.py
+++ b/xknx/remote_value/remote_value.py
@@ -218,7 +218,7 @@ class RemoteValue(ABC, Generic[DPTPayloadType]):
         """Send GroupValueRead telegram for state address to KNX bus."""
         if self.group_address_state is not None:
             # pylint: disable=import-outside-toplevel
-            # TODO: send a ReadRequset and start a timeout from here instead of ValueReader
+            # TODO: send a ReadRequest and start a timeout from here instead of ValueReader
             #       cancel timeout form process(); delete ValueReader
             from xknx.core import ValueReader
 


Test patch:
diff --git a/requirements/testing.txt b/requirements/testing.txt
index 2e605653..cf255e71 100644
--- a/requirements/testing.txt
+++ b/requirements/testing.txt
@@ -5,7 +5,7 @@ coveralls==3.0.0
 flake8==3.8.4
 flake8-isort==4.0.0
 pydocstyle==5.1.1
-pylint==2.6.2
+pylint==2.7.0
 pylint-strict-informational==0.1
 pytest==6.2.2
 pytest-cov==2.11.1
diff --git a/test/config_tests/resources/light/valid_1.yaml b/test/config_tests/resources/light/valid_1.yaml
index f9fd31e6..a885e76b 100644
--- a/test/config_tests/resources/light/valid_1.yaml
+++ b/test/config_tests/resources/light/valid_1.yaml
@@ -17,6 +17,6 @@ color_temperature:
   address: "2/5/7"
   state_address: "2/5/8"
   state_update: "expire 60"
-  mode: absolute
+  mode: ABSOLUTE
   min_kelvin: 2550
   max_kelvin: 6200
\ No newline at end of file
diff --git a/test/config_tests/resources/light/valid_4.yaml b/test/config_tests/resources/light/valid_4.yaml
index 910920f2..ca2b270f 100644
--- a/test/config_tests/resources/light/valid_4.yaml
+++ b/test/config_tests/resources/light/valid_4.yaml
@@ -7,4 +7,4 @@ color_temperature:
   address: "2/5/7"
   state_address: "2/5/8"
   state_update: "expire 60"
-  mode: relative
\ No newline at end of file
+  mode: RELATIVE
\ No newline at end of file
diff --git a/test/config_tests/resources/xknx/valid_1.yaml b/test/config_tests/resources/xknx/valid_1.yaml
index 36287d7e..6348d654 100644
--- a/test/config_tests/resources/xknx/valid_1.yaml
+++ b/test/config_tests/resources/xknx/valid_1.yaml
@@ -50,7 +50,7 @@ light:
       address: "2/5/7"
       state_address: "2/5/8"
       state_update: "expire 60"
-      mode: absolute
+      mode: ABSOLUTE
       min_kelvin: 2550
       max_kelvin: 6200
 fan:
diff --git a/test/config_tests/resources/xknx/valid_2.yaml b/test/config_tests/resources/xknx/valid_2.yaml
index 8ebc3351..8251c183 100644
--- a/test/config_tests/resources/xknx/valid_2.yaml
+++ b/test/config_tests/resources/xknx/valid_2.yaml
@@ -47,7 +47,7 @@ light:
       address: "2/5/7"
       state_address: "2/5/8"
       state_update: "expire 60"
-      mode: absolute
+      mode: ABSOLUTE
       min_kelvin: 2550
       max_kelvin: 6200
 fan:
diff --git a/test/config_tests/resources/xknx/valid_3.yaml b/test/config_tests/resources/xknx/valid_3.yaml
index 0d37e165..d100cf60 100644
--- a/test/config_tests/resources/xknx/valid_3.yaml
+++ b/test/config_tests/resources/xknx/valid_3.yaml
@@ -52,7 +52,7 @@ light:
       address: "2/5/7"
       state_address: "2/5/8"
       state_update: "expire 60"
-      mode: absolute
+      mode: ABSOLUTE
       min_kelvin: 2550
       max_kelvin: 6200
 fan:
diff --git a/test/devices_tests/cover_test.py b/test/devices_tests/cover_test.py
index cce88dcc..cb2b1c39 100644
--- a/test/devices_tests/cover_test.py
+++ b/test/devices_tests/cover_test.py
@@ -374,8 +374,23 @@ class TestCover(unittest.TestCase):
             group_address_position="1/2/3",
             group_address_position_state="1/2/4",
         )
+        # Attempt stopping while not actually moving
         self.loop.run_until_complete(cover_short_stop.stop())
-        self.assertEqual(xknx.telegrams.qsize(), 1)
+        self.assertEqual(xknx.telegrams.qsize(), 0)
+
+        # Attempt stopping while moving down
+        cover_short_stop.travelcalculator.set_position(0)
+        self.loop.run_until_complete(cover_short_stop.set_down())
+        self.loop.run_until_complete(cover_short_stop.stop())
+        self.assertEqual(xknx.telegrams.qsize(), 2)
+        telegram = xknx.telegrams.get_nowait()
+        self.assertEqual(
+            telegram,
+            Telegram(
+                destination_address=GroupAddress("1/2/1"),
+                payload=GroupValueWrite(DPTBinary(1)),
+            ),
+        )
         telegram = xknx.telegrams.get_nowait()
         self.assertEqual(
             telegram,
@@ -385,6 +400,28 @@ class TestCover(unittest.TestCase):
             ),
         )
 
+        # Attempt stopping while moving up
+        cover_short_stop.travelcalculator.set_position(100)
+        self.loop.run_until_complete(cover_short_stop.set_up())
+        self.loop.run_until_complete(cover_short_stop.stop())
+        self.assertEqual(xknx.telegrams.qsize(), 2)
+        telegram = xknx.telegrams.get_nowait()
+        self.assertEqual(
+            telegram,
+            Telegram(
+                destination_address=GroupAddress("1/2/1"),
+                payload=GroupValueWrite(DPTBinary(0)),
+            ),
+        )
+        telegram = xknx.telegrams.get_nowait()
+        self.assertEqual(
+            telegram,
+            Telegram(
+                destination_address=GroupAddress("1/2/2"),
+                payload=GroupValueWrite(DPTBinary(0)),
+            ),
+        )
+
         cover_manual_stop = Cover(
             xknx,
             "TestCover",
@@ -405,6 +442,79 @@ class TestCover(unittest.TestCase):
             ),
         )
 
+    def test_stop_angle(self):
+        """Test stopping cover during angle move / tilting."""
+        xknx = XKNX()
+        cover_short_stop = Cover(
+            xknx,
+            "TestCover",
+            group_address_long="1/2/1",
+            group_address_short="1/2/2",
+            group_address_angle="1/2/5",
+            group_address_angle_state="1/2/6",
+        )
+        # Attempt stopping while not actually tilting
+        self.loop.run_until_complete(cover_short_stop.stop())
+        self.assertEqual(xknx.telegrams.qsize(), 0)
+
+        # Set cover tilt to a dummy start value, since otherwise we cannot
+        # determine later on a tilt direction and without it, stopping the
+        # til process has no effect.
+        self.loop.run_until_complete(
+            cover_short_stop.angle.process(
+                Telegram(
+                    destination_address=GroupAddress("1/2/5"),
+                    payload=GroupValueWrite(DPTArray(0xAA)),
+                )
+            )
+        )
+
+        # Attempt stopping while tilting down
+        self.loop.run_until_complete(cover_short_stop.set_angle(100))
+        self.loop.run_until_complete(cover_short_stop.stop())
+        self.assertEqual(xknx.telegrams.qsize(), 2)
+        telegram = xknx.telegrams.get_nowait()
+        print(telegram)
+        self.assertEqual(
+            telegram,
+            Telegram(
+                destination_address=GroupAddress("1/2/5"),
+                payload=GroupValueWrite(DPTArray(0xFF)),
+            ),
+        )
+        telegram = xknx.telegrams.get_nowait()
+        print(telegram)
+        self.assertEqual(
+            telegram,
+            Telegram(
+                destination_address=GroupAddress("1/2/2"),
+                payload=GroupValueWrite(DPTBinary(1)),
+            ),
+        )
+
+        # Attempt stopping while tilting up
+        self.loop.run_until_complete(cover_short_stop.set_angle(0))
+        self.loop.run_until_complete(cover_short_stop.stop())
+        self.assertEqual(xknx.telegrams.qsize(), 2)
+        telegram = xknx.telegrams.get_nowait()
+        print(telegram)
+        self.assertEqual(
+            telegram,
+            Telegram(
+                destination_address=GroupAddress("1/2/5"),
+                payload=GroupValueWrite(DPTArray(0x00)),
+            ),
+        )
+        telegram = xknx.telegrams.get_nowait()
+        print(telegram)
+        self.assertEqual(
+            telegram,
+            Telegram(
+                destination_address=GroupAddress("1/2/2"),
+                payload=GroupValueWrite(DPTBinary(0)),
+            ),
+        )
+
     #
     # TEST POSITION
     #
@@ -569,7 +679,7 @@ class TestCover(unittest.TestCase):
         )
 
     def test_angle_not_supported(self):
-        """Test changing angle on cover wich does not support angle."""
+        """Test changing angle on cover which does not support angle."""
         xknx = XKNX()
         cover = Cover(
             xknx,
diff --git a/test/devices_tests/weather_test.py b/test/devices_tests/weather_test.py
index 1c59eec3..6f79ca37 100644
--- a/test/devices_tests/weather_test.py
+++ b/test/devices_tests/weather_test.py
@@ -144,7 +144,7 @@ class TestWeather(unittest.TestCase):
         weather._rain_alarm.payload = DPTBinary(1)
         weather._wind_alarm.payload = DPTBinary(1)
 
-        self.assertEqual(weather.ha_current_state(), WeatherCondition.lightning_rainy)
+        self.assertEqual(weather.ha_current_state(), WeatherCondition.LIGHTNING_RAINY)
 
     def test_state_snowy_rainy(self):
         """Test snow rain if frost alarm and rain alarm are true."""
@@ -159,7 +159,7 @@ class TestWeather(unittest.TestCase):
         weather._rain_alarm.payload = DPTBinary(1)
         weather._frost_alarm.payload = DPTBinary(1)
 
-        self.assertEqual(weather.ha_current_state(), WeatherCondition.snowy_rainy)
+        self.assertEqual(weather.ha_current_state(), WeatherCondition.SNOWY_RAINY)
 
     def test_wind_alarm(self):
         """Test basic state mapping."""
@@ -174,7 +174,7 @@ class TestWeather(unittest.TestCase):
 
         weather._wind_alarm.payload = DPTBinary(1)
 
-        self.assertEqual(weather.ha_current_state(), WeatherCondition.windy)
+        self.assertEqual(weather.ha_current_state(), WeatherCondition.WINDY)
 
     def test_rain_alarm(self):
         """Test basic state mapping."""
@@ -189,7 +189,7 @@ class TestWeather(unittest.TestCase):
 
         weather._rain_alarm.payload = DPTBinary(1)
 
-        self.assertEqual(weather.ha_current_state(), WeatherCondition.rainy)
+        self.assertEqual(weather.ha_current_state(), WeatherCondition.RAINY)
 
     def test_cloudy_summer(self):
         """Test cloudy summer if illuminance matches defined interval."""
@@ -211,7 +211,7 @@ class TestWeather(unittest.TestCase):
         summer_date = datetime.datetime(2020, 10, 5, 18, 00)
 
         self.assertEqual(
-            weather.ha_current_state(current_date=summer_date), WeatherCondition.cloudy
+            weather.ha_current_state(current_date=summer_date), WeatherCondition.CLOUDY
         )
 
     def test_sunny_summer(self):
@@ -235,7 +235,7 @@ class TestWeather(unittest.TestCase):
         summer_date = datetime.datetime(2020, 10, 5, 18, 00)
 
         self.assertEqual(
-            weather.ha_current_state(current_date=summer_date), WeatherCondition.sunny
+            weather.ha_current_state(current_date=summer_date), WeatherCondition.SUNNY
         )
 
     def test_sunny_winter(self):
@@ -258,7 +258,7 @@ class TestWeather(unittest.TestCase):
         winter_date = datetime.datetime(2020, 12, 5, 18, 00)
 
         self.assertEqual(
-            weather.ha_current_state(current_date=winter_date), WeatherCondition.sunny
+            weather.ha_current_state(current_date=winter_date), WeatherCondition.SUNNY
         )
 
     def test_cloudy_winter(self):
@@ -282,7 +282,7 @@ class TestWeather(unittest.TestCase):
         winter_date = datetime.datetime(2020, 12, 31, 18, 00)
 
         self.assertEqual(
-            weather.ha_current_state(current_date=winter_date), WeatherCondition.cloudy
+            weather.ha_current_state(current_date=winter_date), WeatherCondition.CLOUDY
         )
 
     def test_day_night(self):
@@ -294,14 +294,14 @@ class TestWeather(unittest.TestCase):
 
         weather._day_night.payload = DPTBinary(0)
 
-        self.assertEqual(weather.ha_current_state(), WeatherCondition.clear_night)
+        self.assertEqual(weather.ha_current_state(), WeatherCondition.CLEAR_NIGHT)
 
     def test_weather_default(self):
         """Test default state mapping."""
         xknx = XKNX()
         weather: Weather = Weather(name="weather", xknx=xknx)
 
-        self.assertEqual(weather.ha_current_state(), WeatherCondition.exceptional)
+        self.assertEqual(weather.ha_current_state(), WeatherCondition.EXCEPTIONAL)
 
     #
     # Create sensor tests
diff --git a/test/knxip_tests/tunnelling_request_test.py b/test/knxip_tests/tunnelling_request_test.py
index 90bbf6d9..679d6bbc 100644
--- a/test/knxip_tests/tunnelling_request_test.py
+++ b/test/knxip_tests/tunnelling_request_test.py
@@ -66,7 +66,7 @@ class Test_KNXIP_TunnelingReq(unittest.TestCase):
             ),
         )
 
-        cemi = CEMIFrame(xknx, code=CEMIMessageCode.L_Data_REQ)
+        cemi = CEMIFrame(xknx, code=CEMIMessageCode.L_DATA_REQ)
         cemi.telegram = Telegram(
             destination_address=GroupAddress("9/0/8"),
             payload=GroupValueWrite(DPTBinary(1)),