Repository: XKNX/xknx
Pull request #543 (2020-12-25 21:54:03)

Typecheck io, knxip modules and xknx.py
<!--
  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.
-->
Typechecked `io` and `knxip` modules and `xknx.py`, some code quality and error message improvements.

Fixes # (issue)

## Type of change
<!--
Please tick the applicable options.
NOTE: Ticking multiple options most likely indicates
that your change is to big and it is suggested to split it into several smaller PRs.
-->

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update

## Checklist:

- [ ] I have performed a self-review of my own code
- [ ] The documentation has been adjusted accordingly
- [ ] The changes generate no new warnings
- [ ] Tests have been added that prove the fix is effective or that the feature works
- [ ] The changes are documented in the changelog
- [ ] The Homeassistant plugin has been adjusted in case of new config options



Patch:
diff --git a/setup.cfg b/setup.cfg
index 6e12973a..81b6d598 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -42,10 +42,18 @@ warn_redundant_casts = true
 warn_unused_configs = true
 
 # add the modules below once we add typing for them so that we fail the build in the future if someone changes something without updating the typings
-[mypy-xknx.core.*,xknx.devices.travelcalculator,xknx.exceptions.*,xknx.io.gateway_scanner,xknx.remote_value.remote_value,xknx.telegram.*,]
+# fully typechecked modules
+[mypy-xknx.xknx,xknx.core.*,xknx.exceptions.*,xknx.io.*,xknx.knxip.*,xknx.telegram.*,]
+strict = true
+ignore_errors = false
+warn_unreachable = true
+# TODO: turn these off, address issues
+implicit_reexport = true
+
+# partly typechecked modules (extra block for better overview)
+[mypy-xknx.devices.travelcalculator,xknx.remote_value.remote_value]
 strict = true
 ignore_errors = false
 warn_unreachable = true
 # TODO: turn these off, address issues
-allow_any_generics = true
 implicit_reexport = true
diff --git a/xknx/config/__init__.py b/xknx/config/__init__.py
index 4ef08d04..81dd8cce 100644
--- a/xknx/config/__init__.py
+++ b/xknx/config/__init__.py
@@ -17,3 +17,22 @@ from .schema import (
     WeatherSchema,
     XKNXSchema,
 )
+
+__all__ = [
+    "Config",
+    "ConfigV1",
+    "BinarySensorSchema",
+    "ClimateSchema",
+    "ConnectionSchema",
+    "CoverSchema",
+    "DateTimeSchema",
+    "ExposeSchema",
+    "FanSchema",
+    "LightSchema",
+    "NotificationSchema",
+    "SceneSchema",
+    "SensorSchema",
+    "SwitchSchema",
+    "WeatherSchema",
+    "XKNXSchema",
+]
diff --git a/xknx/config/config.py b/xknx/config/config.py
index 60e724c4..b40f2d41 100644
--- a/xknx/config/config.py
+++ b/xknx/config/config.py
@@ -6,10 +6,14 @@ Module for reading config files (xknx.yaml).
 """
 from enum import Enum
 import logging
+from typing import TYPE_CHECKING
 
 from .config_v1 import ConfigV1
 from .yaml_loader import load_yaml
 
+if TYPE_CHECKING:
+    from xknx.xknx import XKNX
+
 logger = logging.getLogger("xknx.log")
 
 
@@ -23,24 +27,24 @@ class Version(Enum):
 class Config:
     """Class for parsing xknx.yaml."""
 
-    def __init__(self, xknx):
+    def __init__(self, xknx: "XKNX") -> None:
         """Initialize Config class."""
         self.xknx = xknx
 
-    def read(self, file="xknx.yaml"):
+    def read(self, file: str = "xknx.yaml") -> None:
         """Read config."""
         logger.debug("Reading %s", file)
         doc = load_yaml(file)
         self.parse(doc)
 
     @staticmethod
-    def parse_version(doc):
+    def parse_version(doc) -> Version:
         """Parse the version of the xknx.yaml."""
         if "version" in doc:
             return Version(doc["version"])
         return Version.VERSION_1
 
-    def parse(self, doc):
+    def parse(self, doc) -> None:
         """Parse the config from the YAML."""
         version = Config.parse_version(doc)
         if version is Version.VERSION_1:
diff --git a/xknx/core/state_updater.py b/xknx/core/state_updater.py
index 0aeaebf6..04eeb6d4 100644
--- a/xknx/core/state_updater.py
+++ b/xknx/core/state_updater.py
@@ -157,7 +157,7 @@ class _StateTracker:
         self.tracker_type = tracker_type
         self.update_interval = interval_min * 60
         self._read_state = read_state_awaitable
-        self._task: Optional[asyncio.Task] = None
+        self._task: Optional[asyncio.Task[None]] = None
 
     def start(self) -> None:
         """Start StateTracker - read state on call."""
diff --git a/xknx/core/telegram_queue.py b/xknx/core/telegram_queue.py
index c246ea16..9c6cbc36 100644
--- a/xknx/core/telegram_queue.py
+++ b/xknx/core/telegram_queue.py
@@ -9,7 +9,7 @@ You may register callbacks to be notified if a telegram was pushed to the queue.
 """
 import asyncio
 import logging
-from typing import TYPE_CHECKING, Awaitable, Callable, List, Optional
+from typing import TYPE_CHECKING, Awaitable, Callable, List, Optional, Tuple
 
 from xknx.exceptions import CommunicationError, XKNXException
 from xknx.telegram import AddressFilter, GroupAddress, Telegram, TelegramDirection
@@ -57,7 +57,7 @@ class TelegramQueue:
         self.xknx = xknx
         self.telegram_received_cbs: List[TelegramQueue.Callback] = []
         self.outgoing_queue: asyncio.Queue[Optional[Telegram]] = asyncio.Queue()
-        self._consumer_task: Optional[Awaitable] = None
+        self._consumer_task: Optional[Awaitable[Tuple[None, None]]] = None
 
     def register_telegram_received_cb(
         self,
@@ -137,6 +137,8 @@ class TelegramQueue:
         while not self.xknx.telegrams.empty():
             try:
                 telegram = self.xknx.telegrams.get_nowait()
+                if telegram is None:
+                    return
                 if telegram.direction == TelegramDirection.INCOMING:
                     await self.process_telegram_incoming(telegram)
                 elif telegram.direction == TelegramDirection.OUTGOING:
@@ -154,7 +156,7 @@ class TelegramQueue:
             if isinstance(telegram.payload, GroupValueWrite):
                 await self.xknx.devices.process(telegram)
         else:
-            logger.warning("No KNXIP interface defined")
+            raise CommunicationError("No KNXIP interface defined")
 
     async def process_telegram_incoming(self, telegram: Telegram) -> None:
         """Process incoming telegram."""
diff --git a/xknx/devices/__init__.py b/xknx/devices/__init__.py
index 167804dc..985cbf08 100644
--- a/xknx/devices/__init__.py
+++ b/xknx/devices/__init__.py
@@ -17,3 +17,26 @@ from .sensor import Sensor
 from .switch import Switch
 from .travelcalculator import TravelCalculator, TravelStatus
 from .weather import Weather
+
+__all__ = [
+    "Action",
+    "ActionBase",
+    "ActionCallback",
+    "BinarySensor",
+    "Climate",
+    "ClimateMode",
+    "Cover",
+    "DateTime",
+    "Device",
+    "Devices",
+    "ExposeSensor",
+    "Fan",
+    "Light",
+    "Notification",
+    "Scene",
+    "Sensor",
+    "Switch",
+    "TravelCalculator",
+    "TravelStatus",
+    "Weather",
+]
diff --git a/xknx/devices/devices.py b/xknx/devices/devices.py
index db241627..0deab8e0 100644
--- a/xknx/devices/devices.py
+++ b/xknx/devices/devices.py
@@ -3,18 +3,24 @@ Module for handling a vector/array of devices.
 
 More or less an array with devices. Adds some search functionality to find devices.
 """
+from typing import Awaitable, Callable
+
+from xknx.telegram import Telegram
+
 from .device import Device
 
 
 class Devices:
     """Class for handling a vector/array of devices."""
 
-    def __init__(self):
+    def __init__(self) -> None:
         """Initialize Devices class."""
         self.__devices = []
         self.device_updated_cbs = []
 
-    def register_device_updated_cb(self, device_updated_cb):
+    def register_device_updated_cb(
+        self, device_updated_cb: Callable[[Device], Awaitable[None]]
+    ) -> None:
         """Register callback for devices beeing updated."""
         self.device_updated_cbs.append(device_updated_cb)
 
@@ -64,7 +70,7 @@ class Devices:
         for device_updated_cb in self.device_updated_cbs:
             await device_updated_cb(device)
 
-    async def process(self, telegram):
+    async def process(self, telegram: Telegram) -> None:
         """Process telegram."""
         for device in self.devices_by_group_address(telegram.destination_address):
             await device.process(telegram)
diff --git a/xknx/io/connect.py b/xknx/io/connect.py
index dc654e23..7cefcef5 100644
--- a/xknx/io/connect.py
+++ b/xknx/io/connect.py
@@ -1,4 +1,6 @@
 """Abstraction to send ConnectRequest and wait for ConnectResponse."""
+from typing import TYPE_CHECKING
+
 from xknx.knxip import (
     HPAI,
     ConnectRequest,
@@ -9,11 +11,16 @@ from xknx.knxip import (
 
 from .request_response import RequestResponse
 
+if TYPE_CHECKING:
+    from xknx.xknx import XKNX
+
+    from .udp_client import UDPClient
+
 
 class Connect(RequestResponse):
     """Class to send a ConnectRequest and wait for ConnectResponse.."""
 
-    def __init__(self, xknx, udp_client):
+    def __init__(self, xknx: "XKNX", udp_client: "UDPClient"):
         """Initialize Connect class."""
         self.udp_client = udp_client
         super().__init__(xknx, self.udp_client, ConnectResponse)
@@ -33,7 +40,9 @@ class Connect(RequestResponse):
         )
         return KNXIPFrame.init_from_body(connect_request)
 
-    def on_success_hook(self, knxipframe):
+    def on_success_hook(self, knxipframe: KNXIPFrame) -> None:
         """Set communication channel and identifier after having received a valid answer."""
+        assert isinstance(knxipframe.body, ConnectResponse)
+        assert isinstance(knxipframe.body.identifier, int)
         self.communication_channel = knxipframe.body.communication_channel
         self.identifier = knxipframe.body.identifier
diff --git a/xknx/io/connectionstate.py b/xknx/io/connectionstate.py
index 906a2598..599ec0ec 100644
--- a/xknx/io/connectionstate.py
+++ b/xknx/io/connectionstate.py
@@ -1,14 +1,23 @@
 """Abstraction to send ConnectonStateRequest and wait for ConnectionStateResponse."""
+from typing import TYPE_CHECKING
+
 from xknx.knxip import HPAI, ConnectionStateRequest, ConnectionStateResponse, KNXIPFrame
 
 from .const import CONNECTIONSTATE_REQUEST_TIMEOUT
 from .request_response import RequestResponse
 
+if TYPE_CHECKING:
+    from xknx.xknx import XKNX
+
+    from .udp_client import UDPClient
+
 
 class ConnectionState(RequestResponse):
     """Class to send ConnectonStateRequest and wait for ConnectionStateResponse."""
 
-    def __init__(self, xknx, udp_client, communication_channel_id):
+    def __init__(
+        self, xknx: "XKNX", udp_client: "UDPClient", communication_channel_id: int
+    ):
         """Initialize ConnectionState class."""
         self.udp_client = udp_client
         super().__init__(
diff --git a/xknx/io/disconnect.py b/xknx/io/disconnect.py
index 2222574b..d01caeda 100644
--- a/xknx/io/disconnect.py
+++ b/xknx/io/disconnect.py
@@ -1,13 +1,22 @@
 """Abstraction to send DisconnectRequest and wait for DisconnectResponse."""
+from typing import TYPE_CHECKING
+
 from xknx.knxip import HPAI, DisconnectRequest, DisconnectResponse, KNXIPFrame
 
 from .request_response import RequestResponse
 
+if TYPE_CHECKING:
+    from xknx.xknx import XKNX
+
+    from .udp_client import UDPClient
+
 
 class Disconnect(RequestResponse):
     """Class to send a DisconnectRequest and wait for a DisconnectResponse."""
 
-    def __init__(self, xknx, udp_client, communication_channel_id):
+    def __init__(
+        self, xknx: "XKNX", udp_client: "UDPClient", communication_channel_id: int
+    ):
         """Initialize Disconnect class."""
         self.xknx = xknx
         self.udp_client = udp_client
diff --git a/xknx/io/gateway_scanner.py b/xknx/io/gateway_scanner.py
index 77470c54..75d75f2a 100644
--- a/xknx/io/gateway_scanner.py
+++ b/xknx/io/gateway_scanner.py
@@ -176,9 +176,9 @@ class GatewayScanner:
         self._udp_clients.append(udp_client)
 
         (local_addr, local_port) = udp_client.getsockname()
-        search_request = SearchRequest(
-            self.xknx, discovery_endpoint=HPAI(ip_addr=local_addr, port=local_port)
-        )
+        discovery_endpoint = HPAI(ip_addr=local_addr, port=local_port)
+
+        search_request = SearchRequest(self.xknx, discovery_endpoint=discovery_endpoint)
         udp_client.send(KNXIPFrame.init_from_body(search_request))
 
     def _response_rec_callback(
@@ -197,10 +197,14 @@ class GatewayScanner:
             local_ip=udp_client.local_addr[0],
         )
         try:
-            dib = knx_ip_frame.body[DIBSuppSVCFamilies]
+            dib = next(
+                dib
+                for dib in knx_ip_frame.body.dibs
+                if isinstance(dib, DIBSuppSVCFamilies)
+            )
             gateway.supports_routing = dib.supports(DIBServiceFamily.ROUTING)
             gateway.supports_tunnelling = dib.supports(DIBServiceFamily.TUNNELING)
-        except IndexError:
+        except StopIteration:
             pass
 
         self._add_found_gateway(gateway)
diff --git a/xknx/io/knxip_interface.py b/xknx/io/knxip_interface.py
index c778d75c..67744205 100644
--- a/xknx/io/knxip_interface.py
+++ b/xknx/io/knxip_interface.py
@@ -9,15 +9,22 @@ KNXIPInterface manages KNX/IP Tunneling or Routing connections.
 from enum import Enum
 import ipaddress
 import logging
+from typing import TYPE_CHECKING, Optional, cast
 
 import netifaces
-from xknx.exceptions import XKNXException
+from xknx.exceptions import CommunicationError, XKNXException
 
 from .const import DEFAULT_MCAST_PORT
 from .gateway_scanner import GatewayScanFilter, GatewayScanner
 from .routing import Routing
 from .tunnel import Tunnel
 
+if TYPE_CHECKING:
+    from xknx.telegram import Telegram
+    from xknx.xknx import XKNX
+
+    from .interface import Interface
+
 logger = logging.getLogger("xknx.log")
 
 
@@ -51,8 +58,8 @@ class ConnectionConfig:
     def __init__(
         self,
         connection_type: ConnectionType = ConnectionType.AUTOMATIC,
-        local_ip: str = None,
-        gateway_ip: str = None,
+        local_ip: Optional[str] = None,
+        gateway_ip: Optional[str] = None,
         gateway_port: int = DEFAULT_MCAST_PORT,
         auto_reconnect: bool = False,
         auto_reconnect_wait: int = 3,
@@ -72,7 +79,7 @@ class ConnectionConfig:
             scan_filter.routing = True
         self.scan_filter = scan_filter
 
-    def __eq__(self, other):
+    def __eq__(self, other: object) -> bool:
         """Equality for ConnectionConfig class (used in unit tests)."""
         return self.__dict__ == other.__dict__
 
@@ -80,20 +87,27 @@ class ConnectionConfig:
 class KNXIPInterface:
     """Class for managing KNX/IP Tunneling or Routing connections."""
 
-    def __init__(self, xknx, connection_config=ConnectionConfig()):
+    def __init__(
+        self,
+        xknx: "XKNX",
+        connection_config: ConnectionConfig = ConnectionConfig(),
+    ):
         """Initialize KNXIPInterface class."""
         self.xknx = xknx
-        self.interface = None
+        self.interface: Optional["Interface"] = None
         self.connection_config = connection_config
 
-    async def start(self):
+    async def start(self) -> None:
         """Start interface. Connecting KNX/IP device with the selected method."""
         if (
             self.connection_config.connection_type == ConnectionType.ROUTING
             and self.connection_config.local_ip is not None
         ):
             await self.start_routing(self.connection_config.local_ip)
-        elif self.connection_config.connection_type == ConnectionType.TUNNELING:
+        elif (
+            self.connection_config.connection_type == ConnectionType.TUNNELING
+            and self.connection_config.gateway_ip is not None
+        ):
             await self.start_tunnelling(
                 self.connection_config.local_ip,
                 self.connection_config.gateway_ip,
@@ -104,7 +118,7 @@ class KNXIPInterface:
         else:
             await self.start_automatic(self.connection_config.scan_filter)
 
-    async def start_automatic(self, scan_filter: GatewayScanFilter):
+    async def start_automatic(self, scan_filter: GatewayScanFilter) -> None:
         """Start GatewayScanner and connect to the found device."""
         gatewayscanner = GatewayScanner(self.xknx, scan_filter=scan_filter)
         gateways = await gatewayscanner.scan()
@@ -130,8 +144,13 @@ class KNXIPInterface:
             await self.start_routing(local_interface_ip)
 
     async def start_tunnelling(
-        self, local_ip, gateway_ip, gateway_port, auto_reconnect, auto_reconnect_wait
-    ):
+        self,
+        local_ip: Optional[str],
+        gateway_ip: str,
+        gateway_port: int,
+        auto_reconnect: bool,
+        auto_reconnect_wait: int,
+    ) -> None:
         """Start KNX/IP tunnel."""
         # pylint: disable=too-many-arguments
         validate_ip(gateway_ip, address_name="Gateway IP address")
@@ -165,19 +184,22 @@ class KNXIPInterface:
             await self.interface.disconnect()
             self.interface = None
 
-    def telegram_received(self, telegram):
+    def telegram_received(self, telegram: "Telegram") -> None:
         """Put received telegram into queue. Callback for having received telegram."""
         self.xknx.telegrams.put_nowait(telegram)
 
-    async def send_telegram(self, telegram):
+    async def send_telegram(self, telegram: "Telegram") -> None:
         """Send telegram to connected device (either Tunneling or Routing)."""
-        await self.interface.send_telegram(telegram)
+        if self.interface is not None:
+            await self.interface.send_telegram(telegram)
+        else:
+            raise CommunicationError("KNX/IP interface not connected")
 
     @staticmethod
     def find_local_ip(gateway_ip: str) -> str:
         """Find local IP address on same subnet as gateway."""
 
-        def _scan_interfaces(gateway: ipaddress.IPv4Address) -> str:
+        def _scan_interfaces(gateway: ipaddress.IPv4Address) -> Optional[str]:
             """Return local IP address on same subnet as given gateway."""
             for interface in netifaces.interfaces():
                 try:
@@ -188,7 +210,7 @@ class KNXIPInterface:
                         )
                         if gateway in network:
                             logger.debug("Using interface: %s", interface)
-                            return link["addr"]
+                            return cast(str, link["addr"])
                 except KeyError:
                     logger.debug(
                         "Could not find IPv4 address on interface %s", interface
@@ -209,6 +231,7 @@ class KNXIPInterface:
             )
             default_gateway = _find_default_gateway()
             local_ip = _scan_interfaces(default_gateway)
+        assert isinstance(local_ip, str)
         return local_ip
 
 
diff --git a/xknx/io/request_response.py b/xknx/io/request_response.py
index eed530d7..bef39fe9 100644
--- a/xknx/io/request_response.py
+++ b/xknx/io/request_response.py
@@ -5,8 +5,14 @@ Will report if the corresponding answer was not received.
 """
 import asyncio
 import logging
+from typing import TYPE_CHECKING, Optional, Type
 
-from xknx.knxip import ErrorCode, KNXIPBody, KNXIPFrame
+from xknx.knxip import ErrorCode, KNXIPBodyResponse, KNXIPFrame
+
+from .udp_client import UDPClient
+
+if TYPE_CHECKING:
+    from xknx.xknx import XKNX
 
 logger = logging.getLogger("xknx.log")
 
@@ -16,19 +22,21 @@ class RequestResponse:
 
     def __init__(
         self,
-        xknx,
-        udp_client,
-        awaited_response_class: KNXIPBody,
+        xknx: "XKNX",
+        udp_client: UDPClient,
+        awaited_response_class: Type[KNXIPBodyResponse],
         timeout_in_seconds: float = 1.0,
     ):
         """Initialize RequstResponse class."""
         self.xknx = xknx
         self.udpclient = udp_client
-        self.awaited_response_class: KNXIPBody = awaited_response_class
+        self.awaited_response_class: Type[KNXIPBodyResponse] = awaited_response_class
         self.response_received_or_timeout = asyncio.Event()
         self.success = False
         self.timeout_in_seconds = timeout_in_seconds
 
+        self.response_status_code: Optional[ErrorCode] = None
+
     def create_knxipframe(self) -> KNXIPFrame:
         """Create KNX/IP Frame object to be sent to device."""
         raise NotImplementedError("create_knxipframe has to be implemented")
@@ -59,11 +67,12 @@ class RequestResponse:
         """Build knxipframe (within derived class) and send via UDP."""
         self.udpclient.send(self.create_knxipframe())
 
-    def response_rec_callback(self, knxipframe: KNXIPFrame, _) -> None:
+    def response_rec_callback(self, knxipframe: KNXIPFrame, _: UDPClient) -> None:
         """Verify and handle knxipframe. Callback from internal udpclient."""
         if not isinstance(knxipframe.body, self.awaited_response_class):
             logger.warning("Could not understand knxipframe")
             return
+        self.response_status_code = knxipframe.body.status_code
         self.response_received_or_timeout.set()
         if knxipframe.body.status_code == ErrorCode.E_NO_ERROR:
             self.success = True
@@ -80,5 +89,5 @@ class RequestResponse:
             "Error: KNX bus responded to request of type '%s' with error in '%s': %s",
             self.__class__.__name__,
             self.awaited_response_class.__name__,
-            knxipframe.body.status_code,
+            knxipframe.body.status_code,  # type: ignore
         )
