Repository: XKNX/xknx
Pull request #922 (2022-04-18 20:23:04)

Add DIB parsers for Secured Service Families and Tunneling Info
<!--
  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.
-->
- Add DIBSecuredServiceFamilies parser
- Add DIBTunnelingInfo parser

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:

- [x] I have performed a self-review of my own code
- [ ] The documentation has been adjusted accordingly
- [x] The changes generate no new warnings
- [x] Tests have been added that prove the fix is effective or that the feature works
- [x] The changes are documented in the changelog
- [ ] The Homeassistant plugin has been adjusted in case of new config options


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

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

```diff
@@           Coverage Diff           @@
##             main     #922   +/-   ##
=======================================
  Coverage   95.59%   95.60%           
=======================================
  Files         130      130           
  Lines        7769     7775    +6     
=======================================
+ Hits         7427     7433    +6     
  Misses        342      342           
```


| [Impacted Files](https://codecov.io/gh/XKNX/xknx/pull/922?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) | Coverage Δ | |
|---|---|---|
| [xknx/io/gateway\_scanner.py](https://codecov.io/gh/XKNX/xknx/pull/922/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9pby9nYXRld2F5X3NjYW5uZXIucHk=) | `86.23% <0.00%> (ø)` | |
| [xknx/knxip/dib.py](https://codecov.io/gh/XKNX/xknx/pull/922/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9rbnhpcC9kaWIucHk=) | `88.88% <91.11%> (+0.39%)` | :arrow_up: |
| [xknx/knxip/\_\_init\_\_.py](https://codecov.io/gh/XKNX/xknx/pull/922/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9rbnhpcC9fX2luaXRfXy5weQ==) | `100.00% <100.00%> (ø)` | |
| [xknx/knxip/knxip\_enum.py](https://codecov.io/gh/XKNX/xknx/pull/922/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9rbnhpcC9rbnhpcF9lbnVtLnB5) | `100.00% <100.00%> (ø)` | |
| [xknx/knxip/srp.py](https://codecov.io/gh/XKNX/xknx/pull/922/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9rbnhpcC9zcnAucHk=) | `98.21% <100.00%> (ø)` | |


Patch:
diff --git a/changelog.md b/changelog.md
index 7013685c..9ac321bb 100644
--- a/changelog.md
+++ b/changelog.md
@@ -2,6 +2,10 @@
 
 ## Unreleased changes
 
+### Protocol
+
+- Add DIBSecuredServiceFamilies and DIBTunnelingInfo parser
+
 ### Internal
 
 - Include base class in `DPTBase.parse_transcoder()` lookup
diff --git a/xknx/io/gateway_scanner.py b/xknx/io/gateway_scanner.py
index 405d7d4c..0ef483e5 100644
--- a/xknx/io/gateway_scanner.py
+++ b/xknx/io/gateway_scanner.py
@@ -17,9 +17,11 @@ import netifaces
 from xknx.knxip import (
     DIB,
     HPAI,
+    SRP,
     DIBDeviceInformation,
     DIBServiceFamily,
     DIBSuppSVCFamilies,
+    DIBTypeCode,
     KNXIPBody,
     KNXIPFrame,
     KNXIPServiceType,
@@ -245,11 +247,23 @@ class GatewayScanner:
             ip_addr=self.xknx.multicast_group, port=self.xknx.multicast_port
         )
 
-        search_request: KNXIPBody = SearchRequest(discovery_endpoint=discovery_endpoint)
+        search_request: KNXIPBody
         if search_request_type == KNXIPServiceType.SEARCH_REQUEST_EXTENDED:
             search_request = SearchRequestExtended(
-                discovery_endpoint=discovery_endpoint
+                discovery_endpoint=discovery_endpoint,
+                srps=[
+                    SRP.request_device_description(
+                        [
+                            DIBTypeCode.DEVICE_INFO,
+                            DIBTypeCode.SUPP_SVC_FAMILIES,
+                            DIBTypeCode.SECURED_SERVICE_FAMILIES,
+                            DIBTypeCode.TUNNELING_INFO,
+                        ]
+                    )
+                ],
             )
+        else:
+            search_request = SearchRequest(discovery_endpoint=discovery_endpoint)
         udp_transport.send(KNXIPFrame.init_from_body(search_request))
 
     def _response_rec_callback(
diff --git a/xknx/knxip/__init__.py b/xknx/knxip/__init__.py
index ae810570..721aebe2 100644
--- a/xknx/knxip/__init__.py
+++ b/xknx/knxip/__init__.py
@@ -8,7 +8,14 @@ from .connectionstate_request import ConnectionStateRequest
 from .connectionstate_response import ConnectionStateResponse
 from .description_request import DescriptionRequest
 from .description_response import DescriptionResponse
-from .dib import DIB, DIBDeviceInformation, DIBGeneric, DIBSuppSVCFamilies
+from .dib import (
+    DIB,
+    DIBDeviceInformation,
+    DIBGeneric,
+    DIBSecuredServiceFamilies,
+    DIBSuppSVCFamilies,
+    DIBTunnelingInfo,
+)
 from .disconnect_request import DisconnectRequest
 from .disconnect_response import DisconnectResponse
 from .error_code import ErrorCode
@@ -36,6 +43,7 @@ from .session_authenticate import SessionAuthenticate
 from .session_request import SessionRequest
 from .session_response import SessionResponse
 from .session_status import SessionStatus
+from .srp import SRP
 from .tunnelling_ack import TunnellingAck
 from .tunnelling_request import TunnellingRequest
 
@@ -52,7 +60,9 @@ __all__ = [
     "DIB",
     "DIBDeviceInformation",
     "DIBGeneric",
+    "DIBSecuredServiceFamilies",
     "DIBSuppSVCFamilies",
+    "DIBTunnelingInfo",
     "DisconnectRequest",
     "DisconnectResponse",
     "ErrorCode",
@@ -78,6 +88,7 @@ __all__ = [
     "SessionRequest",
     "SessionResponse",
     "SessionStatus",
+    "SRP",
     "TunnellingAck",
     "TunnellingRequest",
 ]
diff --git a/xknx/knxip/dib.py b/xknx/knxip/dib.py
index 4e93c12a..42c12305 100644
--- a/xknx/knxip/dib.py
+++ b/xknx/knxip/dib.py
@@ -14,6 +14,7 @@ from __future__ import annotations
 
 from abc import ABC, abstractmethod
 import socket
+from typing import NamedTuple
 
 from xknx.exceptions import CouldNotParseKNXIP
 from xknx.telegram import IndividualAddress
@@ -31,10 +32,6 @@ class DIB(ABC):
     classes.
     """
 
-    @abstractmethod
-    def __init__(self) -> None:
-        """Initialize DIB class."""
-
     @abstractmethod
     def calculated_length(self) -> int:
         """Get length of KNX/IP object."""
@@ -60,6 +57,10 @@ class DIB(ABC):
             return DIBDeviceInformation()
         if dtc == DIBTypeCode.SUPP_SVC_FAMILIES:
             return DIBSuppSVCFamilies()
+        if dtc == DIBTypeCode.SECURED_SERVICE_FAMILIES:
+            return DIBSecuredServiceFamilies()
+        if dtc == DIBTypeCode.TUNNELING_INFO:
+            return DIBTunnelingInfo()
         return DIBGeneric()
 
 
@@ -72,7 +73,6 @@ class DIBGeneric(DIB):
 
     def __init__(self) -> None:
         """Initialize DIBGeneric class."""
-        super().__init__()
         # DTC Description Type Code
         self.dtc: DIBTypeCode | int = 0
         # IBD Information Block Data
@@ -117,79 +117,6 @@ class DIBGeneric(DIB):
         return f'<DIB dtc="{self.dtc}" data="{", ".join(f"0x{i:02x}" for i in self.data)}" />'
 
 
-class DIBSuppSVCFamilies(DIB):
-    """Class for serialization and deserialization of KNX DIB Supported Services."""
-
-    class Family:
-        """Class for storing a supported device family."""
-
-        def __init__(self, name: DIBServiceFamily, version: int):
-            """Initialize DIBSuppSVCFamilies.Family."""
-            self.name = name
-            self.version = version
-
-        def to_knx(self) -> bytes:
-            """Serialize to KNX/IP raw data."""
-            return bytes((self.name.value, self.version))
-
-        def __str__(self) -> str:
-            """Return object as readable string."""
-            return f'<Family name="{self.name}" version="{self.version}" />'
-
-        def __eq__(self, other: object) -> bool:
-            """Equal operator."""
-            return self.__dict__ == other.__dict__
-
-    def __init__(self) -> None:
-        """Initialize DIBSuppSVCFamilies class."""
-        super().__init__()
-        self.families: list[DIBSuppSVCFamilies.Family] = []
-
-    def supports(self, name: DIBServiceFamily, version: int | None = None) -> bool:
-        """Return if device supports a given service family by name and optional minimum version."""
-        for family in self.families:
-            if name == family.name:
-                if version is None or family.version >= version:
-                    return True
-        return False
-
-    def calculated_length(self) -> int:
-        """Get length of KNX/IP object."""
-        return len(self.families) * 2 + DIB_HEADER_LENGTH
-
-    def from_knx(self, raw: bytes) -> int:
-        """Parse/deserialize from KNX/IP raw data."""
-        if len(raw) < 2:
-            raise CouldNotParseKNXIP("DIB header too small")
-        length = raw[0]
-        if len(raw) < length:
-            raise CouldNotParseKNXIP("DIB wrong size")
-        if DIBTypeCode(raw[1]) != DIBTypeCode.SUPP_SVC_FAMILIES:
-            raise CouldNotParseKNXIP("DIB is no device info")
-
-        for pos in range(2, length, 2):
-            name = DIBServiceFamily(raw[pos])
-            version = raw[pos + 1]
-            self.families.append(DIBSuppSVCFamilies.Family(name, version))
-        return length
-
-    def to_knx(self) -> bytes:
-        """Serialize to KNX/IP raw data."""
-        return bytes(
-            (
-                self.calculated_length(),
-                DIBTypeCode.SUPP_SVC_FAMILIES.value,
-            )
-        ) + b"".join(family.to_knx() for family in self.families)
-
-    def __str__(self) -> str:
-        """Return object as readable string."""
-        _families_str = ", ".join(
-            f"{family.name} version: {family.version}" for family in self.families
-        )
-        return f'<DIBSuppSVCFamilies families="[{_families_str}]" />'
-
-
 class DIBDeviceInformation(DIB):
     """Class for serialization and deserialization of KNX DIB Device Information Block."""
 
@@ -197,7 +124,6 @@ class DIBDeviceInformation(DIB):
 
     def __init__(self) -> None:
         """Initialize DIBDeviceInformation class."""
-        super().__init__()
         self.knx_medium: KNXMedium = KNXMedium.TP1
         self.programming_mode: bool = False
         self.individual_address: IndividualAddress = IndividualAddress(None)
@@ -285,3 +211,158 @@ class DIBDeviceInformation(DIB):
             f'\n\tmac_address="{self.mac_address}" '
             f'\n\tname="{self.name}" />'
         )
+
+
+class DIBSuppSVCFamilies(DIB):
+    """Class for serialization and deserialization of KNX DIB Supported Services."""
+
+    type_code = DIBTypeCode.SUPP_SVC_FAMILIES
+
+    class Family:
+        """Class for storing a supported device family."""
+
+        def __init__(self, name: DIBServiceFamily, version: int):
+            """Initialize DIBSuppSVCFamilies.Family."""
+            self.name = name
+            self.version = version
+
+        def to_knx(self) -> bytes:
+            """Serialize to KNX/IP raw data."""
+            return bytes((self.name.value, self.version))
+
+        def __str__(self) -> str:
+            """Return object as readable string."""
+            return f'<Family name="{self.name}" version="{self.version}" />'
+
+        def __eq__(self, other: object) -> bool:
+            """Equal operator."""
+            return self.__dict__ == other.__dict__
+
+    def __init__(self) -> None:
+        """Initialize DIBSuppSVCFamilies class."""
+        self.families: list[DIBSuppSVCFamilies.Family] = []
+
+    def supports(self, name: DIBServiceFamily, version: int | None = None) -> bool:
+        """Return if device supports a given service family by name and optional minimum version."""
+        for family in self.families:
+            if name == family.name:
+                if version is None or family.version >= version:
+                    return True
+        return False
+
+    def calculated_length(self) -> int:
+        """Get length of KNX/IP object."""
+        return len(self.families) * 2 + DIB_HEADER_LENGTH
+
+    def from_knx(self, raw: bytes) -> int:
+        """Parse/deserialize from KNX/IP raw data."""
+        if len(raw) < 2:
+            raise CouldNotParseKNXIP("DIB header too small")
+        length = raw[0]
+        if (len(raw) < length) or (length % 2):
+            raise CouldNotParseKNXIP("DIB wrong size")
+        if DIBTypeCode(raw[1]) != self.type_code:
+            raise CouldNotParseKNXIP(
+                f"DIB has wrong type code for {self.__class__.__name__}"
+            )
+
+        for pos in range(2, length, 2):
+            name = DIBServiceFamily(raw[pos])
+            version = raw[pos + 1]
+            self.families.append(DIBSuppSVCFamilies.Family(name, version))
+        return length
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        return bytes(
+            (
+                self.calculated_length(),
+                self.type_code.value,
+            )
+        ) + b"".join(family.to_knx() for family in self.families)
+
+    def __str__(self) -> str:
+        """Return object as readable string."""
+        _families_str = ", ".join(
+            f"{family.name} version: {family.version}" for family in self.families
+        )
+        return f'<{self.__class__.__name__} families="[{_families_str}]" />'
+
+
+class DIBSecuredServiceFamilies(DIBSuppSVCFamilies):
+    """Class for serialization and deserialization of KNX DIB Secured Service Families."""
+
+    type_code = DIBTypeCode.SECURED_SERVICE_FAMILIES
+
+
+class TunnelingSlotStatus(NamedTuple):
+    """Class for storing tunneling slot status."""
+
+    usable: bool
+    authorized: bool
+    free: bool
+
+    def __bytes__(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        return bytes(
+            (
+                0x00,  # reserved
+                self.usable << 2 | self.authorized << 1 | self.free,
+            )
+        )
+
+
+class DIBTunnelingInfo(DIB):
+    """Class for serialization and deserialization of KNX DIB Tunneling Info."""
+
+    def __init__(
+        self, slots: dict[IndividualAddress, TunnelingSlotStatus] | None = None
+    ) -> None:
+        """Initialize DIBTunnelingInfo class."""
+        self.max_apdu_length = 248
+        self.slots = slots or {}
+
+    def calculated_length(self) -> int:
+        """Get length of KNX/IP object."""
+        return 2 + 2 + len(self.slots) * 4
+
+    def from_knx(self, raw: bytes) -> int:
+        """Parse/deserialize from KNX/IP raw data."""
+        if len(raw) < 4:
+            raise CouldNotParseKNXIP("DIB header too small")
+        length = raw[0]
+        if (len(raw) < length) or (length % 4):
+            raise CouldNotParseKNXIP("DIB wrong size")
+        if DIBTypeCode(raw[1]) != DIBTypeCode.TUNNELING_INFO:
+            raise CouldNotParseKNXIP(
+                f"DIB has wrong type code for {self.__class__.__name__}"
+            )
+
+        self.max_apdu_length = int.from_bytes(raw[2:4], "big")
+        for pos in range(4, length, 4):
+            address = IndividualAddress((raw[pos], raw[pos + 1]))
+            status = TunnelingSlotStatus(
+                usable=bool(raw[pos + 3] >> 2 & 0b1),
+                authorized=bool(raw[pos + 3] >> 1 & 0b1),
+                free=bool(raw[pos + 3] & 0b1),
+            )
+            self.slots[address] = status
+        return length
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        return (
+            bytes((self.calculated_length(), DIBTypeCode.TUNNELING_INFO.value))
+            + self.max_apdu_length.to_bytes(2, "big")
+            + b"".join(
+                bytes(address.to_knx()) + bytes(status)
+                for address, status in self.slots.items()
+            )
+        )
+
+    def __str__(self) -> str:
+        """Return object as readable string."""
+        return (
+            f"<{self.__class__.__name__} max_adpu_lenght={self.max_apdu_length} "
+            f"slots={self.slots}/>"
+        )
diff --git a/xknx/knxip/knxip_enum.py b/xknx/knxip/knxip_enum.py
index 606b0cb5..75789cda 100644
--- a/xknx/knxip/knxip_enum.py
+++ b/xknx/knxip/knxip_enum.py
@@ -160,8 +160,8 @@ class DIBTypeCode(Enum):
     # KNX addresses
     KNX_ADDRESSES = 0x05
 
-    # KNX IP Secure Unicast
-    SUPP_SECURE_SVC_FAMILIES = 0x06
+    # KNX IP Secure
+    SECURED_SERVICE_FAMILIES = 0x06
     TUNNELING_INFO = 0x07
     ADDITIONAL_DEVICE_INFO = 0x08
 
diff --git a/xknx/knxip/srp.py b/xknx/knxip/srp.py
index 68ed6f05..ebc3fa48 100644
--- a/xknx/knxip/srp.py
+++ b/xknx/knxip/srp.py
@@ -15,7 +15,7 @@ from .knxip_enum import DIBServiceFamily, DIBTypeCode, SearchRequestParameterTyp
 
 
 class SRP:
-    """Search request parameter for a search request extended."""
+    """Search request parameter for a SearchRequestExtended."""
 
     SRP_HEADER_SIZE = 2
     MANDATORY_BIT_INDEX = 0x07
@@ -91,15 +91,18 @@ class SRP:
             raise CouldNotParseKNXIP("SRP is larger than actual data size.")
 
         return SRP(
-            SearchRequestParameterType(data[1] & 0x7F),
-            bool(data[1] >> SRP.MANDATORY_BIT_INDEX),
-            data[2:size],
+            srp_type=SearchRequestParameterType(data[1] & 0x7F),
+            mandatory=bool(data[1] >> SRP.MANDATORY_BIT_INDEX),
+            data=data[2:size],
         )
 
     @staticmethod
     def with_programming_mode() -> SRP:
         """Create a SRP that limits the response to only devices that are currently in programming mode."""
-        return SRP(SearchRequestParameterType.SELECT_BY_PROGRAMMING_MODE, True)
+        return SRP(
+            srp_type=SearchRequestParameterType.SELECT_BY_PROGRAMMING_MODE,
+            mandatory=True,
+        )
 
     @staticmethod
     def with_mac_address(mac_address: bytes) -> SRP:
@@ -108,7 +111,11 @@ class SRP:
 
         :param mac_address: The mac address to restrict this SRP to
         """
-        return SRP(SearchRequestParameterType.SELECT_BY_MAC_ADDRESS, True, mac_address)
+        return SRP(
+            srp_type=SearchRequestParameterType.SELECT_BY_MAC_ADDRESS,
+            mandatory=True,
+            data=mac_address,
+        )
 
     @staticmethod
     def with_service(family: DIBServiceFamily, family_version: int) -> SRP:
@@ -120,23 +127,26 @@ class SRP:
         :return: Srp with the given parameter
         """
         return SRP(
-            SearchRequestParameterType.SELECT_BY_SERVICE,
-            True,
-            bytes([family.value, family_version]),
+            srp_type=SearchRequestParameterType.SELECT_BY_SERVICE,
+            mandatory=True,
+            data=bytes([family.value, family_version]),
         )
 
     @staticmethod
-    def with_device_description(dibs: list[DIBTypeCode]) -> SRP:
+    def request_device_description(dibs: list[DIBTypeCode]) -> SRP:
         """
-        Create a SRP with a list of DIBs to indicate the server that it should, at least, include.
+        Create a SRP with a list of DIBs the server shall include in the response.
+
+        The server may include in addition any number of other DIBs in the response.
+        The server shall ignore Description types that are not recognized or not supported.
 
         :param dibs: the description types to include in the SRP
         :return: Srp with given parameters
         """
         return SRP(
-            SearchRequestParameterType.REQUEST_DIBS,
-            True,
-            bytes(dib.value for dib in dibs),
+            srp_type=SearchRequestParameterType.REQUEST_DIBS,
+            mandatory=True,
+            data=bytes(dib.value for dib in dibs),
         )
 
     def __eq__(self, other: object) -> bool:


Test patch:
diff --git a/test/knxip_tests/dib_test.py b/test/knxip_tests/dib_test.py
index cd5478c8..e76e1fce 100644
--- a/test/knxip_tests/dib_test.py
+++ b/test/knxip_tests/dib_test.py
@@ -6,8 +6,10 @@ from xknx.knxip import (
     DIB,
     DIBDeviceInformation,
     DIBGeneric,
+    DIBSecuredServiceFamilies,
     DIBServiceFamily,
     DIBSuppSVCFamilies,
+    DIBTunnelingInfo,
     DIBTypeCode,
     KNXMedium,
 )
@@ -109,3 +111,50 @@ class TestKNXIPDIB:
         assert dib.supports(DIBServiceFamily.TUNNELING, version=2)
         assert not dib.supports(DIBServiceFamily.ROUTING)
         assert not dib.supports(DIBServiceFamily.ROUTING, version=2)
+
+    def test_dib_secured_service_families(self):
+        """Test parsing of secured service families."""
+        raw = bytes((0x08, 0x06, 0x03, 0x01, 0x04, 0x01, 0x05, 0x01))
+
+        dib = DIB.determine_dib(raw)
+        assert isinstance(dib, DIBSecuredServiceFamilies)
+        assert dib.from_knx(raw) == 8
+
+        assert dib.families == [
+            DIBSuppSVCFamilies.Family(DIBServiceFamily.DEVICE_MANAGEMENT, 1),
+            DIBSuppSVCFamilies.Family(DIBServiceFamily.TUNNELING, 1),
+            DIBSuppSVCFamilies.Family(DIBServiceFamily.ROUTING, 1),
+        ]
+
+        assert dib.to_knx() == raw
+
+        assert dib.supports(DIBServiceFamily.TUNNELING)
+        assert dib.supports(DIBServiceFamily.TUNNELING, version=1)
+        assert dib.supports(DIBServiceFamily.ROUTING)
+        assert not dib.supports(DIBServiceFamily.ROUTING, version=2)
+        assert dib.supports(DIBServiceFamily.DEVICE_MANAGEMENT)
+
+    def test_dib_tunneling_info(self):
+        """Test parsing of tunneling info."""
+        raw = (
+            b"\x24\x07\x00\xf8\x40\x01\x00\x05\x40\x02\x00\x05\x40\x03\x00\x05"
+            b"\x40\x04\x00\x05\x40\x05\x00\x05\x40\x06\x00\x05\x40\x07\x00\x05"
+            b"\x40\x08\x00\x06"
+        )
+
+        dib = DIB.determine_dib(raw)
+        assert isinstance(dib, DIBTunnelingInfo)
+        assert dib.from_knx(raw) == 36
+
+        assert dib.max_apdu_length == 248
+
+        assert len(dib.slots) == 8
+        for address in ["4.0.1", "4.0.2", "4.0.3", "4.0.4", "4.0.5", "4.0.6", "4.0.7"]:
+            assert dib.slots[IndividualAddress(address)].usable
+            assert not dib.slots[IndividualAddress(address)].authorized
+            assert dib.slots[IndividualAddress(address)].free
+        assert dib.slots[IndividualAddress("4.0.8")].usable
+        assert dib.slots[IndividualAddress("4.0.8")].authorized
+        assert not dib.slots[IndividualAddress("4.0.8")].free
+
+        assert dib.to_knx() == raw
diff --git a/test/knxip_tests/srp_test.py b/test/knxip_tests/srp_test.py
index 8047aeec..7855fe18 100644
--- a/test/knxip_tests/srp_test.py
+++ b/test/knxip_tests/srp_test.py
@@ -27,7 +27,7 @@ class TestKNXIPSRP:
             DIBTypeCode.SUPP_SVC_FAMILIES,
             DIBTypeCode.ADDITIONAL_DEVICE_INFO,
         ]
-        srp: SRP = SRP.with_device_description(dibs)
+        srp: SRP = SRP.request_device_description(dibs)
         assert len(srp) == SRP.SRP_HEADER_SIZE + len(dibs)
         assert srp.type == SearchRequestParameterType.REQUEST_DIBS
 
@@ -68,13 +68,13 @@ class TestKNXIPSRP:
                 bytes.fromhex("02 81"),
             ),
             (
-                SRP.with_device_description(
+                SRP.request_device_description(
                     [DIBTypeCode.SUPP_SVC_FAMILIES, DIBTypeCode.TUNNELING_INFO]
                 ),
                 bytes.fromhex("04 84 02 07"),
             ),
             (
-                SRP.with_device_description([DIBTypeCode.SUPP_SVC_FAMILIES]),
+                SRP.request_device_description([DIBTypeCode.SUPP_SVC_FAMILIES]),
                 bytes.fromhex("04 84 02 00"),
             ),
         ],