Repository: XKNX/xknx
Pull request #551 (2021-01-04 23:22:15)

Typecheck Devices
<!--
  You are awesome! Thanks for contributing to our project!
  Please, DO NOT DELETE ANY TEXT from this template!.
-->
## Description
<!--
Please include a summary of the change and which issue is fixed.
Please also include relevant motivation and context.
List any dependencies that are required for this change.
-->
Typecheck all Devices, RemoteValueClimate* and some minor annotations here and there

It's getting a little tricky now - especially with ClimateMode. But this seems to be the xknx fig leave anyway 😅

The values of RemoteValues are ignored by now - this has to be reverted when RemoteValue and the DPTs are typechecked. eg: `Sensor.resolve_state(self) -> Optional[Any]` or 
```
Light.current_brightness(self) -> Optional[int]:
        """Return current brightness of light."""
        return self.brightness.value  # type: ignore
```
## Type of change
<!--
Please tick the applicable options.
NOTE: Ticking multiple options most likely indicates
that your change is to big and it is suggested to split it into several smaller PRs.
-->

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update

## Checklist:

- [ ] I have performed a self-review of my own code
- [ ] The documentation has been adjusted accordingly
- [ ] The changes generate no new warnings
- [ ] Tests have been added that prove the fix is effective or that the feature works
- [ ] The changes are documented in the changelog
- [ ] The Homeassistant plugin has been adjusted in case of new config options



Patch:
diff --git a/setup.cfg b/setup.cfg
index 9d3b91f7..dcf021dc 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -43,7 +43,7 @@ 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
 # fully typechecked modules
-[mypy-xknx.xknx,xknx.core.*,xknx.exceptions.*,xknx.io.*,xknx.knxip.*,xknx.telegram.*,]
+[mypy-xknx.xknx,xknx.core.*,xknx.devices.*,xknx.exceptions.*,xknx.io.*,xknx.knxip.*,xknx.telegram.*,]
 strict = true
 ignore_errors = false
 warn_unreachable = true
@@ -51,7 +51,7 @@ warn_unreachable = true
 implicit_reexport = true
 
 # partly typechecked modules (extra block for better overview)
-[mypy-xknx.devices.device,xknx.devices.devices,xknx.devices.travelcalculator,xknx.remote_value.remote_value]
+[mypy-xknx.remote_value.remote_value,xknx.remote_value.remote_value_climate_mode]
 strict = true
 ignore_errors = false
 warn_unreachable = true
diff --git a/xknx/devices/action.py b/xknx/devices/action.py
index abc0a500..dffd72fa 100644
--- a/xknx/devices/action.py
+++ b/xknx/devices/action.py
@@ -1,5 +1,9 @@
 """Module for handling commands which may be attached to BinarySensor class."""
 import logging
+from typing import TYPE_CHECKING, Any, Awaitable, Callable, Optional
+
+if TYPE_CHECKING:
+    from xknx.xknx import XKNX
 
 logger = logging.getLogger("xknx.log")
 
@@ -7,13 +11,13 @@ logger = logging.getLogger("xknx.log")
 class ActionBase:
     """Base Class for handling commands."""
 
-    def __init__(self, xknx, hook="on", counter=1):
+    def __init__(self, xknx: "XKNX", hook: str = "on", counter: Optional[int] = 1):
         """Initialize Action_Base class."""
         self.xknx = xknx
         self.hook = hook
         self.counter = counter
 
-    def test_counter(self, counter):
+    def test_counter(self, counter: Optional[int]) -> bool:
         """Test if action filters for specific counter."""
         if self.counter is None:
             # no specific counter_filter -> always true
@@ -22,7 +26,7 @@ class ActionBase:
             return True
         return counter == self.counter
 
-    def test_if_applicable(self, state, counter=None):
+    def test_if_applicable(self, state: bool, counter: Optional[int] = None) -> bool:
         """Test if should be executed for this state and this counter number."""
         if state and (self.hook == "on"):
             return self.test_counter(counter)
@@ -30,15 +34,15 @@ class ActionBase:
             return self.test_counter(counter)
         return False
 
-    async def execute(self):
+    async def execute(self) -> None:
         """Execute action. To be overwritten in derived classes."""
         logger.info("Execute not implemented for %s", self.__class__.__name__)
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return f'<ActionBase hook="{self.hook}" counter="{self.counter}"/>'
 
-    def __eq__(self, other):
+    def __eq__(self, other: object) -> bool:
         """Equal operator."""
         return self.__dict__ == other.__dict__
 
@@ -46,7 +50,14 @@ class ActionBase:
 class Action(ActionBase):
     """Class for handling commands."""
 
-    def __init__(self, xknx, hook="on", target=None, method=None, counter=1):
+    def __init__(
+        self,
+        xknx: "XKNX",
+        hook: str = "on",
+        target: Optional[str] = None,
+        method: Optional[str] = None,
+        counter: int = 1,
+    ):
         """Initialize Action class."""
         # pylint: disable=too-many-arguments
         super().__init__(xknx, hook, counter)
@@ -54,7 +65,7 @@ class Action(ActionBase):
         self.method = method
 
     @classmethod
-    def from_config(cls, xknx, config):
+    def from_config(cls, xknx: "XKNX", config: Any) -> "Action":
         """Initialize object from configuration structure."""
         hook = config.get("hook", "on")
         target = config.get("target")
@@ -62,15 +73,15 @@ class Action(ActionBase):
         counter = config.get("counter", 1)
         return cls(xknx, hook=hook, target=target, method=method, counter=counter)
 
-    async def execute(self):
+    async def execute(self) -> None:
         """Execute action."""