diff --git a/xknx/io/routing.py b/xknx/io/routing.py
index 6c8ccd3a..e86f5133 100644
--- a/xknx/io/routing.py
+++ b/xknx/io/routing.py
@@ -4,6 +4,7 @@ Abstraction for handling KNX/IP routing.
 Routing uses UDP Multicast to broadcast and receive KNX/IP messages.
 """
 import logging
+from typing import TYPE_CHECKING, Callable
 
 from xknx.knxip import (
     CEMIFrame,
@@ -17,13 +18,24 @@ from xknx.telegram import TelegramDirection
 from .interface import Interface
 from .udp_client import UDPClient
 
+if TYPE_CHECKING:
+    from xknx.telegram import Telegram
+    from xknx.xknx import XKNX
+
+    TelegramCallbackType = Callable[[Telegram], None]
+
 logger = logging.getLogger("xknx.log")
 
 
 class Routing(Interface):
     """Class for handling KNX/IP routing."""
 
-    def __init__(self, xknx, telegram_received_callback, local_ip):
+    def __init__(
+        self,
+        xknx: "XKNX",
+        telegram_received_callback: "TelegramCallbackType",
+        local_ip: str,
+    ):
         """Initialize Routing class."""
         self.xknx = xknx
         self.telegram_received_callback = telegram_received_callback
@@ -40,9 +52,9 @@ class Routing(Interface):
             self.response_rec_callback, [KNXIPServiceType.ROUTING_INDICATION]
         )
 
-    def response_rec_callback(self, knxipframe, _):
+    def response_rec_callback(self, knxipframe: KNXIPFrame, _: UDPClient) -> None:
         """Verify and handle knxipframe. Callback from internal udpclient."""
-        if knxipframe.header.service_type_ident != KNXIPServiceType.ROUTING_INDICATION:
+        if not isinstance(knxipframe.body, RoutingIndication):
             logger.warning("Service type not implemented: %s", knxipframe)
         elif knxipframe.body.cemi is None:
             # ignore unsupported CEMI frame
@@ -56,7 +68,7 @@ class Routing(Interface):
             if self.telegram_received_callback is not None:
                 self.telegram_received_callback(telegram)
 
-    async def send_telegram(self, telegram):
+    async def send_telegram(self, telegram: "Telegram") -> None:
         """Send Telegram to routing connected device."""
         cemi = CEMIFrame.init_from_telegram(
             self.xknx,
@@ -67,7 +79,7 @@ class Routing(Interface):
         routing_indication = RoutingIndication(self.xknx, cemi=cemi)
         await self.send_knxipframe(KNXIPFrame.init_from_body(routing_indication))
 
-    async def send_knxipframe(self, knxipframe):
+    async def send_knxipframe(self, knxipframe: KNXIPFrame) -> None:
         """Send KNXIPFrame to connected routing device."""
         self.udpclient.send(knxipframe)
 
@@ -87,7 +99,7 @@ class Routing(Interface):
         self.xknx.connected.set()
         return True
 
-    async def disconnect(self):
+    async def disconnect(self) -> None:
         """Stop routing."""
         await self.udpclient.stop()
         self.xknx.connected.clear()
diff --git a/xknx/io/tunnel.py b/xknx/io/tunnel.py
index 7fa57eaa..bc0d527b 100644
--- a/xknx/io/tunnel.py
+++ b/xknx/io/tunnel.py
@@ -5,6 +5,7 @@ Tunnels connect to KNX/IP devices directly via UDP and build a static UDP connec
 """
 import asyncio
 import logging
