Repository: XKNX/xknx
Pull request #801 (2021-11-13 11:59:57)

Fix automatic connection handling on Mac and Windows
<!--
  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.
-->
Do not use network comparison in gateway scanner as it's OS dependent and will cause issues on Mac and Windows.

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.
-->

- [x] 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
- [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
- [ ] The Homeassistant plugin has been adjusted in case of new config options


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

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

```diff
@@            Coverage Diff             @@
##             main     #801      +/-   ##
==========================================
- Coverage   94.77%   94.77%   -0.01%     
==========================================
  Files         109      109              
  Lines        6837     6831       -6     
==========================================
- Hits         6480     6474       -6     
  Misses        357      357              
```


| [Impacted Files](https://codecov.io/gh/XKNX/xknx/pull/801?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/801/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=XKNX#diff-eGtueC9pby9nYXRld2F5X3NjYW5uZXIucHk=) | `82.17% <100.00%> (-1.00%)` | :arrow_down: |


Patch:
diff --git a/changelog.md b/changelog.md
index ee25294e..3bb1bd98 100644
--- a/changelog.md
+++ b/changelog.md
@@ -2,6 +2,10 @@
 
 ## Unreleased changes
 
+### Internals
+
+- Fix GatewayScanner on MacOS and Windows and only return one instance of a gateway
+
 ### Devices
 
 - Light: Only send to global switch or brightness address if individual colors are also configured
diff --git a/xknx/io/gateway_scanner.py b/xknx/io/gateway_scanner.py
index 215f25e0..55ca6ada 100644
--- a/xknx/io/gateway_scanner.py
+++ b/xknx/io/gateway_scanner.py
@@ -9,7 +9,6 @@ from __future__ import annotations
 
 import asyncio
 from functools import partial
-from ipaddress import IPv4Address, IPv4Network
 import logging
 from typing import TYPE_CHECKING
 
@@ -132,6 +131,7 @@ class GatewayScanner:
             pass
         finally:
             await self._stop()
+
         return self.found_gateways
 
     async def _stop(self) -> None:
@@ -145,15 +145,12 @@ class GatewayScanner:
             try:
                 af_inet = netifaces.ifaddresses(interface)[netifaces.AF_INET]
                 ip_addr = af_inet[0]["addr"]
-                netmask = af_inet[0]["netmask"]
-                await self._search_interface(interface, ip_addr, netmask)
+                await self._search_interface(interface, ip_addr)
             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, netmask: str
-    ) -> None:
+    async def _search_interface(self, interface: str, ip_addr: str) -> None:
         """Send a search request on a specific interface."""
         logger.debug("Searching on %s / %s", interface, ip_addr)
 
@@ -165,7 +162,7 @@ class GatewayScanner:
         )
 
         udp_client.register_callback(
-            partial(self._response_rec_callback, interface=interface, netmask=netmask),
+            partial(self._response_rec_callback, interface=interface),
             [KNXIPServiceType.SEARCH_RESPONSE],
         )
         await udp_client.connect()
@@ -183,21 +180,12 @@ class GatewayScanner:
         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,
@@ -229,7 +217,10 @@ class GatewayScanner:
         self._add_found_gateway(gateway)
 
     def _add_found_gateway(self, gateway: GatewayDescriptor) -> None:
-        if self.scan_filter.match(gateway):
+        if self.scan_filter.match(gateway) and not any(
+            _gateway.individual_address == gateway.individual_address
+            for _gateway in self.found_gateways
+        ):
             self.found_gateways.append(gateway)
             if 0 < self._count_upper_bound <= len(self.found_gateways):
                 self._response_received_event.set()


Test patch:
diff --git a/test/io_tests/gateway_scanner_test.py b/test/io_tests/gateway_scanner_test.py
index 3cc49304..cde18cc4 100644
--- a/test/io_tests/gateway_scanner_test.py
+++ b/test/io_tests/gateway_scanner_test.py
@@ -156,27 +156,17 @@ class TestGatewayScanner:
             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 str(gateway_scanner.found_gateways[0]) == str(self.gateway_desc_both)
+        assert len(gateway_scanner.found_gateways) == 1
 
-        assert gateway_scanner.found_gateways == []
         gateway_scanner._response_rec_callback(
             test_search_response,
             udp_client_mock,
-            interface="en1",
-            netmask="255.255.255.0",
+            interface="eth1",
         )
-        assert gateway_scanner.found_gateways == []
+        assert len(gateway_scanner.found_gateways) == 1
 
     @patch("xknx.io.gateway_scanner.netifaces", autospec=True)
     async def test_scan_timeout(self, netifaces_mock):
@@ -214,8 +204,8 @@ class TestGatewayScanner:
 
         assert _search_interface_mock.call_count == 2
         expected_calls = [
-            ((gateway_scanner, "lo0", "127.0.0.1", "255.0.0.0"),),
-            ((gateway_scanner, "en1", "10.1.1.2", "255.255.255.0"),),
+            ((gateway_scanner, "lo0", "127.0.0.1"),),
+            ((gateway_scanner, "en1", "10.1.1.2"),),
         ]
         assert _search_interface_mock.call_args_list == expected_calls
         assert test_scan == []