-        if self.target is not None:
+        if self.target is not None and self.method is not None:
             if self.target not in self.xknx.devices:
                 logger.warning("Unknown device %s witin action %s.", self.target, self)
                 return
             await self.xknx.devices[self.target].do(self.method)
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return '<Action target="{}" method="{}" {}/>'.format(
             self.target, self.method, super().__str__()
@@ -80,17 +91,23 @@ class Action(ActionBase):
 class ActionCallback(ActionBase):
     """Class for handling commands via callbacks."""
 
-    def __init__(self, xknx, callback, hook="on", counter=1):
+    def __init__(
+        self,
+        xknx: "XKNX",
+        callback: Callable[[], Awaitable[None]],
+        hook: str = "on",
+        counter: int = 1,
+    ):
         """Initialize Action class."""
         # pylint: disable=too-many-arguments
         super().__init__(xknx, hook, counter)
         self.callback = callback
 
-    async def execute(self):
+    async def execute(self) -> None:
         """Execute callback."""
         await self.callback()
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return '<ActionCallback callback="{}" {}/>'.format(
             self.callback.__name__, super().__str__()
diff --git a/xknx/devices/binary_sensor.py b/xknx/devices/binary_sensor.py
index 2880ca20..80b28352 100644
--- a/xknx/devices/binary_sensor.py
+++ b/xknx/devices/binary_sensor.py
@@ -10,12 +10,17 @@ A BinarySensor may also have Actions attached which are executed after state was
 """
 import asyncio
 import time
-from typing import List, Optional
+from typing import TYPE_CHECKING, Any, Iterator, List, Optional, cast
 
 from xknx.remote_value import RemoteValueSwitch
 
 from .action import Action
-from .device import Device
+from .device import Device, DeviceCallbackType
+
+if TYPE_CHECKING:
+    from xknx.telegram import Telegram
+    from xknx.telegram.address import GroupAddressableType
+    from xknx.xknx import XKNX
 
 
 class BinarySensor(Device):
@@ -24,17 +29,17 @@ class BinarySensor(Device):
     # pylint: disable=too-many-instance-attributes
     def __init__(
         self,
-        xknx,
+        xknx: "XKNX",
         name: str,
-        group_address_state=None,
+        group_address_state: "GroupAddressableType" = None,
         invert: Optional[bool] = False,
         sync_state: bool = True,
         ignore_internal_state: bool = False,
-        device_class: str = None,
+        device_class: Optional[str] = None,
         reset_after: Optional[float] = None,
-        actions: List[Action] = None,
+        actions: Optional[List[Action]] = None,
         context_timeout: Optional[float] = None,
-        device_updated_cb=None,
+        device_updated_cb: Optional[DeviceCallbackType] = None,
     ):
         """Initialize BinarySensor class."""
         # pylint: disable=too-many-arguments
@@ -46,14 +51,14 @@ class BinarySensor(Device):
         self.device_class = device_class
         self.ignore_internal_state = ignore_internal_state or bool(context_timeout)
         self.reset_after = reset_after
-        self.state = None
+        self.state: Optional[bool] = None
 
         self._context_timeout = context_timeout
         self._count_set_on = 0
         self._count_set_off = 0
-        self._last_set = None
-        self._reset_task = None
-        self._context_task = None
+        self._last_set: Optional[float] = None
+        self._reset_task: Optional[asyncio.Task[None]] = None
+        self._context_task: Optional[asyncio.Task[None]] = None
         # TODO: log a warning if reset_after and sync_state are true ? This could cause actions to self-fire.
         self.remote_value = RemoteValueSwitch(
             xknx,
@@ -65,11 +70,11 @@ class BinarySensor(Device):
             after_update_cb=self._state_from_remote_value,
         )
 
-    def _iter_remote_values(self):
+    def _iter_remote_values(self) -> Iterator[RemoteValueSwitch]:
         """Iterate the devices RemoteValue classes."""
         yield self.remote_value
 
-    def __del__(self):
+    def __del__(self) -> None:
         """Destructor. Cleaning up if this was not done before."""
         if self._reset_task:
             self._reset_task.cancel()
@@ -78,7 +83,7 @@ class BinarySensor(Device):
             self._context_task.cancel()
 
     @classmethod
-    def from_config(cls, xknx, name, config):
+    def from_config(cls, xknx: "XKNX", name: str, config: Any) -> "BinarySensor":
         """Initialize object from configuration structure."""
         group_address_state = config.get("group_address_state")
         invert = config.get("invert")
@@ -106,11 +111,11 @@ class BinarySensor(Device):
             actions=actions,
         )
 
-    async def _state_from_remote_value(self):
+    async def _state_from_remote_value(self) -> None:
         """Update the internal state from RemoteValue (Callback)."""
         await self._set_internal_state(self.remote_value.value)
 
-    async def _set_internal_state(self, state: bool):
+    async def _set_internal_state(self, state: bool) -> None:
         """Set the internal state of the device. If state was changed after_update hooks and connected Actions are executed."""
         if state != self.state or self.ignore_internal_state:
             self.state = state
@@ -125,7 +130,7 @@ class BinarySensor(Device):
             else:
                 await self._trigger_callbacks()
 
-    async def _counter_task(self, wait_seconds: float):
+    async def _counter_task(self, wait_seconds: float) -> None:
         """Trigger after 1 second to prevent double triggers."""
         await asyncio.sleep(wait_seconds)
         await self._trigger_callbacks()
@@ -135,16 +140,16 @@ class BinarySensor(Device):
 
         await self.after_update()
 
-    async def _trigger_callbacks(self):
+    async def _trigger_callbacks(self) -> None:
         """Trigger callbacks for device and execute actions if any."""
         await self.after_update()
 
         for action in self.actions:
-            if action.test_if_applicable(self.state, self.counter):
+            if action.test_if_applicable(cast(bool, self.state), self.counter):
                 await action.execute()
 
     @property
-    def counter(self):
+    def counter(self) -> int:
         """Return current counter for sensor."""
         return self._count_set_on if self.state else self._count_set_off
 
@@ -159,7 +164,7 @@ class BinarySensor(Device):
             new_set_time = time.time()
             time_diff = new_set_time - self._last_set
             self._last_set = new_set_time
-            return time_diff < self._context_timeout
+            return time_diff < cast(float, self._context_timeout)
 
         if self._context_timeout and within_same_context():
             if state:
@@ -176,24 +181,24 @@ class BinarySensor(Device):
             self._count_set_off = 1
         return 1
 
-    async def process_group_write(self, telegram):
+    async def process_group_write(self, telegram: "Telegram") -> None:
         """Process incoming and outgoing GROUP WRITE telegram."""
         if await self.remote_value.process(telegram, always_callback=True):
             self._process_reset_after()
 
-    async def process_group_response(self, telegram):
+    async def process_group_response(self, telegram: "Telegram") -> None:
         """Process incoming GroupValueResponse telegrams."""
         if await self.remote_value.process(telegram, always_callback=False):
             self._process_reset_after()
 
-    def _process_reset_after(self):
+    def _process_reset_after(self) -> None:
         """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):
+    async def _reset_state(self, wait_seconds: float) -> None:
         await asyncio.sleep(wait_seconds)
         await self._set_internal_state(False)
 
@@ -205,7 +210,7 @@ class BinarySensor(Device):
         """Return if binary sensor is 'off'."""
         return not self.state
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return '<BinarySensor name="{}" remote_value="{}" state="{}"/>'.format(
             self.name, self.remote_value.group_addr_str(), self.state
diff --git a/xknx/devices/climate.py b/xknx/devices/climate.py
index e1e37278..28cc79e4 100644
--- a/xknx/devices/climate.py
+++ b/xknx/devices/climate.py
@@ -6,6 +6,7 @@ Module for managing the climate within a room.
 """
 from enum import Enum
 import logging
+from typing import TYPE_CHECKING, Any, Iterator, Optional, Union, cast
 
 from xknx.remote_value import (
     RemoteValueSetpointShift,
@@ -14,7 +15,13 @@ from xknx.remote_value import (
 )
 
 from .climate_mode import ClimateMode
-from .device import Device
+from .device import Device, DeviceCallbackType
+
+if TYPE_CHECKING:
+    from xknx.remote_value import RemoteValue
+    from xknx.telegram import Telegram
+    from xknx.telegram.address import GroupAddress, GroupAddressableType
+    from xknx.xknx import XKNX
 
 logger = logging.getLogger("xknx.log")
 
@@ -38,24 +45,24 @@ class Climate(Device):
     # pylint: disable=too-many-instance-attributes,invalid-name
     def __init__(
         self,
-        xknx,
-        name,
-        group_address_temperature=None,
-        group_address_target_temperature=None,
-        group_address_target_temperature_state=None,
-        group_address_setpoint_shift=None,
-        group_address_setpoint_shift_state=None,
-        setpoint_shift_mode=DEFAULT_SETPOINT_SHIFT_MODE,
-        setpoint_shift_max=DEFAULT_SETPOINT_SHIFT_MAX,
-        setpoint_shift_min=DEFAULT_SETPOINT_SHIFT_MIN,
-        temperature_step=DEFAULT_TEMPERATURE_STEP,
-        group_address_on_off=None,
-        group_address_on_off_state=None,
-        on_off_invert=False,
-        min_temp=None,
-        max_temp=None,
-        mode=None,
-        device_updated_cb=None,
+        xknx: "XKNX",
+        name: str,
+        group_address_temperature: Optional["GroupAddressableType"] = None,
+        group_address_target_temperature: Optional["GroupAddressableType"] = None,
+        group_address_target_temperature_state: Optional["GroupAddressableType"] = None,
+        group_address_setpoint_shift: Optional["GroupAddressableType"] = None,
+        group_address_setpoint_shift_state: Optional["GroupAddressableType"] = None,
+        setpoint_shift_mode: SetpointShiftMode = DEFAULT_SETPOINT_SHIFT_MODE,
+        setpoint_shift_max: float = DEFAULT_SETPOINT_SHIFT_MAX,
+        setpoint_shift_min: float = DEFAULT_SETPOINT_SHIFT_MIN,
+        temperature_step: float = DEFAULT_TEMPERATURE_STEP,
+        group_address_on_off: Optional["GroupAddressableType"] = None,
+        group_address_on_off_state: Optional["GroupAddressableType"] = None,
+        on_off_invert: bool = False,
+        min_temp: Optional[float] = None,
+        max_temp: Optional[float] = None,
+        mode: Optional[ClimateMode] = None,
+        device_updated_cb: Optional[DeviceCallbackType] = None,
     ):
         """Initialize Climate class."""
         # pylint: disable=too-many-arguments, too-many-locals, too-many-branches, too-many-statements
@@ -84,6 +91,7 @@ class Climate(Device):
             after_update_cb=self.after_update,
         )
 
+        self._setpoint_shift: Union[RemoteValueTemp, RemoteValueSetpointShift]
         if setpoint_shift_mode == SetpointShiftMode.DPT9002:
             self._setpoint_shift = RemoteValueTemp(
                 xknx,
@@ -117,7 +125,7 @@ class Climate(Device):
 
         self.mode = mode
 
-    def _iter_remote_values(self):
+    def _iter_remote_values(self) -> Iterator["RemoteValue"]:
         """Iterate the devices RemoteValue classes."""
         yield from (
             self.temperature,
@@ -127,7 +135,7 @@ class Climate(Device):
         )
 
     @classmethod
-    def from_config(cls, xknx, name, config):
+    def from_config(cls, xknx: "XKNX", name: str, config: Any) -> "Climate":
         """Initialize object from configuration structure."""
         # pylint: disable=too-many-locals
         group_address_temperature = config.get("group_address_temperature")
@@ -160,7 +168,7 @@ class Climate(Device):
         climate_mode = None
         if "mode" in config:
             climate_mode = ClimateMode.from_config(
-                xknx=xknx, name=None, config=config["mode"]
+                xknx=xknx, name=f"{name}_mode", config=config["mode"]
             )
 
         return cls(
@@ -183,28 +191,28 @@ class Climate(Device):
             mode=climate_mode,
         )
 
-    def has_group_address(self, group_address):
+    def has_group_address(self, group_address: "GroupAddress") -> bool:
         """Test if device has given group address."""
         if self.mode is not None and self.mode.has_group_address(group_address):
             return True
         return super().has_group_address(group_address)
 
     @property
-    def is_on(self):
+    def is_on(self) -> bool:
         """Return power status."""
         # None will return False
         return bool(self.on.value)
 
-    async def turn_on(self):
+    async def turn_on(self) -> None:
         """Set power status to on."""
         await self.on.on()
 
-    async def turn_off(self):
+    async def turn_off(self) -> None:
         """Set power status to off."""
         await self.on.off()
 
     @property
-    def initialized_for_setpoint_shift_calculations(self):
+    def initialized_for_setpoint_shift_calculations(self) -> bool:
         """Test if object is initialized for setpoint shift calculations."""
         if not self._setpoint_shift.initialized:
             return False
@@ -216,9 +224,10 @@ class Climate(Device):
             return False
         return True
 
-    async def set_target_temperature(self, target_temperature):
+    async def set_target_temperature(self, target_temperature: float) -> None:
         """Send new target temperature or setpoint_shift to KNX bus."""
-        if self.initialized_for_setpoint_shift_calculations:
+        if self.base_temperature is not None:
+            # implies initialized_for_setpoint_shift_calculations
             temperature_delta = target_temperature - self.base_temperature
             await self.set_setpoint_shift(temperature_delta)
         else:
@@ -228,24 +237,26 @@ class Climate(Device):
             await self.target_temperature.set(validated_temp)
 
     @property
-    def base_temperature(self):
+    def base_temperature(self) -> Optional[float]:
         """
-        Return the base temperature.
+        Return the base temperature when setpoint_shift is initialized.
 
         Base temperature is the default temperature (setpoint-shift=0) for the active climate mode.
         As this value is usually not available via KNX, we have to derive this from the current
         target temperature and the current set point shift.
         """
         if self.initialized_for_setpoint_shift_calculations:
-            return self.target_temperature.value - self.setpoint_shift
+            return cast(float, self.target_temperature.value - self.setpoint_shift)
         return None
 
     @property
-    def setpoint_shift(self):
+    def setpoint_shift(self) -> Optional[float]:
         """Return current offset from base temperature in Kelvin."""
-        return self._setpoint_shift.value
+        return self._setpoint_shift.value  # type: ignore
 
-    def validate_value(self, value, min_value, max_value):
+    def validate_value(
+        self, value: float, min_value: Optional[float], max_value: Optional[float]
+    ) -> float:
         """Check boundaries of temperature and return valid temperature value."""
         if (min_value is not None) and (value < min_value):
             logger.warning("Min value exceeded at %s: %s", self.name, value)
@@ -255,7 +266,7 @@ class Climate(Device):
             return max_value
         return value
 
-    async def set_setpoint_shift(self, offset):
+    async def set_setpoint_shift(self, offset: float) -> None:
         """Send new temperature offset to KNX bus."""
         validated_offset = self.validate_value(
             offset, self.setpoint_shift_min, self.setpoint_shift_max
@@ -267,37 +278,39 @@ class Climate(Device):
             await self.target_temperature.set(base_temperature + validated_offset)
 
     @property
-    def target_temperature_max(self):
+    def target_temperature_max(self) -> Optional[float]:
         """Return the highest possible target temperature."""
         if self.max_temp is not None:
             return self.max_temp
-        if self.initialized_for_setpoint_shift_calculations:
+        if self.base_temperature is not None:
+            # implies initialized_for_setpoint_shift_calculations
             return self.base_temperature + self.setpoint_shift_max
         return None
 
     @property
-    def target_temperature_min(self):
+    def target_temperature_min(self) -> Optional[float]:
         """Return the lowest possible target temperature."""
         if self.min_temp is not None:
             return self.min_temp
-        if self.initialized_for_setpoint_shift_calculations:
+        if self.base_temperature is not None:
+            # implies initialized_for_setpoint_shift_calculations
             return self.base_temperature + self.setpoint_shift_min
         return None
 
-    async def process_group_write(self, telegram):
+    async def process_group_write(self, telegram: "Telegram") -> None:
         """Process incoming and outgoing GROUP WRITE telegram."""
         for remote_value in self._iter_remote_values():
             await remote_value.process(telegram)
         if self.mode is not None:
             await self.mode.process_group_write(telegram)
 
-    async def sync(self, wait_for_result=False):
+    async def sync(self, wait_for_result: bool = False) -> None:
         """Read states of device from KNX bus."""
         await super().sync(wait_for_result=wait_for_result)
         if self.mode is not None:
             await self.mode.sync(wait_for_result=wait_for_result)
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return (
             '<Climate name="{}" '
diff --git a/xknx/devices/climate_mode.py b/xknx/devices/climate_mode.py
index 86062ce2..1a4a504b 100644
--- a/xknx/devices/climate_mode.py
+++ b/xknx/devices/climate_mode.py
@@ -5,16 +5,24 @@ Operation modes can be 'auto', 'comfort', 'standby', 'economy', 'protection' and
 Controller modes use DPT 20.105.
 """
 from itertools import chain
+from typing import TYPE_CHECKING, Any, Iterator, List, Optional, Union
 
-from xknx.dpt import HVACControllerMode, HVACOperationMode
+from xknx.dpt.dpt_hvac_mode import HVACControllerMode, HVACOperationMode
 from xknx.exceptions import DeviceIllegalValue
-from xknx.remote_value import (
+from xknx.remote_value.remote_value_climate_mode import (
     RemoteValueBinaryHeatCool,
     RemoteValueBinaryOperationMode,
     RemoteValueClimateMode,
+    RemoteValueClimateModeBase,
 )
 
-from .device import Device
+from .device import Device, DeviceCallbackType
+
+if TYPE_CHECKING:
+    from xknx.remote_value import RemoteValue
+    from xknx.telegram import Telegram
+    from xknx.telegram.address import GroupAddressableType
+    from xknx.xknx import XKNX
 
 
 class ClimateMode(Device):
@@ -24,29 +32,33 @@ class ClimateMode(Device):
 
     def __init__(
         self,
-        xknx,
-        name,
-        group_address_operation_mode=None,
-        group_address_operation_mode_state=None,
-        group_address_operation_mode_protection=None,
-        group_address_operation_mode_night=None,
-        group_address_operation_mode_comfort=None,
-        group_address_operation_mode_standby=None,
-        group_address_controller_status=None,
-        group_address_controller_status_state=None,
-        group_address_controller_mode=None,
-        group_address_controller_mode_state=None,
-        group_address_heat_cool=None,
-        group_address_heat_cool_state=None,
-        operation_modes=None,
-        controller_modes=None,
-        device_updated_cb=None,
+        xknx: "XKNX",
+        name: str,
+        group_address_operation_mode: Optional["GroupAddressableType"] = None,
+        group_address_operation_mode_state: Optional["GroupAddressableType"] = None,
+        group_address_operation_mode_protection: Optional[
+            "GroupAddressableType"
+        ] = None,
+        group_address_operation_mode_night: Optional["GroupAddressableType"] = None,
+        group_address_operation_mode_comfort: Optional["GroupAddressableType"] = None,
+        group_address_operation_mode_standby: Optional["GroupAddressableType"] = None,
+        group_address_controller_status: Optional["GroupAddressableType"] = None,
+        group_address_controller_status_state: Optional["GroupAddressableType"] = None,
+        group_address_controller_mode: Optional["GroupAddressableType"] = None,
+        group_address_controller_mode_state: Optional["GroupAddressableType"] = None,
+        group_address_heat_cool: Optional["GroupAddressableType"] = None,
+        group_address_heat_cool_state: Optional["GroupAddressableType"] = None,
+        operation_modes: Optional[List[Union[str, HVACOperationMode]]] = None,
+        controller_modes: Optional[List[Union[str, HVACControllerMode]]] = None,
+        device_updated_cb: Optional[DeviceCallbackType] = None,
     ):
         """Initialize ClimateMode class."""
         # pylint: disable=too-many-arguments, too-many-locals, too-many-branches, too-many-statements
         super().__init__(xknx, name, device_updated_cb)
 
-        self.remote_value_operation_mode = RemoteValueClimateMode(
+        self.remote_value_operation_mode: RemoteValueClimateMode[
+            HVACOperationMode
+        ] = RemoteValueClimateMode(
             xknx,
             group_address=group_address_operation_mode,
             group_address_state=group_address_operation_mode_state,
@@ -56,7 +68,9 @@ class ClimateMode(Device):
             climate_mode_type=RemoteValueClimateMode.ClimateModeType.HVAC_MODE,
             after_update_cb=None,
         )
-        self.remote_value_controller_mode = RemoteValueClimateMode(
+        self.remote_value_controller_mode: RemoteValueClimateMode[
+            HVACControllerMode
+        ] = RemoteValueClimateMode(
             xknx,
             group_address=group_address_controller_mode,
             group_address_state=group_address_controller_mode_state,
@@ -66,7 +80,9 @@ class ClimateMode(Device):
             climate_mode_type=RemoteValueClimateMode.ClimateModeType.HVAC_CONTR_MODE,
             after_update_cb=None,
         )
-        self.remote_value_controller_status = RemoteValueClimateMode(
+        self.remote_value_controller_status: RemoteValueClimateMode[
+            HVACOperationMode
+        ] = RemoteValueClimateMode(
             xknx,
             group_address=group_address_controller_status,
             group_address_state=group_address_controller_status_state,
@@ -131,25 +147,25 @@ class ClimateMode(Device):
         self.operation_mode = HVACOperationMode.STANDBY
         self.controller_mode = HVACControllerMode.HEAT
 
-        self._operation_modes = []
+        self._operation_modes: List[HVACOperationMode] = []
         if operation_modes is None:
             self._operation_modes = self.gather_operation_modes()
         else:
-            for mode in operation_modes:
-                if isinstance(mode, str):
-                    self._operation_modes.append(HVACOperationMode(mode))
-                elif isinstance(mode, HVACOperationMode):
-                    self._operation_modes.append(mode)
+            for op_mode in operation_modes:
+                if isinstance(op_mode, str):
+                    self._operation_modes.append(HVACOperationMode(op_mode))
+                elif isinstance(op_mode, HVACOperationMode):
+                    self._operation_modes.append(op_mode)
 
-        self._controller_modes = []
+        self._controller_modes: List[HVACControllerMode] = []
         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)
+            for ct_mode in controller_modes:
+                if isinstance(ct_mode, str):
+                    self._controller_modes.append(HVACControllerMode(ct_mode))
+                elif isinstance(ct_mode, HVACControllerMode):
+                    self._controller_modes.append(ct_mode)
 
         self.supports_operation_mode = any(
             operation_mode.initialized
@@ -169,7 +185,7 @@ class ClimateMode(Device):
         )
 
     @classmethod
-    def from_config(cls, xknx, name, config):
+    def from_config(cls, xknx: "XKNX", name: str, config: Any) -> "ClimateMode":
         """Initialize object from configuration structure."""
         # pylint: disable=too-many-locals
         group_address_operation_mode = config.get("group_address_operation_mode")
@@ -216,7 +232,9 @@ class ClimateMode(Device):
             group_address_heat_cool_state=group_address_heat_cool_state,
         )
 
-    def _iter_remote_values(self):
+    def _iter_remote_values(
+        self,
+    ) -> Iterator["RemoteValue"]:
         """Iterate climate mode RemoteValue classes."""
         return chain(
             self._iter_byte_operation_modes(),
@@ -224,21 +242,25 @@ class ClimateMode(Device):
             self._iter_binary_operation_modes(),
         )
 
-    def _iter_byte_operation_modes(self):
+    def _iter_byte_operation_modes(
+        self,
+    ) -> Iterator[RemoteValueClimateMode[HVACOperationMode]]:
         """Iterate normal DPT 20.102 operation mode remote values."""
         yield from (
             self.remote_value_operation_mode,
             self.remote_value_controller_status,
         )
 
-    def _iter_controller_remote_values(self):
+    def _iter_controller_remote_values(
+        self,
+    ) -> Iterator[RemoteValueClimateModeBase[HVACControllerMode]]:
         """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):
+    def _iter_binary_operation_modes(self) -> Iterator[RemoteValueBinaryOperationMode]:
         """Iterate DPT 1 binary operation modes."""
         yield from (
             self.remote_value_operation_mode_comfort,
@@ -247,28 +269,33 @@ class ClimateMode(Device):
             self.remote_value_operation_mode_standby,
         )
 
-    async def _set_internal_operation_mode(self, operation_mode):
+    async def _set_internal_operation_mode(
+        self, operation_mode: HVACOperationMode
+    ) -> None:
         """Set internal value of operation mode. Call hooks if operation mode was changed."""
         if operation_mode != self.operation_mode:
             self.operation_mode = operation_mode
             await self.after_update()
 
-    async def _set_internal_controller_mode(self, controller_mode):
+    async def _set_internal_controller_mode(
+        self, controller_mode: HVACControllerMode
+    ) -> None:
         """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):
+    async def set_operation_mode(self, operation_mode: HVACOperationMode) -> None:
         """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
         ):
             raise DeviceIllegalValue(
-                "operation (preset) mode not supported", operation_mode
+                "operation (preset) mode not supported", str(operation_mode)
             )
 
+        rv: RemoteValueClimateModeBase[HVACOperationMode]
         for rv in chain(
             self._iter_byte_operation_modes(), self._iter_binary_operation_modes()
         ):
@@ -277,16 +304,17 @@ class ClimateMode(Device):
 
         await self._set_internal_operation_mode(operation_mode)
 
-    async def set_controller_mode(self, controller_mode):
+    async def set_controller_mode(self, controller_mode: HVACControllerMode) -> None:
         """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
+                "controller (HVAC) mode not supported", str(controller_mode)
             )
 
+        rv: RemoteValueClimateModeBase[HVACControllerMode]
         for rv in self._iter_controller_remote_values():
             if rv.writable and controller_mode in rv.supported_operation_modes():
                 await rv.set(controller_mode)
@@ -294,42 +322,40 @@ class ClimateMode(Device):
         await self._set_internal_controller_mode(controller_mode)
 
     @property
-    def operation_modes(self):
+    def operation_modes(self) -> List[HVACOperationMode]:
         """Return all configured operation modes."""
         if not self.supports_operation_mode:
             return []
         return self._operation_modes
 
     @property
-    def controller_modes(self):
+    def controller_modes(self) -> List[HVACControllerMode]:
         """Return all configured controller modes."""
         if not self.supports_controller_mode:
             return []
         return self._controller_modes
 
-    def gather_operation_modes(self):
+    def gather_operation_modes(self) -> List[HVACOperationMode]:
         """Gather operation modes from RemoteValues."""
-        operation_modes = []
+        operation_modes: List[HVACOperationMode] = []
         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):
+    def gather_controller_modes(self) -> List[HVACControllerMode]:
         """Gather controller modes from RemoteValues."""
-        operation_modes = []
+        controller_modes: List[HVACControllerMode] = []
         for rv in self._iter_controller_remote_values():
             if rv.writable:
-                operation_modes.extend(rv.supported_operation_modes())
-
+                controller_modes.extend(rv.supported_operation_modes())
         # remove duplicates
-        return list(set(operation_modes))
+        return list(set(controller_modes))
 
-    async def process_group_write(self, telegram):
+    async def process_group_write(self, telegram: "Telegram") -> None:
         """Process incoming and outgoing GROUP WRITE telegram."""
         if self.supports_operation_mode:
             for rv in self._iter_remote_values():
@@ -345,7 +371,7 @@ class ClimateMode(Device):
                     await self._set_internal_controller_mode(rv.value)
                     return
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return (
             '<ClimateMode name="{}" '
diff --git a/xknx/devices/cover.py b/xknx/devices/cover.py
index 78bdcde4..f207aaae 100644
--- a/xknx/devices/cover.py
+++ b/xknx/devices/cover.py
@@ -8,6 +8,7 @@ It provides functionality for
 * Cover will also predict the current position.
 """
 import logging
+from typing import TYPE_CHECKING, Any, Iterator, Optional
 
 from xknx.remote_value import (
     RemoteValueScaling,
@@ -16,9 +17,15 @@ from xknx.remote_value import (
     RemoteValueUpDown,
 )
 
-from .device import Device
+from .device import Device, DeviceCallbackType
 from .travelcalculator import TravelCalculator
 
+if TYPE_CHECKING:
+    from xknx.remote_value import RemoteValue
+    from xknx.telegram import Telegram
+    from xknx.telegram.address import GroupAddressableType
+    from xknx.xknx import XKNX
+
 logger = logging.getLogger("xknx.log")
 
 
@@ -35,21 +42,21 @@ class Cover(Device):
 
     def __init__(
         self,
-        xknx,
-        name,
-        group_address_long=None,
-        group_address_short=None,
-        group_address_stop=None,
-        group_address_position=None,
-        group_address_position_state=None,
-        group_address_angle=None,
-        group_address_angle_state=None,
-        travel_time_down=DEFAULT_TRAVEL_TIME_DOWN,
-        travel_time_up=DEFAULT_TRAVEL_TIME_UP,
-        invert_position=False,
-        invert_angle=False,
-        device_updated_cb=None,
-        device_class=None,
+        xknx: "XKNX",
+        name: str,
+        group_address_long: Optional["GroupAddressableType"] = None,
+        group_address_short: Optional["GroupAddressableType"] = None,
+        group_address_stop: Optional["GroupAddressableType"] = None,
+        group_address_position: Optional["GroupAddressableType"] = None,
+        group_address_position_state: Optional["GroupAddressableType"] = None,
+        group_address_angle: Optional["GroupAddressableType"] = None,
+        group_address_angle_state: Optional["GroupAddressableType"] = None,
+        travel_time_down: float = DEFAULT_TRAVEL_TIME_DOWN,
+        travel_time_up: float = DEFAULT_TRAVEL_TIME_UP,
+        invert_position: bool = False,
+        invert_angle: bool = False,
+        device_updated_cb: Optional[DeviceCallbackType] = None,
+        device_class: Optional[str] = None,
     ):
         """Initialize Cover class."""
         # pylint: disable=too-many-arguments
@@ -121,7 +128,7 @@ class Cover(Device):
 
         self.device_class = device_class
 
-    def _iter_remote_values(self):
+    def _iter_remote_values(self) -> Iterator["RemoteValue"]:
         """Iterate the devices RemoteValue classes."""
         yield from (
             self.updown,
@@ -133,7 +140,7 @@ class Cover(Device):
         )
 
     @classmethod
-    def from_config(cls, xknx, name, config):
+    def from_config(cls, xknx: "XKNX", name: str, config: Any) -> "Cover":
         """Initialize object from configuration structure."""
         group_address_long = config.get("group_address_long")
         group_address_short = config.get("group_address_short")
@@ -165,7 +172,7 @@ class Cover(Device):
             device_class=device_class,
         )
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return (
             '<Cover name="{}" '
@@ -189,27 +196,27 @@ class Cover(Device):
             )
         )
 
-    async def set_down(self):
+    async def set_down(self) -> None:
         """Move cover down."""
         await self.updown.down()
         self.travelcalculator.start_travel_down()
         await self.after_update()
 
-    async def set_up(self):
+    async def set_up(self) -> None:
         """Move cover up."""
         await self.updown.up()
         self.travelcalculator.start_travel_up()
         await self.after_update()
 
-    async def set_short_down(self):
+    async def set_short_down(self) -> None:
         """Move cover short down."""
         await self.step.increase()
 
-    async def set_short_up(self):
+    async def set_short_up(self) -> None:
         """Move cover short up."""
         await self.step.decrease()
 
-    async def stop(self):
+    async def stop(self) -> None:
         """Stop cover."""
         if self.stop_.writable:
             await self.stop_.on()
@@ -221,7 +228,7 @@ class Cover(Device):
         self.travelcalculator.stop()
         await self.after_update()
 
-    async def set_position(self, position):
+    async def set_position(self, position: int) -> None:
         """Move cover to a desginated postion."""
         if not self.position_target.writable:
             # No direct positioning group address defined
@@ -246,12 +253,12 @@ class Cover(Device):
         else:
             await self.position_target.set(position)
 
-    async def _target_position_from_rv(self):
+    async def _target_position_from_rv(self) -> None:
         """Update the target postion from RemoteValue (Callback)."""
         self.travelcalculator.start_travel(self.position_target.value)
         await self.after_update()
 
-    async def _current_position_from_rv(self):
+    async def _current_position_from_rv(self) -> None:
         """Update the current postion from RemoteValue (Callback)."""
         position_before_update = self.travelcalculator.current_position()
         if self.is_traveling():
@@ -261,14 +268,14 @@ class Cover(Device):
         if position_before_update != self.travelcalculator.current_position():
             await self.after_update()
 
-    async def set_angle(self, angle):
+    async def set_angle(self, angle: int) -> None:
         """Move cover to designated angle."""
         if not self.supports_angle:
             logger.warning("Angle not supported for device %s", self.get_name())
             return
         await self.angle.set(angle)
 
-    async def auto_stop_if_necessary(self):
+    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.
@@ -283,7 +290,7 @@ class Cover(Device):
         ):
             await self.stop()
 
-    async def do(self, action):
+    async def do(self, action: str) -> None:
         """Execute 'do' commands."""
         if action == "up":
             await self.set_up()
@@ -300,12 +307,12 @@ class Cover(Device):
                 "Could not understand action %s for device %s", action, self.get_name()
             )
 
-    async def sync(self, wait_for_result=False):
+    async def sync(self, wait_for_result: bool = False) -> None:
         """Read states of device from KNX bus."""
         await self.position_current.read_state(wait_for_result=wait_for_result)
         await self.angle.read_state(wait_for_result=wait_for_result)
 
-    async def process_group_write(self, telegram):
+    async def process_group_write(self, telegram: "Telegram") -> None:
         """Process incoming and outgoing GROUP WRITE telegram."""
         # call after_update to account for travelcalculator changes
         if await self.updown.process(telegram):
@@ -331,49 +338,49 @@ class Cover(Device):
         await self.position_target.process(telegram)
         await self.angle.process(telegram)
 
-    def current_position(self):
+    def current_position(self) -> Optional[int]:
         """Return current position of cover."""
         return self.travelcalculator.current_position()
 
-    def current_angle(self):
+    def current_angle(self) -> Optional[int]:
         """Return current tilt angle of cover."""
-        return self.angle.value
+        return self.angle.value  # type: ignore
 
-    def is_traveling(self):
+    def is_traveling(self) -> bool:
         """Return if cover is traveling at the moment."""
         return self.travelcalculator.is_traveling()
 
-    def position_reached(self):
+    def position_reached(self) -> bool:
         """Return if cover has reached its final position."""
         return self.travelcalculator.position_reached()
 
-    def is_open(self):
+    def is_open(self) -> bool:
         """Return if cover is open."""
         return self.travelcalculator.is_open()
 
-    def is_closed(self):
+    def is_closed(self) -> bool:
         """Return if cover is closed."""
         return self.travelcalculator.is_closed()
 
-    def is_opening(self):
+    def is_opening(self) -> bool:
         """Return if the cover is opening or not."""
         return self.travelcalculator.is_opening()
 
-    def is_closing(self):
+    def is_closing(self) -> bool:
         """Return if the cover is closing or not."""
         return self.travelcalculator.is_closing()
 
     @property
-    def supports_stop(self):
+    def supports_stop(self) -> bool:
         """Return if cover supports manual stopping."""
         return self.stop_.writable or self.step.writable
 
     @property
-    def supports_position(self):
+    def supports_position(self) -> bool:
         """Return if cover supports direct positioning."""
         return self.position_target.initialized
 
     @property
-    def supports_angle(self):
+    def supports_angle(self) -> bool:
         """Return if cover supports tilt angle."""
         return self.angle.initialized
diff --git a/xknx/devices/datetime.py b/xknx/devices/datetime.py
index 70ed6088..dcf55246 100644
--- a/xknx/devices/datetime.py
+++ b/xknx/devices/datetime.py
@@ -7,10 +7,16 @@ by StateUpdate.
 """
 import asyncio
 import time
+from typing import TYPE_CHECKING, Any, Iterator, Optional
 
 from xknx.remote_value import RemoteValueDateTime
 
-from .device import Device
+from .device import Device, DeviceCallbackType
+
+if TYPE_CHECKING:
+    from xknx.telegram import Telegram
+    from xknx.telegram.address import GroupAddressableType
+    from xknx.xknx import XKNX
 
 
 class DateTime(Device):
@@ -19,12 +25,12 @@ class DateTime(Device):
     # pylint: disable=too-many-arguments
     def __init__(
         self,
-        xknx,
+        xknx: "XKNX",
         name: str,
         broadcast_type: str = "TIME",
         localtime: bool = True,
-        group_address=None,
-        device_updated_cb=None,
+        group_address: Optional["GroupAddressableType"] = None,
+        device_updated_cb: Optional[DeviceCallbackType] = None,
     ):
         """Initialize DateTime class."""
         super().__init__(xknx, name, device_updated_cb)
@@ -40,17 +46,17 @@ class DateTime(Device):
         )
         self._broadcast_task = self._create_broadcast_task(minutes=60)
 
-    def __del__(self):
+    def __del__(self) -> None:
         """Destructor. Cleaning up if this was not done before."""
         if self._broadcast_task:
             self._broadcast_task.cancel()
 
-    def _iter_remote_values(self):
+    def _iter_remote_values(self) -> Iterator[RemoteValueDateTime]:
         """Iterate the devices RemoteValue classes."""
         yield self._remote_value
 
     @classmethod
-    def from_config(cls, xknx, name, config):
+    def from_config(cls, xknx: "XKNX", name: str, config: Any) -> "DateTime":
         """Initialize object from configuration structure."""
         broadcast_type = config.get("broadcast_type", "time").upper()
         group_address = config.get("group_address")
@@ -58,10 +64,12 @@ class DateTime(Device):
             xknx, name, broadcast_type=broadcast_type, group_address=group_address
         )
 
-    def _create_broadcast_task(self, minutes: int = 60):
+    def _create_broadcast_task(
+        self, minutes: int = 60
+    ) -> Optional["asyncio.Task[None]"]:
         """Create an asyncio.Task for broadcasting local time periodically if `localtime` is set."""
 
-        async def broadcast_loop(self, minutes: int):
+        async def broadcast_loop(self: "DateTime", minutes: int) -> None:
             """Endless loop for broadcasting local time."""
             while True:
                 await self.broadcast_localtime()
@@ -72,27 +80,27 @@ class DateTime(Device):
             return loop.create_task(broadcast_loop(self, minutes=minutes))
         return None
 
-    async def broadcast_localtime(self, response=False):
+    async def broadcast_localtime(self, response: bool = False) -> None:
         """Broadcast the local time to KNX bus."""
         await self._remote_value.set(time.localtime(), response=response)
 
-    async def set(self, struct_time: time.struct_time):
+    async def set(self, struct_time: time.struct_time) -> None:
         """Set time and send to KNX bus."""
         await self._remote_value.set(struct_time)
 
-    async def process_group_read(self, telegram):
+    async def process_group_read(self, telegram: "Telegram") -> None:
         """Process incoming GROUP READ telegram."""
         if self.localtime:
             await self.broadcast_localtime(True)
         else:
             await self._remote_value.respond()
 
-    async def sync(self, wait_for_result=False):
+    async def sync(self, wait_for_result: bool = False) -> None:
         """Read state of device from KNX bus. Used here to broadcast time to KNX bus."""
         if self.localtime:
             await self.broadcast_localtime(response=False)
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return '<DateTime name="{}" group_address="{}" broadcast_type="{}" />'.format(
             self.name, self._remote_value.group_addr_str(), self._broadcast_type
diff --git a/xknx/devices/expose_sensor.py b/xknx/devices/expose_sensor.py
index a651cafd..092cc19d 100644
--- a/xknx/devices/expose_sensor.py
+++ b/xknx/devices/expose_sensor.py
@@ -11,22 +11,35 @@ read from an internet service (e.g. Yahoo weather) and exposed to
 ths KNX bus. KNX sensors may show this outside temperature within their
 LCD display.
 """
+from typing import TYPE_CHECKING, Any, Iterator, Optional
+
 from xknx.remote_value import RemoteValueSensor, RemoteValueSwitch
 
-from .device import Device
+from .device import Device, DeviceCallbackType
+
+if TYPE_CHECKING:
+    from xknx.remote_value import RemoteValue
+    from xknx.telegram import Telegram
+    from xknx.telegram.address import GroupAddressableType
+    from xknx.xknx import XKNX
 
 
 class ExposeSensor(Device):
     """Class for managing a sensor."""
 
     def __init__(
-        self, xknx, name, group_address=None, value_type=None, device_updated_cb=None
+        self,
+        xknx: "XKNX",
+        name: str,
+        group_address: Optional["GroupAddressableType"] = None,
+        value_type: Optional[str] = None,
+        device_updated_cb: Optional[DeviceCallbackType] = None,
     ):
         """Initialize Sensor class."""
         # pylint: disable=too-many-arguments
         super().__init__(xknx, name, device_updated_cb)
 
-        self.sensor_value = None
+        self.sensor_value: "RemoteValue"
         if value_type == "binary":
             self.sensor_value = RemoteValueSwitch(
                 xknx,
@@ -45,39 +58,39 @@ class ExposeSensor(Device):
                 value_type=value_type,
             )
 
-    def _iter_remote_values(self):
+    def _iter_remote_values(self) -> Iterator["RemoteValue"]:
         """Iterate the devices RemoteValue classes."""
         yield self.sensor_value
 
     @classmethod
-    def from_config(cls, xknx, name, config):
+    def from_config(cls, xknx: "XKNX", name: str, config: Any) -> "ExposeSensor":
         """Initialize object from configuration structure."""
         group_address = config.get("group_address")
         value_type = config.get("value_type")
 
         return cls(xknx, name, group_address=group_address, value_type=value_type)
 
-    async def process_group_write(self, telegram):
+    async def process_group_write(self, telegram: "Telegram") -> None:
         """Process incoming and outgoing GROUP WRITE telegram."""
         await self.sensor_value.process(telegram)
 
-    async def process_group_read(self, telegram):
+    async def process_group_read(self, telegram: "Telegram") -> None:
         """Process incoming GROUP READ telegram."""
         await self.sensor_value.respond()
 
-    async def set(self, value):
+    async def set(self, value: Any) -> None:
         """Set new value."""
         await self.sensor_value.set(value)
 
-    def unit_of_measurement(self):
+    def unit_of_measurement(self) -> Optional[str]:
         """Return the unit of measurement."""
         return self.sensor_value.unit_of_measurement
 
-    def resolve_state(self):
+    def resolve_state(self) -> Any:
         """Return the current state of the sensor as a human readable string."""
         return self.sensor_value.value
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return '<ExposeSensor name="{}" ' 'sensor="{}" value="{}" unit="{}"/>'.format(
             self.name,
diff --git a/xknx/devices/fan.py b/xknx/devices/fan.py
index a0344a99..e986c53b 100644
--- a/xknx/devices/fan.py
+++ b/xknx/devices/fan.py
@@ -7,10 +7,16 @@ It provides functionality for
 * reading the current speed from KNX bus.
 """
 import logging
+from typing import TYPE_CHECKING, Any, Iterator, Optional
 
 from xknx.remote_value import RemoteValueScaling
 
-from .device import Device
+from .device import Device, DeviceCallbackType
+
+if TYPE_CHECKING:
+    from xknx.telegram import Telegram
+    from xknx.telegram.address import GroupAddressableType
+    from xknx.xknx import XKNX
 
 logger = logging.getLogger("xknx.log")
 
@@ -23,15 +29,15 @@ class Fan(Device):
 
     def __init__(
         self,
-        xknx,
-        name,
-        group_address_speed=None,
-        group_address_speed_state=None,
-        device_updated_cb=None,
+        xknx: "XKNX",
+        name: str,
+        group_address_speed: Optional["GroupAddressableType"] = None,
+        group_address_speed_state: Optional["GroupAddressableType"] = None,
+        device_updated_cb: Optional[DeviceCallbackType] = None,
     ):
         """Initialize fan class."""
         # pylint: disable=too-many-arguments
-        Device.__init__(self, xknx, name, device_updated_cb)
+        super().__init__(xknx, name, device_updated_cb)
 
         self.speed = RemoteValueScaling(
             xknx,
@@ -44,12 +50,12 @@ class Fan(Device):
             range_to=100,
         )
 
-    def _iter_remote_values(self):
+    def _iter_remote_values(self) -> Iterator[RemoteValueScaling]:
         """Iterate the devices RemoteValue classes."""
         yield self.speed
 
     @classmethod
-    def from_config(cls, xknx, name, config):
+    def from_config(cls, xknx: "XKNX", name: str, config: Any) -> "Fan":
         """Initialize object from configuration structure."""
         group_address_speed = config.get("group_address_speed")
         group_address_speed_state = config.get("group_address_speed_state")
@@ -61,17 +67,17 @@ class Fan(Device):
             group_address_speed_state=group_address_speed_state,
         )
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return '<Fan name="{}" ' 'speed="{}" />'.format(
             self.name, self.speed.group_addr_str()
         )
 
-    async def set_speed(self, speed):
+    async def set_speed(self, speed: int) -> None:
         """Set the fan to a desginated speed."""
         await self.speed.set(speed)
 
-    async def do(self, action):
+    async def do(self, action: str) -> None:
         """Execute 'do' commands."""
         if action.startswith("speed:"):
             await self.set_speed(int(action[6:]))
@@ -80,11 +86,11 @@ class Fan(Device):
                 "Could not understand action %s for device %s", action, self.get_name()
             )
 