+from typing import TYPE_CHECKING, Callable, Optional
 
 from xknx.exceptions import CommunicationError, XKNXException
 from xknx.knxip import (
@@ -16,7 +17,7 @@ from xknx.knxip import (
     TunnellingAck,
     TunnellingRequest,
 )
-from xknx.telegram import IndividualAddress, TelegramDirection
+from xknx.telegram import IndividualAddress, Telegram, TelegramDirection
 
 from .connect import Connect
 from .connectionstate import ConnectionState
@@ -26,6 +27,11 @@ from .interface import Interface
 from .tunnelling import Tunnelling
 from .udp_client import UDPClient
 
+if TYPE_CHECKING:
+    from xknx.xknx import XKNX
+
+TelegramCallbackType = Callable[[Telegram], None]
+
 logger = logging.getLogger("xknx.log")
 
 
@@ -36,13 +42,13 @@ class Tunnel(Interface):
 
     def __init__(
         self,
-        xknx,
-        local_ip,
-        gateway_ip,
-        gateway_port,
-        telegram_received_callback=None,
-        auto_reconnect=False,
-        auto_reconnect_wait=3,
+        xknx: "XKNX",
+        local_ip: str,
+        gateway_ip: str,
+        gateway_port: int,
+        telegram_received_callback: Optional["TelegramCallbackType"] = None,
+        auto_reconnect: bool = False,
+        auto_reconnect_wait: int = 3,
     ):
         """Initialize Tunnel class."""
         # pylint: disable=too-many-arguments
@@ -52,23 +58,23 @@ class Tunnel(Interface):
         self.gateway_port = gateway_port
         self.telegram_received_callback = telegram_received_callback
 
-        self.udp_client = None
+        self.udp_client: UDPClient
         self.init_udp_client()
 
         self._src_address = xknx.own_address
         self.sequence_number = 0
-        self.communication_channel = None
+        self.communication_channel: Optional[int] = None
         self.number_heartbeat_failed = 0
 
         self.auto_reconnect = auto_reconnect
         self.auto_reconnect_wait = auto_reconnect_wait
 
-        self._heartbeat_task = None
-        self._reconnect_task = None
+        self._heartbeat_task: Optional[asyncio.Task[None]] = None
+        self._reconnect_task: Optional[asyncio.Task[None]] = None
 
         self._is_reconnecting = False
 
-    def init_udp_client(self):
+    def init_udp_client(self) -> None:
         """Initialize udp_client."""
         self.udp_client = UDPClient(
             self.xknx, (self.local_ip, 0), (self.gateway_ip, self.gateway_port)
@@ -85,7 +91,7 @@ class Tunnel(Interface):
     #
     ####################
 
-    async def connect(self):
+    async def connect(self) -> bool:
         """Connect to a KNX tunneling interface. Returns True on success."""
         try:
             await self.udp_client.connect()
@@ -106,14 +112,14 @@ class Tunnel(Interface):
             self._tunnel_established()
             return True
 
-    def _tunnel_established(self):
+    def _tunnel_established(self) -> None:
         """Set up interface when the tunnel is ready."""
         self.sequence_number = 0
         self.xknx.connected.set()
         # self._stop_reconnect()
         self.start_heartbeat()
 
-    def _tunnel_lost(self):
+    def _tunnel_lost(self) -> None:
         """Prepare for reconnection or shutdown when the connection is lost. Callback."""
         self.xknx.connected.clear()
         self.stop_heartbeat()
@@ -122,31 +128,26 @@ class Tunnel(Interface):
         else:
             raise CommunicationError("Tunnel connection closed.")
 
-    async def _reconnect(self):
+    async def _reconnect(self) -> None:
         """Reconnect to tunnel device."""
-        # only send disconnect request if we ever were connected
-        if self.communication_channel is not None:
-            await self._disconnect_request(True)
-            self.communication_channel = None
+        await self._disconnect_request(True)
         await self.udp_client.stop()
         await asyncio.sleep(self.auto_reconnect_wait)
         if await self.connect():
             logger.info("Successfully reconnected to KNX bus.")
 
-    def _stop_reconnect(self):
+    def _stop_reconnect(self) -> None:
         """Stop reconnect task if running."""
         if self._reconnect_task is not None:
             self._reconnect_task.cancel()
             self._reconnect_task = None
 
-    async def disconnect(self):
+    async def disconnect(self) -> None:
         """Disconnect tunneling connection."""
         self.xknx.connected.clear()
         self.stop_heartbeat()
         self._stop_reconnect()
-        if self.communication_channel is not None:
-            await self._disconnect_request(False)
-            self.communication_channel = None
+        await self._disconnect_request(False)
         await self.udp_client.stop()
 
     ####################
@@ -156,7 +157,7 @@ class Tunnel(Interface):
     ####################
 
     async def _connect_request(self) -> bool:
-        """Connect to tunnelling server. Return True if succeeded."""
+        """Connect to tunnelling server. Set communication_channel and src_address."""
         connect = Connect(self.xknx, self.udp_client)
         await connect.start()
         if connect.success:
@@ -169,10 +170,14 @@ class Tunnel(Interface):
                 connect.identifier,
             )
             return True
-        raise CommunicationError("ConnectRequest failed")
+        raise CommunicationError(
+            f"ConnectRequest failed. Status code: {connect.response_status_code}"
+        )
 
-    async def _connectionstate_request(self):
+    async def _connectionstate_request(self) -> bool:
         """Return state of tunnel. True if tunnel is in good shape."""
+        if self.communication_channel is None:
+            raise CommunicationError("No active communication channel.")
         conn_state = ConnectionState(
             self.xknx,
             self.udp_client,
@@ -181,22 +186,25 @@ class Tunnel(Interface):
         await conn_state.start()
         return conn_state.success
 
-    async def _disconnect_request(self, ignore_error=False):
-        """Disconnect from tunnel device."""
-        disconnect = Disconnect(
-            self.xknx,
-            self.udp_client,
-            communication_channel_id=self.communication_channel,
-        )
-        await disconnect.start()
-        if not disconnect.success and not ignore_error:
-            raise XKNXException("Could not disconnect channel")
-        logger.debug(
-            "Tunnel disconnected (communication_channel: %s)",
-            self.communication_channel,
-        )
+    async def _disconnect_request(self, ignore_error: bool = False) -> None:
+        """Disconnect from tunnel device. Delete communication_channel."""
+        if self.communication_channel is not None:
+            disconnect = Disconnect(
+                self.xknx,
+                self.udp_client,
+                communication_channel_id=self.communication_channel,
+            )
+            await disconnect.start()
+            if not disconnect.success and not ignore_error:
+                self.communication_channel = None
+                raise XKNXException("Could not disconnect channel")
+            logger.debug(
+                "Tunnel disconnected (communication_channel: %s)",
+                self.communication_channel,
+            )
+        self.communication_channel = None
 
-    async def send_telegram(self, telegram):
+    async def send_telegram(self, telegram: Telegram) -> None:
         """
         Send Telegram to routing tunnelling device - retry mechanism.
 
@@ -228,8 +236,12 @@ class Tunnel(Interface):
                     )
         self._increase_sequence_number()
 
-    async def _tunnelling_request(self, telegram):
+    async def _tunnelling_request(self, telegram: Telegram) -> bool:
         """Send Telegram to tunnelling device."""
+        if self.communication_channel is None:
+            raise CommunicationError(
+                "Sending telegram failed. No active communication channel."
+            )
         tunnelling = Tunnelling(
             self.xknx,
             self.udp_client,
@@ -241,7 +253,7 @@ class Tunnel(Interface):
         await tunnelling.start()
         return tunnelling.success
 
-    def _increase_sequence_number(self):
+    def _increase_sequence_number(self) -> None:
         """Increase sequence number."""
         self.sequence_number += 1
         if self.sequence_number == 256:
@@ -253,19 +265,18 @@ class Tunnel(Interface):
     #
     ####################
 
-    def _request_received(self, knxipframe, _udp_client):
+    def _request_received(self, knxipframe: KNXIPFrame, _udp_client: UDPClient) -> None:
         """Handle incoming requests."""
-        # pylint: disable=unused-argument
-        if knxipframe.header.service_type_ident is KNXIPServiceType.TUNNELLING_REQUEST:
+        if isinstance(knxipframe.body, TunnellingRequest):
             self._tunnelling_request_received(knxipframe.body)
-        elif (
-            knxipframe.header.service_type_ident is KNXIPServiceType.DISCONNECT_REQUEST
-        ):
+        elif isinstance(knxipframe.body, DisconnectRequest):
             self._disconnect_request_received(knxipframe.body)
         else:
             logger.warning("Service not implemented: %s", knxipframe)
 
-    def _tunnelling_request_received(self, tunneling_request: TunnellingRequest):
+    def _tunnelling_request_received(
+        self, tunneling_request: TunnellingRequest
+    ) -> None:
         """Handle incoming tunnel request."""
         self._send_tunnelling_ack(
             tunneling_request.communication_channel_id,
@@ -282,7 +293,9 @@ class Tunnel(Interface):
             if self.telegram_received_callback is not None:
                 self.telegram_received_callback(telegram)
 
-    def _send_tunnelling_ack(self, communication_channel_id, sequence_counter):
+    def _send_tunnelling_ack(
+        self, communication_channel_id: int, sequence_counter: int
+    ) -> None:
         """Send tunnelling ACK after tunnelling request received."""
         ack = TunnellingAck(
             self.xknx,
@@ -291,15 +304,20 @@ class Tunnel(Interface):
         )
         self.udp_client.send(KNXIPFrame.init_from_body(ack))
 
-    def _disconnect_request_received(self, disconnect_request: DisconnectRequest):
+    def _disconnect_request_received(
+        self, disconnect_request: DisconnectRequest
+    ) -> None:
         """Handle incoming disconnect request."""
         logger.warning("Received DisconnectRequest from tunnelling sever.")
-        disconnect_response = DisconnectResponse(
-            self.xknx,
-            communication_channel_id=self.communication_channel,
-        )
-        self.udp_client.send(KNXIPFrame.init_from_body(disconnect_response))
-        self.communication_channel = None
+        # We should not receive DisconnectRequest for other communication_channels
+        # If we do we close our communication_channel before reconnection.
+        if disconnect_request.communication_channel_id == self.communication_channel:
+            disconnect_response = DisconnectResponse(
+                self.xknx,
+                communication_channel_id=self.communication_channel,
+            )
+            self.udp_client.send(KNXIPFrame.init_from_body(disconnect_response))
+            self.communication_channel = None
         self._tunnel_lost()
 
     ####################
@@ -308,29 +326,32 @@ class Tunnel(Interface):
     #
     ####################
 
-    def start_heartbeat(self):
+    def start_heartbeat(self) -> None:
         """Start heartbeat for monitoring state of tunnel, as suggested by 03.08.02 KNX Core 5.4."""
         self._heartbeat_task = asyncio.create_task(self.do_heartbeat())
 
-    def stop_heartbeat(self):
+    def stop_heartbeat(self) -> None:
         """Stop heartbeat task if running."""
         if self._heartbeat_task is not None:
             self._heartbeat_task.cancel()
             self._heartbeat_task = None
 
-    async def do_heartbeat(self):
+    async def do_heartbeat(self) -> None:
         """Heartbeat: Worker task, endless loop for sending heartbeat requests."""
         while True:
-            await asyncio.sleep(HEARTBEAT_RATE)
-            if not await self._connectionstate_request():
-                await self._do_heartbeat_failed()
-
-    async def _do_heartbeat_failed(self):
+            try:
+                await asyncio.sleep(HEARTBEAT_RATE)
+                if not await self._connectionstate_request():
+                    await self._do_heartbeat_failed()
+            except CommunicationError as err:
+                logger.warning("Heartbeat to KNX bus failed. %s", err)
+                self._tunnel_lost()
+
+    async def _do_heartbeat_failed(self) -> None:
         """Heartbeat: handling error."""
         # first heartbeat failed - try 3 more times before disconnecting.
         for _heartbeats_failed in range(3):
             if await self._connectionstate_request():
                 return
         # 3 retries failed
-        logger.warning("Heartbeat to KNX bus failed. Reconnecting.")
-        self._tunnel_lost()
+        raise CommunicationError("No answer from tunneling server.")
diff --git a/xknx/io/tunnelling.py b/xknx/io/tunnelling.py
index e60635dc..8deb18c0 100644
--- a/xknx/io/tunnelling.py
+++ b/xknx/io/tunnelling.py
@@ -1,4 +1,6 @@
 """Abstraction to send a TunnelingRequest and wait for TunnelingResponse."""
+from typing import TYPE_CHECKING
+
 from xknx.knxip import (
     CEMIFrame,
     CEMIMessageCode,
@@ -9,18 +11,24 @@ from xknx.knxip import (
 
 from .request_response import RequestResponse
 
+if TYPE_CHECKING:
+    from xknx.telegram import IndividualAddress, Telegram
+    from xknx.xknx import XKNX
+
+    from .udp_client import UDPClient
+
 
 class Tunnelling(RequestResponse):
     """Class to TunnelingRequest and wait for TunnelingResponse."""
 
     def __init__(
         self,
-        xknx,
-        udp_client,
-        telegram,
-        src_address,
-        sequence_counter,
-        communication_channel_id,
+        xknx: "XKNX",
+        udp_client: "UDPClient",
+        telegram: "Telegram",
+        src_address: "IndividualAddress",
+        sequence_counter: int,
+        communication_channel_id: int,
     ):
         """Initialize Tunnelling class."""
         # pylint: disable=too-many-arguments
diff --git a/xknx/io/udp_client.py b/xknx/io/udp_client.py
index b75898e3..38af437a 100644
--- a/xknx/io/udp_client.py
+++ b/xknx/io/udp_client.py
@@ -8,14 +8,16 @@ import asyncio
 import logging
 import socket
 from sys import platform
-from typing import TYPE_CHECKING, Optional, Tuple
+from typing import TYPE_CHECKING, Callable, List, Optional, Tuple, cast
 
-from xknx.exceptions import CouldNotParseKNXIP, XKNXException
-from xknx.knxip import KNXIPFrame
+from xknx.exceptions import CommunicationError, CouldNotParseKNXIP, XKNXException
+from xknx.knxip import KNXIPFrame, KNXIPServiceType
 
 if TYPE_CHECKING:
     from xknx.xknx import XKNX
 
+    CallbackType = Callable[[KNXIPFrame, "UDPClient"], None]
+
 raw_socket_logger = logging.getLogger("xknx.raw_socket")
 logger = logging.getLogger("xknx.log")
 knx_logger = logging.getLogger("xknx.knx")
@@ -29,40 +31,49 @@ class UDPClient:
     class Callback:
         """Callback class for handling callbacks for different 'KNX service types' of received packets."""
 
-        def __init__(self, callback, service_types=None):
+        def __init__(
+            self,
+            callback: "CallbackType",
+            service_types: Optional[List[KNXIPServiceType]] = None,
+        ):
             """Initialize Callback class."""
             self.callback = callback
             self.service_types = service_types or []
 
-        def has_service(self, service_type):
+        def has_service(self, service_type: KNXIPServiceType) -> bool:
             """Test if callback is listening for given service type."""
             return len(self.service_types) == 0 or service_type in self.service_types
 
     class UDPClientFactory(asyncio.DatagramProtocol):
         """Abstraction for managing the asyncio-udp transports."""
 
-        def __init__(self, own_ip, multicast=False, data_received_callback=None):
+        def __init__(
+            self,
+            own_ip: str,
+            multicast: bool = False,
+            data_received_callback: Optional[Callable[[bytes], None]] = None,
+        ):
             """Initialize UDPClientFactory class."""
             self.own_ip = own_ip
             self.multicast = multicast
-            self.transport = None
+            self.transport: Optional[asyncio.BaseTransport] = None
             self.data_received_callback = data_received_callback
 
-        def connection_made(self, transport):
+        def connection_made(self, transport: asyncio.BaseTransport) -> None:
             """Assign transport. Callback after udp connection was made."""
             self.transport = transport
 
-        def datagram_received(self, data, addr):
+        def datagram_received(self, data: bytes, addr: Tuple[str, int]) -> None:
             """Call assigned callback. Callback for datagram received."""
             raw_socket_logger.debug("Received from %s: %s", addr, data.hex())
             if self.data_received_callback is not None:
                 self.data_received_callback(data)
 
-        def error_received(self, exc):
-            """Handle errors. Callback for error received."""
+        def error_received(self, exc: Exception) -> None:
+            """Call when a send or receive operation raises an OSError."""
             logger.warning("Error received: %s", exc)
 
-        def connection_lost(self, exc):
+        def connection_lost(self, exc: Optional[Exception]) -> None:
             """Log error. Callback for connection lost."""
             logger.debug("Closing transport.")
 
@@ -83,10 +94,11 @@ class UDPClient:
         self.local_addr = local_addr
         self.remote_addr = remote_addr
         self.multicast = multicast
-        self.transport = None
-        self.callbacks = []
+        self.callbacks: List["UDPClient.Callback"] = []
+
+        self.transport: Optional[asyncio.DatagramTransport] = None
 
-    def data_received_callback(self, raw: bytes):
+    def data_received_callback(self, raw: bytes) -> None:
         """Parse and process KNXIP frame. Callback for having received an UDP packet."""
         if raw:
             try:
@@ -114,7 +126,11 @@ class UDPClient:
                 "Unhandled %s: %s", knxipframe.header.service_type_ident, knxipframe
             )
 
-    def register_callback(self, callback, service_types=None) -> Callback:
+    def register_callback(
+        self,
+        callback: "CallbackType",
+        service_types: Optional[List[KNXIPServiceType]] = None,
+    ) -> "UDPClient.Callback":
         """Register callback."""
         if service_types is None:
             service_types = []
@@ -123,12 +139,14 @@ class UDPClient:
         self.callbacks.append(callb)
         return callb
 
-    def unregister_callback(self, callb) -> None:
+    def unregister_callback(self, callb: "UDPClient.Callback") -> None:
         """Unregister callback."""
         self.callbacks.remove(callb)
 
     @staticmethod
-    def create_multicast_sock(own_ip, remote_addr):
+    def create_multicast_sock(
+        own_ip: str, remote_addr: Tuple[str, int]
+    ) -> socket.SocketType:
         """Create UDP multicast socket."""
         sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
         sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
@@ -175,7 +193,8 @@ class UDPClient:
             (transport, _) = await loop.create_datagram_endpoint(
                 lambda: udp_client_factory, sock=sock
             )
-            self.transport = transport
+            # TODO: typing - remove cast - loop.create_datagram_endpoint should return a DatagramTransport
+            self.transport = cast(asyncio.DatagramTransport, transport)
 
         else:
             (transport, _) = await loop.create_datagram_endpoint(
@@ -183,9 +202,10 @@ class UDPClient:
                 local_addr=self.local_addr,
                 remote_addr=self.remote_addr,
             )
-            self.transport = transport
+            # TODO: typing - remove cast - loop.create_datagram_endpoint should return a DatagramTransport
+            self.transport = cast(asyncio.DatagramTransport, transport)
 
-    def send(self, knxipframe) -> None:
+    def send(self, knxipframe: KNXIPFrame) -> None:
         """Send KNXIPFrame to socket."""
         knx_logger.debug("Sending: %s", knxipframe)
         if self.transport is None:
@@ -197,15 +217,22 @@ class UDPClient:
             self.transport.sendto(bytes(knxipframe.to_knx()))
 
     def getsockname(self) -> Tuple[str, int]:
-        """Return sockname."""
-        sock = self.transport.get_extra_info("sockname")
-        return sock
+        """Return socket IP and port."""
+        if self.transport is None:
+            raise CommunicationError(
+                "No transport defined. Socket information not resolveable"
+            )
+        return cast(Tuple[str, int], self.transport.get_extra_info("sockname"))
 
     def getremote(self) -> Optional[str]:
         """Return peername."""
-        peer = self.transport.get_extra_info("peername")
-        return peer
+        return (
+            self.transport.get_extra_info("peername")
+            if self.transport is not None
+            else None
+        )
 
     async def stop(self) -> None:
         """Stop UDP socket."""
-        self.transport.close()
+        if self.transport is not None:
+            self.transport.close()
diff --git a/xknx/knxip/__init__.py b/xknx/knxip/__init__.py
index 009b0a49..5e3e84a8 100644
--- a/xknx/knxip/__init__.py
+++ b/xknx/knxip/__init__.py
@@ -1,6 +1,6 @@
 """This package contains all methods for serialization and deserialization of KNX/IP packets."""
 # flake8: noqa
-from .body import KNXIPBody
+from .body import KNXIPBody, KNXIPBodyResponse
 from .cemi_frame import CEMIFrame
 from .connect_request import ConnectRequest
 from .connect_response import ConnectResponse
@@ -30,6 +30,7 @@ from .tunnelling_request import TunnellingRequest
 
 __all__ = [
     "KNXIPBody",
+    "KNXIPBodyResponse",
     "CEMIFrame",
     "ConnectRequest",
     "ConnectResponse",
diff --git a/xknx/knxip/body.py b/xknx/knxip/body.py
index aa88bb0b..542c501c 100644
--- a/xknx/knxip/body.py
+++ b/xknx/knxip/body.py
@@ -1,35 +1,44 @@
 """Basis class for all KNX/IP bodies."""
+from abc import ABC, abstractmethod
 import logging
+from typing import TYPE_CHECKING, ClassVar, List, cast
 
+from .error_code import ErrorCode
 from .knxip_enum import KNXIPServiceType
 
+if TYPE_CHECKING:
+    from xknx.xknx import XKNX
+
 logger = logging.getLogger("xknx.log")
 
 
-class KNXIPBody:
-    """Basis class for all KNX/IP bodies."""
+class KNXIPBody(ABC):
+    """Base class for all KNX/IP bodies."""
 
-    service_type: KNXIPServiceType = None
+    service_type: ClassVar[KNXIPServiceType] = cast(KNXIPServiceType, None)
 
-    def __init__(self, xknx):
+    def __init__(self, xknx: "XKNX"):
         """Initialize KNXIPBody object."""
         self.xknx = xknx
 
-    def calculated_length(self):
+    @abstractmethod
+    def calculated_length(self) -> int:
         """Get length of KNX/IP body."""
-        logger.warning(
-            "'calculated_length()' not implemented for %s", self.__class__.__name__
-        )
 
-    def from_knx(self, raw):
+    @abstractmethod
+    def from_knx(self, raw: bytes) -> int:
         """Parse/deserialize from KNX/IP raw data."""
-        # pylint: disable=unused-argument
-        logger.warning("'from_knx()' not implemented for %s", self.__class__.__name__)
 
-    def to_knx(self):
+    @abstractmethod
+    def to_knx(self) -> List[int]:
         """Serialize to KNX/IP raw data."""
-        logger.warning("'to_knx()' not implemented for %s", self.__class__.__name__)
 
-    def __eq__(self, other):
+    def __eq__(self, other: object) -> bool:
         """Equal operator."""
         return self.__dict__ == other.__dict__
+
+
+class KNXIPBodyResponse(KNXIPBody):
+    """Base class for all KNX/IP response bodies."""
+
+    status_code: ErrorCode = cast(ErrorCode, None)
diff --git a/xknx/knxip/cemi_frame.py b/xknx/knxip/cemi_frame.py
index f3c82039..068c41be 100644
--- a/xknx/knxip/cemi_frame.py
+++ b/xknx/knxip/cemi_frame.py
@@ -11,7 +11,7 @@ Documentation within:
     KNX IP Communication Medium
     File: AN117 v02.01 KNX IP Communication Medium DV.pdf
 """
-from typing import Union
+from typing import TYPE_CHECKING, List, Optional, Union
 
 from xknx.exceptions import ConversionError, CouldNotParseKNXIP, UnsupportedCEMIMessage
 from xknx.telegram import GroupAddress, IndividualAddress, Telegram
@@ -19,6 +19,9 @@ from xknx.telegram.apci import APCI
 
 from .knxip_enum import CEMIFlags, CEMIMessageCode
 
+if TYPE_CHECKING:
+    from xknx.xknx import XKNX
+
 
 class CEMIFrame:
     """Representation of a CEMI Frame."""
@@ -27,13 +30,13 @@ class CEMIFrame:
 
     def __init__(
         self,
-        xknx,
+        xknx: "XKNX",
         code: CEMIMessageCode = CEMIMessageCode.L_DATA_IND,
         flags: int = 0,
         src_addr: IndividualAddress = IndividualAddress(None),
         dst_addr: Union[GroupAddress, IndividualAddress] = GroupAddress(None),
         mpdu_len: int = 0,
-        payload=None,
+        payload: Optional[APCI] = None,
     ):
         """Initialize CEMIFrame object."""
         self.xknx = xknx
@@ -46,11 +49,11 @@ class CEMIFrame:
 
     @staticmethod
     def init_from_telegram(
-        xknx,
+        xknx: "XKNX",
         telegram: Telegram,
         code: CEMIMessageCode = CEMIMessageCode.L_DATA_IND,
         src_addr: IndividualAddress = IndividualAddress(None),
-    ):
+    ) -> "CEMIFrame":
         """Return CEMIFrame from a Telegram."""
         cemi = CEMIFrame(xknx, code=code, src_addr=src_addr)
         # dst_addr, payload and cmd are set by telegram.setter - mpdu_len not needed for outgoing telegram
@@ -68,7 +71,7 @@ class CEMIFrame:
         )
 
     @telegram.setter
-    def telegram(self, telegram: Telegram):
+    def telegram(self, telegram: Telegram) -> None:
         """Set telegram."""
         self.dst_addr = telegram.destination_address
         self.payload = telegram.payload
@@ -92,7 +95,7 @@ class CEMIFrame:
         else:
             raise TypeError()
 
-    def set_hops(self, hops: int):
+    def set_hops(self, hops: int) -> None:
         """Set hops."""
         # Resetting hops
         self.flags &= 0xFFFF ^ 0x0070
@@ -105,7 +108,7 @@ class CEMIFrame:
             raise TypeError()
         return 10 + self.payload.calculated_length()
 
-    def from_knx(self, raw) -> int:
+    def from_knx(self, raw: bytes) -> int:
         """Parse/deserialize from KNX/IP raw data."""
         try:
             self.code = CEMIMessageCode(raw[0])
@@ -129,7 +132,7 @@ class CEMIFrame:
 
         return self.from_knx_data_link_layer(raw)
 
-    def from_knx_data_link_layer(self, cemi) -> int:
+    def from_knx_data_link_layer(self, cemi: bytes) -> int:
         """Parse L_DATA_IND, CEMIMessageCode.L_Data_REQ, CEMIMessageCode.L_DATA_CON."""
         if len(cemi) < 11:
             # eg. ETS Line-Scan issues L_DATA_IND with length 10
@@ -179,7 +182,7 @@ class CEMIFrame:
 
         return 10 + addil + self.mpdu_len
 
-    def to_knx(self):
+    def to_knx(self) -> List[int]:
         """Serialize to KNX/IP raw data."""
         if not isinstance(self.payload, APCI):
             raise TypeError()
@@ -201,7 +204,7 @@ class CEMIFrame:
 
         return data
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return (
             '<CEMIFrame SourceAddress="{}" DestinationAddress="{}" '
@@ -213,6 +216,6 @@ class CEMIFrame:
             )
         )
 
-    def __eq__(self, other):
+    def __eq__(self, other: object) -> bool:
         """Equal operator."""
         return self.__dict__ == other.__dict__
diff --git a/xknx/knxip/connect_request.py b/xknx/knxip/connect_request.py
index 40fced21..090139a2 100644
--- a/xknx/knxip/connect_request.py
+++ b/xknx/knxip/connect_request.py
@@ -3,12 +3,17 @@ Module for Serialization and Deserialization of a KNX Connect Request informatio
 
 Connect requests are used to start a new tunnel connection on a KNX/IP device.
 """
+from typing import TYPE_CHECKING, List
+
 from xknx.exceptions import CouldNotParseKNXIP
 
 from .body import KNXIPBody
 from .hpai import HPAI
 from .knxip_enum import ConnectRequestType, KNXIPServiceType
 
+if TYPE_CHECKING:
+    from xknx.xknx import XKNX
+
 
 class ConnectRequest(KNXIPBody):
     """Representation of a KNX Connect Request."""
@@ -21,8 +26,8 @@ class ConnectRequest(KNXIPBody):
 
     def __init__(
         self,
-        xknx,
-        request_type: ConnectRequestType = None,
+        xknx: "XKNX",
+        request_type: ConnectRequestType = ConnectRequestType.TUNNEL_CONNECTION,
         control_endpoint: HPAI = HPAI(),
         data_endpoint: HPAI = HPAI(),
     ):
@@ -34,14 +39,14 @@ class ConnectRequest(KNXIPBody):
         # KNX layer, 0x02 = TUNNEL_LINKLAYER
         self.flags = 0x02
 
-    def calculated_length(self):
+    def calculated_length(self) -> int:
         """Get length of KNX/IP body."""
         return HPAI.LENGTH + HPAI.LENGTH + ConnectRequest.CRI_LENGTH
 
-    def from_knx(self, raw):
+    def from_knx(self, raw: bytes) -> int:
         """Parse/deserialize from KNX/IP raw data."""
 
-        def cri_from_knx(cri):
+        def cri_from_knx(cri: bytes) -> int:
             """Parse CRI (Connection Request Information)."""
             if cri[0] != ConnectRequest.CRI_LENGTH:
                 raise CouldNotParseKNXIP("CRI has wrong length")
@@ -56,10 +61,10 @@ class ConnectRequest(KNXIPBody):
         pos += cri_from_knx(raw[pos:])
         return pos
 
-    def to_knx(self):
+    def to_knx(self) -> List[int]:
         """Serialize to KNX/IP raw data."""
 
-        def cri_to_knx():
+        def cri_to_knx() -> List[int]:
             """Serialize CRI (Connect Request Information)."""
             cri = []
             cri.append(ConnectRequest.CRI_LENGTH)
@@ -74,7 +79,7 @@ class ConnectRequest(KNXIPBody):
         data.extend(cri_to_knx())
         return data
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return (
             '<ConnectRequest control_endpoint="{}" data_endpoint="{}" '
diff --git a/xknx/knxip/connect_response.py b/xknx/knxip/connect_response.py
index 7eb4aa39..7190cc39 100644
--- a/xknx/knxip/connect_response.py
+++ b/xknx/knxip/connect_response.py
@@ -5,15 +5,20 @@ Connect requests are used to start a new tunnel connection on a KNX/IP device.
 With a Connect Response the receiving party acknowledges the valid processing of the request,
 assigns a communication channel and an individual address for the client.
 """
+from typing import TYPE_CHECKING, List, Optional
+
 from xknx.exceptions import CouldNotParseKNXIP
 
-from .body import KNXIPBody
+from .body import KNXIPBodyResponse
 from .error_code import ErrorCode
 from .hpai import HPAI
 from .knxip_enum import ConnectRequestType, KNXIPServiceType
 
+if TYPE_CHECKING:
+    from xknx.xknx import XKNX
+
 
-class ConnectResponse(KNXIPBody):
+class ConnectResponse(KNXIPBodyResponse):
     """Representation of a KNX Connect Response."""
 
     # pylint: disable=too-many-instance-attributes
@@ -24,12 +29,12 @@ class ConnectResponse(KNXIPBody):
 
     def __init__(
         self,
-        xknx,
+        xknx: "XKNX",
         communication_channel: int = 0,
         status_code: ErrorCode = ErrorCode.E_NO_ERROR,
-        request_type: ConnectRequestType = None,
+        request_type: ConnectRequestType = ConnectRequestType.TUNNEL_CONNECTION,
         control_endpoint: HPAI = HPAI(),
-        identifier: int = None,
+        identifier: Optional[int] = None,
     ):
         """Initialize ConnectResponse class."""
         super().__init__(xknx)
