Repository: XKNX/xknx
Pull request #1075 (2022-11-18 05:14:58)

Get more information from self description `request_description`
<!--
  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.
-->
- [get core version in GatewayDescriptor](https://github.com/XKNX/xknx/commit/ab7e71d06dc3903554820c73d8bd5d249692c45e)
- [request_description: send SearchRequestExtended](https://github.com/XKNX/xknx/commit/3de4e4e3dcf9a0e75d52829464194421edfba4f9) if supported by the device

This can be used to get a hosts (by IP address) individual address and a tunnel servers endpoint slots. With this information we can look up an Interface from a keyfile if needed for secure tunnelling connection.

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)
- [x] 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/1075?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) Report
> Merging [#1075](https://codecov.io/gh/XKNX/xknx/pull/1075?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) (d8c5fa3) 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 **decrease** coverage by `0.06%`.
> The diff coverage is `84.61%`.

[![Impacted file tree graph](https://codecov.io/gh/XKNX/xknx/pull/1075/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/1075?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX)

```diff
@@            Coverage Diff             @@
##             main    #1075      +/-   ##
==========================================
- Coverage   96.36%   96.29%   -0.07%     
==========================================
  Files         138      138              
  Lines        8581     8611      +30     
==========================================
+ Hits         8269     8292      +23     
- Misses        312      319       +7     
```


| [Impacted Files](https://codecov.io/gh/XKNX/xknx/pull/1075?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) | Coverage Δ | |
|---|---|---|
| [xknx/io/self\_description.py](https://codecov.io/gh/XKNX/xknx/pull/1075/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9pby9zZWxmX2Rlc2NyaXB0aW9uLnB5) | `77.33% <76.47%> (-4.81%)` | :arrow_down: |
| [xknx/io/connection.py](https://codecov.io/gh/XKNX/xknx/pull/1075/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9pby9jb25uZWN0aW9uLnB5) | `97.43% <100.00%> (+0.13%)` | :arrow_up: |
| [xknx/io/gateway\_scanner.py](https://codecov.io/gh/XKNX/xknx/pull/1075/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9pby9nYXRld2F5X3NjYW5uZXIucHk=) | `97.85% <100.00%> (+0.03%)` | :arrow_up: |
| [xknx/io/knxip\_interface.py](https://codecov.io/gh/XKNX/xknx/pull/1075/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9pby9rbnhpcF9pbnRlcmZhY2UucHk=) | `85.48% <100.00%> (+0.15%)` | :arrow_up: |
| [xknx/io/routing.py](https://codecov.io/gh/XKNX/xknx/pull/1075/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9pby9yb3V0aW5nLnB5) | `90.83% <100.00%> (+0.21%)` | :arrow_up: |
| [xknx/knxip/dib.py](https://codecov.io/gh/XKNX/xknx/pull/1075/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9rbnhpcC9kaWIucHk=) | `89.94% <100.00%> (+0.12%)` | :arrow_up: |


Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index 64064b73..152b0678 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -24,11 +24,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/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/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/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..dde06c1a 100644
--- a/xknx/io/tunnel.py
+++ b/xknx/io/tunnel.py
@@ -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")
         )


Test patch:
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))