-    async def process_group_write(self, telegram):
+    async def process_group_write(self, telegram: "Telegram") -> None:
         """Process incoming and outgoing GROUP WRITE telegram."""
         await self.speed.process(telegram)
 
     @property
-    def current_speed(self):
+    def current_speed(self) -> Optional[int]:
         """Return current speed of fan."""
-        return self.speed.value
+        return self.speed.value  # type: ignore
diff --git a/xknx/devices/light.py b/xknx/devices/light.py
index 91be195a..49f8059b 100644
--- a/xknx/devices/light.py
+++ b/xknx/devices/light.py
@@ -12,7 +12,16 @@ It provides functionality for
 """
 from enum import Enum
 import logging
-from typing import Optional, Tuple
+from typing import (
+    TYPE_CHECKING,
+    Any,
+    Awaitable,
+    Callable,
+    Iterator,
+    List,
+    Optional,
+    Tuple,
+)
 
 from xknx.remote_value import (
     RemoteValueColorRGB,
@@ -22,7 +31,15 @@ from xknx.remote_value import (
     RemoteValueSwitch,
 )
 
-from .device import Device
+from .device import Device, DeviceCallbackType
+
+if TYPE_CHECKING:
+    from xknx.remote_value import RemoteValue
+    from xknx.telegram import Telegram
+    from xknx.telegram.address import GroupAddressableType
+    from xknx.xknx import XKNX
+
+AsyncCallback = Callable[[], Awaitable[None]]
 
 logger = logging.getLogger("xknx.log")
 
@@ -37,14 +54,14 @@ class ColorTempModes(Enum):
 class _SwitchAndBrightness:
     def __init__(
         self,
-        xknx,
-        name,
+        xknx: "XKNX",
+        name: str,
         feature_name: str,
-        group_address_switch=None,
-        group_address_switch_state=None,
-        group_address_brightness=None,
-        group_address_brightness_state=None,
-        after_update_cb=None,
+        group_address_switch: Optional["GroupAddressableType"] = None,
+        group_address_switch_state: Optional["GroupAddressableType"] = None,
+        group_address_brightness: Optional["GroupAddressableType"] = None,
+        group_address_brightness_state: Optional["GroupAddressableType"] = None,
+        after_update_cb: Optional[AsyncCallback] = None,
     ):
         self.switch = RemoteValueSwitch(
             xknx,
@@ -66,21 +83,21 @@ class _SwitchAndBrightness:
         )
 
     @property
-    def is_on(self):
+    def is_on(self) -> Optional[bool]:
         """Return if light is on."""
-        return self.switch.value
+        return self.switch.value  # type: ignore
 
-    async def set_on(self):
+    async def set_on(self) -> None:
         """Switch light on."""
         if self.switch.initialized:
             await self.switch.on()
 
-    async def set_off(self):
+    async def set_off(self) -> None:
         """Switch light off."""
         if self.switch.initialized:
             await self.switch.off()
 
-    def __eq__(self, other):
+    def __eq__(self, other: object) -> bool:
         """Compare for quality."""
         return self.__dict__ == other.__dict__
 
@@ -95,39 +112,39 @@ class Light(Device):
 
     def __init__(
         self,
-        xknx,
-        name,
-        group_address_switch=None,
-        group_address_switch_state=None,
-        group_address_brightness=None,
-        group_address_brightness_state=None,
-        group_address_color=None,
-        group_address_color_state=None,
-        group_address_rgbw=None,
-        group_address_rgbw_state=None,
-        group_address_tunable_white=None,
-        group_address_tunable_white_state=None,
-        group_address_color_temperature=None,
-        group_address_color_temperature_state=None,
-        group_address_switch_red=None,
-        group_address_switch_red_state=None,
-        group_address_brightness_red=None,
-        group_address_brightness_red_state=None,
-        group_address_switch_green=None,
-        group_address_switch_green_state=None,
-        group_address_brightness_green=None,
-        group_address_brightness_green_state=None,
-        group_address_switch_blue=None,
-        group_address_switch_blue_state=None,
-        group_address_brightness_blue=None,
-        group_address_brightness_blue_state=None,
-        group_address_switch_white=None,
-        group_address_switch_white_state=None,
-        group_address_brightness_white=None,
-        group_address_brightness_white_state=None,
-        min_kelvin=None,
-        max_kelvin=None,
-        device_updated_cb=None,
+        xknx: "XKNX",
+        name: str,
+        group_address_switch: Optional["GroupAddressableType"] = None,
+        group_address_switch_state: Optional["GroupAddressableType"] = None,
+        group_address_brightness: Optional["GroupAddressableType"] = None,
+        group_address_brightness_state: Optional["GroupAddressableType"] = None,
+        group_address_color: Optional["GroupAddressableType"] = None,
+        group_address_color_state: Optional["GroupAddressableType"] = None,
+        group_address_rgbw: Optional["GroupAddressableType"] = None,
+        group_address_rgbw_state: Optional["GroupAddressableType"] = None,
+        group_address_tunable_white: Optional["GroupAddressableType"] = None,
+        group_address_tunable_white_state: Optional["GroupAddressableType"] = None,
+        group_address_color_temperature: Optional["GroupAddressableType"] = None,
+        group_address_color_temperature_state: Optional["GroupAddressableType"] = None,
+        group_address_switch_red: Optional["GroupAddressableType"] = None,
+        group_address_switch_red_state: Optional["GroupAddressableType"] = None,
+        group_address_brightness_red: Optional["GroupAddressableType"] = None,
+        group_address_brightness_red_state: Optional["GroupAddressableType"] = None,
+        group_address_switch_green: Optional["GroupAddressableType"] = None,
+        group_address_switch_green_state: Optional["GroupAddressableType"] = None,
+        group_address_brightness_green: Optional["GroupAddressableType"] = None,
+        group_address_brightness_green_state: Optional["GroupAddressableType"] = None,
+        group_address_switch_blue: Optional["GroupAddressableType"] = None,
+        group_address_switch_blue_state: Optional["GroupAddressableType"] = None,
+        group_address_brightness_blue: Optional["GroupAddressableType"] = None,
+        group_address_brightness_blue_state: Optional["GroupAddressableType"] = None,
+        group_address_switch_white: Optional["GroupAddressableType"] = None,
+        group_address_switch_white_state: Optional["GroupAddressableType"] = None,
+        group_address_brightness_white: Optional["GroupAddressableType"] = None,
+        group_address_brightness_white_state: Optional["GroupAddressableType"] = None,
+        min_kelvin: Optional[int] = None,
+        max_kelvin: Optional[int] = None,
+        device_updated_cb: Optional[DeviceCallbackType] = None,
     ):
         """Initialize Light class."""
         # pylint: disable=too-many-arguments
@@ -236,7 +253,7 @@ class Light(Device):
         self.min_kelvin = min_kelvin
         self.max_kelvin = max_kelvin
 
-    def _iter_remote_values(self):
+    def _iter_remote_values(self) -> Iterator["RemoteValue"]:
         """Iterate the devices RemoteValue classes."""
         yield from (
             self.switch,
@@ -251,19 +268,19 @@ class Light(Device):
             yield color.brightness
 
     @property
-    def supports_brightness(self):
+    def supports_brightness(self) -> bool:
         """Return if light supports brightness."""
         return self.brightness.initialized
 
     @property
-    def supports_color(self):
+    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)]
         )
 
     @property
-    def supports_rgbw(self):
+    def supports_rgbw(self) -> bool:
         """Return if light supports RGBW."""
         return self.rgbw.initialized or all(
             [
@@ -273,19 +290,24 @@ class Light(Device):
         )
 
     @property
-    def supports_tunable_white(self):
+    def supports_tunable_white(self) -> bool:
         """Return if light supports tunable white / relative color temperature."""
         return self.tunable_white.initialized
 
     @property
-    def supports_color_temperature(self):
+    def supports_color_temperature(self) -> bool:
         """Return if light supports absolute color temperature."""
         return self.color_temperature.initialized
 
     @classmethod
     def read_color_from_config(
-        cls, color, config
-    ) -> Tuple[Optional[str], Optional[str], Optional[str], Optional[str]]:
+        cls, color: str, config: Any
+    ) -> Tuple[
+        Optional["GroupAddressableType"],
+        Optional["GroupAddressableType"],
+        Optional["GroupAddressableType"],
+        Optional["GroupAddressableType"],
+    ]:
         """Load color configuration from configuration structure."""
         if "individual_colors" in config and color in config["individual_colors"]:
             sub_config = config["individual_colors"][color]
@@ -298,7 +320,7 @@ class Light(Device):
         return None, None, None, None
 
     @classmethod
-    def from_config(cls, xknx, name, config):
+    def from_config(cls, xknx: "XKNX", name: str, config: Any) -> "Light":
         """Initialize object from configuration structure."""
         group_address_switch = config.get("group_address_switch")
         group_address_switch_state = config.get("group_address_switch_state")
@@ -379,7 +401,7 @@ class Light(Device):
             max_kelvin=max_kelvin,
         )
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         str_brightness = (
             ""
@@ -475,7 +497,7 @@ class Light(Device):
     def state(self) -> Optional[bool]:
         """Return the current switch state of the device."""
         if self.switch.value is not None:
-            return self.switch.value
+            return self.switch.value  # type: ignore
         if any(
             [
                 c.switch.value is not None
@@ -487,14 +509,14 @@ class Light(Device):
             )
         return None
 
-    async def set_on(self):
+    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):
             await color.set_on()
 
-    async def set_off(self):
+    async def set_off(self) -> None:
         """Switch light off."""
         if self.switch.initialized:
             await self.switch.off()
@@ -502,11 +524,11 @@ class Light(Device):
             await color.set_off()
 
     @property
-    def current_brightness(self):
+    def current_brightness(self) -> Optional[int]:
         """Return current brightness of light."""
-        return self.brightness.value
+        return self.brightness.value  # type: ignore
 
-    async def set_brightness(self, brightness):
+    async def set_brightness(self, brightness: int) -> None:
         """Set brightness of light."""
         if not self.supports_brightness:
             logger.warning("Dimming not supported for device %s", self.get_name())
@@ -514,7 +536,7 @@ class Light(Device):
         await self.brightness.set(brightness)
 
     @property
-    def current_color(self):
+    def current_color(self) -> Tuple[Optional[List[int]], Optional[int]]:
         """
         Return current color of light.
 