@@ -37,18 +42,18 @@ class ConnectResponse(KNXIPBody):
         self.communication_channel = communication_channel
         self.status_code = status_code
         self.request_type = request_type
-        self.control_endpoint = HPAI()
+        self.control_endpoint = control_endpoint
         # identifier shall contain KNX Individual Address assigned to this KNXnet/IP Tunnelling connection
         self.identifier = identifier
 
-    def calculated_length(self):
+    def calculated_length(self) -> int:
         """Get length of KNX/IP body."""
         return 2 + HPAI.LENGTH + ConnectResponse.CRD_LENGTH
 
-    def from_knx(self, raw):
+    def from_knx(self, raw: bytes) -> int:
         """Parse/deserialize from KNX/IP raw data."""
 
-        def crd_from_knx(crd):
+        def crd_from_knx(crd: bytes) -> int:
             """Parse CRD (Connection Response Data Block)."""
             if crd[0] != ConnectResponse.CRD_LENGTH:
                 raise CouldNotParseKNXIP("CRD has wrong length")
@@ -70,11 +75,13 @@ class ConnectResponse(KNXIPBody):
             pos += len(raw[pos:])
         return pos
 
-    def to_knx(self):
+    def to_knx(self) -> List[int]:
         """Serialize to KNX/IP raw data."""
 
-        def crd_to_knx():
+        def crd_to_knx() -> List[int]:
             """Serialize CRD (Connect Response Data Block)."""
+            assert self.identifier is not None
+
             crd = []
             crd.append(ConnectResponse.CRD_LENGTH)
             crd.append(self.request_type.value)
