Repository: XKNX/xknx

Issue:
Add SearchRequestExtended
Add search request extended for ip secure

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

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

```diff
@@           Coverage Diff            @@
##             main     #905    +/-   ##
========================================
  Coverage   95.62%   95.62%            
========================================
  Files         128      131     +3     
  Lines        7672     7794   +122     
========================================
+ Hits         7336     7453   +117     
- Misses        336      341     +5     
```


| [Impacted Files](https://codecov.io/gh/XKNX/xknx/pull/905?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/905/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9pby9nYXRld2F5X3NjYW5uZXIucHk=) | `86.36% <75.00%> (-1.26%)` | :arrow_down: |
| [xknx/knxip/knxip.py](https://codecov.io/gh/XKNX/xknx/pull/905/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9rbnhpcC9rbnhpcC5weQ==) | `98.94% <83.33%> (-1.06%)` | :arrow_down: |
| [xknx/knxip/search\_request\_extended.py](https://codecov.io/gh/XKNX/xknx/pull/905/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9rbnhpcC9zZWFyY2hfcmVxdWVzdF9leHRlbmRlZC5weQ==) | `96.77% <96.77%> (ø)` | |
| [xknx/knxip/srp.py](https://codecov.io/gh/XKNX/xknx/pull/905/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% <98.21%> (ø)` | |
| [xknx/knxip/\_\_init\_\_.py](https://codecov.io/gh/XKNX/xknx/pull/905/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/dib.py](https://codecov.io/gh/XKNX/xknx/pull/905/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9rbnhpcC9kaWIucHk=) | `88.48% <100.00%> (ø)` | |
| [xknx/knxip/knxip\_enum.py](https://codecov.io/gh/XKNX/xknx/pull/905/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/search\_response\_extended.py](https://codecov.io/gh/XKNX/xknx/pull/905/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9rbnhpcC9zZWFyY2hfcmVzcG9uc2VfZXh0ZW5kZWQucHk=) | `100.00% <100.00%> (ø)` | |


Patch:
diff --git a/changelog.md b/changelog.md
index a57a2554..38110266 100644
--- a/changelog.md
+++ b/changelog.md
@@ -2,6 +2,10 @@
 
 ## Unreleased changes
 
+### Features
+
+- Add support for SearchRequestExtended to find interfaces that allow IP Secure
+
 ### Bug fixes
 
 - Stop SecureSession keepalive_task when session is stopped (and don't restart it from sending STATUS_CLOSE)
diff --git a/examples/example_gatewayscanner.py b/examples/example_gatewayscanner.py
index bbfff5dc..b2c02eaa 100644
--- a/examples/example_gatewayscanner.py
+++ b/examples/example_gatewayscanner.py
@@ -3,13 +3,16 @@ import asyncio
 
 from xknx import XKNX
 from xknx.io import GatewayScanner
+from xknx.knxip import KNXIPServiceType
 
 
 async def main():
     """Search for available KNX/IP devices with GatewayScanner and print out result if a device was found."""
     xknx = XKNX()
     gatewayscanner = GatewayScanner(xknx)
-    gateways = await gatewayscanner.scan()
+    gateways = await gatewayscanner.scan(
+        search_request_type=KNXIPServiceType.SEARCH_REQUEST_EXTENDED
+    )
 
     if not gateways:
         print("No Gateways found")
diff --git a/xknx/io/gateway_scanner.py b/xknx/io/gateway_scanner.py
index 460f97f2..55b510e4 100644
--- a/xknx/io/gateway_scanner.py
+++ b/xknx/io/gateway_scanner.py
@@ -20,10 +20,13 @@ from xknx.knxip import (
     DIBDeviceInformation,
     DIBServiceFamily,
     DIBSuppSVCFamilies,
+    KNXIPBody,
     KNXIPFrame,
     KNXIPServiceType,
     SearchRequest,
+    SearchRequestExtended,
     SearchResponse,
+    SearchResponseExtended,
 )
 from xknx.telegram import IndividualAddress
 
@@ -48,6 +51,7 @@ class GatewayDescriptor:
         supports_routing: bool = False,
         supports_tunnelling: bool = False,
         supports_tunnelling_tcp: bool = False,
+        supports_secure: bool = False,
         individual_address: IndividualAddress | None = None,
     ):
         """Initialize GatewayDescriptor class."""
@@ -59,6 +63,7 @@ class GatewayDescriptor:
         self.supports_routing = supports_routing
         self.supports_tunnelling = supports_tunnelling
         self.supports_tunnelling_tcp = supports_tunnelling_tcp
+        self.supports_secure = supports_secure
         self.individual_address = individual_address
 
     def parse_dibs(self, dibs: list[DIB]) -> None:
@@ -71,6 +76,9 @@ class GatewayDescriptor:
                     self.supports_tunnelling_tcp = dib.supports(
                         DIBServiceFamily.TUNNELING, version=2
                     )
+                self.supports_secure = dib.supports(
+                    DIBServiceFamily.SECURITY, version=1
+                )
                 continue
             if isinstance(dib, DIBDeviceInformation):
                 self.name = dib.name
@@ -89,6 +97,7 @@ class GatewayDescriptor:
             f"    supports_routing={self.supports_routing},\n"
             f"    supports_tunnelling={self.supports_tunnelling},\n"
             f"    supports_tunnelling_tcp={self.supports_tunnelling_tcp},\n"
+            f"    supports_secure={self.supports_secure},\n"
             f"    individual_address={self.individual_address}\n"
             ")"
         )
@@ -162,13 +171,15 @@ class GatewayScanner:
         self._count_upper_bound = 0
         """Clean value of self.stop_on_found, computed when ``scan`` is called."""
 
-    async def scan(self) -> list[GatewayDescriptor]:
+    async def scan(
+        self, search_request_type: KNXIPServiceType = KNXIPServiceType.SEARCH_REQUEST
+    ) -> list[GatewayDescriptor]:
         """Scan and return a list of GatewayDescriptors on success."""
         if self.stop_on_found is None:
             self._count_upper_bound = 0
         else:
             self._count_upper_bound = max(0, self.stop_on_found)
-        await self._send_search_requests()
+        await self._send_search_requests(search_request_type)
         try:
             await asyncio.wait_for(
                 self._response_received_event.wait(),
@@ -186,7 +197,9 @@ class GatewayScanner:
         for udp_transport in self._udp_transports:
             udp_transport.stop()
 
-    async def _send_search_requests(self) -> None:
+    async def _send_search_requests(
+        self, search_request_type: KNXIPServiceType = KNXIPServiceType.SEARCH_REQUEST
+    ) -> None:
         """Find all interfaces with active IPv4 connection to search for gateways."""
         for interface in netifaces.interfaces():
             try:
@@ -200,9 +213,14 @@ class GatewayScanner:
                 logger.debug("Invalid interface %s: %s", interface, err)
                 continue
             else:
-                await self._search_interface(interface, ip_addr)
+                await self._search_interface(interface, ip_addr, search_request_type)
 
-    async def _search_interface(self, interface: str, ip_addr: str) -> None:
+    async def _search_interface(
+        self,
+        interface: str,
+        ip_addr: str,
+        search_request_type: KNXIPServiceType = KNXIPServiceType.SEARCH_REQUEST,
+    ) -> None:
         """Send a search request on a specific interface."""
         logger.debug("Searching on %s / %s", interface, ip_addr)
 
@@ -215,7 +233,10 @@ class GatewayScanner:
 
         udp_transport.register_callback(
             partial(self._response_rec_callback, interface=interface),
-            [KNXIPServiceType.SEARCH_RESPONSE],
+            [
+                KNXIPServiceType.SEARCH_RESPONSE,
+                KNXIPServiceType.SEARCH_RESPONSE_EXTENDED,
+            ],
         )
         await udp_transport.connect()
 
@@ -225,7 +246,13 @@ class GatewayScanner:
             ip_addr=self.xknx.multicast_group, port=self.xknx.multicast_port
         )
 
-        search_request = SearchRequest(self.xknx, discovery_endpoint=discovery_endpoint)
+        search_request: KNXIPBody = SearchRequest(
+            self.xknx, discovery_endpoint=discovery_endpoint
+        )
+        if search_request_type == KNXIPServiceType.SEARCH_REQUEST_EXTENDED:
+            search_request = SearchRequestExtended(
+                self.xknx, discovery_endpoint=discovery_endpoint
+            )
         udp_transport.send(KNXIPFrame.init_from_body(search_request))
 
     def _response_rec_callback(
@@ -236,7 +263,7 @@ class GatewayScanner:
         interface: str = "",
     ) -> None:
         """Verify and handle knxipframe. Callback from internal udp_transport."""
-        if not isinstance(knx_ip_frame.body, SearchResponse):
+        if not isinstance(knx_ip_frame.body, (SearchResponse, SearchResponseExtended)):
             logger.warning("Could not understand knxipframe")
             return
 
diff --git a/xknx/knxip/__init__.py b/xknx/knxip/__init__.py
index f610a4f5..ae810570 100644
--- a/xknx/knxip/__init__.py
+++ b/xknx/knxip/__init__.py
@@ -24,10 +24,13 @@ from .knxip_enum import (
     HostProtocol,
     KNXIPServiceType,
     KNXMedium,
+    SearchRequestParameterType,
 )
 from .routing_indication import RoutingIndication
 from .search_request import SearchRequest
+from .search_request_extended import SearchRequestExtended
 from .search_response import SearchResponse
+from .search_response_extended import SearchResponseExtended
 from .secure_wrapper import SecureWrapper
 from .session_authenticate import SessionAuthenticate
 from .session_request import SessionRequest
@@ -66,7 +69,10 @@ __all__ = [
     "KNXMedium",
     "RoutingIndication",
     "SearchRequest",
+    "SearchRequestExtended",
+    "SearchRequestParameterType",
     "SearchResponse",
+    "SearchResponseExtended",
     "SecureWrapper",
     "SessionAuthenticate",
     "SessionRequest",
diff --git a/xknx/knxip/dib.py b/xknx/knxip/dib.py
index e729c19d..4e93c12a 100644
--- a/xknx/knxip/dib.py
+++ b/xknx/knxip/dib.py
@@ -20,7 +20,7 @@ from xknx.telegram import IndividualAddress
 
 from .knxip_enum import DIBServiceFamily, DIBTypeCode, KNXMedium
 
-DIB_HEADER_LENGTH = 2  # structure lenght and description type code
+DIB_HEADER_LENGTH = 2  # structure length and description type code
 
 
 class DIB(ABC):
diff --git a/xknx/knxip/knxip.py b/xknx/knxip/knxip.py
index 569071b9..7b7d8534 100644
--- a/xknx/knxip/knxip.py
+++ b/xknx/knxip/knxip.py
@@ -23,7 +23,9 @@ from .header import KNXIPHeader
 from .knxip_enum import KNXIPServiceType
 from .routing_indication import RoutingIndication
 from .search_request import SearchRequest
+from .search_request_extended import SearchRequestExtended
 from .search_response import SearchResponse
+from .search_response_extended import SearchResponseExtended
 from .secure_wrapper import SecureWrapper
 from .session_authenticate import SessionAuthenticate
 from .session_request import SessionRequest
@@ -53,8 +55,12 @@ class KNXIPFrame:
         # Core
         if service_type_ident == KNXIPServiceType.SEARCH_REQUEST:
             body = SearchRequest(self.xknx)
+        elif service_type_ident == KNXIPServiceType.SEARCH_REQUEST_EXTENDED:
+            body = SearchRequestExtended(self.xknx)
         elif service_type_ident == KNXIPServiceType.SEARCH_RESPONSE:
             body = SearchResponse(self.xknx)
+        elif service_type_ident == KNXIPServiceType.SEARCH_RESPONSE_EXTENDED:
+            body = SearchResponseExtended(self.xknx)
         elif service_type_ident == KNXIPServiceType.DESCRIPTION_REQUEST:
             body = DescriptionRequest(self.xknx)
         elif service_type_ident == KNXIPServiceType.DESCRIPTION_RESPONSE:
diff --git a/xknx/knxip/knxip_enum.py b/xknx/knxip/knxip_enum.py
index 42549b0c..606b0cb5 100644
--- a/xknx/knxip/knxip_enum.py
+++ b/xknx/knxip/knxip_enum.py
@@ -160,6 +160,11 @@ class DIBTypeCode(Enum):
     # KNX addresses
     KNX_ADDRESSES = 0x05
 
+    # KNX IP Secure Unicast
+    SUPP_SECURE_SVC_FAMILIES = 0x06
+    TUNNELING_INFO = 0x07
+    ADDITIONAL_DEVICE_INFO = 0x08
+
     # DIB structure for further data defined by device manufacturer.
     MFR_DATA = 0xFE
 
@@ -204,6 +209,9 @@ class DIBServiceFamily(Enum):
     # Object Server'.
     OBJECT_SERVER = 0x08
 
+    # Security - Extended search response only
+    SECURITY = 0x09
+
 
 class SecureSessionStatusCode(Enum):
     """Enum class for KNX/IP Secure session status codes."""
@@ -220,3 +228,22 @@ class SecureSessionStatusCode(Enum):
     STATUS_KEEPALIVE = 0x04
     # The secure session shall be closed
     STATUS_CLOSE = 0x05
+
+
+class SearchRequestParameterType(Enum):
+    """Search Request Parameter (SRP) is used to transfer additional information in a KNXnet/IP SEARCH_REQUEST_EXTENDED."""
+
+    # Used to test behavior of the KNXnet/IP server for unknown SRPs. Don't use!
+    INVALID = 0x00
+
+    # Select only KNXnet/IP Servers that are currently in programming mode
+    SELECT_BY_PROGRAMMING_MODE = 0x01
+
+    # Select only KNXnet/IP Servers that have the given MAC address
+    SELECT_BY_MAC_ADDRESS = 0x02
+
+    # Select only KNXnet/IP Servers that support the given DIBServiceFamily in a given version
+    SELECT_BY_SERVICE = 0x03
+
+    # The Client shall include this SRP to indicate that it is interested in the listed DIBs
+    REQUEST_DIBS = 0x04
diff --git a/xknx/knxip/search_request_extended.py b/xknx/knxip/search_request_extended.py
new file mode 100644
index 00000000..fa880753
--- /dev/null
+++ b/xknx/knxip/search_request_extended.py
@@ -0,0 +1,65 @@
+"""
+Module for Serialization and Deserialization of KNX Extended Search Requests.
+
+Extended Search Requests are used to search for KNX/IP devices within the network that support for instance IP Secure.
+
+See AN184 in version 03 from the KNX specifications.
+"""
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from .body import KNXIPBody
+from .hpai import HPAI
+from .knxip_enum import KNXIPServiceType
+from .srp import SRP
+
+if TYPE_CHECKING:
+    from xknx.xknx import XKNX
+
+
+class SearchRequestExtended(KNXIPBody):
+    """Representation of a KNX Search Request Extended."""
+
+    SERVICE_TYPE = KNXIPServiceType.SEARCH_REQUEST_EXTENDED
+
+    def __init__(
+        self,
+        xknx: XKNX,
+        srps: list[SRP] | None = None,
+        discovery_endpoint: HPAI | None = None,
+    ):
+        """Initialize SearchRequestExtended object."""
+        super().__init__(xknx)
+        self.srps = srps or []
+        self.discovery_endpoint = (
+            discovery_endpoint
+            if discovery_endpoint is not None
+            else HPAI(ip_addr=xknx.multicast_group, port=xknx.multicast_port)
+        )
+
+    def calculated_length(self) -> int:
+        """Get length of KNX/IP body."""
+        return HPAI.LENGTH + sum(srp.payload_size for srp in self.srps)
+
+    def from_knx(self, raw: bytes) -> int:
+        """Parse/deserialize from KNX/IP raw data."""
+        pos: int = self.discovery_endpoint.from_knx(raw)
+        while raw[pos:]:
+            srp = SRP.from_knx(raw[pos:])
+            pos += len(srp)
+            self.srps.append(srp)
+
+        return pos
+
+    def to_knx(self) -> bytes:
+        """Serialize to KNX/IP raw data."""
+        return self.discovery_endpoint.to_knx() + b"".join(
+            bytes(srp) for srp in self.srps
+        )
+
+    def __str__(self) -> str:
+        """Return object as readable string."""
+        return (
+            f'<SearchRequestExtended discovery_endpoint="{self.discovery_endpoint}" />'
+        )
diff --git a/xknx/knxip/search_response_extended.py b/xknx/knxip/search_response_extended.py
new file mode 100644
index 00000000..5612283d
--- /dev/null
+++ b/xknx/knxip/search_response_extended.py
@@ -0,0 +1,40 @@
+"""
+Module for Serialization and Deserialization of KNX Search Response Extended.
+
+Search Requests Extended are used to search for KNX/IP devices within the network.
+A search response extended contains all information of a found device (Name, serial number, supported features.).
+It supports an array-style access to the DIBs (use classname as index). Every KNXnet/ip server shall send
+a search response extended and one device supporting multiple KNX connections may send multiple search responses.
+
+If there were any SRPs (Search Request Parameters) in the Search Request Extended and those were marked as mandatory
+the server shall only reply to those requests if it can fulfill the requirements in the SRP.
+"""
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from .hpai import HPAI
+from .knxip_enum import KNXIPServiceType
+from .search_response import SearchResponse
+
+if TYPE_CHECKING:
+    from xknx.xknx import XKNX
+
+
+class SearchResponseExtended(SearchResponse):
+    """Representation of a KNX Search Extended."""
+
+    SERVICE_TYPE = KNXIPServiceType.SEARCH_RESPONSE_EXTENDED
+
+    def __init__(self, xknx: XKNX, control_endpoint: HPAI = HPAI()):
+        """Initialize SearchResponse object."""
+        super().__init__(xknx, control_endpoint)
+
+    def __str__(self) -> str:
+        """Return object as readable string."""
+        _dibs_str = ",\n".join(dib.__str__() for dib in self.dibs)
+        return (
+            "<SearchResponseExtended "
+            f'control_endpoint="{self.control_endpoint}" '
+            f'dibs="[\n{_dibs_str}\n]" />'
+        )
diff --git a/xknx/knxip/srp.py b/xknx/knxip/srp.py
new file mode 100644
index 00000000..f4f9cd9b
--- /dev/null
+++ b/xknx/knxip/srp.py
@@ -0,0 +1,151 @@
+"""
+Module for handling Search Request Parameters (SRP).
+
+This can be used e.g. to restrict the set of devices that are expected to respond or to influence the type of DIBs
+which the client is interested in.
+
+If the M (Mandatory) bit is set, the server shall only respond to the search request if the complete SRP block
+is satisfied.
+"""
+from __future__ import annotations
+
+from ..exceptions import ConversionError, CouldNotParseKNXIP
+from .knxip_enum import DIBServiceFamily, DIBTypeCode, SearchRequestParameterType
+
+
+class SRP:
+    """Search request parameter for a search request extended."""
+
+    SRP_HEADER_SIZE = 2
+    MANDATORY_BIT_INDEX = 0x07
+
+    # service family and service version have 1 byte each
+    SERVICE_PAYLOAD_LENGTH = 2
+    # mac address has 6 bytes
+    MAC_ADDRESS_PAYLOAD_LENGTH = 6
+
+    def __init__(
+        self,
+        srp_type: SearchRequestParameterType,
+        mandatory: bool = True,
+        data: bytes = bytes(),
+    ):
+        """Initialize a SRP."""
+        self.type = srp_type
+        self.mandatory = mandatory
+        self.data = data
+        self.payload_size = SRP.SRP_HEADER_SIZE
+
+        if self.type == SearchRequestParameterType.SELECT_BY_SERVICE:
+            self.validate_payload_length(SRP.SERVICE_PAYLOAD_LENGTH)
+            self.payload_size = SRP.SRP_HEADER_SIZE + SRP.SERVICE_PAYLOAD_LENGTH
+        elif self.type == SearchRequestParameterType.SELECT_BY_MAC_ADDRESS:
+            self.validate_payload_length(SRP.MAC_ADDRESS_PAYLOAD_LENGTH)
+            self.payload_size = SRP.SRP_HEADER_SIZE + SRP.MAC_ADDRESS_PAYLOAD_LENGTH
+        elif self.type == SearchRequestParameterType.REQUEST_DIBS:
+            if not self.data or len(self.data) < 1:
+                raise ConversionError("Srp DIBs are not present.")
+            # If the Client is interested in an odd number of DIBs
+            # it shall add an additional Description Type 0 to make the structure length even
+            if len(self.data) % 2:
+                self.data = self.data + bytes([0])
+            self.payload_size = SRP.SRP_HEADER_SIZE + len(self.data)
+
+    def validate_payload_length(self, size: int) -> None:
+        """Validate the length of the payload."""
+        if not self.data or len(self.data) != size:
+            raise ConversionError(
+                "Srp parameter payload size does not match.",
+                expected_size=size,
+                srp_type=self.type,
+            )
+
+    def __len__(self) -> int:
+        """Get the payload length."""
+        return self.payload_size
+
+    def __bytes__(self) -> bytes:
+        """Convert this SRP to a bytes object."""
+        return (
+            bytes(
+                [
+                    self.payload_size,
+                    (
+                        self.mandatory << SRP.MANDATORY_BIT_INDEX
+                        | self.type.value & SRP.MANDATORY_BIT_INDEX
+                    ),
+                ]
+            )
+            + self.data
+        )
+
+    @staticmethod
+    def from_knx(data: bytes) -> SRP:
+        """Convert the bytes to a SRP object."""
+        if len(data) < SRP.SRP_HEADER_SIZE:
+            raise CouldNotParseKNXIP("Data too short for SRP object.")
+
+        size: int = data[0]
+        if size > len(data):
+            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],
+        )
+
+    @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)
+
+    @staticmethod
+    def with_mac_address(mac_address: bytes) -> SRP:
+        """
+        Create a SRP that limits the response to only allow a device with the given MAC address.
+
+        :param mac_address: The mac address to restrict this SRP to
+        """
+        return SRP(SearchRequestParameterType.SELECT_BY_MAC_ADDRESS, True, mac_address)
+
+    @staticmethod
+    def with_service(family: DIBServiceFamily, family_version: int) -> SRP:
+        """
+        Create a SRP that limits the response to only allow devices that support the given service family.
+
+        :param family: DIBServiceFamily that this SRP should be limited to
+        :param family_version: The minimum family version so that devices will send a search response extended back
+        :return: Srp with the given parameter
+        """
+        return SRP(
+            SearchRequestParameterType.SELECT_BY_SERVICE,
+            True,
+            bytes([family.value, family_version]),
+        )
+
+    @staticmethod
+    def with_device_description(dibs: list[DIBTypeCode]) -> SRP:
+        """
+        Create a SRP with a list of DIBs to indicate the server that it should, at least, include.
+
+        :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),
+        )
+
+    def __eq__(self, other: object) -> bool:
+        """Define equality for SRPs."""
+        if not isinstance(other, SRP):
+            return False
+
+        return (
+            self.payload_size == other.payload_size
+            and self.type == other.type
+            and self.mandatory == other.mandatory
+            and self.data == other.data
+        )


Test patch:
diff --git a/requirements/testing.txt b/requirements/testing.txt
index ca22e88e..a76b8107 100644
--- a/requirements/testing.txt
+++ b/requirements/testing.txt
@@ -1,5 +1,5 @@
 -r production.txt
-pre-commit==2.17.0
+pre-commit==2.18.1
 isort==5.10.1
 flake8==4.0.1
 flake8-isort==4.1.1
@@ -8,6 +8,6 @@ pylint==2.13.4
 pytest==7.1.1
 pytest-asyncio==0.18.3
 pytest-cov==3.0.0
-setuptools==61.3.0
+setuptools==61.3.1
 tox==3.24.5
 mypy==0.942
diff --git a/test/io_tests/gateway_scanner_test.py b/test/io_tests/gateway_scanner_test.py
index 7caf2e39..7d659477 100644
--- a/test/io_tests/gateway_scanner_test.py
+++ b/test/io_tests/gateway_scanner_test.py
@@ -12,6 +12,7 @@ from xknx.knxip import (
     DIBServiceFamily,
     DIBSuppSVCFamilies,
     KNXIPFrame,
+    KNXIPServiceType,
     SearchResponse,
 )
 from xknx.telegram import IndividualAddress
@@ -212,8 +213,51 @@ class TestGatewayScanner:
 
         assert _search_interface_mock.call_count == 2
         expected_calls = [
-            ((gateway_scanner, "lo0", "127.0.0.1"),),
-            ((gateway_scanner, "en1", "10.1.1.2"),),
+            ((gateway_scanner, "lo0", "127.0.0.1", KNXIPServiceType.SEARCH_REQUEST),),
+            ((gateway_scanner, "en1", "10.1.1.2", KNXIPServiceType.SEARCH_REQUEST),),
+        ]
+        assert _search_interface_mock.call_args_list == expected_calls
+        assert test_scan == []
+
+    @patch("xknx.io.gateway_scanner.netifaces", autospec=True)
+    @patch("xknx.io.GatewayScanner._search_interface", autospec=True)
+    async def test_send_search_request_extended(
+        self, _search_interface_mock, netifaces_mock
+    ):
+        """Test finding all valid interfaces to send search request extended to. No requests are sent."""
+        xknx = XKNX()
+
+        netifaces_mock.interfaces.return_value = self.fake_interfaces
+        netifaces_mock.ifaddresses = lambda interface: self.fake_ifaddresses[interface]
+        netifaces_mock.AF_INET = 2
+
+        async def async_none():
+            return None
+
+        _search_interface_mock.return_value = asyncio.ensure_future(async_none())
+
+        gateway_scanner = GatewayScanner(xknx, timeout_in_seconds=0)
+
+        test_scan = await gateway_scanner.scan(KNXIPServiceType.SEARCH_REQUEST_EXTENDED)
+
+        assert _search_interface_mock.call_count == 2
+        expected_calls = [
+            (
+                (
+                    gateway_scanner,
+                    "lo0",
+                    "127.0.0.1",
+                    KNXIPServiceType.SEARCH_REQUEST_EXTENDED,
+                ),
+            ),
+            (
+                (
+                    gateway_scanner,
+                    "en1",
+                    "10.1.1.2",
+                    KNXIPServiceType.SEARCH_REQUEST_EXTENDED,
+                ),
+            ),
         ]
         assert _search_interface_mock.call_args_list == expected_calls
         assert test_scan == []
diff --git a/test/knxip_tests/knxip_test.py b/test/knxip_tests/knxip_test.py
index 1050e0b8..bba427c6 100644
--- a/test/knxip_tests/knxip_test.py
+++ b/test/knxip_tests/knxip_test.py
@@ -19,7 +19,7 @@ class TestKNXIPFrame:
 
         with pytest.raises(CouldNotParseKNXIP):
             # this is not yet implemented in xknx
-            knxipframe.init(KNXIPServiceType.SEARCH_REQUEST_EXTENDED)
+            knxipframe.init(KNXIPServiceType.REMOTE_DIAG_RESPONSE)
 
     def test_double_frame(self):
         """Test parsing KNX/IP frame from streaming data containing two frames."""
diff --git a/test/knxip_tests/search_request_extended_test.py b/test/knxip_tests/search_request_extended_test.py
new file mode 100644
index 00000000..59b3287c
--- /dev/null
+++ b/test/knxip_tests/search_request_extended_test.py
@@ -0,0 +1,84 @@
+"""Unit test for KNX/IP SearchRequest Extended objects."""
+from xknx import XKNX
+from xknx.knxip import HPAI, KNXIPFrame, SearchRequestExtended
+from xknx.knxip.srp import SRP
+
+
+class TestKNXIPSearchRequestExtended:
+    """Test class for KNX/IP SearchRequest Extended objects."""
+
+    def test_search_request_extended(self):
+        """Test parsing and streaming SearchRequest Extended KNX/IP packet."""
+        raw = bytes.fromhex("06 10 02 0b 00 0e 08 01 e0 00 17 0C 0E 57")
+        xknx = XKNX()
+        knxipframe = KNXIPFrame(xknx)
+        knxipframe.from_knx(raw)
+
+        assert isinstance(knxipframe.body, SearchRequestExtended)
+        assert knxipframe.body.discovery_endpoint == HPAI(
+            ip_addr="224.0.23.12", port=3671
+        )
+
+        knxipframe2 = KNXIPFrame.init_from_body(
+            SearchRequestExtended(
+                xknx, discovery_endpoint=HPAI(ip_addr="224.0.23.12", port=3671)
+            )
+        )
+
+        assert knxipframe2.to_knx() == raw
+
+    def test_search_request_extended_srp(self):
+        """Test parsing and streaming SearchRequest Extended with SRPs KNX/IP packet."""
+        raw = bytes.fromhex("06 10 02 0b 00 10 08 01 e0 00 17 0c 0e 57 02 81")
+        xknx = XKNX()
+        knxipframe = KNXIPFrame(xknx)
+        knxipframe.from_knx(raw)
+
+        assert isinstance(knxipframe.body, SearchRequestExtended)
+        assert knxipframe.body.discovery_endpoint == HPAI(
+            ip_addr="224.0.23.12", port=3671
+        )
+        assert len(knxipframe.body.srps) == 1
+        assert knxipframe.body.srps[0] == SRP.with_programming_mode()
+
+        knxipframe2 = KNXIPFrame.init_from_body(
+            SearchRequestExtended(
+                xknx,
+                srps=[SRP.with_programming_mode()],
+                discovery_endpoint=HPAI(ip_addr="224.0.23.12", port=3671),
+            )
+        )
+
+        assert knxipframe2.to_knx() == raw
+
+    def test_search_request_extended_multiple_srp(self):
+        """Test parsing and streaming SearchRequest Extended with SRPs KNX/IP packet."""
+        raw = bytes.fromhex(
+            "06 10 02 0b 00 18 08 01 e0 00 17 0c 0e 57 02 81 08 82 01 02 03 04 05 06"
+        )
+        xknx = XKNX()
+        knxipframe = KNXIPFrame(xknx)
+        knxipframe.from_knx(raw)
+
+        assert isinstance(knxipframe.body, SearchRequestExtended)
+        assert knxipframe.body.discovery_endpoint == HPAI(
+            ip_addr="224.0.23.12", port=3671
+        )
+        assert len(knxipframe.body.srps) == 2
+        assert knxipframe.body.srps[0] == SRP.with_programming_mode()
+        assert knxipframe.body.srps[1] == SRP.with_mac_address(
+            bytes([1, 2, 3, 4, 5, 6])
+        )
+
+        knxipframe2 = KNXIPFrame.init_from_body(
+            SearchRequestExtended(
+                xknx,
+                srps=[
+                    SRP.with_programming_mode(),
+                    SRP.with_mac_address(bytes([1, 2, 3, 4, 5, 6])),
+                ],
+                discovery_endpoint=HPAI(ip_addr="224.0.23.12", port=3671),
+            )
+        )
+
+        assert knxipframe2.to_knx() == raw
diff --git a/test/knxip_tests/search_response_extended_test.py b/test/knxip_tests/search_response_extended_test.py
new file mode 100644
index 00000000..7ca8352b
--- /dev/null
+++ b/test/knxip_tests/search_response_extended_test.py
@@ -0,0 +1,56 @@
+"""Unit test for KNX/IP SearchResponseExtended objects."""
+from xknx import XKNX
+from xknx.knxip import (
+    HPAI,
+    DIBDeviceInformation,
+    DIBServiceFamily,
+    DIBSuppSVCFamilies,
+    KNXIPFrame,
+    SearchResponseExtended,
+)
+
+
+class TestKNXIPSearchResponseExtended:
+    """Test class for KNX/IP SearchResponse Extended objects."""
+
+    def test_search_response_extended(self):
+        """Test parsing and streaming SearchResponse KNX/IP packet."""
+        raw = bytes.fromhex(
+            "06 10 02 0C 00 50 08 01 C0 A8 2A 0A 0E 57 36 01"
+            "02 00 11 00 00 00 11 22 33 44 55 66 E0 00 17 0C"
+            "01 02 03 04 05 06 47 69 72 61 20 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 0C 02 02 01 03 02 04 01 05 01 07 01"
+        )
+        xknx = XKNX()
+        knxipframe = KNXIPFrame(xknx)
+        assert knxipframe.from_knx(raw) == 80
+        assert knxipframe.to_knx() == raw
+
+        assert isinstance(knxipframe.body, SearchResponseExtended)
+        assert knxipframe.body.control_endpoint == HPAI("192.168.42.10", 3671)
+        assert len(knxipframe.body.dibs) == 2
+        # Specific testing of parsing and serializing of
+        # DIBDeviceInformation and DIBSuppSVCFamilies is
+        # done within knxip_dib_test.py
+        assert isinstance(knxipframe.body.dibs[0], DIBDeviceInformation)
+        assert isinstance(knxipframe.body.dibs[1], DIBSuppSVCFamilies)
+        assert knxipframe.body.device_name == "Gira KNX/IP-Router"
+        assert knxipframe.body.dibs[1].supports(DIBServiceFamily.ROUTING)
+        assert knxipframe.body.dibs[1].supports(DIBServiceFamily.TUNNELING)
+        assert not knxipframe.body.dibs[1].supports(DIBServiceFamily.OBJECT_SERVER)
+
+        search_response = SearchResponseExtended(
+            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)
+
+        assert knxipframe2.to_knx() == raw
+
+    def test_unknown_device_name(self):
+        """Test device_name if no DIBDeviceInformation is present."""
+        xknx = XKNX()
+        search_response = SearchResponseExtended(xknx)
+        assert search_response.device_name == "UNKNOWN"
diff --git a/test/knxip_tests/srp_test.py b/test/knxip_tests/srp_test.py
new file mode 100644
index 00000000..8047aeec
--- /dev/null
+++ b/test/knxip_tests/srp_test.py
@@ -0,0 +1,85 @@
+"""Unit test for KNX/IP SRP objects."""
+import pytest
+
+from xknx.exceptions import ConversionError, CouldNotParseKNXIP
+from xknx.knxip import DIBServiceFamily, DIBTypeCode, SearchRequestParameterType
+from xknx.knxip.srp import SRP
+
+
+class TestKNXIPSRP:
+    """Test class for KNX/IP SRP objects."""
+
+    def test_basic(self):
+        """Test SRP with mac contains mac address."""
+        srp: SRP = SRP.with_mac_address(bytes([1, 2, 3, 4, 5, 6]))
+        assert len(srp) == SRP.SRP_HEADER_SIZE + 6
+        assert srp.type == SearchRequestParameterType.SELECT_BY_MAC_ADDRESS
+
+        srp: SRP = SRP.with_service(DIBServiceFamily.TUNNELING, 2)
+        assert len(srp) == SRP.SRP_HEADER_SIZE + 2
+        assert srp.type == SearchRequestParameterType.SELECT_BY_SERVICE
+
+        srp: SRP = SRP.with_programming_mode()
+        assert len(srp) == SRP.SRP_HEADER_SIZE
+        assert srp.type == SearchRequestParameterType.SELECT_BY_PROGRAMMING_MODE
+
+        dibs: list[DIBTypeCode] = [
+            DIBTypeCode.SUPP_SVC_FAMILIES,
+            DIBTypeCode.ADDITIONAL_DEVICE_INFO,
+        ]
+        srp: SRP = SRP.with_device_description(dibs)
+        assert len(srp) == SRP.SRP_HEADER_SIZE + len(dibs)
+        assert srp.type == SearchRequestParameterType.REQUEST_DIBS
+
+    def test_invalid_payload_raises(self):
+        """Test SRP with invalid data size raises."""
+        with pytest.raises(ConversionError):
+            SRP.with_mac_address(bytes([1, 2, 3, 4, 5, 6, 7, 8]))
+
+        with pytest.raises(ConversionError):
+            SRP(SearchRequestParameterType.SELECT_BY_SERVICE, True)
+
+        with pytest.raises(ConversionError):
+            SRP(SearchRequestParameterType.REQUEST_DIBS, True)
+
+    def test_invalid_payload_from_knx_raises(self):
+        """Test from_knx with invalid data size raises."""
+        # size is too big
+        with pytest.raises(CouldNotParseKNXIP):
+            SRP.from_knx(bytes.fromhex("08 82"))
+
+        # too small
+        with pytest.raises(CouldNotParseKNXIP):
+            SRP.from_knx(bytes.fromhex("08"))
+
+    @pytest.mark.parametrize(
+        "srp,expected_bytes",
+        [
+            (
+                SRP.with_mac_address(bytes([1, 2, 3, 4, 5, 6])),
+                bytes.fromhex("08 82 01 02 03 04 05 06"),
+            ),
+            (
+                SRP.with_service(DIBServiceFamily.TUNNELING, 2),
+                bytes.fromhex("04 83 04 02"),
+            ),
+            (
+                SRP.with_programming_mode(),
+                bytes.fromhex("02 81"),
+            ),
+            (
+                SRP.with_device_description(
+                    [DIBTypeCode.SUPP_SVC_FAMILIES, DIBTypeCode.TUNNELING_INFO]
+                ),
+                bytes.fromhex("04 84 02 07"),
+            ),
+            (
+                SRP.with_device_description([DIBTypeCode.SUPP_SVC_FAMILIES]),
+                bytes.fromhex("04 84 02 00"),
+            ),
+        ],
+    )
+    def test_to_and_from_knx(self, srp, expected_bytes):
+        """Test to and from KNX."""
+        assert bytes(srp) == expected_bytes
+        assert SRP.from_knx(expected_bytes) == srp
diff --git a/test/str_test.py b/test/str_test.py
index b5cff428..bfedb656 100644
--- a/test/str_test.py
+++ b/test/str_test.py
@@ -49,6 +49,7 @@ from xknx.knxip import (
     RoutingIndication,
     SearchRequest,
     SearchResponse,
+    SearchResponseExtended,
     TunnellingAck,
     TunnellingRequest,
 )
@@ -612,6 +613,21 @@ class TestStringRepresentations:
             ']" />'
         )
 
+    def test_search_response_extended(self):
+        """Test string representation of KNX/IP SearchResponseExtended."""
+        xknx = XKNX()
+        search_response = SearchResponseExtended(xknx)
+        search_response.control_endpoint = HPAI(ip_addr="192.168.42.1", port=33941)
+        search_response.dibs.append(DIBGeneric())
+        search_response.dibs.append(DIBGeneric())
+        assert (
+            str(search_response)
+            == '<SearchResponseExtended control_endpoint="192.168.42.1:33941/udp" dibs="[\n'
+            '<DIB dtc="0" data="" />,\n'
+            '<DIB dtc="0" data="" />\n'
+            ']" />'
+        )
+
     def test_tunnelling_request(self):
         """Test string representation of KNX/IP TunnellingRequest."""
         xknx = XKNX()