@@ -537,7 +559,7 @@ class Light(Device):
             return None, self.white.brightness.value
         return colors, self.white.brightness.value
 
-    async def set_color(self, color, white=None):
+    async def set_color(self, color: List[int], white: Optional[int] = None) -> None:
         """
         Set color of a light device.
 
@@ -579,11 +601,11 @@ class Light(Device):
             logger.warning("Colors not supported for device %s", self.get_name())
 
     @property
-    def current_tunable_white(self):
+    def current_tunable_white(self) -> Optional[int]:
         """Return current relative color temperature of light."""
-        return self.tunable_white.value
+        return self.tunable_white.value  # type: ignore
 
-    async def set_tunable_white(self, tunable_white):
+    async def set_tunable_white(self, tunable_white: int) -> None:
         """Set relative color temperature of light."""
         if not self.supports_tunable_white:
             logger.warning("Tunable white not supported for device %s", self.get_name())
@@ -591,11 +613,11 @@ class Light(Device):
         await self.tunable_white.set(tunable_white)
 
     @property
-    def current_color_temperature(self):
+    def current_color_temperature(self) -> Optional[int]:
         """Return current absolute color temperature of light."""
-        return self.color_temperature.value
+        return self.color_temperature.value  # type: ignore
 
-    async def set_color_temperature(self, color_temperature):
+    async def set_color_temperature(self, color_temperature: int) -> None:
         """Set absolute color temperature of light."""
         if not self.supports_color_temperature:
             logger.warning(
@@ -605,7 +627,7 @@ class Light(Device):
             return
         await self.color_temperature.set(color_temperature)
 
-    async def do(self, action):
+    async def do(self, action: str) -> None:
         """Execute 'do' commands."""
         if action == "on":
             await self.set_on()
@@ -622,7 +644,7 @@ class Light(Device):
                 "Could not understand action %s for device %s", action, self.get_name()
             )
 
-    async def process_group_write(self, telegram):
+    async def process_group_write(self, telegram: "Telegram") -> None:
         """Process incoming and outgoing GROUP WRITE telegram."""
         for remote_value in self._iter_remote_values():
             await remote_value.process(telegram)
diff --git a/xknx/devices/notification.py b/xknx/devices/notification.py
index 9acf5e42..405df121 100644
--- a/xknx/devices/notification.py
+++ b/xknx/devices/notification.py
@@ -1,9 +1,15 @@
 """Module for managing a notification via KNX."""
 import logging
+from typing import TYPE_CHECKING, Any, Iterator, Optional
 
 from xknx.remote_value import RemoteValueString
 
-from .device import Device
+from .device import Device, DeviceCallbackType
+
+if TYPE_CHECKING:
+    from xknx.telegram import Telegram
+    from xknx.telegram.address import GroupAddressableType
+    from xknx.xknx import XKNX
 
 logger = logging.getLogger("xknx.log")
 
@@ -13,11 +19,11 @@ class Notification(Device):
 
     def __init__(
         self,
-        xknx,
-        name,
-        group_address=None,
-        group_address_state=None,
-        device_updated_cb=None,
+        xknx: "XKNX",
+        name: str,
+        group_address: Optional["GroupAddressableType"] = None,
+        group_address_state: Optional["GroupAddressableType"] = None,
+        device_updated_cb: Optional[DeviceCallbackType] = None,
     ):
         """Initialize notification class."""
         # pylint: disable=too-many-arguments
@@ -32,12 +38,12 @@ class Notification(Device):
             after_update_cb=self.after_update,
         )
 
-    def _iter_remote_values(self):
+    def _iter_remote_values(self) -> Iterator[RemoteValueString]:
         """Iterate the devices RemoteValue classes."""
         yield self._message
 
     @classmethod
-    def from_config(cls, xknx, name, config):
+    def from_config(cls, xknx: "XKNX", name: str, config: Any) -> "Notification":
         """Initialize object from configuration structure."""
         group_address = config.get("group_address")
         group_address_state = config.get("group_address_state")
@@ -50,20 +56,20 @@ class Notification(Device):
         )
 
     @property
-    def message(self):
+    def message(self) -> Optional[str]:
         """Return the current message."""
-        return self._message.value
+        return self._message.value  # type: ignore
 
-    async def set(self, message):
+    async def set(self, message: str) -> None:
         """Set message."""
         cropped_message = message[:14]
         await self._message.set(cropped_message)
 
-    async def process_group_write(self, telegram):
+    async def process_group_write(self, telegram: "Telegram") -> None:
         """Process incoming and outgoing GROUP WRITE telegram."""
         await self._message.process(telegram)
 
-    async def do(self, action):
+    async def do(self, action: str) -> None:
         """Execute 'do' commands."""
         if action.startswith("message:"):
             await self.set(action[8:])
@@ -72,7 +78,7 @@ class Notification(Device):
                 "Could not understand action %s for device %s", action, self.get_name()
             )
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return '<Notification name="{}" message="{}" />'.format(
             self.name, self._message.group_addr_str()
diff --git a/xknx/devices/scene.py b/xknx/devices/scene.py
index cd90ce3c..524955da 100644
--- a/xknx/devices/scene.py
+++ b/xknx/devices/scene.py
@@ -1,9 +1,14 @@
 """Module for managing a KNX scene."""
 import logging
+from typing import TYPE_CHECKING, Any, Iterator, Optional
 
 from xknx.remote_value import RemoteValueSceneNumber
 
-from .device import Device
+from .device import Device, DeviceCallbackType
+
+if TYPE_CHECKING:
+    from xknx.telegram.address import GroupAddressableType
+    from xknx.xknx import XKNX
 
 logger = logging.getLogger("xknx.log")
 
@@ -12,7 +17,12 @@ class Scene(Device):
     """Class for managing a scene."""
 
     def __init__(
-        self, xknx, name, group_address=None, scene_number=1, device_updated_cb=None
+        self,
+        xknx: "XKNX",
+        name: str,
+        group_address: Optional["GroupAddressableType"] = None,
+        scene_number: int = 1,
+        device_updated_cb: Optional[DeviceCallbackType] = None,
     ):
         """Initialize Sceneclass."""
         # pylint: disable=too-many-arguments
@@ -28,12 +38,12 @@ class Scene(Device):
         )
         self.scene_number = int(scene_number)
 
-    def _iter_remote_values(self):
+    def _iter_remote_values(self) -> Iterator[RemoteValueSceneNumber]:
         """Iterate the devices RemoteValue classes."""
         yield self.scene_value
 
     @classmethod
-    def from_config(cls, xknx, name, config):
+    def from_config(cls, xknx: "XKNX", name: str, config: Any) -> "Scene":
         """Initialize object from configuration structure."""
         group_address = config.get("group_address")
         scene_number = int(config.get("scene_number"))
@@ -41,17 +51,17 @@ class Scene(Device):
             xknx, name=name, group_address=group_address, scene_number=scene_number
         )
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return '<Scene name="{}" ' 'scene_value="{}" scene_number="{}" />'.format(
             self.name, self.scene_value.group_addr_str(), self.scene_number
         )
 
-    async def run(self):
+    async def run(self) -> None:
         """Activate scene."""
         await self.scene_value.set(self.scene_number)
 
-    async def do(self, action):
+    async def do(self, action: str) -> None:
         """Execute 'do' commands."""
         if action == "run":
             await self.run()
diff --git a/xknx/devices/sensor.py b/xknx/devices/sensor.py
index 22cdfa39..480dfd61 100644
--- a/xknx/devices/sensor.py
+++ b/xknx/devices/sensor.py
@@ -6,9 +6,17 @@ It provides functionality for
 * reading the current state from KNX bus.
 * watching for state updates from KNX bus.
 """