@@ -90,7 +97,7 @@ class ConnectResponse(KNXIPBody):
 
         return data
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return (
             '<ConnectResponse communication_channel="{}" '
diff --git a/xknx/knxip/connectionstate_request.py b/xknx/knxip/connectionstate_request.py
index 66d0b585..c59ffe4c 100644
--- a/xknx/knxip/connectionstate_request.py
+++ b/xknx/knxip/connectionstate_request.py
@@ -3,12 +3,17 @@ Module for Serialization and Deserialization of a KNX Connetionstate Request inf
 
 Connectionstate requests are used to determine if a tunnel connection is still active and valid.
 """
+from typing import TYPE_CHECKING, List
+
 from xknx.exceptions import CouldNotParseKNXIP
 
 from .body import KNXIPBody
 from .hpai import HPAI
 from .knxip_enum import KNXIPServiceType
 
+if TYPE_CHECKING:
+    from xknx.xknx import XKNX
+
 
 class ConnectionStateRequest(KNXIPBody):
     """Representation of a KNX Connection State Request."""
@@ -18,21 +23,24 @@ class ConnectionStateRequest(KNXIPBody):
     service_type = KNXIPServiceType.CONNECTIONSTATE_REQUEST
 
     def __init__(
-        self, xknx, communication_channel_id: int = 1, control_endpoint: HPAI = HPAI()
+        self,
+        xknx: "XKNX",
+        communication_channel_id: int = 1,
+        control_endpoint: HPAI = HPAI(),
     ):
         """Initialize ConnectionStateRequest object."""
         super().__init__(xknx)
         self.communication_channel_id = communication_channel_id
         self.control_endpoint = control_endpoint
 
-    def calculated_length(self):
+    def calculated_length(self) -> int:
         """Get length of KNX/IP body."""
         return 2 + HPAI.LENGTH
 
-    def from_knx(self, raw):
+    def from_knx(self, raw: bytes) -> int:
         """Parse/deserialize from KNX/IP raw data."""
 
-        def info_from_knx(info):
+        def info_from_knx(info: bytes) -> int:
             """Parse info bytes."""
             if len(info) < 2:
                 raise CouldNotParseKNXIP("Info has wrong length")
@@ -44,10 +52,10 @@ class ConnectionStateRequest(KNXIPBody):
         pos += self.control_endpoint.from_knx(raw[pos:])
         return pos
 
-    def to_knx(self):
+    def to_knx(self) -> List[int]:
         """Serialize to KNX/IP raw data."""
 
-        def info_to_knx():
+        def info_to_knx() -> List[int]:
             """Serialize information bytes."""
             info = []
             info.append(self.communication_channel_id)
@@ -59,7 +67,7 @@ class ConnectionStateRequest(KNXIPBody):
         data.extend(self.control_endpoint.to_knx())
         return data
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return (
             '<ConnectionStateRequest CommunicationChannelID="{}", '
diff --git a/xknx/knxip/connectionstate_response.py b/xknx/knxip/connectionstate_response.py
index df738da8..cca623a3 100644
--- a/xknx/knxip/connectionstate_response.py
+++ b/xknx/knxip/connectionstate_response.py
@@ -4,14 +4,19 @@ Module for Serialization and Deserialization of a KNX Connectionstate Response i
 Connectionstate requests are used to determine if a tunnel connection is still active and valid.
 With a connectionstate response the receiving party acknowledges the valid processing of the request.
 """
+from typing import TYPE_CHECKING, List
+
 from xknx.exceptions import CouldNotParseKNXIP
 
-from .body import KNXIPBody
+from .body import KNXIPBodyResponse
 from .error_code import ErrorCode
 from .knxip_enum import KNXIPServiceType
 
+if TYPE_CHECKING:
+    from xknx.xknx import XKNX
+
 
-class ConnectionStateResponse(KNXIPBody):
+class ConnectionStateResponse(KNXIPBodyResponse):
     """Representation of a KNX Connection State Response."""
 
     # pylint: disable=too-many-instance-attributes
@@ -20,7 +25,7 @@ class ConnectionStateResponse(KNXIPBody):
 
     def __init__(
         self,
-        xknx,
+        xknx: "XKNX",
         communication_channel_id: int = 1,
         status_code: ErrorCode = ErrorCode.E_NO_ERROR,
     ):
@@ -29,14 +34,14 @@ class ConnectionStateResponse(KNXIPBody):
         self.communication_channel_id = communication_channel_id
         self.status_code = status_code
 
-    def calculated_length(self):
+    def calculated_length(self) -> int:
         """Get length of KNX/IP body."""
         return 2
 
-    def from_knx(self, raw):
+    def from_knx(self, raw: bytes) -> int:
         """Parse/deserialize from KNX/IP raw data."""
 
-        def info_from_knx(info):
+        def info_from_knx(info: bytes) -> int:
             """Parse info bytes."""
             if len(info) < 2:
                 raise CouldNotParseKNXIP("info has wrong length")
@@ -47,10 +52,10 @@ class ConnectionStateResponse(KNXIPBody):
         pos = info_from_knx(raw)
         return pos
 
-    def to_knx(self):
+    def to_knx(self) -> List[int]:
         """Serialize to KNX/IP raw data."""
 
-        def info_to_knx():
+        def info_to_knx() -> List[int]:
             """Serialize information bytes."""
             info = []
             info.append(self.communication_channel_id)
@@ -61,7 +66,7 @@ class ConnectionStateResponse(KNXIPBody):
         data.extend(info_to_knx())
         return data
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return (
             '<ConnectionStateResponse CommunicationChannelID="{}" '
diff --git a/xknx/knxip/dib.py b/xknx/knxip/dib.py
index bb68b23e..48d4d346 100644
--- a/xknx/knxip/dib.py
+++ b/xknx/knxip/dib.py
@@ -10,6 +10,8 @@ A KNX/IP Search Response may contain several DIBs of different types:
 * DIBGeneric:           General Information
                         (fallback for unknown dib type codes)
 """
+from abc import ABC, abstractmethod
+from typing import Generator, List, Union
 
 from xknx.exceptions import CouldNotParseKNXIP
 from xknx.telegram import IndividualAddress
@@ -17,7 +19,7 @@ from xknx.telegram import IndividualAddress
 from .knxip_enum import DIBServiceFamily, DIBTypeCode, KNXMedium
 
 
-class DIB:
+class DIB(ABC):
     """
     Base class for DIB (Description Information Block).
 
@@ -25,20 +27,24 @@ class DIB:
     classes.
     """
 
-    def __init__(self):
+    @abstractmethod
+    def __init__(self) -> None:
         """Initialize DIB class."""
 
-    def calculated_length(self):
+    @abstractmethod
+    def calculated_length(self) -> int:
         """Get length of KNX/IP object."""
 
-    def from_knx(self, raw):
+    @abstractmethod
+    def from_knx(self, raw: bytes) -> int:
         """Parse/deserialize from KNX/IP raw data."""
 
-    def to_knx(self):
+    @abstractmethod
+    def to_knx(self) -> List[int]:
         """Serialize to KNX/IP raw data."""
 
     @staticmethod
-    def determine_dib(raw):
+    def determine_dib(raw: bytes) -> "DIB":
         """Determine dib type out of dib type code."""
         if len(raw) < 2:
             raise CouldNotParseKNXIP("could not parse DIB header")
@@ -58,19 +64,19 @@ class DIBGeneric(DIB):
     Fallback for not implemented DIBTypeCodes.
     """
 
-    def __init__(self):
+    def __init__(self) -> None:
         """Initialize DIBGeneric class."""
         super().__init__()
         # DTC Description Type Code
-        self.dtc = None
+        self.dtc: Union[DIBTypeCode, int] = 0
         # IBD Information Block Data
-        self.data = []
+        self.data: Union[List[int], bytes] = []
 
-    def calculated_length(self):
+    def calculated_length(self) -> int:
         """Get length of KNX/IP object."""
         return len(self.data)
 
-    def from_knx(self, raw):
+    def from_knx(self, raw: bytes) -> int:
         """Parse/deserialize from KNX/IP raw data."""
         if len(raw) < 2:
             raise CouldNotParseKNXIP("could not parse DIB header")
@@ -78,21 +84,28 @@ class DIBGeneric(DIB):
         dib_length = raw[0]
         if len(raw) < dib_length:
             raise CouldNotParseKNXIP("DIB wrong length")
-
-        self.dtc = DIBTypeCode(raw[1])
+        try:
+            self.dtc = DIBTypeCode(raw[1])
+        except ValueError:
+            self.dtc = raw[1]
         self.data = raw[:dib_length]
 
         return dib_length
 
-    def to_knx(self):
+    def to_knx(self) -> List[int]:
         """Serialize to KNX/IP raw data."""
+        if not isinstance(self.dtc, DIBTypeCode):
+            try:
+                self.dtc = DIBTypeCode(self.dtc)
+            except ValueError:
+                raise CouldNotParseKNXIP("DTC invalid")
         data = []
         data.append(len(self.data))
         data.append(self.dtc.value)
         data.extend(self.data[2:])
         return data
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return '<DIB dtc="{}" data="{}" />'.format(
             self.dtc, ", ".join("0x%02x" % i for i in self.data)
@@ -107,36 +120,36 @@ class DIBSuppSVCFamilies(DIB):
     class Family:
         """Class for storing a supported device family."""
 
-        def __init__(self, name=None, version=None):
+        def __init__(self, name: DIBServiceFamily, version: int):
             """Initialize DIBSuppSVCFamilies.Family."""
             self.name = name
             self.version = version
 
-        def __str__(self):
+        def __str__(self) -> str:
             """Return object as readable string."""
             return f'<Family name="{self.name}" version="{self.version}" />'
 
-        def __eq__(self, other):
+        def __eq__(self, other: object) -> bool:
             """Equal operator."""
             return self.__dict__ == other.__dict__
 
-    def __init__(self):
+    def __init__(self) -> None:
         """Initialize DIBSuppSVCFamilies class."""
         super().__init__()
-        self.families = []
+        self.families: List[DIBSuppSVCFamilies.Family] = []
 
-    def supports(self, name):
+    def supports(self, name: DIBServiceFamily) -> bool:
         """Return if device supports a given service family by name."""
         for family in self.families:
             if name == family.name:
                 return True
         return False
 
-    def calculated_length(self):
+    def calculated_length(self) -> int:
         """Get length of KNX/IP object."""
         return len(self.families) * 2 + 2
 
-    def from_knx(self, raw):
+    def from_knx(self, raw: bytes) -> int:
         """Parse/deserialize from KNX/IP raw data."""
         if len(raw) < 2:
             raise CouldNotParseKNXIP("DIB header too small")
@@ -152,7 +165,7 @@ class DIBSuppSVCFamilies(DIB):
             self.families.append(DIBSuppSVCFamilies.Family(name, version))
         return length
 
-    def to_knx(self):
+    def to_knx(self) -> List[int]:
         """Serialize to KNX/IP raw data."""
         data = []
         data.append(len(self.families) * 2 + 2)
@@ -162,7 +175,7 @@ class DIBSuppSVCFamilies(DIB):
             data.append(family.version)
         return data
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return '<DIBSuppSVCFamilies families="[{}]" />'.format(
             ", ".join(
@@ -178,24 +191,24 @@ class DIBDeviceInformation(DIB):
 
     LENGTH = 54
 
-    def __init__(self):
+    def __init__(self) -> None:
         """Initialize DIBDeviceInformation class."""
         super().__init__()
-        self.knx_medium = KNXMedium.TP1
-        self.programming_mode = False
-        self.individual_address = IndividualAddress(None)
-        self.installation_number = 0
-        self.project_number = 0
-        self.serial_number = ""
-        self.multicast_address = "224.0.23.12"
-        self.mac_address = ""
-        self.name = ""
-
-    def calculated_length(self):
+        self.knx_medium: KNXMedium = KNXMedium.TP1
+        self.programming_mode: bool = False
+        self.individual_address: IndividualAddress = IndividualAddress(None)
+        self.installation_number: int = 0
+        self.project_number: int = 0
+        self.serial_number: str = ""
+        self.multicast_address: str = "224.0.23.12"
+        self.mac_address: str = ""
+        self.name: str = ""
+
+    def calculated_length(self) -> int:
         """Get length of KNX/IP object."""
         return DIBDeviceInformation.LENGTH
 
-    def from_knx(self, raw):
+    def from_knx(self, raw: bytes) -> int:
         """Parse/deserialize from KNX/IP raw data."""
         if len(raw) < DIBDeviceInformation.LENGTH:
             raise CouldNotParseKNXIP("wrong connection header length")
@@ -217,20 +230,20 @@ class DIBDeviceInformation(DIB):
         self.name = "".join(map(chr, raw[24:54])).rstrip("\0")
         return DIBDeviceInformation.LENGTH
 
-    def to_knx(self):
+    def to_knx(self) -> List[int]:
         """Serialize to KNX/IP raw data."""
 
-        def hex_notation_to_knx(serial_number):
+        def hex_notation_to_knx(serial_number: str) -> Generator[int, None, None]:
             """Serialize hex notation."""
             for part in serial_number.split(":"):
                 yield int(part, 16)
 
-        def ip_to_knx(ip_addr):
+        def ip_to_knx(ip_addr: str) -> Generator[int, None, None]:
             """Serialize ip."""
             for part in ip_addr.split("."):
                 yield int(part)
 
-        def str_to_knx(string, length):
+        def str_to_knx(string: str, length: int) -> Generator[int, None, None]:
             """Serialize string."""
             if len(string) > length - 1:
                 string = string[: length - 1]
@@ -256,7 +269,7 @@ class DIBDeviceInformation(DIB):
         data.extend(str_to_knx(self.name, 30))
         return data
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return (
             "<DIBDeviceInformation "
diff --git a/xknx/knxip/disconnect_request.py b/xknx/knxip/disconnect_request.py
index 2f38796b..a73e7fec 100644
--- a/xknx/knxip/disconnect_request.py
+++ b/xknx/knxip/disconnect_request.py
@@ -3,12 +3,17 @@ Module for Serialization and Deserialization of a KNX Disconnect Request informa
 
 Disconnect requests are used to disconnect a tunnel from a KNX/IP device.
 """
+from typing import TYPE_CHECKING, List
+
 from xknx.exceptions import CouldNotParseKNXIP
 
 from .body import KNXIPBody
 from .hpai import HPAI
 from .knxip_enum import KNXIPServiceType
 
+if TYPE_CHECKING:
+    from xknx.xknx import XKNX
+
 
 class DisconnectRequest(KNXIPBody):
     """Representation of a KNX Disconnect Request."""
@@ -18,7 +23,10 @@ class DisconnectRequest(KNXIPBody):
     service_type = KNXIPServiceType.DISCONNECT_REQUEST
 
     def __init__(
-        self, xknx, communication_channel_id: int = 1, control_endpoint: HPAI = HPAI()
+        self,
+        xknx: "XKNX",
+        communication_channel_id: int = 1,
+        control_endpoint: HPAI = HPAI(),
     ):
         """Initialize DisconnectRequest object."""
         super().__init__(xknx)
@@ -26,14 +34,14 @@ class DisconnectRequest(KNXIPBody):
         self.communication_channel_id = communication_channel_id
         self.control_endpoint = control_endpoint
 
-    def calculated_length(self):
+    def calculated_length(self) -> int:
         """Get length of KNX/IP body."""
         return 2 + HPAI.LENGTH
 
-    def from_knx(self, raw):
+    def from_knx(self, raw: bytes) -> int:
         """Parse/deserialize from KNX/IP raw data."""
 
-        def info_from_knx(info):
+        def info_from_knx(info: bytes) -> int:
             """Parse info bytes."""
             if len(info) < 2:
                 raise CouldNotParseKNXIP("Disconnect info has wrong length")
@@ -45,10 +53,10 @@ class DisconnectRequest(KNXIPBody):
         pos += self.control_endpoint.from_knx(raw[pos:])
         return pos
 
-    def to_knx(self):
+    def to_knx(self) -> List[int]:
         """Serialize to KNX/IP raw data."""
 
-        def info_to_knx():
+        def info_to_knx() -> List[int]:
             """Serialize information bytes."""
             info = []
             info.append(self.communication_channel_id)
@@ -60,7 +68,7 @@ class DisconnectRequest(KNXIPBody):
         data.extend(self.control_endpoint.to_knx())
         return data
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return (
             '<DisconnectRequest CommunicationChannelID="{}" '
diff --git a/xknx/knxip/disconnect_response.py b/xknx/knxip/disconnect_response.py
index 955355c2..730fbb85 100644
--- a/xknx/knxip/disconnect_response.py
+++ b/xknx/knxip/disconnect_response.py
@@ -4,14 +4,19 @@ Module for Serialization and Deserialization of a KNX Disconnect Response inform
 Disconnect requests are used to disconnect a tunnel from a KNX/IP device.
 With a Disconnect Response the receiving party acknowledges the valid processing of the request.
 """
+from typing import TYPE_CHECKING, List
+
 from xknx.exceptions import CouldNotParseKNXIP
 
-from .body import KNXIPBody
+from .body import KNXIPBodyResponse
 from .error_code import ErrorCode
 from .knxip_enum import KNXIPServiceType
 
+if TYPE_CHECKING:
+    from xknx.xknx import XKNX
+
 
-class DisconnectResponse(KNXIPBody):
+class DisconnectResponse(KNXIPBodyResponse):
     """Representation of a KNX Disconnect Response."""
 
     # pylint: disable=too-many-instance-attributes
@@ -20,7 +25,7 @@ class DisconnectResponse(KNXIPBody):
 
     def __init__(
         self,
-        xknx,
+        xknx: "XKNX",
         communication_channel_id: int = 1,
         status_code: ErrorCode = ErrorCode.E_NO_ERROR,
     ):
@@ -30,14 +35,14 @@ class DisconnectResponse(KNXIPBody):
         self.communication_channel_id = communication_channel_id
         self.status_code = status_code
 
-    def calculated_length(self):
+    def calculated_length(self) -> int:
         """Get length of KNX/IP body."""
         return 2
 
-    def from_knx(self, raw):
+    def from_knx(self, raw: bytes) -> int:
         """Parse/deserialize from KNX/IP raw data."""
 
-        def info_from_knx(info):
+        def info_from_knx(info: bytes) -> int:
             """Parse info bytes."""
             if len(info) < 2:
                 raise CouldNotParseKNXIP("Disconnect info has wrong length")
@@ -48,10 +53,10 @@ class DisconnectResponse(KNXIPBody):
         pos = info_from_knx(raw)
         return pos
 
-    def to_knx(self):
+    def to_knx(self) -> List[int]:
         """Serialize to KNX/IP raw data."""
 
-        def info_to_knx():
+        def info_to_knx() -> List[int]:
             """Serialize information bytes."""
             info = []
             info.append(self.communication_channel_id)
@@ -62,7 +67,7 @@ class DisconnectResponse(KNXIPBody):
         data.extend(info_to_knx())
         return data
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return (
             '<DisconnectResponse CommunicationChannelID="{}" '
diff --git a/xknx/knxip/header.py b/xknx/knxip/header.py
index 746b75c2..68fa424e 100644
--- a/xknx/knxip/header.py
+++ b/xknx/knxip/header.py
@@ -1,4 +1,6 @@
 """Module for serialization and deserialization of KNX/IP Header."""
+from typing import ClassVar, List
+
 from xknx.exceptions import CouldNotParseKNXIP
 
 from .body import KNXIPBody
@@ -8,19 +10,18 @@ from .knxip_enum import KNXIPServiceType
 class KNXIPHeader:
     """Class for serialization and deserialization of KNX/IP Header."""
 
-    HEADERLENGTH = 0x06
-    PROTOCOLVERSION = 0x10
+    HEADERLENGTH: ClassVar[int] = 0x06
+    PROTOCOLVERSION: ClassVar[int] = 0x10
 
-    def __init__(self, xknx):
+    def __init__(self) -> None:
         """Initialize KNXIPHeader class."""
-        self.xknx = xknx
         self.header_length = KNXIPHeader.HEADERLENGTH
         self.protocol_version = KNXIPHeader.PROTOCOLVERSION
         self.service_type_ident = KNXIPServiceType.ROUTING_INDICATION
         self.b4_reserve = 0
         self.total_length = 0  # to be set later
 
-    def from_knx(self, data):
+    def from_knx(self, data: bytes) -> int:
         """Parse/deserialize from KNX/IP raw data."""
         if len(data) < KNXIPHeader.HEADERLENGTH:
             raise CouldNotParseKNXIP("wrong connection header length")
@@ -41,13 +42,13 @@ class KNXIPHeader:
         self.total_length = data[5]
         return KNXIPHeader.HEADERLENGTH
 
-    def set_length(self, body):
+    def set_length(self, body: KNXIPBody) -> None:
         """Set length of full KNX/IP packet from body + fixed header length."""
         if not isinstance(body, KNXIPBody):
             raise TypeError()
         self.total_length = KNXIPHeader.HEADERLENGTH + body.calculated_length()
 
-    def to_knx(self):
+    def to_knx(self) -> List[int]:
         """Serialize to KNX/IP raw data."""
         data = []
         data.append(self.header_length)
@@ -58,7 +59,7 @@ class KNXIPHeader:
         data.append(self.total_length & 255)
         return data
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return (
             '<KNXIPHeader HeaderLength="{}" ProtocolVersion="{}" '
@@ -71,6 +72,6 @@ class KNXIPHeader:
             )
         )
 
-    def __eq__(self, other):
+    def __eq__(self, other: object) -> bool:
         """Equal operator."""
         return self.__dict__ == other.__dict__
diff --git a/xknx/knxip/hpai.py b/xknx/knxip/hpai.py
index 8d88ed30..3367603e 100644
--- a/xknx/knxip/hpai.py
+++ b/xknx/knxip/hpai.py
@@ -3,6 +3,8 @@ Module for serialization and deserialization of KNX HPAI (Host Protocol Address
 
 A HPAI contains an IP address and a port.
 """
+from typing import Generator, List
+
 from xknx.exceptions import ConversionError, CouldNotParseKNXIP
 
 
@@ -17,7 +19,7 @@ class HPAI:
         self.ip_addr = ip_addr
         self.port = port
 
-    def from_knx(self, raw):
+    def from_knx(self, raw: bytes) -> int:
         """Parse/deserialize from KNX/IP raw data."""
         if len(raw) < HPAI.LENGTH:
             raise CouldNotParseKNXIP("wrong HPAI length")
@@ -29,10 +31,10 @@ class HPAI:
         self.port = raw[6] * 256 + raw[7]
         return HPAI.LENGTH
 
-    def to_knx(self):
+    def to_knx(self) -> List[int]:
         """Serialize to KNX/IP raw data."""
 
-        def ip_addr_to_bytes(ip_addr):
+        def ip_addr_to_bytes(ip_addr: str) -> Generator[int, None, None]:
             """Serialize ip address to byte array."""
             if not isinstance(ip_addr, str):
                 raise ConversionError("ip_addr is not a string")
@@ -47,10 +49,10 @@ class HPAI:
         data.append(self.port & 255)
         return data
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return f"<HPAI {self.ip_addr}:{self.port} />"
 
-    def __eq__(self, other):
+    def __eq__(self, other: object) -> bool:
         """Equal operator."""
         return self.__dict__ == other.__dict__
diff --git a/xknx/knxip/knxip.py b/xknx/knxip/knxip.py
index e4f42222..f555c31c 100644
--- a/xknx/knxip/knxip.py
+++ b/xknx/knxip/knxip.py
@@ -4,6 +4,8 @@ Module for serialization and deserialization of KNX/IP packets.
 It consists of a header and a body.
 Depending on the service_type_ident different types of body classes are instanciated.
 """
+from typing import TYPE_CHECKING, List, Optional
+
 from xknx.exceptions import CouldNotParseKNXIP
 
 from .body import KNXIPBody
@@ -21,83 +23,86 @@ from .search_response import SearchResponse
 from .tunnelling_ack import TunnellingAck
 from .tunnelling_request import TunnellingRequest
 
+if TYPE_CHECKING:
+    from xknx.xknx import XKNX
+
 
 class KNXIPFrame:
     """Class for KNX/IP Frames."""
 
-    def __init__(self, xknx):
+    def __init__(self, xknx: "XKNX"):
         """Initialize object."""
         self.xknx = xknx
-        self.header = KNXIPHeader(xknx)
-        self.body = None
+        self.header = KNXIPHeader()
+        self.body: Optional[KNXIPBody] = None
 
-    def init(self, service_type_ident: KNXIPServiceType) -> None:
+    def init(self, service_type_ident: KNXIPServiceType) -> KNXIPBody:
         """Init object by service_type_ident. Will instanciate a body object depending on service_type_ident."""
         self.header.service_type_ident = service_type_ident
 
+        body: KNXIPBody
         if service_type_ident == KNXIPServiceType.ROUTING_INDICATION:
-            self.body = RoutingIndication(self.xknx)
+            body = RoutingIndication(self.xknx)
         elif service_type_ident == KNXIPServiceType.CONNECT_REQUEST:
-            self.body = ConnectRequest(self.xknx)
+            body = ConnectRequest(self.xknx)
         elif service_type_ident == KNXIPServiceType.CONNECT_RESPONSE:
-            self.body = ConnectResponse(self.xknx)
+            body = ConnectResponse(self.xknx)
         elif service_type_ident == KNXIPServiceType.TUNNELLING_REQUEST:
-            self.body = TunnellingRequest(self.xknx)
+            body = TunnellingRequest(self.xknx)
         elif service_type_ident == KNXIPServiceType.TUNNELLING_ACK:
-            self.body = TunnellingAck(self.xknx)
+            body = TunnellingAck(self.xknx)
         elif service_type_ident == KNXIPServiceType.SEARCH_REQUEST:
-            self.body = SearchRequest(self.xknx)
+            body = SearchRequest(self.xknx)
         elif service_type_ident == KNXIPServiceType.SEARCH_RESPONSE:
-            self.body = SearchResponse(self.xknx)
+            body = SearchResponse(self.xknx)
         elif service_type_ident == KNXIPServiceType.DISCONNECT_REQUEST:
-            self.body = DisconnectRequest(self.xknx)
+            body = DisconnectRequest(self.xknx)
         elif service_type_ident == KNXIPServiceType.DISCONNECT_RESPONSE:
-            self.body = DisconnectResponse(self.xknx)
+            body = DisconnectResponse(self.xknx)
         elif service_type_ident == KNXIPServiceType.CONNECTIONSTATE_REQUEST:
-            self.body = ConnectionStateRequest(self.xknx)
+            body = ConnectionStateRequest(self.xknx)
         elif service_type_ident == KNXIPServiceType.CONNECTIONSTATE_RESPONSE:
-            self.body = ConnectionStateResponse(self.xknx)
+            body = ConnectionStateResponse(self.xknx)
         else:
             raise CouldNotParseKNXIP(
                 f"KNXIPServiceType not implemented: {service_type_ident.name}"
             )
+        self.body = body
+        return body
 
     @staticmethod
-    def init_from_body(knxip_body: KNXIPBody):
+    def init_from_body(knxip_body: KNXIPBody) -> "KNXIPFrame":
         """Return KNXIPFrame from KNXIPBody."""
         knxipframe = KNXIPFrame(knxip_body.xknx)
         knxipframe.header.service_type_ident = knxip_body.__class__.service_type
         knxipframe.body = knxip_body
-        knxipframe.normalize()
+        knxipframe.header.set_length(knxip_body)
         return knxipframe
 
-    def from_knx(self, data):
+    def from_knx(self, data: bytes) -> int:
         """Parse/deserialize from KNX/IP raw data."""
         pos = self.header.from_knx(data)
 
-        self.init(self.header.service_type_ident)
-        pos += self.body.from_knx(data[pos:])
+        pos += self.init(self.header.service_type_ident).from_knx(data[pos:])
 
         if pos != len(data):
             raise CouldNotParseKNXIP("KNXIP data has wrong length")
 
         return pos
 
-    def normalize(self):
-        """Normalize internal data. Necessary step for serialization."""
-        self.header.set_length(self.body)
-
-    def to_knx(self):
+    def to_knx(self) -> List[int]:
         """Serialize to KNX/IP raw data."""
+        if self.body is None:
+            raise CouldNotParseKNXIP("No body defined in KNXIPFrame.")
         data = []
         data.extend(self.header.to_knx())
         data.extend(self.body.to_knx())
         return data
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return f'<KNXIPFrame {self.header}\n body="{self.body}" />'
 
-    def __eq__(self, other):
+    def __eq__(self, other: object) -> bool:
         """Equal operator."""
         return self.__dict__ == other.__dict__
diff --git a/xknx/knxip/routing_indication.py b/xknx/knxip/routing_indication.py
index 2d4aaa4d..25ee9415 100644
--- a/xknx/knxip/routing_indication.py
+++ b/xknx/knxip/routing_indication.py
@@ -4,13 +4,16 @@ Module for Serialization and Deserialization of KNX Routing Indications.
 Routing indications are used to transport CEMI Messages.
 """
 import logging
+from typing import TYPE_CHECKING, List, Optional
 
-from xknx.exceptions import UnsupportedCEMIMessage
+from xknx.exceptions import CouldNotParseKNXIP, UnsupportedCEMIMessage
 
 from .body import KNXIPBody
 from .cemi_frame import CEMIFrame
 from .knxip_enum import CEMIMessageCode, KNXIPServiceType
 
+if TYPE_CHECKING:
+    from xknx.xknx import XKNX
 logger = logging.getLogger("xknx.log")
 
 
@@ -21,21 +24,24 @@ class RoutingIndication(KNXIPBody):
 
     service_type = KNXIPServiceType.ROUTING_INDICATION
 
-    def __init__(self, xknx, cemi: CEMIFrame = None):
+    def __init__(self, xknx: "XKNX", cemi: Optional[CEMIFrame] = None):
         """Initialize SearchRequest object."""
         super().__init__(xknx)
-        self.cemi = (
+        self.cemi: Optional[CEMIFrame] = (
             cemi
             if cemi is not None
             else CEMIFrame(xknx, code=CEMIMessageCode.L_DATA_IND)
         )
 
-    def calculated_length(self):
+    def calculated_length(self) -> int:
         """Get length of KNX/IP body."""
+        assert self.cemi is not None
         return self.cemi.calculated_length()
 
-    def from_knx(self, raw):
+    def from_knx(self, raw: bytes) -> int:
         """Parse/deserialize from KNX/IP raw data."""
+        assert self.cemi is not None
+
         try:
             return self.cemi.from_knx(raw)
         except UnsupportedCEMIMessage as unsupported_cemi_err:
@@ -43,10 +49,12 @@ class RoutingIndication(KNXIPBody):
             self.cemi = None
             return len(raw)
 
-    def to_knx(self):
+    def to_knx(self) -> List[int]:
         """Serialize to KNX/IP raw data."""
+        if self.cemi is None:
+            raise CouldNotParseKNXIP("No CEMIFrame defined.")
         return self.cemi.to_knx()
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return f'<RoutingIndication cemi="{self.cemi}" />'
diff --git a/xknx/knxip/search_request.py b/xknx/knxip/search_request.py
index 64c3c4af..4ebc5dc6 100644
--- a/xknx/knxip/search_request.py
+++ b/xknx/knxip/search_request.py
@@ -3,10 +3,15 @@ Module for Serialization and Deserialization of KNX Search Requests.
 
 Search Requests are used to search for KNX/IP devices within the network.
 """
+from typing import TYPE_CHECKING, List, Optional
+
 from .body import KNXIPBody
 from .hpai import HPAI
 from .knxip_enum import KNXIPServiceType
 
+if TYPE_CHECKING:
+    from xknx.xknx import XKNX
+
 
 class SearchRequest(KNXIPBody):
     """Representation of a KNX Connect Request."""
@@ -15,7 +20,7 @@ class SearchRequest(KNXIPBody):
 
     service_type = KNXIPServiceType.SEARCH_REQUEST
 
-    def __init__(self, xknx, discovery_endpoint: HPAI = None):
+    def __init__(self, xknx: "XKNX", discovery_endpoint: Optional[HPAI] = None):
         """Initialize SearchRequest object."""
         super().__init__(xknx)
         self.discovery_endpoint = (
@@ -24,22 +29,22 @@ class SearchRequest(KNXIPBody):
             else HPAI(ip_addr=xknx.multicast_group, port=xknx.multicast_port)
         )
 
-    def calculated_length(self):
+    def calculated_length(self) -> int:
         """Get length of KNX/IP body."""
         return HPAI.LENGTH
 
-    def from_knx(self, raw):
+    def from_knx(self, raw: bytes) -> int:
         """Parse/deserialize from KNX/IP raw data."""
         pos = self.discovery_endpoint.from_knx(raw)
         return pos
 
-    def to_knx(self):
+    def to_knx(self) -> List[int]:
         """Serialize to KNX/IP raw data."""
         data = []
         data.extend(self.discovery_endpoint.to_knx())
         return data
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return '<SearchRequest discovery_endpoint="{}" />'.format(
             self.discovery_endpoint
diff --git a/xknx/knxip/search_response.py b/xknx/knxip/search_response.py
index 748239f7..7d2c1722 100644
--- a/xknx/knxip/search_response.py
+++ b/xknx/knxip/search_response.py
@@ -6,11 +6,16 @@ A search response contains all information of a found device (Name, serial numbe
 It supports an array-style access to the DIBs (use classname as index). Every KNXnet/ip server shall send
 a search response and one device supporting multiple KNX connections may send multiple search responses.
 """
+from typing import TYPE_CHECKING, List
+
 from .body import KNXIPBody
 from .dib import DIB, DIBDeviceInformation
 from .hpai import HPAI
 from .knxip_enum import KNXIPServiceType
 
+if TYPE_CHECKING:
+    from xknx.xknx import XKNX
+
 
 class SearchResponse(KNXIPBody):
     """Representation of a KNX Search Response."""
@@ -19,17 +24,17 @@ class SearchResponse(KNXIPBody):
 
     service_type = KNXIPServiceType.SEARCH_RESPONSE
 
-    def __init__(self, xknx, control_endpoint: HPAI = HPAI()):
+    def __init__(self, xknx: "XKNX", control_endpoint: HPAI = HPAI()):
         """Initialize SearchResponse object."""
         super().__init__(xknx)
         self.control_endpoint = control_endpoint
-        self.dibs = []
+        self.dibs: List[DIB] = []
 
-    def calculated_length(self):
+    def calculated_length(self) -> int:
         """Get length of KNX/IP body."""
         return HPAI.LENGTH + sum([dib.calculated_length() for dib in self.dibs])
 
-    def from_knx(self, raw):
+    def from_knx(self, raw: bytes) -> int:
         """Parse/deserialize from KNX/IP raw data."""
         pos = self.control_endpoint.from_knx(raw)
         while raw[pos:]:
@@ -39,14 +44,14 @@ class SearchResponse(KNXIPBody):
         return pos
 
     @property
-    def device_name(self):
+    def device_name(self) -> str:
         """Return name of device."""
         for dib in self.dibs:
             if isinstance(dib, DIBDeviceInformation):
                 return dib.name
         return "UNKNOWN"
 
-    def to_knx(self):
+    def to_knx(self) -> List[int]:
         """Serialize to KNX/IP raw data."""
         data = []
         data.extend(self.control_endpoint.to_knx())
@@ -54,15 +59,8 @@ class SearchResponse(KNXIPBody):
             data.extend(dib.to_knx())
         return data
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return '<SearchResponse control_endpoint="{}" dibs="[\n{}\n]" />'.format(
             self.control_endpoint, ",\n".join(dib.__str__() for dib in self.dibs)
         )
-
-    def __getitem__(self, clazz):
-        """Return the first DIB of given class."""
-        for dib in self.dibs:
-            if isinstance(dib, clazz):
-                return dib
-        raise IndexError
diff --git a/xknx/knxip/tunnelling_ack.py b/xknx/knxip/tunnelling_ack.py
index f414c164..8dc1f879 100644
--- a/xknx/knxip/tunnelling_ack.py
+++ b/xknx/knxip/tunnelling_ack.py
@@ -4,15 +4,20 @@ Module for Serialization and Deserialization of a KNX Tunnelling ACK information
 Tunneling requests are used to transmit a KNX telegram within an existing KNX tunnel connection.
 With a Tunnelling ACK the receiving party acknowledges the valid processing of the request.
 """
+from typing import TYPE_CHECKING, List
+
 from xknx.exceptions import CouldNotParseKNXIP
 
-from .body import KNXIPBody
+from .body import KNXIPBodyResponse
 from .error_code import ErrorCode
 from .knxip_enum import KNXIPServiceType
 
+if TYPE_CHECKING:
+    from xknx.xknx import XKNX
+
 
-class TunnellingAck(KNXIPBody):
-    """Representation of a KNX Tunnelling Request."""
+class TunnellingAck(KNXIPBodyResponse):
+    """Representation of a KNX Tunnelling Ack."""
 
     # pylint: disable=too-many-instance-attributes
 
@@ -21,7 +26,7 @@ class TunnellingAck(KNXIPBody):
     BODY_LENGTH = 4
 
     def __init__(
-        self, xknx, communication_channel_id: int = 1, sequence_counter: int = 0
+        self, xknx: "XKNX", communication_channel_id: int = 1, sequence_counter: int = 0
     ):
         """Initialize TunnellingAck object."""
         super().__init__(xknx)
@@ -29,14 +34,14 @@ class TunnellingAck(KNXIPBody):
         self.sequence_counter = sequence_counter
         self.status_code = ErrorCode.E_NO_ERROR
 
-    def calculated_length(self):
+    def calculated_length(self) -> int:
         """Get length of KNX/IP body."""
         return TunnellingAck.BODY_LENGTH
 
-    def from_knx(self, raw):
+    def from_knx(self, raw: bytes) -> int:
         """Parse/deserialize from KNX/IP raw data."""
 
-        def ack_from_knx(header):
+        def ack_from_knx(header: bytes) -> int:
             """Parse ACK information."""
             if header[0] != TunnellingAck.BODY_LENGTH:
                 raise CouldNotParseKNXIP("connection header wrong length")
@@ -50,10 +55,10 @@ class TunnellingAck(KNXIPBody):
         pos = ack_from_knx(raw)
         return pos
 
-    def to_knx(self):
+    def to_knx(self) -> List[int]:
         """Serialize to KNX/IP raw data."""
 
-        def ack_to_knx():
+        def ack_to_knx() -> List[int]:
             """Serialize ACK information."""
             ack = []
             ack.append(TunnellingAck.BODY_LENGTH)
@@ -66,7 +71,7 @@ class TunnellingAck(KNXIPBody):
         data.extend(ack_to_knx())
         return data
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return (
             '<TunnellingAck communication_channel_id="{}" '
diff --git a/xknx/knxip/tunnelling_request.py b/xknx/knxip/tunnelling_request.py
index 83133a1a..4c1755de 100644
--- a/xknx/knxip/tunnelling_request.py
+++ b/xknx/knxip/tunnelling_request.py
@@ -4,6 +4,7 @@ Module for Serialization and Deserialization of a KNX Tunnelling Request informa
 Tunnelling requests are used to transmit a KNX telegram within an existing KNX tunnel connection.
 """
 import logging
+from typing import TYPE_CHECKING, List, Optional
 
 from xknx.exceptions import CouldNotParseKNXIP, UnsupportedCEMIMessage
 
@@ -11,6 +12,9 @@ from .body import KNXIPBody
 from .cemi_frame import CEMIFrame, CEMIMessageCode
 from .knxip_enum import KNXIPServiceType
 
+if TYPE_CHECKING:
+    from xknx.xknx import XKNX
+
 logger = logging.getLogger("xknx.log")
 
 
@@ -25,30 +29,32 @@ class TunnellingRequest(KNXIPBody):
 
     def __init__(
         self,
-        xknx,
+        xknx: "XKNX",
         communication_channel_id: int = 1,
         sequence_counter: int = 0,
-        cemi: CEMIFrame = None,
+        cemi: Optional[CEMIFrame] = None,
     ):
         """Initialize TunnellingRequest object."""
         super().__init__(xknx)
 
         self.communication_channel_id = communication_channel_id
         self.sequence_counter = sequence_counter
-        self.cemi = (
+        self.cemi: Optional[CEMIFrame] = (
             cemi
             if cemi is not None
             else CEMIFrame(xknx, code=CEMIMessageCode.L_Data_REQ)
         )
 
-    def calculated_length(self):
+    def calculated_length(self) -> int:
         """Get length of KNX/IP body."""
+        assert self.cemi is not None
         return TunnellingRequest.HEADER_LENGTH + self.cemi.calculated_length()
 
-    def from_knx(self, raw):
+    def from_knx(self, raw: bytes) -> int:
         """Parse/deserialize from KNX/IP raw data."""
+        assert self.cemi is not None
 
-        def header_from_knx(header):
+        def header_from_knx(header: bytes) -> int:
             """Parse header bytes."""
             if header[0] != TunnellingRequest.HEADER_LENGTH:
                 raise CouldNotParseKNXIP("connection header wrong length")
@@ -68,10 +74,12 @@ class TunnellingRequest(KNXIPBody):
             return len(raw)
         return pos
 
-    def to_knx(self):
+    def to_knx(self) -> List[int]:
         """Serialize to KNX/IP raw data."""
+        if self.cemi is None:
+            raise CouldNotParseKNXIP("No CEMIFrame defined.")
 
-        def header_to_knx():
+        def header_to_knx() -> List[int]:
             """Serialize header."""
             cri = []
             cri.append(TunnellingRequest.HEADER_LENGTH)
@@ -85,7 +93,7 @@ class TunnellingRequest(KNXIPBody):
         data.extend(self.cemi.to_knx())
         return data
 
-    def __str__(self):
+    def __str__(self) -> str:
         """Return object as readable string."""
         return (
             '<TunnellingRequest communication_channel_id="{}" '
diff --git a/xknx/telegram/address.py b/xknx/telegram/address.py
index 3836421c..0fe058c4 100644
--- a/xknx/telegram/address.py
+++ b/xknx/telegram/address.py
@@ -48,7 +48,7 @@ class BaseAddress:  # pylint: disable=too-few-public-methods
         """Initialize instance variables needed by all subclasses."""
         self.raw: int = 0
 
-    def to_knx(self) -> Tuple:
+    def to_knx(self) -> Tuple[int, int]:
         """
         Serialize to KNX/IP raw data.
 
diff --git a/xknx/telegram/address_filter.py b/xknx/telegram/address_filter.py
index 20276b4f..dde85615 100644
--- a/xknx/telegram/address_filter.py
+++ b/xknx/telegram/address_filter.py
@@ -35,7 +35,7 @@ class AddressFilter:
 
     def __init__(self, pattern: str) -> None:
         """Initialize AddressFilter class."""
-        self.level_filters: List = []
+        self.level_filters: List["AddressFilter.LevelFilter"] = []
         self._parse_pattern(pattern)
 
     def _parse_pattern(self, pattern: str) -> None:
@@ -55,6 +55,10 @@ class AddressFilter:
         return self._match_free(address)
 
     def _match_level3(self, address: GroupAddress) -> bool:
+        if address.main is None or address.middle is None:
+            raise ConnectionError(
+                f"Match level 3 incompatible with address level {address.levels}"
+            )
         return bool(
             self.level_filters[0].match(address.main)
             and self.level_filters[1].match(address.middle)
@@ -62,6 +66,10 @@ class AddressFilter:
         )
 
     def _match_level2(self, address: GroupAddress) -> bool:
+        if address.main is None:
+            raise ConnectionError(
+                f"Match level 2 incompatible with address level {address.levels}"
+            )
         return bool(
             self.level_filters[0].match(address.main)
             and self.level_filters[1].match(address.sub)
@@ -130,7 +138,7 @@ class AddressFilter:
         # pylint: disable=too-few-public-methods
         def __init__(self, pattern: str) -> None:
             """Initialize LevelFilter."""
-            self.ranges: List = []
+            self.ranges: List["AddressFilter.Range"] = []
             self._parse_pattern(pattern)
 
         def _parse_pattern(self, pattern: str) -> None:
diff --git a/xknx/xknx.py b/xknx/xknx.py
index eff9a526..313372a6 100644
--- a/xknx/xknx.py
+++ b/xknx/xknx.py
@@ -5,17 +5,19 @@ from logging.handlers import TimedRotatingFileHandler
 import os
 import signal
 from sys import platform
+from types import TracebackType
+from typing import Awaitable, Callable, Optional, Type, Union
 
 from xknx.config import Config
 from xknx.core import StateUpdater, TelegramQueue
-from xknx.devices import Devices
+from xknx.devices import Device, Devices
 from xknx.io import (
     DEFAULT_MCAST_GRP,
     DEFAULT_MCAST_PORT,
     ConnectionConfig,
     KNXIPInterface,
 )
-from xknx.telegram import GroupAddressType, IndividualAddress
+from xknx.telegram import GroupAddressType, IndividualAddress, Telegram
 
 from .__version__ import __version__ as VERSION
 
@@ -32,27 +34,27 @@ class XKNX:
 
     def __init__(
         self,
-        config=None,
-        own_address=DEFAULT_ADDRESS,
-        address_format=GroupAddressType.LONG,
-        telegram_received_cb=None,
-        device_updated_cb=None,
-        rate_limit=DEFAULT_RATE_LIMIT,
-        multicast_group=DEFAULT_MCAST_GRP,
-        multicast_port=DEFAULT_MCAST_PORT,
-        log_directory=None,
-        state_updater=False,
-        daemon_mode=False,
-        connection_config=ConnectionConfig(),
-    ):
+        config: Optional[str] = None,
+        own_address: Union[str, IndividualAddress] = DEFAULT_ADDRESS,
+        address_format: GroupAddressType = GroupAddressType.LONG,
+        telegram_received_cb: Optional[Callable[[Telegram], Awaitable[None]]] = None,
+        device_updated_cb: Optional[Callable[[Device], Awaitable[None]]] = None,
+        rate_limit: int = DEFAULT_RATE_LIMIT,
+        multicast_group: str = DEFAULT_MCAST_GRP,
+        multicast_port: int = DEFAULT_MCAST_PORT,
+        log_directory: Optional[str] = None,
+        state_updater: bool = False,
+        daemon_mode: bool = False,
+        connection_config: ConnectionConfig = ConnectionConfig(),
+    ) -> None:
         """Initialize XKNX class."""
         # pylint: disable=too-many-arguments
         self.devices = Devices()
-        self.telegrams = asyncio.Queue()
+        self.telegrams: asyncio.Queue[Optional[Telegram]] = asyncio.Queue()
         self.sigint_received = asyncio.Event()
         self.telegram_queue = TelegramQueue(self)
         self.state_updater = StateUpdater(self)
-        self.knxip_interface = None
+        self.knxip_interface: Optional[KNXIPInterface] = None
         self.started = asyncio.Event()
         self.connected = asyncio.Event()
         self.address_format = address_format
@@ -77,7 +79,7 @@ class XKNX:
         if device_updated_cb is not None:
             self.devices.register_device_updated_cb(device_updated_cb)
 
-    def __del__(self):
+    def __del__(self) -> None:
         """Destructor. Cleaning up if this was not done before."""
         if self.started.is_set():
             try:
@@ -86,16 +88,21 @@ class XKNX:
             except RuntimeError as exp:
                 logger.warning("Could not close loop, reason: %s", exp)
 
-    async def __aenter__(self):
+    async def __aenter__(self) -> "XKNX":
         """Start XKNX from context manager."""
         await self.start()
         return self
 
-    async def __aexit__(self, exc_type, exc, traceback):
+    async def __aexit__(
+        self,
+        exc_type: Optional[Type[BaseException]],
+        exc: Optional[BaseException],
+        traceback: Optional[TracebackType],
+    ) -> None:
         """Stop XKNX from context manager."""
         await self.stop()
 
-    async def start(self):
+    async def start(self) -> None:
         """Start XKNX module. Connect to KNX/IP devices and start state updater."""
         self.knxip_interface = KNXIPInterface(
             self, connection_config=self.connection_config
@@ -113,17 +120,17 @@ class XKNX:
         if self.daemon_mode:
             await self.loop_until_sigint()
 
-    async def join(self):
+    async def join(self) -> None:
         """Wait until all telegrams were processed."""
         await self.telegrams.join()
 
-    async def _stop_knxip_interface_if_exists(self):
+    async def _stop_knxip_interface_if_exists(self) -> None:
         """Stop KNXIPInterface if initialized."""
         if self.knxip_interface is not None:
             await self.knxip_interface.stop()
             self.knxip_interface = None
 
-    async def stop(self):
+    async def stop(self) -> None:
         """Stop XKNX module."""
         self.state_updater.stop()
         await self.join()
@@ -131,10 +138,10 @@ class XKNX:
         await self._stop_knxip_interface_if_exists()
         self.started.clear()
 
-    async def loop_until_sigint(self):
+    async def loop_until_sigint(self) -> None:
         """Loop until Crtl-C was pressed."""
 
-        def sigint_handler():
+        def sigint_handler() -> None:
             """End loop."""
             self.sigint_received.set()
 
@@ -147,7 +154,7 @@ class XKNX:
         await self.sigint_received.wait()
 
     @staticmethod
-    def setup_logging(log_directory: str):
+    def setup_logging(log_directory: str) -> None:
         """Configure logging to file."""
         if not os.path.isdir(log_directory):
             logger.warning("The provided log directory does not exist.")


Test patch:
diff --git a/test/core_tests/telegram_queue_test.py b/test/core_tests/telegram_queue_test.py
index 6ab5f98c..0c27b794 100644
--- a/test/core_tests/telegram_queue_test.py
+++ b/test/core_tests/telegram_queue_test.py
@@ -5,7 +5,7 @@ from unittest.mock import MagicMock, Mock, patch
 
 from xknx import XKNX
 from xknx.dpt import DPTBinary
-from xknx.exceptions import CouldNotParseTelegram
+from xknx.exceptions import CommunicationError, CouldNotParseTelegram
 from xknx.telegram import AddressFilter, GroupAddress, Telegram, TelegramDirection
 from xknx.telegram.apci import GroupValueWrite
 
@@ -201,10 +201,8 @@ class TestTelegramQueue(unittest.TestCase):
         devices_process.assert_called_once_with(telegram)
 
     @patch("xknx.io.KNXIPInterface")
-    @patch("logging.Logger.warning")
-    def test_outgoing(self, logger_warning_mock, if_mock):
+    def test_outgoing(self, if_mock):
         """Test outgoing telegrams in telegram queue."""
-        # pylint: disable=no-self-use
         xknx = XKNX()
 
         async_if_send_telegram = asyncio.Future()
@@ -218,10 +216,10 @@ class TestTelegramQueue(unittest.TestCase):
         )
 
         # log a warning if there is no KNXIP interface instanciated
-        self.loop.run_until_complete(
-            xknx.telegram_queue.process_telegram_outgoing(telegram)
-        )
-        logger_warning_mock.assert_called_once_with("No KNXIP interface defined")
+        with self.assertRaises(CommunicationError):
+            self.loop.run_until_complete(
+                xknx.telegram_queue.process_telegram_outgoing(telegram)
+            )
         if_mock.send_telegram.assert_not_called()
 
         # if we have an interface send the telegram
diff --git a/test/io_tests/connect_test.py b/test/io_tests/connect_test.py
index 7f322a20..6e61dfb9 100644
--- a/test/io_tests/connect_test.py
+++ b/test/io_tests/connect_test.py
@@ -7,6 +7,7 @@ from xknx import XKNX
 from xknx.io import Connect, UDPClient
 from xknx.knxip import (
     HPAI,
+    ConnectRequest,
     ConnectRequestType,
     ConnectResponse,
     ErrorCode,
@@ -37,12 +38,15 @@ class TestConnect(unittest.TestCase):
         self.assertEqual(connect.awaited_response_class, ConnectResponse)
 
         # Expected KNX/IP-Frame:
-        exp_knxipframe = KNXIPFrame(xknx)
-        exp_knxipframe.init(KNXIPServiceType.CONNECT_REQUEST)
-        exp_knxipframe.body.control_endpoint = HPAI(ip_addr="192.168.1.3", port=4321)
-        exp_knxipframe.body.data_endpoint = HPAI(ip_addr="192.168.1.3", port=4321)
-        exp_knxipframe.body.request_type = ConnectRequestType.TUNNEL_CONNECTION
-        exp_knxipframe.normalize()
+        exp_knxipframe = KNXIPFrame.init_from_body(
+            ConnectRequest(
+                xknx,
+                request_type=ConnectRequestType.TUNNEL_CONNECTION,
+                control_endpoint=HPAI(ip_addr="192.168.1.3", port=4321),
+                data_endpoint=HPAI(ip_addr="192.168.1.3", port=4321),
+            )
+        )
+
         with patch("xknx.io.UDPClient.send") as mock_udp_send, patch(
             "xknx.io.UDPClient.getsockname"
         ) as mock_udp_getsockname:
diff --git a/test/io_tests/connectionstate_test.py b/test/io_tests/connectionstate_test.py
index 4c5a1336..e0fc124a 100644
--- a/test/io_tests/connectionstate_test.py
+++ b/test/io_tests/connectionstate_test.py
@@ -7,6 +7,7 @@ from xknx import XKNX
 from xknx.io import ConnectionState, UDPClient
 from xknx.knxip import (
     HPAI,
+    ConnectionStateRequest,
     ConnectionStateResponse,
     ErrorCode,
     KNXIPFrame,
@@ -42,11 +43,13 @@ class TestConnectionState(unittest.TestCase):
         )
 
         # Expected KNX/IP-Frame:
-        exp_knxipframe = KNXIPFrame(xknx)
-        exp_knxipframe.init(KNXIPServiceType.CONNECTIONSTATE_REQUEST)
-        exp_knxipframe.body.communication_channel_id = communication_channel_id
-        exp_knxipframe.body.control_endpoint = HPAI(ip_addr="192.168.1.3", port=4321)
-        exp_knxipframe.normalize()
+        exp_knxipframe = KNXIPFrame.init_from_body(
+            ConnectionStateRequest(
+                xknx,
+                communication_channel_id=communication_channel_id,
+                control_endpoint=HPAI(ip_addr="192.168.1.3", port=4321),
+            )
+        )
         with patch("xknx.io.UDPClient.send") as mock_udp_send, patch(
             "xknx.io.UDPClient.getsockname"
         ) as mock_udp_getsockname:
diff --git a/test/io_tests/disconnect_test.py b/test/io_tests/disconnect_test.py
index f602021e..fc0a6ba6 100644
--- a/test/io_tests/disconnect_test.py
+++ b/test/io_tests/disconnect_test.py
@@ -5,7 +5,14 @@ from unittest.mock import patch
 
 from xknx import XKNX
 from xknx.io import Disconnect, UDPClient
-from xknx.knxip import HPAI, DisconnectResponse, ErrorCode, KNXIPFrame, KNXIPServiceType
+from xknx.knxip import (
+    HPAI,
+    DisconnectRequest,
+    DisconnectResponse,
+    ErrorCode,
+    KNXIPFrame,
+    KNXIPServiceType,
+)
 
 
 class TestDisconnect(unittest.TestCase):
@@ -32,11 +39,13 @@ class TestDisconnect(unittest.TestCase):
         self.assertEqual(disconnect.communication_channel_id, communication_channel_id)
 
         # Expected KNX/IP-Frame:
-        exp_knxipframe = KNXIPFrame(xknx)
-        exp_knxipframe.init(KNXIPServiceType.DISCONNECT_REQUEST)
-        exp_knxipframe.body.communication_channel_id = communication_channel_id
-        exp_knxipframe.body.control_endpoint = HPAI(ip_addr="192.168.1.3", port=4321)
-        exp_knxipframe.normalize()
+        exp_knxipframe = KNXIPFrame.init_from_body(
+            DisconnectRequest(
+                xknx,
+                communication_channel_id=communication_channel_id,
+                control_endpoint=HPAI(ip_addr="192.168.1.3", port=4321),
+            )
+        )
         with patch("xknx.io.UDPClient.send") as mock_udp_send, patch(
             "xknx.io.UDPClient.getsockname"
         ) as mock_udp_getsockname:
diff --git a/test/io_tests/gateway_scanner_test.py b/test/io_tests/gateway_scanner_test.py
index 853f3dc2..aa22cdc6 100644
--- a/test/io_tests/gateway_scanner_test.py
+++ b/test/io_tests/gateway_scanner_test.py
@@ -12,8 +12,6 @@ from xknx.knxip import (
     DIBServiceFamily,
     DIBSuppSVCFamilies,
     KNXIPFrame,
-    KNXIPHeader,
-    KNXIPServiceType,
     SearchResponse,
 )
 from xknx.telegram import IndividualAddress
@@ -186,44 +184,35 @@ class TestGatewayScanner(unittest.TestCase):
 
 def fake_router_search_response(xknx: XKNX) -> KNXIPFrame:
     """Return the KNXIPFrame of a KNX/IP Router with a SearchResponse body."""
-    _frame_header = KNXIPHeader(xknx)
-    _frame_header.service_type_ident = KNXIPServiceType.SEARCH_RESPONSE
-    _frame_body = SearchResponse(xknx)
-    _frame_body.control_endpoint = HPAI(ip_addr="192.168.42.10", port=3671)
-
-    _device_information = DIBDeviceInformation()
-    _device_information.name = "Gira KNX/IP-Router"
-    _device_information.serial_number = "11:22:33:44:55:66"
-    _device_information.individual_address = IndividualAddress("1.1.0")
-    _device_information.mac_address = "01:02:03:04:05:06"
-
-    _svc_families = DIBSuppSVCFamilies()
-    _svc_families.families.append(
+    frame_body = SearchResponse(xknx)
+    frame_body.control_endpoint = HPAI(ip_addr="192.168.42.10", port=3671)
+
+    device_information = DIBDeviceInformation()
+    device_information.name = "Gira KNX/IP-Router"
+    device_information.serial_number = "11:22:33:44:55:66"
+    device_information.individual_address = IndividualAddress("1.1.0")
+    device_information.mac_address = "01:02:03:04:05:06"
+
+    svc_families = DIBSuppSVCFamilies()
+    svc_families.families.append(
         DIBSuppSVCFamilies.Family(name=DIBServiceFamily.CORE, version=1)
     )
-    _svc_families.families.append(
+    svc_families.families.append(
         DIBSuppSVCFamilies.Family(name=DIBServiceFamily.DEVICE_MANAGEMENT, version=2)
     )
-    _svc_families.families.append(
+    svc_families.families.append(
         DIBSuppSVCFamilies.Family(name=DIBServiceFamily.TUNNELING, version=1)
     )
-    _svc_families.families.append(
+    svc_families.families.append(
         DIBSuppSVCFamilies.Family(name=DIBServiceFamily.ROUTING, version=1)
     )
-    _svc_families.families.append(
+    svc_families.families.append(
         DIBSuppSVCFamilies.Family(
             name=DIBServiceFamily.REMOTE_CONFIGURATION_DIAGNOSIS, version=1
         )
     )
 
-    _frame_body.dibs.append(_device_information)
-    _frame_body.dibs.append(_svc_families)
-    _frame_header.set_length(_frame_body)
-
-    search_response = KNXIPFrame(xknx)
-    search_response.init(KNXIPServiceType.SEARCH_RESPONSE)
-    search_response.header = _frame_header
-    search_response.body = _frame_body
-    search_response.normalize()
+    frame_body.dibs.append(device_information)
+    frame_body.dibs.append(svc_families)
 
-    return search_response
+    return KNXIPFrame.init_from_body(frame_body)
diff --git a/test/io_tests/tunnelling_test.py b/test/io_tests/tunnelling_test.py
index 47ffd17d..a56783a8 100644
--- a/test/io_tests/tunnelling_test.py
+++ b/test/io_tests/tunnelling_test.py
@@ -6,7 +6,13 @@ from unittest.mock import patch
 from xknx import XKNX
 from xknx.dpt import DPTArray
 from xknx.io import Tunnelling, UDPClient
-from xknx.knxip import ErrorCode, KNXIPFrame, KNXIPServiceType, TunnellingAck
+from xknx.knxip import (
+    ErrorCode,
+    KNXIPFrame,
+    KNXIPServiceType,
+    TunnellingAck,
+    TunnellingRequest,
+)
 from xknx.telegram import GroupAddress, IndividualAddress, Telegram
 from xknx.telegram.apci import GroupValueWrite
 
@@ -49,13 +55,14 @@ class TestTunnelling(unittest.TestCase):
         self.assertEqual(tunnelling.communication_channel_id, communication_channel_id)
 
         # Expected KNX/IP-Frame:
-        exp_knxipframe = KNXIPFrame(xknx)
-        exp_knxipframe.init(KNXIPServiceType.TUNNELLING_REQUEST)
-        exp_knxipframe.body.cemi.telegram = telegram
-        exp_knxipframe.body.cemi.src_addr = src_address
-        exp_knxipframe.body.communication_channel_id = communication_channel_id
-        exp_knxipframe.body.sequence_counter = sequence_counter
-        exp_knxipframe.normalize()
+        tunnelling_request = TunnellingRequest(
+            xknx,
+            communication_channel_id=communication_channel_id,
+            sequence_counter=sequence_counter,
+        )
+        tunnelling_request.cemi.telegram = telegram
+        tunnelling_request.cemi.src_addr = src_address
+        exp_knxipframe = KNXIPFrame.init_from_body(tunnelling_request)
         with patch("xknx.io.UDPClient.send") as mock_udp_send, patch(
             "xknx.io.UDPClient.getsockname"
         ) as mock_udp_getsockname:
diff --git a/test/knxip_tests/body_test.py b/test/knxip_tests/body_test.py
index f63e068f..5f500201 100644
--- a/test/knxip_tests/body_test.py
+++ b/test/knxip_tests/body_test.py
@@ -1,52 +1,27 @@
-"""Unit test for KNX/IP ConnectionStateRequests."""
-import asyncio
+"""Unit test for KNX/IP Body base class."""
 import unittest
 from unittest.mock import patch
 
 from xknx import XKNX
-from xknx.knxip import KNXIPBody
+from xknx.knxip import KNXIPBody, KNXIPBodyResponse
 
 
 class Test_KNXIPBody(unittest.TestCase):
-    """Test class for KNX/IP ConnectionStateRequests."""
-
-    # pylint: disable=too-many-public-methods,invalid-name
+    """Test base class for KNX/IP bodys."""
 
     def setUp(self):
         """Set up test class."""
-        self.loop = asyncio.new_event_loop()
-        asyncio.set_event_loop(self.loop)
-
-    def tearDown(self):
-        """Tear down test class."""
-        self.loop.close()
-
-    def test_warn_calculated_length(self):
-        """Test correct warn message if calculated_length is missing."""
-        xknx = XKNX()
-        body = KNXIPBody(xknx)
-        with patch("logging.Logger.warning") as mock_warn:
-            body.calculated_length()
-            mock_warn.assert_called_with(
-                "'calculated_length()' not implemented for %s", "KNXIPBody"
-            )
-
-    def test_warn_to_knx(self):
-        """Test correct warn message if to_knx is missing."""
-        xknx = XKNX()
-        body = KNXIPBody(xknx)
-        with patch("logging.Logger.warning") as mock_warn:
-            body.to_knx()
-            mock_warn.assert_called_with(
-                "'to_knx()' not implemented for %s", "KNXIPBody"
-            )
-
-    def test_warn_from_knx(self):
-        """Test correct warn message if from_knx is missing."""
-        xknx = XKNX()
-        body = KNXIPBody(xknx)
-        with patch("logging.Logger.warning") as mock_warn:
-            body.from_knx((0x75, 0x0B, 0x1C, 0x17, 0x07, 0x18, 0x00, 0x00))
-            mock_warn.assert_called_with(
-                "'from_knx()' not implemented for %s", "KNXIPBody"
-            )
+        self.xknx = XKNX()
+
+    @patch.multiple(KNXIPBody, __abstractmethods__=set())
+    def test_body_attributes(self):
+        """Test attributes of KNXIPBody base class."""
+        body = KNXIPBody(self.xknx)
+        self.assertTrue(hasattr(body, "service_type"))
+
+    @patch.multiple(KNXIPBodyResponse, __abstractmethods__=set())
+    def test_response_attributes(self):
+        """Test attributes of KNXIPBodyResponse base class."""
+        response = KNXIPBodyResponse(self.xknx)
+        self.assertTrue(hasattr(response, "service_type"))
+        self.assertTrue(hasattr(response, "status_code"))
diff --git a/test/knxip_tests/connect_request_test.py b/test/knxip_tests/connect_request_test.py
index 8cf873d8..a4b903dd 100644
--- a/test/knxip_tests/connect_request_test.py
+++ b/test/knxip_tests/connect_request_test.py
@@ -72,12 +72,13 @@ class Test_KNXIP_ConnectRequest(unittest.TestCase):
             knxipframe.body.data_endpoint, HPAI(ip_addr="192.168.42.1", port=52393)
         )
 
-        knxipframe2 = KNXIPFrame(xknx)
-        knxipframe2.init(KNXIPServiceType.CONNECT_REQUEST)
-        knxipframe2.body.request_type = ConnectRequestType.TUNNEL_CONNECTION
-        knxipframe2.body.control_endpoint = HPAI(ip_addr="192.168.42.1", port=33941)
-        knxipframe2.body.data_endpoint = HPAI(ip_addr="192.168.42.1", port=52393)
-        knxipframe2.normalize()
+        connect_request = ConnectRequest(
+            xknx,
+            request_type=ConnectRequestType.TUNNEL_CONNECTION,
+            control_endpoint=HPAI(ip_addr="192.168.42.1", port=33941),
+            data_endpoint=HPAI(ip_addr="192.168.42.1", port=52393),
+        )
+        knxipframe2 = KNXIPFrame.init_from_body(connect_request)
 
         self.assertEqual(knxipframe2.to_knx(), list(raw))
 
diff --git a/test/knxip_tests/connect_response_test.py b/test/knxip_tests/connect_response_test.py
index 3ca0047f..36560584 100644
--- a/test/knxip_tests/connect_response_test.py
+++ b/test/knxip_tests/connect_response_test.py
@@ -66,14 +66,15 @@ class Test_KNXIP_ConnectResponse(unittest.TestCase):
         )
         self.assertEqual(knxipframe.body.identifier, 4607)
 
-        knxipframe2 = KNXIPFrame(xknx)
-        knxipframe2.init(KNXIPServiceType.CONNECT_RESPONSE)
-        knxipframe2.status_code = ErrorCode.E_NO_ERROR
-        knxipframe2.body.communication_channel = 1
-        knxipframe2.body.request_type = ConnectRequestType.TUNNEL_CONNECTION
-        knxipframe2.body.control_endpoint = HPAI(ip_addr="192.168.42.10", port=3671)
-        knxipframe2.body.identifier = 4607
-        knxipframe2.normalize()
+        connect_response = ConnectResponse(
+            xknx,
+            communication_channel=1,
+            status_code=ErrorCode.E_NO_ERROR,
+            request_type=ConnectRequestType.TUNNEL_CONNECTION,
+            control_endpoint=HPAI(ip_addr="192.168.42.10", port=3671),
+            identifier=4607,
+        )
+        knxipframe2 = KNXIPFrame.init_from_body(connect_response)
 
         self.assertEqual(knxipframe2.to_knx(), list(raw))
 
@@ -167,14 +168,15 @@ class Test_KNXIP_ConnectResponse(unittest.TestCase):
         self.assertEqual(knxipframe.body.status_code, ErrorCode.E_NO_MORE_CONNECTIONS)
         self.assertEqual(knxipframe.body.communication_channel, 192)
 
-        knxipframe2 = KNXIPFrame(xknx)
-        knxipframe2.init(KNXIPServiceType.CONNECT_RESPONSE)
-        knxipframe2.body.status_code = ErrorCode.E_NO_MORE_CONNECTIONS
-        knxipframe2.body.communication_channel = 192
-        knxipframe2.body.request_type = ConnectRequestType.TUNNEL_CONNECTION
-        knxipframe2.body.control_endpoint = HPAI(ip_addr="10.1.0.41", port=3671)
-        knxipframe2.body.identifier = 0
-        knxipframe2.normalize()
+        connect_response = ConnectResponse(
+            xknx,
+            communication_channel=192,
+            status_code=ErrorCode.E_NO_MORE_CONNECTIONS,
+            request_type=ConnectRequestType.TUNNEL_CONNECTION,
+            control_endpoint=HPAI(ip_addr="10.1.0.41", port=3671),
+            identifier=0,
+        )
+        knxipframe2 = KNXIPFrame.init_from_body(connect_response)
 
         self.assertEqual(knxipframe2.to_knx(), list(raw))
 
diff --git a/test/knxip_tests/connectionstate_request_test.py b/test/knxip_tests/connectionstate_request_test.py
index af5aa737..5eb7ecb0 100644
--- a/test/knxip_tests/connectionstate_request_test.py
+++ b/test/knxip_tests/connectionstate_request_test.py
@@ -4,7 +4,7 @@ import unittest
 
 from xknx import XKNX
 from xknx.exceptions import CouldNotParseKNXIP
-from xknx.knxip import HPAI, ConnectionStateRequest, KNXIPFrame, KNXIPServiceType
+from xknx.knxip import HPAI, ConnectionStateRequest, KNXIPFrame
 
 
 class Test_KNXIP_ConnStateReq(unittest.TestCase):
@@ -52,11 +52,12 @@ class Test_KNXIP_ConnStateReq(unittest.TestCase):
             knxipframe.body.control_endpoint, HPAI(ip_addr="192.168.200.12", port=50100)
         )
 
-        knxipframe2 = KNXIPFrame(xknx)
-        knxipframe2.init(KNXIPServiceType.CONNECTIONSTATE_REQUEST)
-        knxipframe2.body.communication_channel_id = 21
-        knxipframe2.body.control_endpoint = HPAI(ip_addr="192.168.200.12", port=50100)
-        knxipframe2.normalize()
+        connectionstate_request = ConnectionStateRequest(
+            xknx,
+            communication_channel_id=21,
+            control_endpoint=HPAI(ip_addr="192.168.200.12", port=50100),
+        )
+        knxipframe2 = KNXIPFrame.init_from_body(connectionstate_request)
 
         self.assertEqual(knxipframe2.to_knx(), list(raw))
 
diff --git a/test/knxip_tests/connectionstate_response_test.py b/test/knxip_tests/connectionstate_response_test.py
index 92c28315..dcba2f23 100644
--- a/test/knxip_tests/connectionstate_response_test.py
+++ b/test/knxip_tests/connectionstate_response_test.py
@@ -4,7 +4,7 @@ import unittest
 
 from xknx import XKNX
 from xknx.exceptions import CouldNotParseKNXIP
-from xknx.knxip import ConnectionStateResponse, ErrorCode, KNXIPFrame, KNXIPServiceType
+from xknx.knxip import ConnectionStateResponse, ErrorCode, KNXIPFrame
 
 
 class Test_KNXIP_ConnStateResp(unittest.TestCase):
@@ -33,11 +33,10 @@ class Test_KNXIP_ConnStateResp(unittest.TestCase):
         self.assertEqual(knxipframe.body.communication_channel_id, 21)
         self.assertEqual(knxipframe.body.status_code, ErrorCode.E_CONNECTION_ID)
 
-        knxipframe2 = KNXIPFrame(xknx)
-        knxipframe2.init(KNXIPServiceType.CONNECTIONSTATE_RESPONSE)
-        knxipframe2.body.communication_channel_id = 21
-        knxipframe2.body.status_code = ErrorCode.E_CONNECTION_ID
-        knxipframe2.normalize()
+        connectionstate_response = ConnectionStateResponse(
+            xknx, communication_channel_id=21, status_code=ErrorCode.E_CONNECTION_ID
+        )
+        knxipframe2 = KNXIPFrame.init_from_body(connectionstate_response)
 
         self.assertEqual(knxipframe2.to_knx(), list(raw))
 
diff --git a/test/knxip_tests/disconnect_request_test.py b/test/knxip_tests/disconnect_request_test.py
index 71eea205..01d9a12f 100644
--- a/test/knxip_tests/disconnect_request_test.py
+++ b/test/knxip_tests/disconnect_request_test.py
@@ -4,7 +4,7 @@ import unittest
 
 from xknx import XKNX
 from xknx.exceptions import CouldNotParseKNXIP
-from xknx.knxip import HPAI, DisconnectRequest, KNXIPFrame, KNXIPServiceType
+from xknx.knxip import HPAI, DisconnectRequest, KNXIPFrame
 
 
 class Test_KNXIP_DisconnectReq(unittest.TestCase):
@@ -52,11 +52,12 @@ class Test_KNXIP_DisconnectReq(unittest.TestCase):
             knxipframe.body.control_endpoint, HPAI(ip_addr="192.168.200.12", port=50100)
         )
 
-        knxipframe2 = KNXIPFrame(xknx)
-        knxipframe2.init(KNXIPServiceType.DISCONNECT_REQUEST)
-        knxipframe2.body.communication_channel_id = 21
-        knxipframe2.body.control_endpoint = HPAI(ip_addr="192.168.200.12", port=50100)
-        knxipframe2.normalize()
+        disconnect_request = DisconnectRequest(
+            xknx,
+            communication_channel_id=21,
+            control_endpoint=HPAI(ip_addr="192.168.200.12", port=50100),
+        )
+        knxipframe2 = KNXIPFrame.init_from_body(disconnect_request)
 
         self.assertEqual(knxipframe2.to_knx(), list(raw))
 
diff --git a/test/knxip_tests/disconnect_response_test.py b/test/knxip_tests/disconnect_response_test.py
index b4b064be..0f43216d 100644
--- a/test/knxip_tests/disconnect_response_test.py
+++ b/test/knxip_tests/disconnect_response_test.py
@@ -4,7 +4,7 @@ import unittest
 
 from xknx import XKNX
 from xknx.exceptions import CouldNotParseKNXIP
-from xknx.knxip import DisconnectResponse, ErrorCode, KNXIPFrame, KNXIPServiceType
+from xknx.knxip import DisconnectResponse, ErrorCode, KNXIPFrame
 
 
 class Test_KNXIP_DisconnectResp(unittest.TestCase):
@@ -35,11 +35,12 @@ class Test_KNXIP_DisconnectResp(unittest.TestCase):
             knxipframe.body.status_code, ErrorCode.E_NO_MORE_UNIQUE_CONNECTIONS
         )
 
-        knxipframe2 = KNXIPFrame(xknx)
-        knxipframe2.init(KNXIPServiceType.DISCONNECT_RESPONSE)
-        knxipframe2.body.communication_channel_id = 21
-        knxipframe2.body.status_code = ErrorCode.E_NO_MORE_UNIQUE_CONNECTIONS
-        knxipframe2.normalize()
+        disconnect_response = DisconnectResponse(
+            xknx,
+            communication_channel_id=21,
+            status_code=ErrorCode.E_NO_MORE_UNIQUE_CONNECTIONS,
+        )
+        knxipframe2 = KNXIPFrame.init_from_body(disconnect_response)
 
         self.assertEqual(knxipframe2.to_knx(), list(raw))
 
diff --git a/test/knxip_tests/header_test.py b/test/knxip_tests/header_test.py
index 9b9c0164..2f3e1bd0 100644
--- a/test/knxip_tests/header_test.py
+++ b/test/knxip_tests/header_test.py
@@ -24,8 +24,7 @@ class Test_KNXIP_Header(unittest.TestCase):
     def test_from_knx(self):
         """Test parsing and streaming wrong Header (wrong length byte)."""
         raw = (0x06, 0x10, 0x04, 0x21, 0x00, 0x0A)
-        xknx = XKNX()
-        header = KNXIPHeader(xknx)
+        header = KNXIPHeader()
         self.assertEqual(header.from_knx(raw), 6)
         self.assertEqual(header.header_length, 6)
         self.assertEqual(header.protocol_version, 16)
@@ -37,39 +36,35 @@ class Test_KNXIP_Header(unittest.TestCase):
     def test_set_length(self):
         """Test setting length."""
         xknx = XKNX()
-        header = KNXIPHeader(xknx)
+        header = KNXIPHeader()
         header.set_length(DisconnectRequest(xknx))
         # 6 (header) + 2 + 8 (HPAI length)
         self.assertEqual(header.total_length, 16)
 
     def test_set_length_error(self):
         """Test setting length with wrong type."""
-        xknx = XKNX()
-        header = KNXIPHeader(xknx)
+        header = KNXIPHeader()
         with self.assertRaises(TypeError):
             header.set_length(2)
 
     def test_from_knx_wrong_header(self):
         """Test parsing and streaming wrong Header (wrong length)."""
         raw = (0x06, 0x10, 0x04, 0x21, 0x00)
-        xknx = XKNX()
-        header = KNXIPHeader(xknx)
+        header = KNXIPHeader()
         with self.assertRaises(CouldNotParseKNXIP):
             header.from_knx(raw)
 
     def test_from_knx_wrong_header2(self):
         """Test parsing and streaming wrong Header (wrong length byte)."""
         raw = (0x05, 0x10, 0x04, 0x21, 0x00, 0x0A)
-        xknx = XKNX()
-        header = KNXIPHeader(xknx)
+        header = KNXIPHeader()
         with self.assertRaises(CouldNotParseKNXIP):
             header.from_knx(raw)
 
     def test_from_knx_wrong_header3(self):
         """Test parsing and streaming wrong Header (wrong protocol version)."""
         raw = (0x06, 0x11, 0x04, 0x21, 0x00, 0x0A)
-        xknx = XKNX()
-        header = KNXIPHeader(xknx)
+        header = KNXIPHeader()
         with self.assertRaises(CouldNotParseKNXIP):
             header.from_knx(raw)
 
@@ -77,7 +72,6 @@ class Test_KNXIP_Header(unittest.TestCase):
         """Test parsing and streaming wrong Header (unsupported service type)."""
         # 0x0000 as service type
         raw = (0x06, 0x10, 0x00, 0x00, 0x00, 0x0A)
-        xknx = XKNX()
-        header = KNXIPHeader(xknx)
+        header = KNXIPHeader()
         with self.assertRaises(CouldNotParseKNXIP):
             header.from_knx(raw)
diff --git a/test/knxip_tests/routing_indication_test.py b/test/knxip_tests/routing_indication_test.py
index 4a2dc6f0..e7909175 100644
--- a/test/knxip_tests/routing_indication_test.py
+++ b/test/knxip_tests/routing_indication_test.py
@@ -44,11 +44,10 @@ class Test_KNXIP(unittest.TestCase):
         """Test parsing and streaming CEMIFrame KNX/IP."""
         raw = bytes.fromhex("0610053000122900BCD012020151020040F0")
         xknx = XKNX()
+
         knxipframe = KNXIPFrame(xknx)
         knxipframe.from_knx(raw)
 
-        knxipframe.normalize()
-
         self.assertEqual(knxipframe.header.to_knx(), list(raw[0:6]))
         self.assertEqual(knxipframe.body.to_knx(), list(raw[6:]))
         self.assertEqual(knxipframe.to_knx(), list(raw))
@@ -56,9 +55,6 @@ class Test_KNXIP(unittest.TestCase):
     def test_telegram_set(self):
         """Test parsing and streaming CEMIFrame KNX/IP packet with DPTArray/DPTTime as payload."""
         xknx = XKNX()
-        knxipframe = KNXIPFrame(xknx)
-        knxipframe.init(KNXIPServiceType.ROUTING_INDICATION)
-        knxipframe.body.cemi.src_addr = IndividualAddress("1.2.2")
 
         telegram = Telegram(
             destination_address=GroupAddress(337),
@@ -66,11 +62,11 @@ class Test_KNXIP(unittest.TestCase):
                 DPTArray(DPTTime().to_knx(time.strptime("13:23:42", "%H:%M:%S")))
             ),
         )
-
-        knxipframe.body.cemi.telegram = telegram
-
-        knxipframe.body.cemi.set_hops(5)
-        knxipframe.normalize()
+        cemi = CEMIFrame(xknx, src_addr=IndividualAddress("1.2.2"))
+        cemi.telegram = telegram
+        cemi.set_hops(5)
+        routing_indication = RoutingIndication(xknx, cemi=cemi)
+        knxipframe = KNXIPFrame.init_from_body(routing_indication)
 
         raw = bytes.fromhex("0610053000142900BCD012020151040080 0d 17 2a")
 
@@ -117,12 +113,11 @@ class Test_KNXIP(unittest.TestCase):
             ),
         )
 
-        knxipframe2 = KNXIPFrame(xknx)
-        knxipframe2.init(KNXIPServiceType.ROUTING_INDICATION)
-        knxipframe2.body.cemi.src_addr = IndividualAddress("15.15.249")
-        knxipframe2.body.cemi.telegram = telegram
-        knxipframe2.body.cemi.set_hops(5)
-        knxipframe2.normalize()
+        cemi = CEMIFrame(xknx, src_addr=IndividualAddress("15.15.249"))
+        cemi.telegram = telegram
+        cemi.set_hops(5)
+        routing_indication = RoutingIndication(xknx, cemi=cemi)
+        knxipframe2 = KNXIPFrame.init_from_body(routing_indication)
 
         self.assertEqual(knxipframe2.header.to_knx(), list(raw[0:6]))
         self.assertEqual(knxipframe2.body.to_knx(), list(raw[6:]))
@@ -145,12 +140,11 @@ class Test_KNXIP(unittest.TestCase):
             ),
         )
 
-        knxipframe2 = KNXIPFrame(xknx)
-        knxipframe2.init(KNXIPServiceType.ROUTING_INDICATION)
-        knxipframe2.body.cemi.src_addr = IndividualAddress("15.15.249")
-        knxipframe2.body.cemi.telegram = telegram
-        knxipframe2.body.cemi.set_hops(5)
-        knxipframe2.normalize()
+        cemi = CEMIFrame(xknx, src_addr=IndividualAddress("15.15.249"))
+        cemi.telegram = telegram
+        cemi.set_hops(5)
+        routing_indication = RoutingIndication(xknx, cemi=cemi)
+        knxipframe2 = KNXIPFrame.init_from_body(routing_indication)
 
         self.assertEqual(knxipframe2.header.to_knx(), list(raw[0:6]))
         self.assertEqual(knxipframe2.body.to_knx(), list(raw[6:]))
@@ -173,12 +167,11 @@ class Test_KNXIP(unittest.TestCase):
             ),
         )
 
-        knxipframe2 = KNXIPFrame(xknx)
-        knxipframe2.init(KNXIPServiceType.ROUTING_INDICATION)
-        knxipframe2.body.cemi.src_addr = IndividualAddress("15.15.249")
-        knxipframe2.body.cemi.telegram = telegram
-        knxipframe2.body.cemi.set_hops(5)
-        knxipframe2.normalize()
+        cemi = CEMIFrame(xknx, src_addr=IndividualAddress("15.15.249"))
+        cemi.telegram = telegram
+        cemi.set_hops(5)
+        routing_indication = RoutingIndication(xknx, cemi=cemi)
+        knxipframe2 = KNXIPFrame.init_from_body(routing_indication)
 
         self.assertEqual(knxipframe2.header.to_knx(), list(raw[0:6]))
         self.assertEqual(knxipframe2.body.to_knx(), list(raw[6:]))
@@ -201,12 +194,11 @@ class Test_KNXIP(unittest.TestCase):
             ),
         )
 
-        knxipframe2 = KNXIPFrame(xknx)
-        knxipframe2.init(KNXIPServiceType.ROUTING_INDICATION)
-        knxipframe2.body.cemi.src_addr = IndividualAddress("1.4.2")
-        knxipframe2.body.cemi.telegram = telegram
-        knxipframe2.body.cemi.set_hops(5)
-        knxipframe2.normalize()
+        cemi = CEMIFrame(xknx, src_addr=IndividualAddress("1.4.2"))
+        cemi.telegram = telegram
+        cemi.set_hops(5)
+        routing_indication = RoutingIndication(xknx, cemi=cemi)
+        knxipframe2 = KNXIPFrame.init_from_body(routing_indication)
 
         self.assertEqual(knxipframe2.header.to_knx(), list(raw[0:6]))
         self.assertEqual(knxipframe2.body.to_knx(), list(raw[6:]))
@@ -229,12 +221,11 @@ class Test_KNXIP(unittest.TestCase):
             ),
         )
 
