Repository: XKNX/xknx
Pull request #1077 (2022-11-20 07:29:40)

Move individual address configuration to ConnectionConfig
<!--
  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.
-->
Removed `own_address` from `XKNX` class. `ConnectionConfig` `individual_address` can be used to set a source address for routing instead.

In a later addition this individual address can be used to look up a secure interface from a keyfile when using secure tunnelling.

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:

- [ ] The documentation has been adjusted accordingly
- [ ] Tests have been added that prove the fix is effective or that the feature works
- [x] The changes are documented in the changelog (docs/changelog.md)

codecov[bot]: # [Codecov](https://codecov.io/gh/XKNX/xknx/pull/1077?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) Report
> Merging [#1077](https://codecov.io/gh/XKNX/xknx/pull/1077?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) (637aa7a) into [main](https://codecov.io/gh/XKNX/xknx/commit/5a1b5112c8908d8b7f9460f4b2c17178028eea8d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) (5a1b511) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/XKNX/xknx/pull/1077/graphs/tree.svg?width=650&height=150&src=pr&token=irWbIygS84&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX)](https://codecov.io/gh/XKNX/xknx/pull/1077?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX)

```diff
@@           Coverage Diff           @@
##             main    #1077   +/-   ##
=======================================
  Coverage   96.36%   96.36%           
=======================================
  Files         138      138           
  Lines        8581     8594   +13     
=======================================
+ Hits         8269     8282   +13     
  Misses        312      312           
```


| [Impacted Files](https://codecov.io/gh/XKNX/xknx/pull/1077?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) | Coverage Δ | |
|---|---|---|
| [xknx/xknx.py](https://codecov.io/gh/XKNX/xknx/pull/1077/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC94a254LnB5) | `88.29% <ø> (-0.25%)` | :arrow_down: |
| [xknx/io/connection.py](https://codecov.io/gh/XKNX/xknx/pull/1077/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9pby9jb25uZWN0aW9uLnB5) | `97.56% <100.00%> (+0.26%)` | :arrow_up: |
| [xknx/io/const.py](https://codecov.io/gh/XKNX/xknx/pull/1077/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9pby9jb25zdC5weQ==) | `100.00% <100.00%> (ø)` | |
| [xknx/io/knxip\_interface.py](https://codecov.io/gh/XKNX/xknx/pull/1077/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9pby9rbnhpcF9pbnRlcmZhY2UucHk=) | `85.63% <100.00%> (+0.31%)` | :arrow_up: |
| [xknx/io/routing.py](https://codecov.io/gh/XKNX/xknx/pull/1077/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9pby9yb3V0aW5nLnB5) | `90.90% <100.00%> (+0.28%)` | :arrow_up: |
| [xknx/io/tunnel.py](https://codecov.io/gh/XKNX/xknx/pull/1077/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9pby90dW5uZWwucHk=) | `88.72% <100.00%> (ø)` | |


Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index 64064b73..14b00d77 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -8,6 +8,10 @@ nav_order: 2
 
 # Unreleased changes
 
+### Internal
+
+- Removed `own_address` from `XKNX` class. `ConnectionConfig` `individual_address` can be used to set a source address for routing instead.
+
 ### Connection
 
 - GatewayScanFilter now also matches secure enabled gateways by default. The `secure` argument as been replaced by `secure_tunnelling` and `secure_routing` arguments. When multiple methods are `True` a gateway is matched if one of them is supported. Non-secure methods don't match if secure is required for that gateway.
@@ -24,11 +28,21 @@ nav_order: 2
 - Fix wrong length of AuthorizeRequest.
 - Raise sane error messages in Management.
 
+# Internal
+
+- Self description queries more information from Core v2 devices via SearchRequestExtended.
+
 # Bugfixes
 
 - No mutable default arguments. Fixes unexpected behaviour like GatewayScanner not finding all interfaces.
 - Separate discovery multicast group from routing group. Add `multicast_group` and `multicast_port` `ConnectionConfig` parameters.
 
+# 1.2.1 Hotfix release 2022-11-20
+
+### Bugfixes
+
+- Fix Latency parsing in .knxkeys keyring files
+
 # 1.2.0 Secure Routing 2022-10-10
 
 ### Features
diff --git a/docs/xknx.md b/docs/xknx.md
index f8f75dff..2efb2e33 100644
--- a/docs/xknx.md
+++ b/docs/xknx.md
@@ -20,7 +20,6 @@ The `XKNX()` object is the core element of any XKNX installation. It should be o
 
 ```python
 xknx = XKNX(
-    own_address=DEFAULT_ADDRESS,
     address_format=GroupAddressType.LONG,
     connection_state_changed_cb=None,
     telegram_received_cb=None,
@@ -37,7 +36,6 @@ xknx = XKNX(
 
 The constructor of the XKNX object takes several parameters:
 
-- `own_address` may be used to specify the individual (physical) KNX address of the XKNX daemon. If not speficied it uses `15.15.250`.
 - `address_format` may be used to specify the type of group addresses to use. Possible values are:
   - FREE: integer or hex representation
   - SHORT: representation like '1/34' without middle groups
diff --git a/xknx/__version__.py b/xknx/__version__.py
index 59c7d334..1818cb74 100644
--- a/xknx/__version__.py
+++ b/xknx/__version__.py
@@ -1,3 +1,3 @@
 """XKNX version."""
 
-__version__ = "1.2.0"
+__version__ = "1.2.1"
diff --git a/xknx/io/connection.py b/xknx/io/connection.py
index 4cac67b6..97b8109b 100644
--- a/xknx/io/connection.py
+++ b/xknx/io/connection.py
@@ -3,6 +3,8 @@ from __future__ import annotations
 
 from enum import Enum, auto
 
+from xknx.telegram.address import IndividualAddress, IndividualAddressableType
+
 from .const import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT
 from .gateway_scanner import GatewayScanFilter
 
@@ -28,6 +30,7 @@ class ConnectionConfig:
         * ROUTING use KNX/IP multicast routing.
         * TUNNELING connect to a specific KNX/IP tunneling device via UDP.
         * TUNNELING_TCP connect to a specific KNX/IP tunneling v2 device via TCP.
+    * individual address: the individual address used as source address for routing.
     * local_ip: Local ip of the interface though which KNXIPInterface should connect.
     * gateway_ip: IP of KNX/IP tunneling device.
     * gateway_port: Port of KNX/IP tunneling device.
@@ -47,6 +50,7 @@ class ConnectionConfig:
         self,
         *,
         connection_type: ConnectionType = ConnectionType.AUTOMATIC,
+        individual_address: IndividualAddressableType | None = None,
         local_ip: str | None = None,
         local_port: int = 0,
         gateway_ip: str | None = None,
@@ -62,6 +66,9 @@ class ConnectionConfig:
     ):
         """Initialize ConnectionConfig class."""
         self.connection_type = connection_type
+        self.individual_address = (
+            IndividualAddress(individual_address) if individual_address else None
+        )
         self.local_ip = local_ip
         self.local_port = local_port
         self.gateway_ip = gateway_ip
diff --git a/xknx/io/const.py b/xknx/io/const.py
index ef1f7e5e..441f5d8b 100644
--- a/xknx/io/const.py
+++ b/xknx/io/const.py
@@ -1,14 +1,18 @@
 """KNX Constants used within io."""
+from typing import Final
 
-DEFAULT_MCAST_GRP = "224.0.23.12"
-DEFAULT_MCAST_PORT = 3671
+from xknx.telegram.address import IndividualAddress
 
-CONNECTION_ALIVE_TIME = 120
-CONNECTIONSTATE_REQUEST_TIMEOUT = 10
-HEARTBEAT_RATE = CONNECTION_ALIVE_TIME - (CONNECTIONSTATE_REQUEST_TIMEOUT * 5)
+DEFAULT_INDIVIDUAL_ADDRESS: Final = IndividualAddress("15.15.250")
+DEFAULT_MCAST_GRP: Final = "224.0.23.12"
+DEFAULT_MCAST_PORT: Final = 3671
+
+CONNECTION_ALIVE_TIME: Final = 120
+CONNECTIONSTATE_REQUEST_TIMEOUT: Final = 10
+HEARTBEAT_RATE: Final = CONNECTION_ALIVE_TIME - (CONNECTIONSTATE_REQUEST_TIMEOUT * 5)
 
 # Maximum time an authenticated secure session may remain unused (without
 # any communication over this session) until the session will be dropped.
-SESSION_TIMEOUT = 60
-SESSION_KEEPALIVE_RATE = SESSION_TIMEOUT - 10
-XKNX_SERIAL_NUMBER = bytes.fromhex("00 00 78 6b 6e 78")
+SESSION_TIMEOUT: Final = 60
+SESSION_KEEPALIVE_RATE: Final = SESSION_TIMEOUT - 10
+XKNX_SERIAL_NUMBER: Final = bytes.fromhex("00 00 78 6b 6e 78")
diff --git a/xknx/io/gateway_scanner.py b/xknx/io/gateway_scanner.py
index e5ab9d6f..834585a8 100644
--- a/xknx/io/gateway_scanner.py
+++ b/xknx/io/gateway_scanner.py
@@ -72,6 +72,7 @@ class GatewayDescriptor:
         self.supports_tunnelling_tcp = supports_tunnelling_tcp
         self.supports_secure = supports_secure
 
+        self.core_version: int = 0
         self.routing_requires_secure: bool | None = None
         self.tunnelling_requires_secure: bool | None = None
         self.tunnelling_slots: dict[IndividualAddress, TunnelingSlotStatus] = {}
@@ -84,12 +85,11 @@ class GatewayDescriptor:
                 self.individual_address = dib.individual_address
                 continue
             if isinstance(dib, DIBSuppSVCFamilies):
+                self.core_version = dib.version(DIBServiceFamily.CORE) or 0
                 self.supports_routing = dib.supports(DIBServiceFamily.ROUTING)
-                if dib.supports(DIBServiceFamily.TUNNELING):
+                if _tunnelling_version := dib.version(DIBServiceFamily.TUNNELING):
                     self.supports_tunnelling = True
-                    self.supports_tunnelling_tcp = dib.supports(
-                        DIBServiceFamily.TUNNELING, version=2
-                    )
+                    self.supports_tunnelling_tcp = _tunnelling_version >= 2
                 self.supports_secure = dib.supports(
                     DIBServiceFamily.SECURITY, version=1
                 )
@@ -114,6 +114,7 @@ class GatewayDescriptor:
             f"    individual_address={self.individual_address}\n"
             f"    local_interface={self.local_interface},\n"
             f"    local_ip={self.local_ip},\n"
+            f"    core_version={self.core_version},\n"
             f"    supports_routing={self.supports_routing},\n"
             f"    supports_tunnelling={self.supports_tunnelling},\n"
             f"    supports_tunnelling_tcp={self.supports_tunnelling_tcp},\n"
diff --git a/xknx/io/knxip_interface.py b/xknx/io/knxip_interface.py
index 510e84f3..068dd401 100644
--- a/xknx/io/knxip_interface.py
+++ b/xknx/io/knxip_interface.py
@@ -25,7 +25,7 @@ from xknx.secure import load_key_ring
 from xknx.telegram import IndividualAddress, Telegram
 
 from .connection import ConnectionConfig, ConnectionType
-from .const import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT
+from .const import DEFAULT_INDIVIDUAL_ADDRESS, DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT
 from .gateway_scanner import GatewayDescriptor, GatewayScanner
 from .routing import Routing, SecureRouting
 from .tunnel import SecureTunnel, TCPTunnel, UDPTunnel, _Tunnel
@@ -316,17 +316,21 @@ class KNXIPInterface:
         if local_ip is None:
             raise XKNXException("No network interface found.")
         util.validate_ip(local_ip, address_name="Local IP address")
+        individual_address = (
+            self.connection_config.individual_address or DEFAULT_INDIVIDUAL_ADDRESS
+        )
 
         logger.debug(
             "Starting Routing from %s as %s via %s:%s",
             local_ip,
-            self.xknx.own_address,
+            individual_address,
             multicast_group,
             multicast_port,
         )
         self._interface = Routing(
             self.xknx,
-            self.telegram_received,
+            individual_address=individual_address,
+            telegram_received_callback=self.telegram_received,
             local_ip=local_ip,
             multicast_group=multicast_group,
             multicast_port=multicast_port,
@@ -346,17 +350,21 @@ class KNXIPInterface:
         if local_ip is None:
             raise XKNXException("No network interface found.")
         util.validate_ip(local_ip, address_name="Local IP address")
+        individual_address = (
+            self.connection_config.individual_address or DEFAULT_INDIVIDUAL_ADDRESS
+        )
 
         logger.debug(
             "Starting Secure Routing from %s as %s via %s:%s",
             local_ip,
-            self.xknx.own_address,
+            individual_address,
             multicast_group,
             multicast_port,
         )
         self._interface = SecureRouting(
             self.xknx,
-            self.telegram_received,
+            individual_address=individual_address,
+            telegram_received_callback=self.telegram_received,
             local_ip=local_ip,
             backbone_key=backbone_key,
             latency_ms=latency_ms,
diff --git a/xknx/io/routing.py b/xknx/io/routing.py
index 16b4082b..4cbf6b53 100644
--- a/xknx/io/routing.py
+++ b/xknx/io/routing.py
@@ -24,9 +24,9 @@ from xknx.knxip import (
     RoutingIndication,
     RoutingLostMessage,
 )
-from xknx.telegram import Telegram, TelegramDirection
+from xknx.telegram import IndividualAddress, Telegram, TelegramDirection
 
-from .const import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT
+from .const import DEFAULT_INDIVIDUAL_ADDRESS, DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT
 from .interface import Interface, TelegramCallbackType
 from .ip_secure import SecureGroup
 from .transport import KNXIPTransport, UDPTransport
@@ -138,6 +138,7 @@ class Routing(Interface):
     def __init__(
         self,
         xknx: XKNX,
+        individual_address: IndividualAddress | None,
         telegram_received_callback: TelegramCallbackType,
         local_ip: str,
         multicast_group: str = DEFAULT_MCAST_GRP,
@@ -145,6 +146,7 @@ class Routing(Interface):
     ):
         """Initialize Routing class."""
         self.xknx = xknx
+        self.individual_address = individual_address or DEFAULT_INDIVIDUAL_ADDRESS
         self.telegram_received_callback = telegram_received_callback
         self.local_ip = local_ip
         self.multicast_group = multicast_group
@@ -177,7 +179,7 @@ class Routing(Interface):
 
     async def connect(self) -> bool:
         """Start routing."""
-        self.xknx.current_address = self.xknx.own_address
+        self.xknx.current_address = self.individual_address
         await self.xknx.connection_manager.connection_state_changed(
             XknxConnectionState.CONNECTING
         )
@@ -219,7 +221,7 @@ class Routing(Interface):
         cemi = CEMIFrame.init_from_telegram(
             telegram=telegram,
             code=CEMIMessageCode.L_DATA_IND,
-            src_addr=self.xknx.own_address,
+            src_addr=self.individual_address,
         )
         routing_indication = RoutingIndication(cemi=cemi)
 
@@ -258,7 +260,7 @@ class Routing(Interface):
         if routing_indication.cemi is None:
             # Don't handle invalid cemi frames (None)
             return
-        if routing_indication.cemi.src_addr == self.xknx.own_address:
+        if routing_indication.cemi.src_addr == self.individual_address:
             logger.debug("Ignoring own packet")
             return
 
@@ -283,6 +285,7 @@ class SecureRouting(Routing):
     def __init__(
         self,
         xknx: XKNX,
+        individual_address: IndividualAddress | None,
         telegram_received_callback: TelegramCallbackType,
         local_ip: str,
         backbone_key: bytes,
@@ -295,7 +298,8 @@ class SecureRouting(Routing):
         self.latency_ms = latency_ms or DEFAULT_LATENCY_TOLERANCE_MS
         super().__init__(
             xknx,
-            telegram_received_callback,
+            individual_address=individual_address,
+            telegram_received_callback=telegram_received_callback,
             local_ip=local_ip,
             multicast_group=multicast_group,
             multicast_port=multicast_port,
diff --git a/xknx/io/self_description.py b/xknx/io/self_description.py
index 7dc3b02c..24096891 100644
--- a/xknx/io/self_description.py
+++ b/xknx/io/self_description.py
@@ -1,6 +1,7 @@
 """Abstraction to send DescriptionRequest and wait for DescriptionResponse."""
 from __future__ import annotations
 
+from abc import ABC, abstractmethod
 import asyncio
 import logging
 from typing import TYPE_CHECKING, Final
@@ -8,7 +9,16 @@ from typing import TYPE_CHECKING, Final
 from xknx.exceptions import CommunicationError
 from xknx.io import util
 from xknx.io.gateway_scanner import GatewayDescriptor
-from xknx.knxip import HPAI, DescriptionRequest, DescriptionResponse, KNXIPFrame
+from xknx.knxip import (
+    HPAI,
+    SRP,
+    DescriptionRequest,
+    DescriptionResponse,
+    DIBTypeCode,
+    KNXIPFrame,
+    SearchRequestExtended,
+    SearchResponseExtended,
+)
 
 from .const import DEFAULT_MCAST_PORT
 from .transport import UDPTransport
@@ -47,25 +57,38 @@ async def request_description(
     except OSError as err:
         logger.warning("Could not setup socket: %s", err)
         return None
-
-    local_hpai: HPAI
-    if route_back:
-        local_hpai = HPAI()
     else:
-        local_addr = transport.getsockname()
-        local_hpai = HPAI(*local_addr)
+        local_hpai: HPAI
+        if route_back:
+            local_hpai = HPAI()
+        else:
+            local_addr = transport.getsockname()
+            local_hpai = HPAI(*local_addr)
+
+        description_query = DescriptionQuery(
+            transport=transport,
+            local_hpai=local_hpai,
+        )
+        await description_query.start()
+        gateway = description_query.gateway_descriptor
+        if gateway is None:
+            return None
+        if gateway.core_version >= 2:
+            search_extended_query = SearchExtendedQuery(
+                transport=transport,
+                local_hpai=local_hpai,
+            )
+            await search_extended_query.start()
+            gateway = search_extended_query.gateway_descriptor
 
-    description_query = DescriptionQuery(
-        transport=transport,
-        local_hpai=local_hpai,
-    )
-    await description_query.start()
-    transport.stop()
-    return description_query.gateway_descriptor
+        return gateway
+    finally:
+        transport.stop()
 
 
-class DescriptionQuery:
-    """Class to send a DescriptionRequest and wait for DescriptionResponse."""
+class _SelfDescriptionQuery(ABC):
+
+    expected_response_class: type[DescriptionResponse] | type[SearchResponseExtended]
 
     def __init__(
         self,
@@ -79,18 +102,18 @@ class DescriptionQuery:
         self.gateway_descriptor: GatewayDescriptor | None = None
         self.response_received_event = asyncio.Event()
 
+    @abstractmethod
     def create_knxipframe(self) -> KNXIPFrame:
         """Create KNX/IP Frame object to be sent to device."""
-        description_request = DescriptionRequest(control_endpoint=self.local_hpai)
-        return KNXIPFrame.init_from_body(description_request)
 
     async def start(self) -> None:
         """Start. Send request and wait for an answer."""
         callb = self.transport.register_callback(
-            self.response_rec_callback, [DescriptionResponse.SERVICE_TYPE]
+            self.response_rec_callback, [self.expected_response_class.SERVICE_TYPE]
         )
+        frame = self.create_knxipframe()
         try:
-            self.transport.send(self.create_knxipframe())
+            self.transport.send(frame)
             await asyncio.wait_for(
                 self.response_received_event.wait(),
                 timeout=DESCRIPTION_TIMEOUT,
@@ -102,7 +125,7 @@ class DescriptionQuery:
                 self.__class__.__name__,
             )
         except CommunicationError as err:
-            logger.warning("Sending DescriptionRequest failed: %s", err)
+            logger.warning("Sending %s failed: %s", frame.body.__class__.__name__, err)
         finally:
             # cleanup to not leave callbacks (for asyncio.CancelledError)
             self.transport.unregister_callback(callb)
@@ -111,8 +134,10 @@ class DescriptionQuery:
         self, knxipframe: KNXIPFrame, source: HPAI, _: KNXIPTransport
     ) -> None:
         """Verify and handle knxipframe. Callback from internal transport."""
-        if not isinstance(knxipframe.body, DescriptionResponse):
-            logger.warning("Wrong knxipframe for DescriptionRequest: %s", knxipframe)
+        if not isinstance(knxipframe.body, self.expected_response_class):
+            logger.warning(
+                "Wrong knxipframe for %s: %s", self.__class__.__name__, knxipframe
+            )
             return
         self.response_received_event.set()
         # Set gateway descriptior attribute
@@ -121,5 +146,39 @@ class DescriptionQuery:
             port=self.transport.remote_addr[1],
             local_ip=self.transport.getsockname()[0],
         )
-        gateway.parse_dibs(knxipframe.body.dibs)
+        gateway.parse_dibs(knxipframe.body.dibs)  # type: ignore[union-attr]
         self.gateway_descriptor = gateway
+
+
+class DescriptionQuery(_SelfDescriptionQuery):
+    """Class to send a DescriptionRequest and wait for DescriptionResponse."""
+
+    expected_response_class = DescriptionResponse
+
+    def create_knxipframe(self) -> KNXIPFrame:
+        """Create KNX/IP Frame object to be sent to device."""
+        description_request = DescriptionRequest(control_endpoint=self.local_hpai)
+        return KNXIPFrame.init_from_body(description_request)
+
+
+class SearchExtendedQuery(_SelfDescriptionQuery):
+    """Class to send a SearchRequestExtended and wait for SearchResponseExtended to a single device."""
+
+    expected_response_class = SearchResponseExtended
+
+    def create_knxipframe(self) -> KNXIPFrame:
+        """Create KNX/IP Frame object to be sent to device."""
+        search_extended_request = SearchRequestExtended(
+            discovery_endpoint=self.local_hpai,
+            srps=[
+                SRP.request_device_description(
+                    [
+                        DIBTypeCode.DEVICE_INFO,
+                        DIBTypeCode.SUPP_SVC_FAMILIES,
+                        DIBTypeCode.SECURED_SERVICE_FAMILIES,
+                        DIBTypeCode.TUNNELING_INFO,
+                    ]
+                )
+            ],
+        )
+        return KNXIPFrame.init_from_body(search_extended_request)
diff --git a/xknx/io/tunnel.py b/xknx/io/tunnel.py
index 10e68b05..202bc083 100644
--- a/xknx/io/tunnel.py
+++ b/xknx/io/tunnel.py
@@ -70,7 +70,7 @@ class _Tunnel(Interface):
         self._initial_connection = True
         self._is_reconnecting = False
         self._reconnect_task: asyncio.Task[None] | None = None
-        self._src_address = xknx.own_address
+        self._src_address = IndividualAddress(0)
         self._send_telegram_lock = asyncio.Lock()
         self._tunnelling_request_confirmation_event = asyncio.Event()
 
@@ -200,7 +200,7 @@ class _Tunnel(Interface):
             self._src_address = IndividualAddress(connect.identifier)
             self.xknx.current_address = self._src_address
             logger.debug(
-                "Tunnel established communication_channel=%s, address=%s",
+                "Tunnel established. communication_channel=%s, address=%s",
                 connect.communication_channel,
                 self._src_address,
             )
@@ -377,7 +377,7 @@ class _Tunnel(Interface):
         self, disconnect_request: DisconnectRequest
     ) -> None:
         """Handle incoming disconnect request."""
-        logger.warning("Received DisconnectRequest from tunnelling sever.")
+        logger.warning("Received DisconnectRequest from tunnelling server.")
         # 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:
diff --git a/xknx/knxip/dib.py b/xknx/knxip/dib.py
index 1ade4b19..9ac4f6ab 100644
--- a/xknx/knxip/dib.py
+++ b/xknx/knxip/dib.py
@@ -251,6 +251,13 @@ class _DIBServiceFamilies(DIB):
                     return True
         return False
 
+    def version(self, name: DIBServiceFamily) -> int | None:
+        """Return version of a given service family."""
+        return next(
+            (family.version for family in self.families if name == family.name),
+            None,
+        )
+
     def calculated_length(self) -> int:
         """Get length of KNX/IP object."""
         return len(self.families) * 2 + DIB_HEADER_LENGTH
diff --git a/xknx/secure/keyring.py b/xknx/secure/keyring.py
index 2459def8..e2d3e0c6 100644
--- a/xknx/secure/keyring.py
+++ b/xknx/secure/keyring.py
@@ -129,16 +129,17 @@ class XMLInterface(AttributeReader):
 class XMLBackbone(AttributeReader):
     """Backbone in a knxkeys file."""
 
-    decrypted_key: bytes
-    key: str
-    latency: int
-    multicast_address: str
+    decrypted_key: bytes | None = None
+    key: str | None = None
+    latency: int | None = None
+    multicast_address: str | None = None
 
     def parse_xml(self, node: Document) -> None:
         """Parse all needed attributes from the given node map."""
         attributes = node.attributes
         self.key = self.get_attribute_value(attributes.get("Key"))
-        self.latency = int(self.get_attribute_value(attributes.get("Latency")))
+        if latency := self.get_attribute_value(attributes.get("Latency")):
+            self.latency = int(latency)
         self.multicast_address = self.get_attribute_value(
             attributes.get("MulticastAddress")
         )
diff --git a/xknx/xknx.py b/xknx/xknx.py
index 80027987..12aacf26 100644
--- a/xknx/xknx.py
+++ b/xknx/xknx.py
@@ -36,12 +36,10 @@ logger = logging.getLogger("xknx.log")
 class XKNX:
     """Class for reading and writing KNX/IP packets."""
 
-    DEFAULT_ADDRESS = "15.15.250"
     DEFAULT_RATE_LIMIT = 20
 
     def __init__(
         self,
-        own_address: str | IndividualAddress = DEFAULT_ADDRESS,
         address_format: GroupAddressType = GroupAddressType.LONG,
         telegram_received_cb: Callable[[Telegram], Awaitable[None]] | None = None,
         device_updated_cb: Callable[[Device], Awaitable[None]] | None = None,
@@ -71,7 +69,6 @@ class XKNX:
         self.daemon_mode = daemon_mode
         self.multicast_group = multicast_group
         self.multicast_port = multicast_port
-        self.own_address = IndividualAddress(own_address)
         self.rate_limit = rate_limit
         self.sigint_received = asyncio.Event()
         self.started = asyncio.Event()


Test patch:
diff --git a/test/io_tests/routing_test.py b/test/io_tests/routing_test.py
index 6739a23f..b98c0346 100644
--- a/test/io_tests/routing_test.py
+++ b/test/io_tests/routing_test.py
@@ -4,6 +4,7 @@ from unittest.mock import AsyncMock, Mock, call, patch
 
 from xknx import XKNX
 from xknx.io import Routing
+from xknx.io.const import DEFAULT_INDIVIDUAL_ADDRESS
 from xknx.io.routing import (
     BUSY_DECREMENT_TIME,
     BUSY_INCREMENT_COOLDOWN,
@@ -12,7 +13,7 @@ from xknx.io.routing import (
     _RoutingFlowControl,
 )
 from xknx.knxip import CEMIFrame, KNXIPFrame, RoutingBusy, RoutingIndication
-from xknx.telegram import Telegram, TelegramDirection, tpci
+from xknx.telegram import IndividualAddress, Telegram, TelegramDirection, tpci
 
 
 class TestRouting:
@@ -29,7 +30,8 @@ class TestRouting:
         """Test Routing for responding to a transport layer connection."""
         routing = Routing(
             self.xknx,
-            self.tg_received_mock,
+            individual_address=None,
+            telegram_received_callback=self.tg_received_mock,
             local_ip="192.168.1.1",
         )
         # L_Data.ind T_Connect from 1.0.250 to 1.0.255 (xknx tunnel endpoint)
@@ -45,7 +47,8 @@ class TestRouting:
         response_frame = KNXIPFrame.init_from_body(
             RoutingIndication(
                 cemi=CEMIFrame.init_from_telegram(
-                    telegram=response_telegram, src_addr=self.xknx.own_address
+                    telegram=response_telegram,
+                    src_addr=DEFAULT_INDIVIDUAL_ADDRESS,
                 )
             )
         )
@@ -67,7 +70,8 @@ class TestRouting:
         """Test class for received RoutingLostMessage frames."""
         routing = Routing(
             self.xknx,
-            AsyncMock(),
+            individual_address=None,
+            telegram_received_callback=AsyncMock(),
             local_ip="192.168.1.1",
         )
         raw = bytes((0x06, 0x10, 0x05, 0x31, 0x00, 0x0A, 0x04, 0x00, 0x00, 0x05))
diff --git a/test/io_tests/self_description_test.py b/test/io_tests/self_description_test.py
index c7bd8e24..2c06fc05 100644
--- a/test/io_tests/self_description_test.py
+++ b/test/io_tests/self_description_test.py
@@ -4,12 +4,42 @@ from unittest.mock import Mock, patch
 
 from xknx.io.self_description import DescriptionQuery, request_description
 from xknx.io.transport.udp_transport import UDPTransport
-from xknx.knxip import HPAI, DescriptionRequest, KNXIPFrame
+from xknx.knxip import HPAI, DescriptionRequest, KNXIPFrame, SearchRequestExtended
+from xknx.telegram import IndividualAddress
 
 
 class TestSelfDescription:
     """Test class for KNX/IP self description."""
 
+    # Core v1 device MDT KNX IP Router SCN-IP100.02
+    description_response_v1_raw = bytes.fromhex(
+        "06 10 02 04 00 46 36 01 02 00 20 00 00 00 00"
+        "83 47 7f 01 24 e0 00 17 0c cc 1b e0 80 04 c4"
+        "4d 44 54 20 4b 4e 58 20 49 50 20 52 6f 75 74"
+        "65 72 00 00 00 00 00 00 00 00 00 00 00 00 00"
+        "0a 02 02 01 03 01 04 01 05 01"
+    )
+    # Core v2 responses from a KNX IP Router 752 secure
+    description_response_raw = bytes.fromhex(
+        "06 10 02 04 00 4e 36 01 02 00 50 00 00 00 00"
+        "c5 01 04 d5 9d e0 00 17 0d 00 24 6d 02 94 c4"
+        "4b 4e 58 20 49 50 20 52 6f 75 74 65 72 20 37"
+        "35 32 20 73 65 63 75 72 65 00 00 00 00 00 00"
+        "0a 02 02 02 03 02 04 02 05 02 08 08 00 00 00"
+        "37 09 1a"
+    )
+    search_response_extended_raw = bytes.fromhex(
+        "06 10 02 0c 00 84 08 01 0a 01 01 5f 0e 57 36"
+        "01 02 00 50 00 00 00 00 c5 01 04 d5 9d e0 00"
+        "17 0d 00 24 6d 02 94 c4 4b 4e 58 20 49 50 20"
+        "52 6f 75 74 65 72 20 37 35 32 20 73 65 63 75"
+        "72 65 00 00 00 00 00 00 0c 02 02 02 03 02 04"
+        "02 05 02 09 01 08 06 03 01 04 01 05 01 24 07"
+        "00 37 50 01 ff fd 50 02 ff fd 50 03 ff fd 50"
+        "04 ff fd 50 05 ff fd 50 06 ff fd 50 07 ff fd"
+        "50 08 ff fd 08 08 00 00 00 37 09 1a"
+    )
+
     async def test_description_query(self, time_travel):
         """Test DescriptionQuery class."""
         local_addr = ("127.0.0.1", 12345)
@@ -21,13 +51,7 @@ class TestSelfDescription:
         description_request = KNXIPFrame.init_from_body(
             DescriptionRequest(control_endpoint=HPAI(*local_addr))
         )
-        description_response_raw = bytes.fromhex(
-            "06 10 02 04 00 48 36 01 02 00 10 00 00 00 00"
-            "08 2d 40 83 4d e0 00 17 0c 00 0a b3 27 4a 32"
-            "4b 4e 58 2f 49 50 2d 52 6f 75 74 65 72 00 00"
-            "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00"
-            "0c 02 02 02 03 02 04 02 05 02 07 01",
-        )
+
         description_query = DescriptionQuery(
             transport=transport_mocked, local_hpai=HPAI(*local_addr)
         )
@@ -36,28 +60,98 @@ class TestSelfDescription:
         transport_mocked.send.assert_called_once_with(description_request)
 
         transport_mocked.data_received_callback(
-            raw=description_response_raw, source=remote_addr
+            raw=self.description_response_raw, source=remote_addr
         )
         await task
         assert description_query.gateway_descriptor is not None
-        assert description_query.gateway_descriptor.name == "KNX/IP-Router"
+        assert description_query.gateway_descriptor.name == "KNX IP Router 752 secure"
+
+    async def test_request_description_v1(
+        self,
+        time_travel,
+    ):
+        """Test request_description function with Core v1 device."""
+        local_addr = ("127.0.0.1", 12345)
+        remote_addr = ("127.0.0.2", 54321)
+
+        with patch(
+            "xknx.io.self_description.UDPTransport.connect"
+        ) as transport_connect_mock, patch(
+            "xknx.io.self_description.UDPTransport.getsockname", return_value=local_addr
+        ), patch(
+            "xknx.io.self_description.UDPTransport.send"
+        ) as transport_send_mock, patch(
+            "xknx.io.self_description.UDPTransport.stop"
+        ) as transport_stop_mock, patch(
+            "xknx.io.self_description.DescriptionQuery", wraps=DescriptionQuery
+        ) as description_query_mock:
+            task = asyncio.create_task(request_description(remote_addr[0]))
+            await time_travel(0)
+            transport_connect_mock.assert_called_once_with()
+            assert transport_send_mock.call_count == 1
+            assert isinstance(
+                transport_send_mock.call_args[0][0].body, DescriptionRequest
+            )
+            _transport = description_query_mock.call_args.kwargs["transport"]
+            _transport.data_received_callback(
+                self.description_response_v1_raw, remote_addr
+            )
+            gateway = await task
+            transport_stop_mock.assert_called_once()
+            assert transport_send_mock.call_count == 1
+
+            assert gateway.core_version == 1
+            assert gateway.name == "MDT KNX IP Router"
+            assert gateway.tunnelling_requires_secure is None
+            assert gateway.individual_address == IndividualAddress("2.0.0")
 
-    async def test_request_description(self, time_travel):
-        """Test request_description function."""
+    async def test_request_description_extended(
+        self,
+        time_travel,
+    ):
+        """Test request_description function with Core v2 device."""
         local_addr = ("127.0.0.1", 12345)
         remote_addr = ("127.0.0.2", 54321)
 
         with patch(
-            "xknx.io.transport.UDPTransport.connect"
+            "xknx.io.self_description.UDPTransport.connect"
         ) as transport_connect_mock, patch(
-            "xknx.io.transport.UDPTransport.getsockname", return_value=local_addr
-        ) as transport_getsockname_mock, patch(
-            "xknx.io.transport.UDPTransport.stop"
+            "xknx.io.self_description.UDPTransport.getsockname", return_value=local_addr
+        ), patch(
+            "xknx.io.self_description.UDPTransport.send"
+        ) as transport_send_mock, patch(
+            "xknx.io.self_description.UDPTransport.stop"
         ) as transport_stop_mock, patch(
-            "xknx.io.self_description.DescriptionQuery.start"
-        ) as description_query_start_mock:
-            await request_description(remote_addr[0])
+            "xknx.io.self_description.DescriptionQuery", wraps=DescriptionQuery
+        ) as description_query_mock:
+            task = asyncio.create_task(request_description(remote_addr[0]))
+            await time_travel(0)
             transport_connect_mock.assert_called_once_with()
-            transport_getsockname_mock.assert_called_once_with()
-            transport_stop_mock.assert_called_once_with()
-            description_query_start_mock.assert_called_once_with()
+            assert transport_send_mock.call_count == 1
+            assert isinstance(
+                transport_send_mock.call_args[0][0].body, DescriptionRequest
+            )
+
+            _transport = description_query_mock.call_args.kwargs["transport"]
+            _transport.data_received_callback(
+                self.description_response_raw, remote_addr
+            )
+            await time_travel(0)
+            assert transport_send_mock.call_count == 2
+            assert isinstance(
+                transport_send_mock.call_args[0][0].body, SearchRequestExtended
+            )
+
+            _transport = description_query_mock.call_args.kwargs["transport"]
+            _transport.data_received_callback(
+                self.search_response_extended_raw, remote_addr
+            )
+
+            gateway = await task
+            transport_stop_mock.assert_called_once()
+            assert transport_send_mock.call_count == 2
+
+            assert gateway.core_version == 2
+            assert gateway.name == "KNX IP Router 752 secure"
+            assert gateway.tunnelling_requires_secure is True
+            assert gateway.individual_address == IndividualAddress("5.0.0")
diff --git a/test/knxip_tests/dib_test.py b/test/knxip_tests/dib_test.py
index e76e1fce..1619ca58 100644
--- a/test/knxip_tests/dib_test.py
+++ b/test/knxip_tests/dib_test.py
@@ -88,6 +88,11 @@ class TestKNXIPDIB:
         assert not dib.supports(DIBServiceFamily.TUNNELING, version=2)
         assert dib.supports(DIBServiceFamily.ROUTING, version=1)
 
+        assert dib.version(DIBServiceFamily.CORE) == 1
+        assert dib.version(DIBServiceFamily.DEVICE_MANAGEMENT) == 2
+        assert dib.version(DIBServiceFamily.TUNNELING) == 1
+        assert dib.version(DIBServiceFamily.ROUTING) == 1
+
     def test_dib_sup_svc_families_interface(self):
         """Test parsing of svc families."""
         raw = bytes((0x0A, 0x02, 0x02, 0x02, 0x03, 0x02, 0x04, 0x02, 0x07, 0x01))
@@ -112,6 +117,11 @@ class TestKNXIPDIB:
         assert not dib.supports(DIBServiceFamily.ROUTING)
         assert not dib.supports(DIBServiceFamily.ROUTING, version=2)
 
+        assert dib.version(DIBServiceFamily.CORE) == 2
+        assert dib.version(DIBServiceFamily.DEVICE_MANAGEMENT) == 2
+        assert dib.version(DIBServiceFamily.TUNNELING) == 2
+        assert dib.version(DIBServiceFamily.ROUTING) is None
+
     def test_dib_secured_service_families(self):
         """Test parsing of secured service families."""
         raw = bytes((0x08, 0x06, 0x03, 0x01, 0x04, 0x01, 0x05, 0x01))