+from typing import TYPE_CHECKING, Any, Iterator, Optional, Union
+
 from xknx.remote_value import RemoteValueControl, RemoteValueSensor
 
-from .device import Device
+from .device import Device, DeviceCallbackType
+
+if TYPE_CHECKING:
+    from xknx.remote_value import RemoteValue
+    from xknx.telegram import Telegram
+    from xknx.telegram.address import GroupAddressableType
+    from xknx.xknx import XKNX
 
 
 class Sensor(Device):
@@ -16,18 +24,19 @@ class Sensor(Device):
 
     def __init__(
         self,
-        xknx,
-        name,
-        group_address_state=None,
+        xknx: "XKNX",
+        name: str,
+        group_address_state: Optional["GroupAddressableType"] = None,
         sync_state: bool = True,
         always_callback: bool = False,
-        value_type: str = None,
-        device_updated_cb=None,
+        value_type: Optional[str] = None,
+        device_updated_cb: Optional[DeviceCallbackType] = None,
     ):
         """Initialize Sensor class."""
         # pylint: disable=too-many-arguments
         super().__init__(xknx, name, device_updated_cb)
-        self.always_callback = always_callback
+
+        self.sensor_value: Union[RemoteValueControl, RemoteValueSensor]
         if value_type in [
             "stepwise_dimming",
             "stepwise_blinds",
@@ -51,13 +60,14 @@ class Sensor(Device):
                 device_name=self.name,
                 after_update_cb=self.after_update,
             )