-        knxipframe2 = KNXIPFrame(xknx)
-        knxipframe2.init(KNXIPServiceType.ROUTING_INDICATION)
-        knxipframe2.body.cemi.src_addr = IndividualAddress("15.15.249")
-        knxipframe2.body.cemi.telegram = telegram
-        knxipframe2.body.cemi.set_hops(5)
-        knxipframe2.normalize()
+        cemi = CEMIFrame(xknx, src_addr=IndividualAddress("15.15.249"))
+        cemi.telegram = telegram
+        cemi.set_hops(5)
+        routing_indication = RoutingIndication(xknx, cemi=cemi)
+        knxipframe2 = KNXIPFrame.init_from_body(routing_indication)
 
         self.assertEqual(knxipframe2.header.to_knx(), list(raw[0:6]))
         self.assertEqual(knxipframe2.body.to_knx(), list(raw[6:]))
@@ -257,12 +248,11 @@ class Test_KNXIP(unittest.TestCase):
             ),
         )
 
-        knxipframe2 = KNXIPFrame(xknx)
-        knxipframe2.init(KNXIPServiceType.ROUTING_INDICATION)
-        knxipframe2.body.cemi.src_addr = IndividualAddress("1.3.1")
-        knxipframe2.body.cemi.telegram = telegram
-        knxipframe2.body.cemi.set_hops(5)
-        knxipframe2.normalize()
+        cemi = CEMIFrame(xknx, src_addr=IndividualAddress("1.3.1"))
+        cemi.telegram = telegram
+        cemi.set_hops(5)
+        routing_indication = RoutingIndication(xknx, cemi=cemi)
+        knxipframe2 = KNXIPFrame.init_from_body(routing_indication)
 
         self.assertEqual(knxipframe2.header.to_knx(), list(raw[0:6]))
         self.assertEqual(knxipframe2.body.to_knx(), list(raw[6:]))
