Repository: XKNX/xknx
Pull request #788 (2021-10-31 23:13:33)

Add individual address to gateway descriptor and correct source address
<!--
  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 individual address to gateway descriptor and correct source address for outgoing telegrams.

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:

- [x] I have performed a self-review of my own code
- [x] 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
- [x] The Homeassistant plugin has been adjusted in case of new config options


codecov[bot]: # [Codecov](https://codecov.io/gh/XKNX/xknx/pull/788?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) Report
> Merging [#788](https://codecov.io/gh/XKNX/xknx/pull/788?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) (997bf4e) into [main](https://codecov.io/gh/XKNX/xknx/commit/bde3007b4aefefbf1c3819b090e5e124c9e5f3b5?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) (bde3007) will **decrease** coverage by `0.03%`.
> The diff coverage is `84.21%`.

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

```diff
@@            Coverage Diff             @@
##             main     #788      +/-   ##
==========================================
- Coverage   94.78%   94.75%   -0.04%     
==========================================
  Files         109      109              
  Lines        6791     6806      +15     
==========================================
+ Hits         6437     6449      +12     
- Misses        354      357       +3     
```


| [Impacted Files](https://codecov.io/gh/XKNX/xknx/pull/788?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX) | Coverage Δ | |
|---|---|---|
| [xknx/core/payload\_reader.py](https://codecov.io/gh/XKNX/xknx/pull/788/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9jb3JlL3BheWxvYWRfcmVhZGVyLnB5) | `92.50% <ø> (ø)` | |
| [xknx/core/value\_reader.py](https://codecov.io/gh/XKNX/xknx/pull/788/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9jb3JlL3ZhbHVlX3JlYWRlci5weQ==) | `100.00% <ø> (ø)` | |
| [xknx/io/tunnel.py](https://codecov.io/gh/XKNX/xknx/pull/788/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9pby90dW5uZWwucHk=) | `38.23% <0.00%> (-0.23%)` | :arrow_down: |
| [xknx/remote\_value/remote\_value.py](https://codecov.io/gh/XKNX/xknx/pull/788/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9yZW1vdGVfdmFsdWUvcmVtb3RlX3ZhbHVlLnB5) | `100.00% <ø> (ø)` | |
| [xknx/io/gateway\_scanner.py](https://codecov.io/gh/XKNX/xknx/pull/788/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9pby9nYXRld2F5X3NjYW5uZXIucHk=) | `83.17% <87.50%> (+0.19%)` | :arrow_up: |
| [xknx/io/\_\_init\_\_.py](https://codecov.io/gh/XKNX/xknx/pull/788/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9pby9fX2luaXRfXy5weQ==) | `100.00% <100.00%> (ø)` | |
| [xknx/xknx.py](https://codecov.io/gh/XKNX/xknx/pull/788/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC94a254LnB5) | `86.40% <100.00%> (+0.13%)` | :arrow_up: |


Patch:
diff --git a/changelog.md b/changelog.md
index 79af2f21..a8ad004e 100644
--- a/changelog.md
+++ b/changelog.md
@@ -2,7 +2,9 @@
 
 ## Unreleased changes
 
-### Internals
+### Devices
+
+- Added `always_callback` option to NumericValue and RawValue
 
 ## 0.18.11 Task Registry 2021-10-16
 
diff --git a/docs/numeric_value.md b/docs/numeric_value.md
index 472fc41e..0c15ab7d 100644
--- a/docs/numeric_value.md
+++ b/docs/numeric_value.md
@@ -20,6 +20,7 @@ NumericValue devices send values to the KNX bus. Received values update the devi
 - `respond_to_read` if `True` GroupValueRead requests to the `group_address` are answered. Defaults to `False`
 - `sync_state` defines if and how often the value should be actively read from the bus. If `False` no GroupValueRead telegrams will be sent to its group address. Defaults to `True`
 - `value_type` controls how the value should be encoded / decoded. The attribut may have may have parseable value types representing numeric values.
+- `always_callback` defines if a callback shall be triggered for consecutive GroupValueWrite telegrams with same payload. Defaults to `False`
 - `device_updated_cb` awaitable callback for each update.
 
 ## [](#header-2)Example
diff --git a/docs/raw_value.md b/docs/raw_value.md
index a0854dd9..95491904 100644
--- a/docs/raw_value.md
+++ b/docs/raw_value.md
@@ -20,6 +20,7 @@ RawValue devices send uint values to the KNX bus. Received values update the dev
 - `group_address_state` is the KNX group address of the raw value device.
 - `respond_to_read` if `True` GroupValueRead requests to the `group_address` are answered. Defaults to `False`
 - `sync_state` defines if and how often the value should be actively read from the bus. If `False` no GroupValueRead telegrams will be sent to its group address. Defaults to `True`
+- `always_callback` defines if a callback shall be triggered for consecutive GroupValueWrite telegrams with same payload. Defaults to `False`
 - `device_updated_cb` awaitable callback for each update.
 
 ## [](#header-2)Example
diff --git a/docs/sensor.md b/docs/sensor.md
index 93788d8c..e97c46d2 100644
--- a/docs/sensor.md
+++ b/docs/sensor.md
@@ -13,13 +13,13 @@ Sensors are monitoring temperature, air humidity, pressure etc. from KNX bus.
 
 ## [](#header-2)Interface
 
-* `xknx` is the XKNX object.
-* `name` is the name of the object.
-* `group_address_state` is the KNX group address of the sensor device.
-* `sync_state` defines if the value should be actively read from the bus. If `False` no GroupValueRead telegrams will be sent to its group address. Defaults to `True`
-* `always_callback` defines if a callback/update should always be triggered no matter if the previous and the new state are identical.
-* `value_type` controls how the value should be rendered in a human readable representation. The attribut may have may have the values `percent`, `temperature`, `illuminance`, `speed_ms` or `current`.
-* `device_updated_cb` awaitable callback for each update.
+- `xknx` is the XKNX object.
+- `name` is the name of the object.
+- `group_address_state` is the KNX group address of the sensor device.
+- `sync_state` defines if the value should be actively read from the bus. If `False` no GroupValueRead telegrams will be sent to its group address. Defaults to `True`
+- `always_callback` defines if a callback shall be triggered for consecutive GroupValueWrite telegrams with same payload. Defaults to `False`
+- `value_type` controls how the value should be rendered in a human readable representation. The attribut may have may have the values `percent`, `temperature`, `illuminance`, `speed_ms` or `current`.
+- `device_updated_cb` awaitable callback for each update.
 
 ## [](#header-2)Example
 
diff --git a/xknx/core/payload_reader.py b/xknx/core/payload_reader.py
index 2c2b0b31..b1b942f7 100644
--- a/xknx/core/payload_reader.py
+++ b/xknx/core/payload_reader.py
@@ -81,7 +81,11 @@ class PayloadReader:
     async def send_telegram(self, payload: APCI) -> None:
         """Send the telegram."""
         await self.xknx.telegrams.put(
-            Telegram(destination_address=self.address, payload=payload)
+            Telegram(
+                destination_address=self.address,
+                payload=payload,
+                source_address=self.xknx.current_address,
+            )
         )
 
     async def telegram_received(
diff --git a/xknx/core/value_reader.py b/xknx/core/value_reader.py
index 60081d90..bfb3e5cd 100644
--- a/xknx/core/value_reader.py
+++ b/xknx/core/value_reader.py
@@ -70,7 +70,9 @@ class ValueReader:
     async def send_group_read(self) -> None:
         """Send group read."""
         telegram = Telegram(
-            destination_address=self.group_address, payload=GroupValueRead()
+            destination_address=self.group_address,
+            payload=GroupValueRead(),
+            source_address=self.xknx.current_address,
         )
         await self.xknx.telegrams.put(telegram)
 
diff --git a/xknx/devices/numeric_value.py b/xknx/devices/numeric_value.py
index c4d9d41d..2d5fbb6d 100644
--- a/xknx/devices/numeric_value.py
+++ b/xknx/devices/numeric_value.py
@@ -32,10 +32,12 @@ class NumericValue(Device):
         respond_to_read: bool = False,
         sync_state: bool | int | float | str = True,
         value_type: int | str | None = None,
+        always_callback: bool = False,
         device_updated_cb: DeviceCallbackType | None = None,
     ):
         """Initialize Sensor class."""
         super().__init__(xknx, name, device_updated_cb)
+        self.always_callback = always_callback
         self.respond_to_read = respond_to_read
         self.sensor_value = RemoteValueNumeric(
             xknx,
@@ -58,7 +60,7 @@ class NumericValue(Device):
 
     async def process_group_write(self, telegram: "Telegram") -> None:
         """Process incoming and outgoing GROUP WRITE telegram."""
-        await self.sensor_value.process(telegram)
+        await self.sensor_value.process(telegram, always_callback=self.always_callback)
 
     async def process_group_read(self, telegram: "Telegram") -> None:
         """Process incoming GroupValueResponse telegrams."""
diff --git a/xknx/devices/raw_value.py b/xknx/devices/raw_value.py
index 4825139e..bfd66013 100644
--- a/xknx/devices/raw_value.py
+++ b/xknx/devices/raw_value.py
@@ -32,10 +32,12 @@ class RawValue(Device):
         group_address_state: GroupAddressesType | None = None,
         respond_to_read: bool = False,
         sync_state: bool | int | float | str = True,
+        always_callback: bool = False,
         device_updated_cb: DeviceCallbackType | None = None,
     ):
         """Initialize Sensor class."""
         super().__init__(xknx, name, device_updated_cb)
+        self.always_callback = always_callback
         self.respond_to_read = respond_to_read
         self.remote_value = RemoteValueRaw(
             xknx,
@@ -58,7 +60,7 @@ class RawValue(Device):
 
     async def process_group_write(self, telegram: "Telegram") -> None:
         """Process incoming and outgoing GROUP WRITE telegram."""
-        await self.remote_value.process(telegram)
+        await self.remote_value.process(telegram, always_callback=self.always_callback)
 
     async def process_group_read(self, telegram: "Telegram") -> None:
         """Process incoming GroupValueResponse telegrams."""
diff --git a/xknx/io/__init__.py b/xknx/io/__init__.py
index 4785c262..60d908d4 100644
--- a/xknx/io/__init__.py
+++ b/xknx/io/__init__.py
@@ -9,7 +9,7 @@ This package contains all objects managing Tunneling and Routing Connections..
 # flake8: noqa
 from .connection import ConnectionConfig, ConnectionType
 from .const import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT
-from .gateway_scanner import GatewayScanFilter, GatewayScanner
+from .gateway_scanner import GatewayDescriptor, GatewayScanFilter, GatewayScanner
 from .knxip_interface import KNXIPInterface
 from .routing import Routing
 from .tunnel import Tunnel
diff --git a/xknx/io/gateway_scanner.py b/xknx/io/gateway_scanner.py
index 07759dc6..215f25e0 100644
--- a/xknx/io/gateway_scanner.py
+++ b/xknx/io/gateway_scanner.py
@@ -9,12 +9,14 @@ from __future__ import annotations
 
 import asyncio
 from functools import partial
+from ipaddress import IPv4Address, IPv4Network
 import logging
 from typing import TYPE_CHECKING
 
 import netifaces
 from xknx.knxip import (
     HPAI,
+    DIBDeviceInformation,
     DIBServiceFamily,
     DIBSuppSVCFamilies,
     KNXIPFrame,
@@ -22,6 +24,7 @@ from xknx.knxip import (
     SearchRequest,
     SearchResponse,
 )
+from xknx.telegram import IndividualAddress
 
 from .udp_client import UDPClient
 
@@ -43,6 +46,7 @@ class GatewayDescriptor:
         local_ip: str,
         supports_tunnelling: bool = False,
         supports_routing: bool = False,
+        individual_address: IndividualAddress | None = None,
     ):
         """Initialize GatewayDescriptor class."""
         self.name = name
@@ -52,16 +56,11 @@ class GatewayDescriptor:
         self.local_ip = local_ip
         self.supports_routing = supports_routing
         self.supports_tunnelling = supports_tunnelling
+        self.individual_address = individual_address
 
     def __str__(self) -> str:
         """Return object as readable string."""
-        return (
-            f'<GatewayDescriptor name="{self.name}" '
-            f'addr="{self.ip_addr}:{self.port}" '
-            f'local="{self.local_ip}@{self.local_interface}" '
-            f'routing="{self.supports_routing}" '
-            f'tunnelling="{self.supports_tunnelling}" />'
-        )
+        return f"{self.individual_address} - {self.name} @ {self.ip_addr}:{self.port}"
 
 
 class GatewayScanFilter:
@@ -146,12 +145,15 @@ class GatewayScanner:
             try:
                 af_inet = netifaces.ifaddresses(interface)[netifaces.AF_INET]
                 ip_addr = af_inet[0]["addr"]
-                await self._search_interface(interface, ip_addr)
+                netmask = af_inet[0]["netmask"]
+                await self._search_interface(interface, ip_addr, netmask)
             except KeyError:
                 logger.info("Could not connect to an KNX/IP device on %s", interface)
                 continue
 
-    async def _search_interface(self, interface: str, ip_addr: str) -> None:
+    async def _search_interface(
+        self, interface: str, ip_addr: str, netmask: str
+    ) -> None:
         """Send a search request on a specific interface."""
         logger.debug("Searching on %s / %s", interface, ip_addr)
 
@@ -163,7 +165,7 @@ class GatewayScanner:
         )
 
         udp_client.register_callback(
-            partial(self._response_rec_callback, interface=interface),
+            partial(self._response_rec_callback, interface=interface, netmask=netmask),
             [KNXIPServiceType.SEARCH_RESPONSE],
         )
         await udp_client.connect()
@@ -177,13 +179,25 @@ class GatewayScanner:
         udp_client.send(KNXIPFrame.init_from_body(search_request))
 
     def _response_rec_callback(
-        self, knx_ip_frame: KNXIPFrame, udp_client: UDPClient, interface: str = ""
+        self,
+        knx_ip_frame: KNXIPFrame,
+        udp_client: UDPClient,
+        interface: str = "",
+        netmask: str = "",
     ) -> None:
         """Verify and handle knxipframe. Callback from internal udpclient."""
         if not isinstance(knx_ip_frame.body, SearchResponse):
             logger.warning("Could not understand knxipframe")
             return
 
+        address: IPv4Address = IPv4Address(knx_ip_frame.body.control_endpoint.ip_addr)
+        network: IPv4Network = IPv4Network(
+            f"{udp_client.local_addr[0]}/{netmask}", False
+        )
+
+        if address not in network:
+            return
+
         gateway = GatewayDescriptor(
             name=knx_ip_frame.body.device_name,
             ip_addr=knx_ip_frame.body.control_endpoint.ip_addr,
@@ -202,6 +216,16 @@ class GatewayScanner:
         except StopIteration:
             pass
 
+        try:
+            device_infos = next(
+                device_infos
+                for device_infos in knx_ip_frame.body.dibs
+                if isinstance(device_infos, DIBDeviceInformation)
+            )
+            gateway.individual_address = device_infos.individual_address
+        except StopIteration:
+            pass
+
         self._add_found_gateway(gateway)
 
     def _add_found_gateway(self, gateway: GatewayDescriptor) -> None:
diff --git a/xknx/io/tunnel.py b/xknx/io/tunnel.py
index 2c700963..e2ff3107 100644
--- a/xknx/io/tunnel.py
+++ b/xknx/io/tunnel.py
@@ -184,6 +184,7 @@ class Tunnel(Interface):
             self.communication_channel = connect.communication_channel
             # Use the individual address provided by the tunnelling server
             self._src_address = IndividualAddress(connect.identifier)
+            self.xknx.current_address = self._src_address
             logger.debug(
                 "Tunnel established communication_channel=%s, id=%s",
                 connect.communication_channel,
diff --git a/xknx/remote_value/remote_value.py b/xknx/remote_value/remote_value.py
index db43d5b1..404a5c16 100644
--- a/xknx/remote_value/remote_value.py
+++ b/xknx/remote_value/remote_value.py
@@ -213,6 +213,7 @@ class RemoteValue(ABC, Generic[DPTPayloadType, ValueType]):
                     if response
                     else GroupValueWrite(payload)
                 ),
+                source_address=self.xknx.current_address,
             )
             await self.xknx.telegrams.put(telegram)
 
diff --git a/xknx/xknx.py b/xknx/xknx.py
index c1fde746..9f4e331e 100644
--- a/xknx/xknx.py
+++ b/xknx/xknx.py
@@ -72,6 +72,7 @@ class XKNX:
         self.connection_config = connection_config
         self.daemon_mode = daemon_mode
         self.version = VERSION
+        self.current_address = IndividualAddress(0)
 
         if log_directory is not None:
             self.setup_logging(log_directory)


Test patch:
diff --git a/requirements/testing.txt b/requirements/testing.txt
index 986738c7..e32168eb 100644
--- a/requirements/testing.txt
+++ b/requirements/testing.txt
@@ -8,6 +8,6 @@ pylint==2.11.1
 pytest==6.2.5
 pytest-asyncio==0.16.0
 pytest-cov==3.0.0
-setuptools==58.3.0
+setuptools==58.4.0
 tox==3.24.4
 mypy==0.910
diff --git a/test/devices_tests/numeric_value_test.py b/test/devices_tests/numeric_value_test.py
index 8fb45f64..c635bece 100644
--- a/test/devices_tests/numeric_value_test.py
+++ b/test/devices_tests/numeric_value_test.py
@@ -256,6 +256,36 @@ class TestNumericValue:
         await sensor.process(telegram)
         after_update_callback.assert_called_with(sensor)
         assert sensor.last_telegram == telegram
+        # consecutive telegrams with same payload shall only trigger one callback
+        after_update_callback.reset_mock()
+        await sensor.process(telegram)
+        after_update_callback.assert_not_called()
+
+    async def test_process_callback_always(self):
+        """Test process / reading telegrams from telegram queue. Test if callback is called."""
+
+        xknx = XKNX()
+        sensor = NumericValue(
+            xknx,
+            "TestSensor",
+            group_address="1/2/3",
+            value_type="temperature",
+            always_callback=True,
+        )
+        after_update_callback = AsyncMock()
+        sensor.register_device_updated_cb(after_update_callback)
+
+        telegram = Telegram(
+            destination_address=GroupAddress("1/2/3"),
+            payload=GroupValueWrite(DPTArray((0x01, 0x02))),
+        )
+        await sensor.process(telegram)
+        after_update_callback.assert_called_with(sensor)
+        assert sensor.last_telegram == telegram
+        # every telegram shall trigger callback
+        after_update_callback.reset_mock()
+        await sensor.process(telegram)
+        after_update_callback.assert_called_with(sensor)
 
     async def test_process_callback_set(self):
         """Test setting value. Test if callback is called."""
diff --git a/test/devices_tests/raw_value_test.py b/test/devices_tests/raw_value_test.py
index 469fdd59..c7b42ef2 100644
--- a/test/devices_tests/raw_value_test.py
+++ b/test/devices_tests/raw_value_test.py
@@ -1,4 +1,6 @@
 """Unit test for RawValue objects."""
+from unittest.mock import AsyncMock
+
 import pytest
 from xknx import XKNX
 from xknx.devices import RawValue
@@ -137,6 +139,61 @@ class TestRawValue:
         )
         assert xknx.telegrams.qsize() == 0
 
+    #
+    # TEST PROCESS CALLBACK
+    #
+
+    async def test_process_callback(self):
+        """Test process / reading telegrams from telegram queue. Test if callback is called."""
+
+        xknx = XKNX()
+        sensor = RawValue(
+            xknx,
+            "TestSensor",
+            2,
+            group_address="1/2/3",
+        )
+        after_update_callback = AsyncMock()
+        sensor.register_device_updated_cb(after_update_callback)
+
+        telegram = Telegram(
+            destination_address=GroupAddress("1/2/3"),
+            payload=GroupValueWrite(DPTArray((0x01, 0x02))),
+        )
+        await sensor.process(telegram)
+        after_update_callback.assert_called_with(sensor)
+        assert sensor.last_telegram == telegram
+        # consecutive telegrams with same payload shall only trigger one callback
+        after_update_callback.reset_mock()
+        await sensor.process(telegram)
+        after_update_callback.assert_not_called()
+
+    async def test_process_callback_always(self):
+        """Test process / reading telegrams from telegram queue. Test if callback is called."""
+
+        xknx = XKNX()
+        sensor = RawValue(
+            xknx,
+            "TestSensor",
+            2,
+            group_address="1/2/3",
+            always_callback=True,
+        )
+        after_update_callback = AsyncMock()
+        sensor.register_device_updated_cb(after_update_callback)
+
+        telegram = Telegram(
+            destination_address=GroupAddress("1/2/3"),
+            payload=GroupValueWrite(DPTArray((0x01, 0x02))),
+        )
+        await sensor.process(telegram)
+        after_update_callback.assert_called_with(sensor)
+        assert sensor.last_telegram == telegram
+        # every telegram shall trigger callback
+        after_update_callback.reset_mock()
+        await sensor.process(telegram)
+        after_update_callback.assert_called_with(sensor)
+
     #
     # TEST SET
     #
diff --git a/test/io_tests/gateway_scanner_test.py b/test/io_tests/gateway_scanner_test.py
index 901e614e..3cc49304 100644
--- a/test/io_tests/gateway_scanner_test.py
+++ b/test/io_tests/gateway_scanner_test.py
@@ -47,6 +47,7 @@ class TestGatewayScanner:
         local_ip="192.168.42.50",
         supports_tunnelling=True,
         supports_routing=True,
+        individual_address=IndividualAddress("1.1.0"),
     )
     gateway_desc_neither = GatewayDescriptor(
         name="AC/S 1.1.1 Application Control",
@@ -152,10 +153,31 @@ class TestGatewayScanner:
 
         assert gateway_scanner.found_gateways == []
         gateway_scanner._response_rec_callback(
-            test_search_response, udp_client_mock, interface="en1"
+            test_search_response,
+            udp_client_mock,
+            interface="en1",
+            netmask="255.255.255.0",
         )
         assert str(gateway_scanner.found_gateways[0]) == str(self.gateway_desc_both)
 
+    def test_search_response_wrong_network(self):
+        """Test function of gateway scanner when network iface doesnt match search response."""
+        xknx = XKNX()
+        gateway_scanner = GatewayScanner(xknx)
+        test_search_response = fake_router_search_response(xknx)
+        udp_client_mock = create_autospec(UDPClient)
+        udp_client_mock.local_addr = ("192.168.100.50", 0)
+        udp_client_mock.getsockname.return_value = ("192.168.100.50", 0)
+
+        assert gateway_scanner.found_gateways == []
+        gateway_scanner._response_rec_callback(
+            test_search_response,
+            udp_client_mock,
+            interface="en1",
+            netmask="255.255.255.0",
+        )
+        assert gateway_scanner.found_gateways == []
+
     @patch("xknx.io.gateway_scanner.netifaces", autospec=True)
     async def test_scan_timeout(self, netifaces_mock):
         """Test gateway scanner timeout."""
@@ -192,8 +214,8 @@ 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", "255.0.0.0"),),
+            ((gateway_scanner, "en1", "10.1.1.2", "255.255.255.0"),),
         ]
         assert _search_interface_mock.call_args_list == expected_calls
         assert test_scan == []
diff --git a/test/str_test.py b/test/str_test.py
index 5dc22695..1f4dc50a 100644
--- a/test/str_test.py
+++ b/test/str_test.py
@@ -664,11 +664,9 @@ class TestStringRepresentations:
             local_ip="192.168.2.50",
             supports_tunnelling=True,
             supports_routing=False,
+            individual_address=IndividualAddress("1.1.1"),
         )
-        assert (
-            str(gateway_descriptor)
-            == '<GatewayDescriptor name="KNX-Interface" addr="192.168.2.3:1234" local="192.168.2.50@en1" routing="False" tunnelling="True" />'
-        )
+        assert str(gateway_descriptor) == "1.1.1 - KNX-Interface @ 192.168.2.3:1234"
 
     #
     # Routing Indication