+        self.always_callback = always_callback
 
-    def _iter_remote_values(self):
+    def _iter_remote_values(self) -> Iterator["RemoteValue"]:
         """Iterate the devices RemoteValue classes."""
         yield self.sensor_value
 
     @classmethod
-    def from_config(cls, xknx, name, config):
+    def from_config(cls, xknx: "XKNX", name: str, config: Any) -> "Sensor":
         """Initialize object from configuration structure."""
         group_address_state = config.get("group_address_state")
         sync_state = config.get("sync_state", True)
@@ -73,27 +83,27 @@ class Sensor(Device):
             value_type=value_type,
         )
 
-    async def process_group_write(self, telegram):
+    async def process_group_write(self, telegram: "Telegram") -> None:
         """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):
+    async def process_group_response(self, telegram: "Telegram") -> None:
         """Process incoming GroupValueResponse telegrams."""
         await self.sensor_value.process(telegram)
 
-    def unit_of_measurement(self):
+    def unit_of_measurement(self) -> Optional[str]:
         """Return the unit of measurement."""
         return self.sensor_value.unit_of_measurement
 
-    def ha_device_class(self):
+    def ha_device_class(self) -> Optional[str]:
         """Return the home assistant device class as string."""
         return self.sensor_value.ha_device_class
 
-    def resolve_state(self):
+    def resolve_state(self) -> Optional[Any]:
         """Return the current state of the sensor as a human readable string."""
         return self.sensor_value.value
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return '<Sensor name="{}" ' 'sensor="{}" value="{}" unit="{}"/>'.format(
             self.name,
diff --git a/xknx/devices/switch.py b/xknx/devices/switch.py
index 0c58578e..ca89b108 100644
--- a/xknx/devices/switch.py
+++ b/xknx/devices/switch.py
@@ -8,11 +8,16 @@ It provides functionality for
 """
 import asyncio
 import logging
-from typing import Optional
+from typing import TYPE_CHECKING, Any, Iterator, Optional
 
 from xknx.remote_value import RemoteValueSwitch
 
-from .device import Device
+from .device import Device, DeviceCallbackType
+
+if TYPE_CHECKING:
+    from xknx.telegram import Telegram
+    from xknx.telegram.address import GroupAddressableType
+    from xknx.xknx import XKNX
 
 logger = logging.getLogger("xknx.log")
 
@@ -22,20 +27,20 @@ class Switch(Device):
 
     def __init__(
         self,
-        xknx,
+        xknx: "XKNX",
         name: str,
-        group_address=None,
-        group_address_state=None,
+        group_address: Optional["GroupAddressableType"] = None,
+        group_address_state: Optional["GroupAddressableType"] = None,
         invert: Optional[bool] = False,
         reset_after: Optional[float] = None,
-        device_updated_cb=None,
+        device_updated_cb: Optional[DeviceCallbackType] = None,
     ):
         """Initialize Switch class."""
         # pylint: disable=too-many-arguments
         super().__init__(xknx, name, device_updated_cb)
 
         self.reset_after = reset_after
-        self._reset_task = None
+        self._reset_task: Optional[asyncio.Task[None]] = None
 
         self.switch = RemoteValueSwitch(
             xknx,
@@ -46,17 +51,17 @@ class Switch(Device):
             after_update_cb=self.after_update,
         )
 
-    def _iter_remote_values(self):
+    def _iter_remote_values(self) -> Iterator[RemoteValueSwitch]:
         """Iterate the devices RemoteValue classes."""
         yield self.switch
 
-    def __del__(self):
+    def __del__(self) -> None:
         """Destructor. Cleaning up if this was not done before."""
         if self._reset_task:
             self._reset_task.cancel()
 
     @classmethod
-    def from_config(cls, xknx, name, config):
+    def from_config(cls, xknx: "XKNX", name: str, config: Any) -> "Switch":
         """Initialize object from configuration structure."""
         group_address = config.get("group_address")
         group_address_state = config.get("group_address_state")
@@ -75,17 +80,17 @@ class Switch(Device):
     @property
     def state(self) -> Optional[bool]:
         """Return the current switch state of the device."""
-        return self.switch.value
+        return self.switch.value  # type: ignore
 
-    async def set_on(self):
+    async def set_on(self) -> None:
         """Switch on switch."""
         await self.switch.on()
 
-    async def set_off(self):
+    async def set_off(self) -> None:
         """Switch off switch."""
         await self.switch.off()
 
-    async def do(self, action):
+    async def do(self, action: str) -> None:
         """Execute 'do' commands."""
         if action == "on":
             await self.set_on()
@@ -96,7 +101,7 @@ class Switch(Device):
                 "Could not understand action %s for device %s", action, self.get_name()
             )
 
-    async def process_group_write(self, telegram):
+    async def process_group_write(self, telegram: "Telegram") -> None:
         """Process incoming and outgoing GROUP WRITE telegram."""
         if await self.switch.process(telegram):
             if self.reset_after is not None and self.switch.value:
@@ -106,11 +111,11 @@ class Switch(Device):
                     self._reset_state(self.reset_after)
                 )
 