@@ -276,12 +266,12 @@ class Test_KNXIP(unittest.TestCase):
             source_address=IndividualAddress("1.3.1"),
         )
         xknx = XKNX()
-        knxipframe = KNXIPFrame(xknx)
-        knxipframe.init(KNXIPServiceType.ROUTING_INDICATION)
-        knxipframe.body.cemi.src_addr = IndividualAddress("1.3.1")
-        knxipframe.body.cemi.telegram = telegram
-        knxipframe.body.cemi.set_hops(5)
-        knxipframe.normalize()
+
+        cemi = CEMIFrame(xknx, src_addr=IndividualAddress("1.3.1"))
+        cemi.telegram = telegram
+        cemi.set_hops(5)
+        routing_indication = RoutingIndication(xknx, cemi=cemi)
+        knxipframe = KNXIPFrame.init_from_body(routing_indication)
 
         raw = bytes.fromhex("0610053000112900BCD0130101510100BF")
 
diff --git a/test/knxip_tests/search_request_test.py b/test/knxip_tests/search_request_test.py
index 2ad0be8b..b174f790 100644
--- a/test/knxip_tests/search_request_test.py
+++ b/test/knxip_tests/search_request_test.py
@@ -3,7 +3,7 @@ import asyncio
 import unittest
 
 from xknx import XKNX
-from xknx.knxip import HPAI, KNXIPFrame, KNXIPServiceType, SearchRequest
+from xknx.knxip import HPAI, KNXIPFrame, SearchRequest
 
 
 class Test_KNXIP_Discovery(unittest.TestCase):
@@ -47,9 +47,10 @@ class Test_KNXIP_Discovery(unittest.TestCase):
             knxipframe.body.discovery_endpoint, HPAI(ip_addr="224.0.23.12", port=3671)
         )
 
-        knxipframe2 = KNXIPFrame(xknx)
-        knxipframe2.init(KNXIPServiceType.SEARCH_REQUEST)
-        knxipframe2.body.discovery_endpoint = HPAI(ip_addr="224.0.23.12", port=3671)
-        knxipframe2.normalize()
+        knxipframe2 = KNXIPFrame.init_from_body(
+            SearchRequest(
+                xknx, discovery_endpoint=HPAI(ip_addr="224.0.23.12", port=3671)
+            )
+        )
 
         self.assertEqual(knxipframe2.to_knx(), list(raw))
diff --git a/test/knxip_tests/search_response_test.py b/test/knxip_tests/search_response_test.py
index a8d7d659..f625a6df 100644
--- a/test/knxip_tests/search_response_test.py
+++ b/test/knxip_tests/search_response_test.py
@@ -9,7 +9,6 @@ from xknx.knxip import (
     DIBServiceFamily,
     DIBSuppSVCFamilies,
     KNXIPFrame,
-    KNXIPServiceType,
     SearchResponse,
 )
 
@@ -132,12 +131,13 @@ class Test_KNXIP_Discovery(unittest.TestCase):
             knxipframe.body.dibs[1].supports(DIBServiceFamily.OBJECT_SERVER)
         )
 
-        knxipframe2 = KNXIPFrame(xknx)
-        knxipframe2.init(KNXIPServiceType.SEARCH_RESPONSE)
-        knxipframe2.body.control_endpoint = HPAI(ip_addr="192.168.42.10", port=3671)
-        knxipframe2.body.dibs.append(knxipframe.body.dibs[0])
-        knxipframe2.body.dibs.append(knxipframe.body.dibs[1])
-        knxipframe2.normalize()
+        search_response = SearchResponse(
+            xknx, control_endpoint=HPAI(ip_addr="192.168.42.10", port=3671)
+        )
+        search_response.dibs.append(knxipframe.body.dibs[0])
+        search_response.dibs.append(knxipframe.body.dibs[1])
+        knxipframe2 = KNXIPFrame.init_from_body(search_response)
+
         self.assertEqual(knxipframe2.to_knx(), list(raw))
 
     def test_unknown_device_name(self):
diff --git a/test/knxip_tests/tunnelling_ack_test.py b/test/knxip_tests/tunnelling_ack_test.py
index df57439f..4c6efa82 100644
--- a/test/knxip_tests/tunnelling_ack_test.py
+++ b/test/knxip_tests/tunnelling_ack_test.py
@@ -4,7 +4,7 @@ import unittest
 
 from xknx import XKNX
 from xknx.exceptions import CouldNotParseKNXIP
-from xknx.knxip import ErrorCode, KNXIPFrame, KNXIPServiceType, TunnellingAck
+from xknx.knxip import ErrorCode, KNXIPFrame, TunnellingAck
 
 
 class Test_KNXIP_TunnelingReq(unittest.TestCase):
@@ -33,11 +33,10 @@ class Test_KNXIP_TunnelingReq(unittest.TestCase):
         self.assertEqual(knxipframe.body.sequence_counter, 23)
         self.assertEqual(knxipframe.body.status_code, ErrorCode.E_NO_ERROR)
 
-        knxipframe2 = KNXIPFrame(xknx)
-        knxipframe2.init(KNXIPServiceType.TUNNELLING_ACK)
-        knxipframe2.body.communication_channel_id = 42
-        knxipframe2.body.sequence_counter = 23
-        knxipframe2.normalize()
+        tunnelling_ack = TunnellingAck(
+            xknx, communication_channel_id=42, sequence_counter=23
+        )
+        knxipframe2 = KNXIPFrame.init_from_body(tunnelling_ack)
 
         self.assertEqual(knxipframe2.to_knx(), list(raw))
 
diff --git a/test/knxip_tests/tunnelling_request_test.py b/test/knxip_tests/tunnelling_request_test.py
index 8cbc92d5..90bbf6d9 100644
--- a/test/knxip_tests/tunnelling_request_test.py
+++ b/test/knxip_tests/tunnelling_request_test.py
@@ -5,7 +5,7 @@ import unittest
 from xknx import XKNX
 from xknx.dpt import DPTBinary
 from xknx.exceptions import CouldNotParseKNXIP
-from xknx.knxip import CEMIFrame, KNXIPFrame, KNXIPServiceType, TunnellingRequest
+from xknx.knxip import CEMIFrame, CEMIMessageCode, KNXIPFrame, TunnellingRequest
 from xknx.telegram import GroupAddress, Telegram
 from xknx.telegram.apci import GroupValueWrite
 
@@ -66,14 +66,15 @@ class Test_KNXIP_TunnelingReq(unittest.TestCase):
             ),
         )
 
-        knxipframe2 = KNXIPFrame(xknx)
-        knxipframe2.init(KNXIPServiceType.TUNNELLING_REQUEST)
-        knxipframe2.body.cemi.telegram = Telegram(
+        cemi = CEMIFrame(xknx, code=CEMIMessageCode.L_Data_REQ)
+        cemi.telegram = Telegram(
             destination_address=GroupAddress("9/0/8"),
             payload=GroupValueWrite(DPTBinary(1)),
         )
-        knxipframe2.body.sequence_counter = 23
-        knxipframe2.normalize()
+        tunnelling_request = TunnellingRequest(
+            xknx, communication_channel_id=1, sequence_counter=23, cemi=cemi
+        )
+        knxipframe2 = KNXIPFrame.init_from_body(tunnelling_request)
 
         self.assertEqual(knxipframe2.to_knx(), list(raw))
 
diff --git a/test/str_test.py b/test/str_test.py
index 5ca9751e..3e4c4444 100644
--- a/test/str_test.py
+++ b/test/str_test.py
@@ -534,8 +534,7 @@ class TestStringRepresentations(unittest.TestCase):
 
     def test_header(self):
         """Test string representation of KNX/IP-Header."""
-        xknx = XKNX()
-        header = KNXIPHeader(xknx)
+        header = KNXIPHeader()
         header.total_length = 42
         self.assertEqual(
             str(header),
@@ -633,8 +632,8 @@ class TestStringRepresentations(unittest.TestCase):
         self.assertEqual(
             str(search_response),
             '<SearchResponse control_endpoint="<HPAI 192.168.42.1:33941 />" dibs="[\n'
-            '<DIB dtc="None" data="" />,\n'
-            '<DIB dtc="None" data="" />\n'
+            '<DIB dtc="0" data="" />,\n'
+            '<DIB dtc="0" data="" />\n'
             ']" />',
         )