-    async def _reset_state(self, wait_seconds: float):
+    async def _reset_state(self, wait_seconds: float) -> None:
         await asyncio.sleep(wait_seconds)
         await self.set_off()
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return '<Switch name="{}" switch="{}" />'.format(
             self.name, self.switch.group_addr_str()
diff --git a/xknx/devices/travelcalculator.py b/xknx/devices/travelcalculator.py
index ed9291c1..d4bec3ad 100644
--- a/xknx/devices/travelcalculator.py
+++ b/xknx/devices/travelcalculator.py
@@ -27,7 +27,7 @@ class TravelCalculator:
 
     # pylint: disable=too-many-instance-attributes
 
-    def __init__(self, travel_time_down: int, travel_time_up: int) -> None:
+    def __init__(self, travel_time_down: float, travel_time_up: float) -> None:
         """Initialize TravelCalculator class."""
         self.last_known_position: Optional[int] = None
         self.position_confirmed: bool = False
diff --git a/xknx/devices/weather.py b/xknx/devices/weather.py
index 4e888e16..efa39d0c 100644
--- a/xknx/devices/weather.py
+++ b/xknx/devices/weather.py
@@ -13,12 +13,17 @@ It provides functionality for
 """
 from datetime import date, datetime
 from enum import Enum
-from typing import Callable, Iterator, Optional
+from typing import TYPE_CHECKING, Any, Callable, Iterator, Optional, Tuple
 
 from xknx.remote_value import RemoteValue, RemoteValueSensor, RemoteValueSwitch
 
 from . import BinarySensor, Sensor
-from .device import Device
+from .device import Device, DeviceCallbackType
+
+if TYPE_CHECKING:
+    from xknx.telegram import Telegram
+    from xknx.telegram.address import GroupAddressableType
+    from xknx.xknx import XKNX
 
 
 class WeatherCondition(Enum):
@@ -56,7 +61,9 @@ SEASONS = [
 ]
 
 # Define current condition
-ILLUMINANCE_MAPPING = (
+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),
@@ -71,23 +78,23 @@ class Weather(Device):
     # pylint: disable=too-many-locals
     def __init__(
         self,
-        xknx,
+        xknx: "XKNX",
         name: str,
-        group_address_temperature: str = None,
-        group_address_brightness_south: Optional[str] = None,
-        group_address_brightness_north: Optional[str] = None,
-        group_address_brightness_west: Optional[str] = None,
-        group_address_brightness_east: Optional[str] = None,
-        group_address_wind_speed: Optional[str] = None,
-        group_address_rain_alarm: Optional[str] = None,
-        group_address_frost_alarm: Optional[str] = None,
-        group_address_wind_alarm: Optional[str] = None,
-        group_address_day_night: Optional[str] = None,
-        group_address_air_pressure: Optional[str] = None,
-        group_address_humidity: Optional[str] = None,
+        group_address_temperature: Optional["GroupAddressableType"] = None,
+        group_address_brightness_south: Optional["GroupAddressableType"] = None,
+        group_address_brightness_north: Optional["GroupAddressableType"] = None,
+        group_address_brightness_west: Optional["GroupAddressableType"] = None,
+        group_address_brightness_east: Optional["GroupAddressableType"] = None,
+        group_address_wind_speed: Optional["GroupAddressableType"] = None,
+        group_address_rain_alarm: Optional["GroupAddressableType"] = None,
+        group_address_frost_alarm: Optional["GroupAddressableType"] = None,
+        group_address_wind_alarm: Optional["GroupAddressableType"] = None,
+        group_address_day_night: Optional["GroupAddressableType"] = None,
+        group_address_air_pressure: Optional["GroupAddressableType"] = None,
+        group_address_humidity: Optional["GroupAddressableType"] = None,
         expose_sensors: bool = False,
         sync_state: bool = True,
-        device_updated_cb=None,
+        device_updated_cb: Optional[DeviceCallbackType] = None,
     ) -> None:
         """Initialize Weather class."""
         # pylint: disable=too-many-arguments
@@ -225,20 +232,20 @@ class Weather(Device):
             self._humidity,
         ]
 
-    async def process_group_write(self, telegram):
+    async def process_group_write(self, telegram: "Telegram") -> None:
         """Process incoming and outgoing GROUP WRITE telegram."""
         for remote_value in self._iter_remote_values():
             await remote_value.process(telegram)
 
     @property
-    def temperature(self) -> float:
+    def temperature(self) -> Optional[float]:
         """Return current temperature."""
-        return self._temperature.value
+        return self._temperature.value  # type: ignore
 
     @property
     def brightness_south(self) -> float:
         """Return brightness south."""
-        return (
+        return (  # type: ignore
             0.0
             if self._brightness_south.value is None
             else self._brightness_south.value
@@ -247,7 +254,7 @@ class Weather(Device):
     @property
     def brightness_north(self) -> float:
         """Return brightness north."""
-        return (
+        return (  # type: ignore
             0.0
             if self._brightness_north.value is None
             else self._brightness_north.value
@@ -256,51 +263,51 @@ class Weather(Device):
     @property
     def brightness_east(self) -> float:
         """Return brightness east."""
-        return (
+        return (  # type: ignore
             0.0 if self._brightness_east.value is None else self._brightness_east.value
         )
 
     @property
     def brightness_west(self) -> float:
         """Return brightness west."""
-        return (
+        return (  # type: ignore
             0.0 if self._brightness_west.value is None else self._brightness_west.value
         )
 
     @property
     def wind_speed(self) -> Optional[float]:
         """Return wind speed in m/s."""
-        return self._wind_speed.value
+        return self._wind_speed.value  # type: ignore
 
     @property
     def rain_alarm(self) -> Optional[bool]:
         """Return True if rain alarm False if not."""
-        return self._rain_alarm.value
+        return self._rain_alarm.value  # type: ignore
 
     @property
     def wind_alarm(self) -> Optional[bool]:
         """Return True if wind alarm False if not."""
-        return self._wind_alarm.value
+        return self._wind_alarm.value  # type: ignore
 
     @property
     def frost_alarm(self) -> Optional[bool]:
         """Return True if frost alarm False if not."""
-        return self._frost_alarm.value
+        return self._frost_alarm.value  # type: ignore
 
     @property
     def day_night(self) -> Optional[bool]:
         """Return day or night."""
-        return self._day_night.value
+        return self._day_night.value  # type: ignore
 
     @property
-    def air_pressure(self):
+    def air_pressure(self) -> Optional[float]:
         """Return pressure in Pa."""
-        return self._air_pressure.value
+        return self._air_pressure.value  # type: ignore
 
     @property
     def humidity(self) -> Optional[float]:
         """Return humidity in %."""
-        return self._humidity.value
+        return self._humidity.value  # type: ignore
 
     @property
     def max_brightness(self) -> float:
@@ -312,33 +319,13 @@ class Weather(Device):
             self.brightness_east,
         )
 
-    def expose_sensors(self):
+    def expose_sensors(self) -> None:
         """Expose sensors to xknx."""
         for suffix, group_address in (
-            (
-                "_rain_alarm",
-                None
-                if not self._rain_alarm.initialized
-                else self._rain_alarm.group_address_state.raw,
-            ),
-            (
-                "_wind_alarm",
-                None
-                if not self._wind_alarm.initialized
-                else self._wind_alarm.group_address_state.raw,
-            ),
-            (
-                "_frost_alarm",
-                None
-                if not self._frost_alarm.initialized
-                else self._frost_alarm.group_address_state.raw,
-            ),
-            (
-                "_day_night",
-                None
-                if not self._day_night.initialized
-                else self._day_night.group_address_state.raw,
-            ),
+            ("_rain_alarm", self._rain_alarm.group_address_state),
+            ("_wind_alarm", self._wind_alarm.group_address_state),
+            ("_frost_alarm", self._frost_alarm.group_address_state),
+            ("_day_night", self._day_night.group_address_state),
         ):
             if group_address is not None:
                 BinarySensor(
@@ -350,58 +337,42 @@ class Weather(Device):
         for suffix, group_address, value_type in (
             (
                 "_temperature",
-                None
-                if not self._temperature.initialized
-                else self._temperature.group_address_state.raw,
+                self._temperature.group_address_state,
                 "temperature",
             ),
             (
                 "_brightness_south",
-                None
-                if not self._brightness_south.initialized
-                else self._brightness_south.group_address_state.raw,
+                self._brightness_south.group_address_state,
                 "illuminance",
             ),
             (
                 "_brightness_north",
-                None
-                if not self._brightness_north.initialized
-                else self._brightness_north.group_address_state.raw,
+                self._brightness_north.group_address_state,
                 "illuminance",
             ),
             (
                 "_brightness_west",
-                None
-                if not self._brightness_west.initialized
-                else self._brightness_west.group_address_state.raw,
+                self._brightness_west.group_address_state,
                 "illuminance",
             ),
             (
                 "_brightness_east",
-                None
-                if not self._brightness_east.initialized
-                else self._brightness_east.group_address_state.raw,
+                self._brightness_east.group_address_state,
                 "illuminance",
             ),
             (
                 "_wind_speed",
-                None
-                if not self._wind_speed.initialized
-                else self._wind_speed.group_address_state.raw,
+                self._wind_speed.group_address_state,
                 "wind_speed_ms",
             ),
             (
                 "_air_pressure",
-                None
-                if not self._air_pressure.initialized
-                else self._air_pressure.group_address_state.raw,
+                self._air_pressure.group_address_state,
                 "pressure",
             ),
             (
                 "_humidity",
-                None
-                if not self._humidity.initialized
-                else self._humidity.group_address_state.raw,
+                self._humidity.group_address_state,
                 "humidity",
             ),
         ):
@@ -414,10 +385,10 @@ class Weather(Device):
                 )
 
     # pylint: disable=too-many-return-statements
-    def ha_current_state(self, current_date=date.today()) -> WeatherCondition:
+    def ha_current_state(self, current_date: date = date.today()) -> WeatherCondition:
         """Return the current state for home assistant."""
 
-        def _get_season(now):
+        def _get_season(now: date) -> Season:
             """Return winter or summer."""
             if isinstance(now, datetime):
                 now = now.date()
@@ -452,7 +423,7 @@ class Weather(Device):
         return WeatherCondition.exceptional
 
     @classmethod
-    def from_config(cls, xknx, name, config):
+    def from_config(cls, xknx: "XKNX", name: str, config: Any) -> "Weather":
         """Initialize object from configuration structure."""
         group_address_temperature = config.get("group_address_temperature")
         group_address_brightness_south = config.get("group_address_brightness_south")
diff --git a/xknx/dpt/__init__.py b/xknx/dpt/__init__.py
index bd8cb703..13fb10e3 100644
--- a/xknx/dpt/__init__.py
+++ b/xknx/dpt/__init__.py
@@ -169,13 +169,7 @@ from .dpt_4byte_int import (
 )
 from .dpt_date import DPTDate
 from .dpt_datetime import DPTDateTime
-from .dpt_hvac_mode import (
-    DPTControllerStatus,
-    DPTHVACContrMode,
-    DPTHVACMode,
-    HVACControllerMode,
-    HVACOperationMode,
-)
+from .dpt_hvac_mode import DPTControllerStatus, DPTHVACContrMode, DPTHVACMode
 from .dpt_scaling import DPTAngle, DPTScaling
 from .dpt_string import DPTString
 from .dpt_time import DPTTime
diff --git a/xknx/dpt/dpt.py b/xknx/dpt/dpt.py
index c6c42abf..4cc3c2b9 100644
--- a/xknx/dpt/dpt.py
+++ b/xknx/dpt/dpt.py
@@ -141,7 +141,7 @@ class DPTBinary:
         """Equal operator."""
         return DPTComparator.compare(self, other)
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return f'<DPTBinary value="{self.value}" />'
 
@@ -167,7 +167,7 @@ class DPTArray:
         """Equal operator."""
         return DPTComparator.compare(self, other)
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return '<DPTArray value="[{}]" />'.format(",".join(hex(b) for b in self.value))
 
diff --git a/xknx/dpt/dpt_4bit_control.py b/xknx/dpt/dpt_4bit_control.py
index 51233688..6b93f3fc 100644
--- a/xknx/dpt/dpt_4bit_control.py
+++ b/xknx/dpt/dpt_4bit_control.py
@@ -179,7 +179,7 @@ class TitleEnum(Enum):
     Ensures values are rendered nicely, e.g. in home assistant.
     """
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return string representation."""
         # pylint: disable=no-member
         return self.name.title()
diff --git a/xknx/dpt/dpt_date.py b/xknx/dpt/dpt_date.py
index b6d8ab28..4d387469 100644
--- a/xknx/dpt/dpt_date.py
+++ b/xknx/dpt/dpt_date.py
@@ -52,7 +52,7 @@ class DPTDate(DPTBase):
         return (value.tm_mday, value.tm_mon, _knx_year(value.tm_year))
 
     @staticmethod
-    def _test_range(day, month, year):
+    def _test_range(day: int, month: int, year: int) -> bool:
         """Test if the values are in the correct range."""
         if day < 1 or day > 31:
             return False
diff --git a/xknx/remote_value/remote_value.py b/xknx/remote_value/remote_value.py
index 1fffbe9b..319f3615 100644
--- a/xknx/remote_value/remote_value.py
+++ b/xknx/remote_value/remote_value.py
@@ -111,15 +111,11 @@ class RemoteValue(ABC):
 
         return group_address in _internal_addresses()
 
+    @staticmethod
     @abstractmethod
     # TODO: typing - remove Optional
-    def payload_valid(self, payload: Optional["DPTPayload"]) -> bool:
+    def payload_valid(payload: Optional["DPTPayload"]) -> bool:
         """Test if telegram payload may be parsed - to be implemented in derived class.."""
-        # pylint: disable=unused-argument
-        logger.warning(
-            "'payload_valid()' not implemented for %s", self.__class__.__name__
-        )
-        return True
 
     @abstractmethod
     def from_knx(self, payload: "DPTPayload") -> Any:
@@ -144,7 +140,7 @@ class RemoteValue(ABC):
         ):
             raise CouldNotParseTelegram(
                 "payload not a GroupValueWrite or GroupValueResponse",
-                payload=telegram.payload,
+                payload=str(telegram.payload),
                 destination_address=str(telegram.destination_address),
                 source_address=str(telegram.source_address),
                 device_name=self.device_name,
diff --git a/xknx/remote_value/remote_value_climate_mode.py b/xknx/remote_value/remote_value_climate_mode.py
index 6b9868c3..017319d9 100644
--- a/xknx/remote_value/remote_value_climate_mode.py
+++ b/xknx/remote_value/remote_value_climate_mode.py
@@ -3,8 +3,20 @@ Module for managing an climate mode remote values.
 
 DPT .
 """
+from abc import abstractmethod
 from enum import Enum
-from typing import List
+from typing import (
+    TYPE_CHECKING,
+    Any,
+    Awaitable,
+    Callable,
+    Generic,
+    List,
+    Optional,
+    TypeVar,
+    Union,
+    cast,
+)
 
 from xknx.dpt import (
     DPTArray,
@@ -12,15 +24,33 @@ from xknx.dpt import (
     DPTControllerStatus,
     DPTHVACContrMode,
     DPTHVACMode,
-    HVACOperationMode,
 )
+from xknx.dpt.dpt_hvac_mode import HVACControllerMode, HVACOperationMode
 from xknx.exceptions import ConversionError, CouldNotParseTelegram
 
-from ..dpt.dpt_hvac_mode import HVACControllerMode
 from .remote_value import RemoteValue
 
+if TYPE_CHECKING:
+    from xknx.telegram.address import GroupAddressableType
+    from xknx.xknx import XKNX
+
+    AsyncCallback = Callable[[], Awaitable[None]]
+    DPTPayload = Union[DPTArray, DPTBinary]
+
+HVACModeType = TypeVar("HVACModeType", "HVACControllerMode", "HVACOperationMode")
+
+
+class RemoteValueClimateModeBase(RemoteValue, Generic[HVACModeType]):
+    """Base class for binary climate mode remote values."""
+
+    @abstractmethod
+    def supported_operation_modes(
+        self,
+    ) -> List["HVACModeType"]:
+        """Return a list of all supported operation modes."""
+
 
-class RemoteValueClimateMode(RemoteValue):
+class RemoteValueClimateMode(RemoteValueClimateModeBase[HVACModeType]):
     """Abstraction for remote value of KNX climate modes."""
 
     class ClimateModeType(Enum):
@@ -32,15 +62,15 @@ class RemoteValueClimateMode(RemoteValue):
 
     def __init__(
         self,
-        xknx,
-        group_address=None,
-        group_address_state=None,
-        sync_state=True,
-        device_name=None,
-        feature_name="Climate Mode",
-        climate_mode_type=None,
-        after_update_cb=None,
-        passive_group_addresses: List[str] = None,
+        xknx: "XKNX",
+        group_address: Optional["GroupAddressableType"] = None,
+        group_address_state: Optional["GroupAddressableType"] = None,
+        sync_state: bool = True,
+        device_name: Optional[str] = None,
+        feature_name: str = "Climate Mode",
+        climate_mode_type: Optional[ClimateModeType] = None,
+        after_update_cb: Optional["AsyncCallback"] = None,
+        passive_group_addresses: Optional[List["GroupAddressableType"]] = None,
     ):
         """Initialize remote value of KNX climate mode."""
         # pylint: disable=too-many-arguments
@@ -57,57 +87,62 @@ class RemoteValueClimateMode(RemoteValue):
         if not isinstance(climate_mode_type, self.ClimateModeType):
             raise ConversionError(
                 "invalid climate mode type",
-                climate_mode_type=climate_mode_type,
-                device_name=device_name,
+                climate_mode_type=str(climate_mode_type),
+                device_name=str(device_name),
                 feature_name=feature_name,
             )
         self._climate_mode_transcoder = climate_mode_type.value
 
-    def supported_operation_modes(self):
+    def supported_operation_modes(self) -> List["HVACModeType"]:
         """Return a list of all supported operation modes."""
         return list(self._climate_mode_transcoder.SUPPORTED_MODES.values())
 
-    def payload_valid(self, payload):
+    @staticmethod
+    def payload_valid(payload: Optional["DPTPayload"]) -> bool:
         """Test if telegram payload may be parsed."""
         return isinstance(payload, DPTArray) and len(payload.value) == 1
 
-    def to_knx(self, value):
+    def to_knx(self, value: Any) -> "DPTPayload":
         """Convert value to payload."""
         return DPTArray(self._climate_mode_transcoder.to_knx(value))
 
-    def from_knx(self, payload):
+    def from_knx(self, payload: "DPTPayload") -> Optional[HVACModeType]:
         """Convert current payload to value."""
-        return self._climate_mode_transcoder.from_knx(payload.value)
+        # TODO: typing - remove cast
+        return cast(
+            Optional[HVACModeType],
+            self._climate_mode_transcoder.from_knx(payload.value),
+        )
 
 
-class _RemoteValueBinaryClimateMode(RemoteValue):
-    """Base class for binary climate mode remote values."""
+class RemoteValueBinaryOperationMode(RemoteValueClimateModeBase[HVACOperationMode]):
+    """Abstraction for remote value of split up KNX climate modes."""
 
     def __init__(
         self,
-        xknx,
-        group_address=None,
-        group_address_state=None,
-        sync_state=True,
-        device_name=None,
-        feature_name="Climate Mode Binary",
-        after_update_cb=None,
-        operation_mode=None,
+        xknx: "XKNX",
+        group_address: Optional["GroupAddressableType"] = None,
+        group_address_state: Optional["GroupAddressableType"] = None,
+        sync_state: bool = True,
+        device_name: Optional[str] = None,
+        feature_name: str = "Climate Mode Binary",
+        after_update_cb: Optional["AsyncCallback"] = None,
+        operation_mode: Optional[HVACOperationMode] = None,
     ):
         """Initialize remote value of KNX DPT 1 representing a climate operation mode."""
         # pylint: disable=too-many-arguments
         if not isinstance(operation_mode, HVACOperationMode):
             raise ConversionError(
                 "Invalid operation mode type",
-                operation_mode=operation_mode,
-                device_name=device_name,
+                operation_mode=str(operation_mode),
+                device_name=str(device_name),
                 feature_name=feature_name,
             )
         if operation_mode not in self.supported_operation_modes():
             raise ConversionError(
                 "Operation mode not supported for binary mode object",
-                operation_mode=operation_mode,
-                device_name=device_name,
+                operation_mode=str(operation_mode),
+                device_name=str(device_name),
                 feature_name=feature_name,
             )
         self.operation_mode = operation_mode
@@ -122,15 +157,11 @@ class _RemoteValueBinaryClimateMode(RemoteValue):
         )
 
     @staticmethod
-    def supported_operation_modes():
-        """Return a list of the configured operation mode."""
-        raise NotImplementedError("supported_operation_modes has to return a list")
-
-    def payload_valid(self, payload):
+    def payload_valid(payload: Optional["DPTPayload"]) -> bool:
         """Test if telegram payload may be parsed."""
         return isinstance(payload, DPTBinary)
 
-    def to_knx(self, value):
+    def to_knx(self, value: Any) -> "DPTPayload":
         """Convert value to payload."""
         if isinstance(value, HVACOperationMode):
             # foreign operation modes will set the RemoteValue to False
@@ -142,12 +173,8 @@ class _RemoteValueBinaryClimateMode(RemoteValue):
             feature_name=self.feature_name,
         )
 
-
-class RemoteValueBinaryOperationMode(_RemoteValueBinaryClimateMode):
-    """Abstraction for remote value of split up KNX climate modes."""
-
     @staticmethod
-    def supported_operation_modes():
+    def supported_operation_modes() -> List[HVACOperationMode]:
         """Return a list of the configured operation mode."""
         return [
             HVACOperationMode.COMFORT,
@@ -156,7 +183,7 @@ class RemoteValueBinaryOperationMode(_RemoteValueBinaryClimateMode):
             HVACOperationMode.STANDBY,
         ]
 
-    def from_knx(self, payload):
+    def from_knx(self, payload: "DPTPayload") -> Optional[HVACOperationMode]:
         """Convert current payload to value."""
         if payload == DPTBinary(1):
             return self.operation_mode
@@ -164,40 +191,40 @@ class RemoteValueBinaryOperationMode(_RemoteValueBinaryClimateMode):
             return None
         raise CouldNotParseTelegram(
             "payload invalid",
-            payload=payload,
+            payload=str(payload),
             device_name=self.device_name,
             feature_name=self.feature_name,
         )
 
 
-class RemoteValueBinaryHeatCool(RemoteValue):
+class RemoteValueBinaryHeatCool(RemoteValueClimateModeBase[HVACControllerMode]):
     """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,
+        xknx: "XKNX",
+        group_address: Optional["GroupAddressableType"] = None,
+        group_address_state: Optional["GroupAddressableType"] = None,
+        sync_state: bool = True,
+        device_name: Optional[str] = None,
+        feature_name: str = "Controller Mode Heat/Cool",
+        after_update_cb: Optional["AsyncCallback"] = None,
+        controller_mode: Optional[HVACControllerMode] = 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,
+                controller_mode=str(controller_mode),
+                device_name=str(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,
+                controller_mode=str(controller_mode),
+                device_name=str(device_name),
                 feature_name=feature_name,
             )
         self.controller_mode = controller_mode
@@ -211,16 +238,17 @@ class RemoteValueBinaryHeatCool(RemoteValue):
             after_update_cb=after_update_cb,
         )
 
-    def payload_valid(self, payload):
+    @staticmethod
+    def payload_valid(payload: Optional["DPTPayload"]) -> bool:
         """Test if telegram payload may be parsed."""
         return isinstance(payload, DPTBinary)
 
     @staticmethod
-    def supported_operation_modes():
+    def supported_operation_modes() -> List[HVACControllerMode]:
         """Return a list of the configured operation mode."""
         return [HVACControllerMode.HEAT, HVACControllerMode.COOL]
 
-    def to_knx(self, value):
+    def to_knx(self, value: Any) -> "DPTPayload":
         """Convert value to payload."""
         if isinstance(value, HVACControllerMode):
             # foreign operation modes will set the RemoteValue to False
@@ -232,7 +260,7 @@ class RemoteValueBinaryHeatCool(RemoteValue):
             feature_name=self.feature_name,
         )
 
-    def from_knx(self, payload):
+    def from_knx(self, payload: "DPTPayload") -> Optional[HVACControllerMode]:
         """Convert current payload to value."""
         if payload == DPTBinary(1):
             return self.controller_mode
@@ -248,7 +276,7 @@ class RemoteValueBinaryHeatCool(RemoteValue):
             )
         raise CouldNotParseTelegram(
             "payload invalid",
-            payload=payload,
+            payload=str(payload),
             device_name=self.device_name,
             feature_name=self.feature_name,
         )
diff --git a/xknx/remote_value/remote_value_control.py b/xknx/remote_value/remote_value_control.py
index cf5c031c..96ea9d51 100644
--- a/xknx/remote_value/remote_value_control.py
+++ b/xknx/remote_value/remote_value_control.py
@@ -4,7 +4,7 @@ Module for managing a control remote value.
 Examples are switching commands with priority control, relative dimming or blinds control commands.
 DPT 2.yyy and DPT 3.yyy
 """
-from typing import List
+from typing import List, Optional
 
 from xknx.dpt import DPTBase, DPTBinary
 from xknx.exceptions import ConversionError
@@ -61,11 +61,11 @@ class RemoteValueControl(RemoteValue):
         return self.dpt_class.from_knx(payload.value, invert=self.invert)
 
     @property
-    def unit_of_measurement(self):
+    def unit_of_measurement(self) -> Optional[str]:
         """Return the unit of measurement."""
         return self.dpt_class.unit
 
     @property
-    def ha_device_class(self):
+    def ha_device_class(self) -> Optional[str]:
         """Return a string representing the home assistant device class."""
         return getattr(self.dpt_class, "ha_device_class", None)
diff --git a/xknx/remote_value/remote_value_sensor.py b/xknx/remote_value/remote_value_sensor.py
index 06a75397..afde29ce 100644
--- a/xknx/remote_value/remote_value_sensor.py
+++ b/xknx/remote_value/remote_value_sensor.py
@@ -4,7 +4,7 @@ Module for managing a remote value typically used within a sensor.
 The module maps a given value_type to a DPT class and uses this class
 for serialization and deserialization of the KNX value.
 """
-from typing import List
+from typing import List, Optional
 
 from xknx.dpt import DPTArray, DPTBase
 from xknx.exceptions import ConversionError
@@ -62,11 +62,11 @@ class RemoteValueSensor(RemoteValue):
         return self.dpt_class.from_knx(payload.value)
 
     @property
-    def unit_of_measurement(self):
+    def unit_of_measurement(self) -> Optional[str]:
         """Return the unit of measurement."""
         return self.dpt_class.unit
 
     @property
-    def ha_device_class(self):
+    def ha_device_class(self) -> Optional[str]:
         """Return a string representing the home assistant device class."""
         return getattr(self.dpt_class, "ha_device_class", None)
diff --git a/xknx/remote_value/remote_value_step.py b/xknx/remote_value/remote_value_step.py
index e2243043..ae62c7e6 100644
--- a/xknx/remote_value/remote_value_step.py
+++ b/xknx/remote_value/remote_value_step.py
@@ -79,10 +79,10 @@ class RemoteValueStep(RemoteValue):
             feature_name=self.feature_name,
         )
 
-    async def increase(self):
+    async def increase(self) -> None:
         """Increase value."""
         await self.set(self.Direction.INCREASE)
 
-    async def decrease(self):
+    async def decrease(self) -> None:
         """Decrease the value."""
         await self.set(self.Direction.DECREASE)
diff --git a/xknx/remote_value/remote_value_switch.py b/xknx/remote_value/remote_value_switch.py
index 4351eff9..3a7ba8aa 100644
--- a/xknx/remote_value/remote_value_switch.py
+++ b/xknx/remote_value/remote_value_switch.py
@@ -55,7 +55,7 @@ class RemoteValueSwitch(RemoteValue):
             feature_name=self.feature_name,
         )
 
-    def from_knx(self, payload):
+    def from_knx(self, payload: DPTBinary) -> bool:
         """Convert current payload to value."""
         if payload == DPTBinary(0):
             return self.invert
@@ -68,11 +68,11 @@ class RemoteValueSwitch(RemoteValue):
             feature_name=self.feature_name,
         )
 
-    async def off(self):
+    async def off(self) -> None:
         """Set value to OFF."""
         await self.set(False)
 
-    async def on(self):
+    async def on(self) -> None:
         """Set value to ON."""
         # pylint: disable=invalid-name
         await self.set(True)
diff --git a/xknx/remote_value/remote_value_updown.py b/xknx/remote_value/remote_value_updown.py
index 9c4502d3..7fbd3ad8 100644
--- a/xknx/remote_value/remote_value_updown.py
+++ b/xknx/remote_value/remote_value_updown.py
@@ -76,11 +76,11 @@ class RemoteValueUpDown(RemoteValue):
             feature_name=self.feature_name,
         )
 
-    async def down(self):
+    async def down(self) -> None:
         """Set value to down."""
         await self.set(self.Direction.DOWN)
 
-    async def up(self):
+    async def up(self) -> None:
         """Set value to UP."""
         # pylint: disable=invalid-name
         await self.set(self.Direction.UP)
diff --git a/xknx/telegram/telegram.py b/xknx/telegram/telegram.py
index b702c233..d6c44321 100644
--- a/xknx/telegram/telegram.py
+++ b/xknx/telegram/telegram.py
@@ -14,9 +14,10 @@ It contains
 
 """
 from enum import Enum
-from typing import Any, Union
+from typing import Optional, Union
 
 from .address import GroupAddress, IndividualAddress
+from .apci import APCI
 
 
 class TelegramDirection(Enum):
@@ -35,7 +36,7 @@ class Telegram:
         self,
         destination_address: Union[GroupAddress, IndividualAddress] = GroupAddress(0),
         direction: TelegramDirection = TelegramDirection.OUTGOING,
-        payload: Any = None,
+        payload: Optional[APCI] = None,
         source_address: IndividualAddress = IndividualAddress(0),
     ) -> None:
         """Initialize Telegram class."""


Test patch:
diff --git a/test/config_tests/config_v1_test.py b/test/config_tests/config_v1_test.py
index be7b10bc..a850da19 100644
--- a/test/config_tests/config_v1_test.py
+++ b/test/config_tests/config_v1_test.py
@@ -382,7 +382,9 @@ class TestConfig(unittest.TestCase):
         self.assertEqual(
             TestConfig.xknx.devices["Office.Climate"].mode,
             ClimateMode(
-                TestConfig.xknx, name=None, group_address_operation_mode="1/7/6"
+                TestConfig.xknx,
+                name="Office.Climate_mode",
+                group_address_operation_mode="1/7/6",
             ),
         )
 
@@ -392,7 +394,7 @@ class TestConfig(unittest.TestCase):
             TestConfig.xknx.devices["Attic.Climate"].mode,
             ClimateMode(
                 TestConfig.xknx,
-                name=None,
+                name="Attic.Climate_mode",
                 group_address_operation_mode_protection="1/7/8",
                 group_address_operation_mode_night="1/7/9",
                 group_address_operation_mode_comfort="1/7/10",
@@ -405,7 +407,7 @@ class TestConfig(unittest.TestCase):
             TestConfig.xknx.devices["Bath.Climate"].mode,
             ClimateMode(
                 TestConfig.xknx,
-                name=None,
+                name="Bath.Climate_mode",
                 group_address_operation_mode="1/7/6",
                 group_address_operation_mode_state="1/7/7",
             ),
@@ -417,7 +419,7 @@ class TestConfig(unittest.TestCase):
             TestConfig.xknx.devices["Cellar.Climate"].mode,
             ClimateMode(
                 TestConfig.xknx,
-                name=None,
+                name="Cellar.Climate_mode",
                 group_address_controller_status="1/7/12",
                 group_address_controller_status_state="1/7/13",
             ),
diff --git a/test/devices_tests/climate_test.py b/test/devices_tests/climate_test.py
index 19dcfeec..69c75e0a 100644
--- a/test/devices_tests/climate_test.py
+++ b/test/devices_tests/climate_test.py
@@ -15,9 +15,8 @@ from xknx.dpt import (
     DPTHVACMode,
     DPTTemperature,
     DPTValue1Count,
-    HVACOperationMode,
 )
-from xknx.dpt.dpt_hvac_mode import HVACControllerMode
+from xknx.dpt.dpt_hvac_mode import HVACControllerMode, HVACOperationMode
 from xknx.exceptions import CouldNotParseTelegram, DeviceIllegalValue
 from xknx.telegram import GroupAddress, Telegram
 from xknx.telegram.apci import GroupValueRead, GroupValueWrite
diff --git a/test/dpt_tests/dpt_hvac_mode_test.py b/test/dpt_tests/dpt_hvac_mode_test.py
index 0c45cb8a..9608ed84 100644
--- a/test/dpt_tests/dpt_hvac_mode_test.py
+++ b/test/dpt_tests/dpt_hvac_mode_test.py
@@ -1,7 +1,8 @@
 """Unit test for KNX DPT HVAC Operation modes."""
 import unittest
 
-from xknx.dpt import DPTControllerStatus, DPTHVACMode, HVACOperationMode
+from xknx.dpt import DPTControllerStatus, DPTHVACMode
+from xknx.dpt.dpt_hvac_mode import HVACOperationMode
 from xknx.exceptions import ConversionError, CouldNotParseKNXIP
 
 
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 4ce93b59..c34ccf0b 100644
--- a/test/remote_value_tests/remote_value_climate_mode_test.py
+++ b/test/remote_value_tests/remote_value_climate_mode_test.py
@@ -3,15 +3,14 @@ import asyncio
 import unittest
 
 from xknx import XKNX
-from xknx.dpt import DPTArray, DPTBinary, HVACOperationMode
-from xknx.dpt.dpt_hvac_mode import HVACControllerMode
+from xknx.dpt import DPTArray, DPTBinary
+from xknx.dpt.dpt_hvac_mode import HVACControllerMode, HVACOperationMode
 from xknx.exceptions import ConversionError, CouldNotParseTelegram
 from xknx.remote_value import (
     RemoteValueBinaryHeatCool,
     RemoteValueBinaryOperationMode,
     RemoteValueClimateMode,
 )
-from xknx.remote_value.remote_value_climate_mode import _RemoteValueBinaryClimateMode
 from xknx.telegram import GroupAddress, Telegram
 from xknx.telegram.apci import GroupValueWrite
 
@@ -137,12 +136,6 @@ class TestRemoteValueDptValue1Ucount(unittest.TestCase):
         with self.assertRaises(ConversionError):
             RemoteValueBinaryHeatCool(xknx, controller_mode=None)
 
-    def test_supported_operation_modes_not_implemented(self):
-        """Test from_knx function with unsupported operation."""
-        xknx = XKNX()
-        with self.assertRaises(NotImplementedError):
-            _RemoteValueBinaryClimateMode.supported_operation_modes()
-
     def test_to_knx_error_operation_mode(self):
         """Test to_knx function with wrong parameter."""
         xknx = XKNX()
diff --git a/test/remote_value_tests/remote_value_datetime_test.py b/test/remote_value_tests/remote_value_datetime_test.py
index c1b18ce4..1b452326 100644
--- a/test/remote_value_tests/remote_value_datetime_test.py
+++ b/test/remote_value_tests/remote_value_datetime_test.py
@@ -4,16 +4,9 @@ import time
 import unittest
 
 from xknx import XKNX
-from xknx.dpt import DPTArray, DPTBinary, HVACOperationMode
-from xknx.exceptions import ConversionError, CouldNotParseTelegram
-from xknx.remote_value import (
-    RemoteValueBinaryHeatCool,
-    RemoteValueBinaryOperationMode,
-    RemoteValueClimateMode,
-    RemoteValueDateTime,
-)
-from xknx.remote_value.remote_value_climate_mode import _RemoteValueBinaryClimateMode
-from xknx.telegram import GroupAddress, Telegram
+from xknx.dpt import DPTArray
+from xknx.exceptions import ConversionError
+from xknx.remote_value import RemoteValueDateTime
 
 
 class TestRemoteValueDateTime(unittest.TestCase):
diff --git a/test/remote_value_tests/remote_value_test.py b/test/remote_value_tests/remote_value_test.py
index c68a97e1..3c44346c 100644
--- a/test/remote_value_tests/remote_value_test.py
+++ b/test/remote_value_tests/remote_value_test.py
@@ -24,16 +24,6 @@ class TestRemoteValue(unittest.TestCase):
         """Tear down test class."""
         self.loop.close()
 
-    def test_warn_payload_valid(self):
-        """Test for warning if payload_valid is not implemented."""
-        xknx = XKNX()
-        remote_value = RemoteValue(xknx)
-        with patch("logging.Logger.warning") as mock_warn:
-            remote_value.payload_valid(DPTBinary(0))
-            mock_warn.assert_called_with(
-                "'payload_valid()' not implemented for %s", "RemoteValue"
-            )
-
     def test_info_set_uninitialized(self):
         """Test for info if RemoteValue is not initialized."""
         xknx = XKNX()