From 9d18afb737e0ba9d2e0608efb7647c6163e0fa6e Mon Sep 17 00:00:00 2001 From: farmio Date: Sat, 23 Feb 2019 01:23:25 +0100 Subject: [PATCH 01/11] detect local_ip for tunneling connection --- xknx/io/knxip_interface.py | 44 +++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/xknx/io/knxip_interface.py b/xknx/io/knxip_interface.py index 770d7b3ef..5736f7da9 100644 --- a/xknx/io/knxip_interface.py +++ b/xknx/io/knxip_interface.py @@ -6,6 +6,9 @@ * provides callbacks after having received a telegram from the network. """ +import ipaddress +import netifaces + from enum import Enum from platform import system as get_os_name @@ -67,14 +70,7 @@ def __init__(self, def __eq__(self, other): """Equality for ConnectionConfig class (used in unit tests).""" - return self.connection_type == other.connection_type and \ - self.local_ip == other.local_ip and \ - self.gateway_ip == other.gateway_ip and \ - self.gateway_port == other.gateway_port and \ - self.auto_reconnect == other.auto_reconnect and \ - self.auto_reconnect_wait == other.auto_reconnect_wait and \ - self.scan_filter == other.scan_filter and \ - self.bind_to_multicast_addr == other.bind_to_multicast_addr + return self.__dict__ == other.__dict__ class KNXIPInterface(): @@ -126,6 +122,8 @@ async def start_tunnelling(self, local_ip, gateway_ip, gateway_port, auto_reconnect, auto_reconnect_wait): """Start KNX/IP tunnel.""" # pylint: disable=too-many-arguments + if local_ip is None: + local_ip = self.find_local_ip(gateway_ip=gateway_ip) self.xknx.logger.debug("Starting tunnel to %s:%s from %s", gateway_ip, gateway_port, local_ip) self.interface = Tunnel( self.xknx, @@ -162,3 +160,33 @@ def telegram_received(self, telegram): async def send_telegram(self, telegram): """Send telegram to connected device (either Tunneling or Routing).""" await self.interface.send_telegram(telegram) + + def find_local_ip(self, gateway_ip): + """Find local IP address on same subnet as gateway.""" + def _scan_interfaces(gateway: ipaddress.IPv4Address): + for interface in netifaces.interfaces(): + try: + af_inet = netifaces.ifaddresses(interface)[netifaces.AF_INET] + for link in af_inet: + network = ipaddress.IPv4Network((link["addr"], + link["netmask"]), + strict=False) + if gateway in network: + self.xknx.logger.debug("Using interface: %s", interface) + return link["addr"] + except KeyError: + self.xknx.logger.debug("Could not find IPv4 address on interface %s", interface) + continue + raise XKNXException("No interface on same subnet as gateway found.") + + gateway = ipaddress.IPv4Address(gateway_ip) + try: + local_ip = _scan_interfaces(gateway) + return local_ip + except XKNXException: + self.xknx.logger.debug( + "No interface on same subnet as gateway found. Falling back to default gateway.") + gws = netifaces.gateways() + gateway = ipaddress.IPv4Address(gws['default'][netifaces.AF_INET][0]) + local_ip = _scan_interfaces(gateway) + return local_ip From 8c075523581629df704b84dbbbbc56313c4a8dbb Mon Sep 17 00:00:00 2001 From: farmio Date: Sat, 23 Feb 2019 20:59:17 +0100 Subject: [PATCH 02/11] auto detect local_ip for routing connection --- xknx/io/knxip_interface.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/xknx/io/knxip_interface.py b/xknx/io/knxip_interface.py index 5736f7da9..f8bea77fc 100644 --- a/xknx/io/knxip_interface.py +++ b/xknx/io/knxip_interface.py @@ -7,11 +7,10 @@ """ import ipaddress -import netifaces - from enum import Enum from platform import system as get_os_name +import netifaces from xknx.exceptions import XKNXException from .const import DEFAULT_MCAST_PORT @@ -87,10 +86,16 @@ async def start(self): if self.connection_config.connection_type == ConnectionType.AUTOMATIC: await self.start_automatic( self.connection_config.scan_filter) - elif self.connection_config.connection_type == ConnectionType.ROUTING: + elif self.connection_config.connection_type == ConnectionType.ROUTING and \ + self.connection_config.local_ip is not None: await self.start_routing( self.connection_config.local_ip, self.connection_config.bind_to_multicast_addr) + elif self.connection_config.connection_type == ConnectionType.ROUTING and \ + self.connection_config.local_ip is None: + self.connection_config.scan_filter.routing = True + await self.start_automatic( + self.connection_config.scan_filter) elif self.connection_config.connection_type == ConnectionType.TUNNELING: await self.start_tunnelling( self.connection_config.local_ip, @@ -101,14 +106,15 @@ async def start(self): async def start_automatic(self, scan_filter: GatewayScanFilter): """Start GatewayScanner and connect to the found device.""" - gatewayscanner = GatewayScanner(self.xknx, scan_filter=scan_filter) + gatewayscanner = GatewayScanner(self.xknx, scan_filter=scan_filter, stop_on_found=1) gateways = await gatewayscanner.scan() if not gateways: raise XKNXException("No Gateways found") gateway = gateways[0] - if gateway.supports_tunnelling: + if gateway.supports_tunnelling and \ + scan_filter.routing is not True: await self.start_tunnelling(gateway.local_ip, gateway.ip_addr, gateway.port, From e2b16aa230aadbd575d6162f5db9a407feabe720 Mon Sep 17 00:00:00 2001 From: farmio Date: Sat, 23 Feb 2019 20:59:57 +0100 Subject: [PATCH 03/11] tests, documentation for optional local_ip --- docs/configuration.md | 4 ++-- .../custom_components/xknx/__init__.py | 4 ++-- test/config_test.py | 15 +++++++++++++++ xknx/core/config.py | 10 ++++------ 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 7a859892f..a31ffcf51 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -18,11 +18,11 @@ The configuration file can contain three sections. - The `connection` section can be used to specify the connection to the KNX interface. - `auto` for automatic discovery of a KNX interface - `tunneling` for a UDP unicast connection - - `local_ip` (required) sets the ip address that is used by xknx - `gateway_ip` (required) sets the ip address of the KNX tunneling interface - `gateway_port` (optional) sets the port the KNX tunneling interface is listening on + - `local_ip` (optional) sets the ip address that is used by xknx - `routing` for a UDP multicast connection - - `local_ip` (required) sets the ip address that is used by xknx + - `local_ip` (optional) sets the ip address that is used by xknx - Within the `groups` sections all devices are defined. For each type of device more then one section might be specified. You need to append numbers or strings to differentiate the entries, as in the example below. The appended number or string must be unique. How to use diff --git a/home-assistant-plugin/custom_components/xknx/__init__.py b/home-assistant-plugin/custom_components/xknx/__init__.py index 30f7509cd..9696d1afa 100644 --- a/home-assistant-plugin/custom_components/xknx/__init__.py +++ b/home-assistant-plugin/custom_components/xknx/__init__.py @@ -44,12 +44,12 @@ TUNNELING_SCHEMA = vol.Schema({ vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_XKNX_LOCAL_IP): cv.string, vol.Optional(CONF_PORT): cv.port, + vol.Optional(CONF_XKNX_LOCAL_IP): cv.string, }) ROUTING_SCHEMA = vol.Schema({ - vol.Required(CONF_XKNX_LOCAL_IP): cv.string, + vol.Optional(CONF_XKNX_LOCAL_IP): cv.string, }) EXPOSE_SCHEMA = vol.Schema({ diff --git a/test/config_test.py b/test/config_test.py index 6674b1d10..360b85cfa 100644 --- a/test/config_test.py +++ b/test/config_test.py @@ -64,6 +64,15 @@ def test_config_connection(self): gateway_port=6000) ), (""" + connection: + tunneling: + gateway_ip: '192.168.1.2' + """, + ConnectionConfig( + connection_type=ConnectionType.TUNNELING, + gateway_ip="192.168.1.2") + ), + (""" connection: routing: local_ip: '192.168.1.2' @@ -71,6 +80,12 @@ def test_config_connection(self): ConnectionConfig( connection_type=ConnectionType.ROUTING, local_ip="192.168.1.2") + ), + (""" + connection: + routing: + """, + ConnectionConfig(connection_type=ConnectionType.ROUTING) ) ] for yaml_string, expected_conn in test_configs: diff --git a/xknx/core/config.py b/xknx/core/config.py index 964467972..67705e08a 100644 --- a/xknx/core/config.py +++ b/xknx/core/config.py @@ -65,14 +65,12 @@ def _parse_connection_prefs(self, conn_type, prefs): if hasattr(prefs, '__iter__'): for pref, value in prefs.items(): try: - if pref.startswith("local_ip"): - conn.local_ip = value - elif pref.startswith("gateway_ip"): + if pref.startswith("gateway_ip"): conn.gateway_ip = value elif pref.startswith("gateway_port"): - # don't overwrite default if None - if value is not None: - conn.gateway_port = value + conn.gateway_port = value + elif pref.startswith("local_ip"): + conn.local_ip = value except XKNXException as ex: self.xknx.logger.error("Error while reading config file: Could not parse %s: %s", pref, ex) self.xknx.connection_config = conn From 46a21408676f50e1f8f8c0e8d3b7651e3b44f88b Mon Sep 17 00:00:00 2001 From: farmio Date: Tue, 26 Feb 2019 23:17:12 +0100 Subject: [PATCH 04/11] small typos --- test/knxip_search_request_test.py | 2 +- test/knxip_search_response_test.py | 2 +- xknx/io/gateway_scanner.py | 2 +- xknx/io/knxip_interface.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/knxip_search_request_test.py b/test/knxip_search_request_test.py index 10c38bc66..70db8d61d 100644 --- a/test/knxip_search_request_test.py +++ b/test/knxip_search_request_test.py @@ -20,7 +20,7 @@ def tearDown(self): """Tear down test class.""" self.loop.close() - def test_connect_request(self): + def test_search_request(self): """Test parsing and streaming SearchRequest KNX/IP packet.""" raw = ((0x06, 0x10, 0x02, 0x01, 0x00, 0x0e, 0x08, 0x01, 0xe0, 0x00, 0x17, 0x0c, 0x0e, 0x57)) diff --git a/test/knxip_search_response_test.py b/test/knxip_search_response_test.py index 377ce0e6b..b4d0df0a6 100644 --- a/test/knxip_search_response_test.py +++ b/test/knxip_search_response_test.py @@ -22,7 +22,7 @@ def tearDown(self): """Tear down test class.""" self.loop.close() - def test_connect_request(self): + def test_search_response(self): """Test parsing and streaming SearchResponse KNX/IP packet.""" raw = ((0x06, 0x10, 0x02, 0x02, 0x00, 0x50, 0x08, 0x01, 0xc0, 0xa8, 0x2a, 0x0a, 0x0e, 0x57, 0x36, 0x01, diff --git a/xknx/io/gateway_scanner.py b/xknx/io/gateway_scanner.py index 6ab951251..c98547b97 100644 --- a/xknx/io/gateway_scanner.py +++ b/xknx/io/gateway_scanner.py @@ -43,7 +43,7 @@ def __init__(self, def __str__(self): """Return object as readable string.""" - return ''.format( self.name, self.ip_addr, self.port, diff --git a/xknx/io/knxip_interface.py b/xknx/io/knxip_interface.py index f8bea77fc..1fd5e1816 100644 --- a/xknx/io/knxip_interface.py +++ b/xknx/io/knxip_interface.py @@ -106,7 +106,7 @@ async def start(self): async def start_automatic(self, scan_filter: GatewayScanFilter): """Start GatewayScanner and connect to the found device.""" - gatewayscanner = GatewayScanner(self.xknx, scan_filter=scan_filter, stop_on_found=1) + gatewayscanner = GatewayScanner(self.xknx, scan_filter=scan_filter) gateways = await gatewayscanner.scan() if not gateways: From 1ddfe96c214b63330f39882a5811a9fd065bf0bb Mon Sep 17 00:00:00 2001 From: farmio Date: Tue, 26 Feb 2019 23:17:29 +0100 Subject: [PATCH 05/11] tests for io/gateway_scanner --- test/io_gateway_scanner_test.py | 120 ++++++++++++++++++++++++++++++++ test/str_test.py | 18 +++++ 2 files changed, 138 insertions(+) create mode 100644 test/io_gateway_scanner_test.py diff --git a/test/io_gateway_scanner_test.py b/test/io_gateway_scanner_test.py new file mode 100644 index 000000000..9e6da683e --- /dev/null +++ b/test/io_gateway_scanner_test.py @@ -0,0 +1,120 @@ +"""Unit test for KNX/IP gateway scanner.""" +import asyncio +import unittest +from unittest.mock import patch, create_autospec + +from xknx import XKNX +from xknx.io import GatewayScanFilter, GatewayScanner, UDPClient +from xknx.io.gateway_scanner import GatewayDescriptor +from xknx.knx import PhysicalAddress +from xknx.knxip import ( + HPAI, DIBDeviceInformation, DIBServiceFamily, DIBSuppSVCFamilies, + KNXIPBody, KNXIPHeader, KNXIPFrame, KNXIPServiceType, SearchResponse) + + +class TestGatewayScanner(unittest.TestCase): + """Test class for xknx/io/GatewayScanner objects.""" + + def setUp(self): + """Set up test class.""" + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.loop) + + def tearDown(self): + """Tear down test class.""" + self.loop.close() + + def test_gateway_scan_filter_match(self): + """Test match function of gateway filter.""" + # pylint: disable=too-many-locals + gateway_1 = GatewayDescriptor(name='KNX-Interface', + ip_addr='10.1.1.11', + port=3761, + local_interface='en1', + local_ip='110.1.1.100', + supports_tunnelling=True, + supports_routing=False) + gateway_2 = GatewayDescriptor(name='KNX-Router', + ip_addr='10.1.1.12', + port=3761, + local_interface='en1', + local_ip='10.1.1.100', + supports_tunnelling=False, + supports_routing=True) + filter_tunnel = GatewayScanFilter(tunnelling=True) + filter_router = GatewayScanFilter(routing=True) + filter_name = GatewayScanFilter(name="KNX-Router") + filter_no_tunnel = GatewayScanFilter(tunnelling=False) + + self.assertTrue(filter_tunnel.match(gateway_1)) + self.assertFalse(filter_tunnel.match(gateway_2)) + self.assertFalse(filter_router.match(gateway_1)) + self.assertTrue(filter_router.match(gateway_2)) + self.assertFalse(filter_name.match(gateway_1)) + self.assertTrue(filter_name.match(gateway_2)) + self.assertFalse(filter_no_tunnel.match(gateway_1)) + self.assertTrue(filter_no_tunnel.match(gateway_2)) + + def test_search_response_reception(self): + """Test function of gateway scanner.""" + xknx = XKNX(loop=self.loop) + gateway_scanner = GatewayScanner(xknx) + search_response = fake_router_search_response(xknx) + udp_client = create_autospec(UDPClient) + udp_client.local_addr = ("192.168.42.50", 0, "en1") + udp_client.getsockname.return_value = ("192.168.42.50", 0) + router_gw_descriptor = GatewayDescriptor(name="Gira KNX/IP-Router", + ip_addr="192.168.42.10", + port=3671, + local_interface="en1", + local_ip="192.168.42.50", + supports_tunnelling=True, + supports_routing=True) + + self.assertEqual(gateway_scanner.found_gateways, []) + gateway_scanner._response_rec_callback(search_response, udp_client) + self.assertEqual(str(gateway_scanner.found_gateways[0]), + str(router_gw_descriptor)) + + +def fake_router_search_response(xknx: XKNX) -> SearchResponse: + """Return the SearchResponse of a KNX/IP Router.""" + _frame_header = KNXIPHeader(xknx) + _frame_header.service_type_ident = KNXIPServiceType.SEARCH_RESPONSE + _frame_body = SearchResponse(xknx) + _frame_body.control_endpoint = HPAI(ip_addr="192.168.42.10", port=3671) + + _device_information = DIBDeviceInformation() + _device_information.name = "Gira KNX/IP-Router" + _device_information.serial_number = "11:22:33:44:55:66" + _device_information.individual_address = PhysicalAddress("1.1.0") + _device_information.mac_address = "01:02:03:04:05:06" + + _svc_families = DIBSuppSVCFamilies() + _svc_families.families.append( + DIBSuppSVCFamilies.Family(name=DIBServiceFamily.CORE, + version=1)) + _svc_families.families.append( + DIBSuppSVCFamilies.Family(name=DIBServiceFamily.DEVICE_MANAGEMENT, + version=2)) + _svc_families.families.append( + DIBSuppSVCFamilies.Family(name=DIBServiceFamily.TUNNELING, + version=1)) + _svc_families.families.append( + DIBSuppSVCFamilies.Family(name=DIBServiceFamily.ROUTING, + version=1)) + _svc_families.families.append( + DIBSuppSVCFamilies.Family(name=DIBServiceFamily.REMOTE_CONFIGURATION_DIAGNOSIS, + version=1)) + + _frame_body.dibs.append(_device_information) + _frame_body.dibs.append(_svc_families) + _frame_header.set_length(_frame_body) + + search_response = KNXIPFrame(xknx) + search_response.init(KNXIPServiceType.SEARCH_RESPONSE) + search_response.header = _frame_header + search_response.body = _frame_body + search_response.normalize() + + return search_response diff --git a/test/str_test.py b/test/str_test.py index 527f709a1..b860ede5e 100644 --- a/test/str_test.py +++ b/test/str_test.py @@ -10,6 +10,7 @@ from xknx.exceptions import ( ConversionError, CouldNotParseAddress, CouldNotParseKNXIP, CouldNotParseTelegram, DeviceIllegalValue) +from xknx.io.gateway_scanner import GatewayDescriptor from xknx.knx import ( DPTArray, DPTBinary, GroupAddress, PhysicalAddress, Telegram) from xknx.knxip import ( @@ -563,3 +564,20 @@ def test_knxip_frame(self): '\n' ' body="" />') + + # + # Gateway Scanner + # + def test_gateway_descriptor(self): + """Test string representation of GatewayDescriptor.""" + gateway_descriptor = GatewayDescriptor(name='KNX-Interface', + ip_addr='192.168.2.3', + port=1234, + local_interface='en1', + local_ip='192.168.2.50', + supports_tunnelling=True, + supports_routing=False) + self.assertEqual( + str(gateway_descriptor), + '' + ) From bcc722a995c807738149e3170532eff9c28d23bc Mon Sep 17 00:00:00 2001 From: farmio Date: Tue, 26 Feb 2019 23:27:19 +0100 Subject: [PATCH 06/11] sort imports --- test/io_gateway_scanner_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/io_gateway_scanner_test.py b/test/io_gateway_scanner_test.py index 9e6da683e..669433329 100644 --- a/test/io_gateway_scanner_test.py +++ b/test/io_gateway_scanner_test.py @@ -1,7 +1,6 @@ """Unit test for KNX/IP gateway scanner.""" import asyncio import unittest -from unittest.mock import patch, create_autospec from xknx import XKNX from xknx.io import GatewayScanFilter, GatewayScanner, UDPClient @@ -9,7 +8,7 @@ from xknx.knx import PhysicalAddress from xknx.knxip import ( HPAI, DIBDeviceInformation, DIBServiceFamily, DIBSuppSVCFamilies, - KNXIPBody, KNXIPHeader, KNXIPFrame, KNXIPServiceType, SearchResponse) + KNXIPFrame, KNXIPHeader, KNXIPServiceType, SearchResponse) class TestGatewayScanner(unittest.TestCase): @@ -57,10 +56,11 @@ def test_gateway_scan_filter_match(self): def test_search_response_reception(self): """Test function of gateway scanner.""" + # pylint: disable=protected-access xknx = XKNX(loop=self.loop) gateway_scanner = GatewayScanner(xknx) search_response = fake_router_search_response(xknx) - udp_client = create_autospec(UDPClient) + udp_client = unittest.mock.create_autospec(UDPClient) udp_client.local_addr = ("192.168.42.50", 0, "en1") udp_client.getsockname.return_value = ("192.168.42.50", 0) router_gw_descriptor = GatewayDescriptor(name="Gira KNX/IP-Router", From 7268f25b2b6156e4aa9aaac27f4f01dd03b53d57 Mon Sep 17 00:00:00 2001 From: farmio Date: Thu, 28 Feb 2019 14:01:59 +0100 Subject: [PATCH 07/11] review changes --- xknx/core/config.py | 13 ++++++------ xknx/io/knxip_interface.py | 43 +++++++++++++++++++------------------- 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/xknx/core/config.py b/xknx/core/config.py index 67705e08a..2481d5b74 100644 --- a/xknx/core/config.py +++ b/xknx/core/config.py @@ -50,11 +50,11 @@ def parse_connection(self, doc): and hasattr(doc["connection"], '__iter__'): for conn, prefs in doc["connection"].items(): try: - if conn.startswith("auto"): + if conn == "auto": self._parse_connection_prefs(ConnectionType.AUTOMATIC, prefs) - elif conn.startswith("tunneling"): + elif conn == "tunneling": self._parse_connection_prefs(ConnectionType.TUNNELING, prefs) - elif conn.startswith("routing"): + elif conn == "routing": self._parse_connection_prefs(ConnectionType.ROUTING, prefs) except XKNXException as ex: self.xknx.logger.error("Error while reading config file: Could not parse %s: %s", conn, ex) @@ -65,11 +65,11 @@ def _parse_connection_prefs(self, conn_type, prefs): if hasattr(prefs, '__iter__'): for pref, value in prefs.items(): try: - if pref.startswith("gateway_ip"): + if pref == "gateway_ip": conn.gateway_ip = value - elif pref.startswith("gateway_port"): + elif pref == "gateway_port": conn.gateway_port = value - elif pref.startswith("local_ip"): + elif pref == "local_ip": conn.local_ip = value except XKNXException as ex: self.xknx.logger.error("Error while reading config file: Could not parse %s: %s", pref, ex) @@ -85,6 +85,7 @@ def parse_groups(self, doc): def parse_group(self, doc, group): """Parse a group entry of xknx.yaml.""" try: + print(group) if group.startswith("light"): self.parse_group_light(doc["groups"][group]) elif group.startswith("switch"): diff --git a/xknx/io/knxip_interface.py b/xknx/io/knxip_interface.py index 1fd5e1816..997d3d84b 100644 --- a/xknx/io/knxip_interface.py +++ b/xknx/io/knxip_interface.py @@ -86,16 +86,15 @@ async def start(self): if self.connection_config.connection_type == ConnectionType.AUTOMATIC: await self.start_automatic( self.connection_config.scan_filter) - elif self.connection_config.connection_type == ConnectionType.ROUTING and \ - self.connection_config.local_ip is not None: - await self.start_routing( - self.connection_config.local_ip, - self.connection_config.bind_to_multicast_addr) - elif self.connection_config.connection_type == ConnectionType.ROUTING and \ - self.connection_config.local_ip is None: - self.connection_config.scan_filter.routing = True - await self.start_automatic( - self.connection_config.scan_filter) + elif self.connection_config.connection_type == ConnectionType.ROUTING: + if self.connection_config.local_ip is not None: + await self.start_routing( + self.connection_config.local_ip, + self.connection_config.bind_to_multicast_addr) + else: + prefer_routing = self.connection_config.scan_filter + prefer_routing.routing = True + await self.start_automatic(prefer_routing) elif self.connection_config.connection_type == ConnectionType.TUNNELING: await self.start_tunnelling( self.connection_config.local_ip, @@ -167,9 +166,10 @@ async def send_telegram(self, telegram): """Send telegram to connected device (either Tunneling or Routing).""" await self.interface.send_telegram(telegram) - def find_local_ip(self, gateway_ip): + def find_local_ip(self, gateway_ip: str) -> str: """Find local IP address on same subnet as gateway.""" - def _scan_interfaces(gateway: ipaddress.IPv4Address): + def _scan_interfaces(gateway: ipaddress.IPv4Address) -> str: + """Return local IP address on same subnet as given gateway.""" for interface in netifaces.interfaces(): try: af_inet = netifaces.ifaddresses(interface)[netifaces.AF_INET] @@ -183,16 +183,17 @@ def _scan_interfaces(gateway: ipaddress.IPv4Address): except KeyError: self.xknx.logger.debug("Could not find IPv4 address on interface %s", interface) continue - raise XKNXException("No interface on same subnet as gateway found.") + + def _find_default_gateway() -> ipaddress.IPv4Address: + """Return IP address of default gateway.""" + gws = netifaces.gateways() + return ipaddress.IPv4Address(gws['default'][netifaces.AF_INET][0]) gateway = ipaddress.IPv4Address(gateway_ip) - try: - local_ip = _scan_interfaces(gateway) - return local_ip - except XKNXException: + local_ip = _scan_interfaces(gateway) + if local_ip is None: self.xknx.logger.debug( "No interface on same subnet as gateway found. Falling back to default gateway.") - gws = netifaces.gateways() - gateway = ipaddress.IPv4Address(gws['default'][netifaces.AF_INET][0]) - local_ip = _scan_interfaces(gateway) - return local_ip + default_gateway = _find_default_gateway() + local_ip = _scan_interfaces(default_gateway) + return local_ip From 61a4c96eff8bfb10d293627b54c14b4dd1be6b9f Mon Sep 17 00:00:00 2001 From: farmio Date: Thu, 28 Feb 2019 20:11:12 +0100 Subject: [PATCH 08/11] Update config.py --- xknx/core/config.py | 1 - 1 file changed, 1 deletion(-) diff --git a/xknx/core/config.py b/xknx/core/config.py index 2481d5b74..1b70e9508 100644 --- a/xknx/core/config.py +++ b/xknx/core/config.py @@ -85,7 +85,6 @@ def parse_groups(self, doc): def parse_group(self, doc, group): """Parse a group entry of xknx.yaml.""" try: - print(group) if group.startswith("light"): self.parse_group_light(doc["groups"][group]) elif group.startswith("switch"): From 6264aa78f88d372b16d813963cfa9d8f6ba626f5 Mon Sep 17 00:00:00 2001 From: farmio Date: Sat, 2 Mar 2019 09:57:16 +0100 Subject: [PATCH 09/11] init GatewayScanFilter considering ConnectionType --- xknx/core/config.py | 24 ++++++++++++------------ xknx/io/gateway_scanner.py | 4 ++++ xknx/io/knxip_interface.py | 25 ++++++++++++------------- 3 files changed, 28 insertions(+), 25 deletions(-) diff --git a/xknx/core/config.py b/xknx/core/config.py index 1b70e9508..be9771c1b 100644 --- a/xknx/core/config.py +++ b/xknx/core/config.py @@ -50,30 +50,30 @@ def parse_connection(self, doc): and hasattr(doc["connection"], '__iter__'): for conn, prefs in doc["connection"].items(): try: - if conn == "auto": - self._parse_connection_prefs(ConnectionType.AUTOMATIC, prefs) - elif conn == "tunneling": - self._parse_connection_prefs(ConnectionType.TUNNELING, prefs) + if conn == "tunneling": + conn = ConnectionType.TUNNELING elif conn == "routing": - self._parse_connection_prefs(ConnectionType.ROUTING, prefs) + conn = ConnectionType.ROUTING + else: + conn = ConnectionType.AUTOMATIC + self._parse_connection_prefs(conn, prefs) except XKNXException as ex: self.xknx.logger.error("Error while reading config file: Could not parse %s: %s", conn, ex) - def _parse_connection_prefs(self, conn_type, prefs): - conn = ConnectionConfig() - conn.connection_type = conn_type + def _parse_connection_prefs(self, conn_type: ConnectionType, prefs) -> None: + connection_config = ConnectionConfig(connection_type=conn_type) if hasattr(prefs, '__iter__'): for pref, value in prefs.items(): try: if pref == "gateway_ip": - conn.gateway_ip = value + connection_config.gateway_ip = value elif pref == "gateway_port": - conn.gateway_port = value + connection_config.gateway_port = value elif pref == "local_ip": - conn.local_ip = value + connection_config.local_ip = value except XKNXException as ex: self.xknx.logger.error("Error while reading config file: Could not parse %s: %s", pref, ex) - self.xknx.connection_config = conn + self.xknx.connection_config = connection_config def parse_groups(self, doc): """Parse the group section of xknx.yaml.""" diff --git a/xknx/io/gateway_scanner.py b/xknx/io/gateway_scanner.py index c98547b97..0c99f334b 100644 --- a/xknx/io/gateway_scanner.py +++ b/xknx/io/gateway_scanner.py @@ -77,6 +77,10 @@ def match(self, gateway: GatewayDescriptor) -> bool: return False return True + def __eq__(self, other): + """Equality for GatewayScanFilter class (used in unit tests).""" + return self.__dict__ == other.__dict__ + class GatewayScanner(): """Class for searching KNX/IP devices.""" diff --git a/xknx/io/knxip_interface.py b/xknx/io/knxip_interface.py index 997d3d84b..2caa4215a 100644 --- a/xknx/io/knxip_interface.py +++ b/xknx/io/knxip_interface.py @@ -64,8 +64,12 @@ def __init__(self, self.gateway_port = gateway_port self.auto_reconnect = auto_reconnect self.auto_reconnect_wait = auto_reconnect_wait - self.scan_filter = scan_filter self.bind_to_multicast_addr = bind_to_multicast_addr + if connection_type == ConnectionType.TUNNELING: + scan_filter.tunnelling = True + elif connection_type == ConnectionType.ROUTING: + scan_filter.routing = True + self.scan_filter = scan_filter def __eq__(self, other): """Equality for ConnectionConfig class (used in unit tests).""" @@ -83,18 +87,11 @@ def __init__(self, xknx, connection_config=ConnectionConfig()): async def start(self): """Start interface. Connecting KNX/IP device with the selected method.""" - if self.connection_config.connection_type == ConnectionType.AUTOMATIC: - await self.start_automatic( - self.connection_config.scan_filter) - elif self.connection_config.connection_type == ConnectionType.ROUTING: - if self.connection_config.local_ip is not None: - await self.start_routing( - self.connection_config.local_ip, - self.connection_config.bind_to_multicast_addr) - else: - prefer_routing = self.connection_config.scan_filter - prefer_routing.routing = True - await self.start_automatic(prefer_routing) + if self.connection_config.connection_type == ConnectionType.ROUTING and \ + self.connection_config.local_ip is not None: + await self.start_routing( + self.connection_config.local_ip, + self.connection_config.bind_to_multicast_addr) elif self.connection_config.connection_type == ConnectionType.TUNNELING: await self.start_tunnelling( self.connection_config.local_ip, @@ -102,6 +99,8 @@ async def start(self): self.connection_config.gateway_port, self.connection_config.auto_reconnect, self.connection_config.auto_reconnect_wait) + else: + await self.start_automatic(self.connection_config.scan_filter) async def start_automatic(self, scan_filter: GatewayScanFilter): """Start GatewayScanner and connect to the found device.""" From 25666c0799e5aea982c0ba98fadd124c05a7f640 Mon Sep 17 00:00:00 2001 From: farmio Date: Sun, 7 Apr 2019 21:04:01 +0200 Subject: [PATCH 10/11] raise if no gateway_ip for tunneling connection --- xknx/core/config.py | 12 ++++++++---- xknx/io/knxip_interface.py | 6 +++++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/xknx/core/config.py b/xknx/core/config.py index be9771c1b..95e7a2bfa 100644 --- a/xknx/core/config.py +++ b/xknx/core/config.py @@ -51,14 +51,18 @@ def parse_connection(self, doc): for conn, prefs in doc["connection"].items(): try: if conn == "tunneling": - conn = ConnectionType.TUNNELING + if prefs is None or \ + not "gateway_ip" in prefs: + raise XKNXException("`gateway_ip` is required for tunneling connection.") + conn_type = ConnectionType.TUNNELING elif conn == "routing": - conn = ConnectionType.ROUTING + conn_type = ConnectionType.ROUTING else: - conn = ConnectionType.AUTOMATIC - self._parse_connection_prefs(conn, prefs) + conn_type = ConnectionType.AUTOMATIC + self._parse_connection_prefs(conn_type, prefs) except XKNXException as ex: self.xknx.logger.error("Error while reading config file: Could not parse %s: %s", conn, ex) + raise ex def _parse_connection_prefs(self, conn_type: ConnectionType, prefs) -> None: connection_config = ConnectionConfig(connection_type=conn_type) diff --git a/xknx/io/knxip_interface.py b/xknx/io/knxip_interface.py index 2caa4215a..bc73eb418 100644 --- a/xknx/io/knxip_interface.py +++ b/xknx/io/knxip_interface.py @@ -126,9 +126,13 @@ async def start_tunnelling(self, local_ip, gateway_ip, gateway_port, auto_reconnect, auto_reconnect_wait): """Start KNX/IP tunnel.""" # pylint: disable=too-many-arguments + try: + ipaddress.IPv4Address(gateway_ip) + except ipaddress.AddressValueError as ex: + raise XKNXException("Gateway IP address is not a valid IPv4 address.") from ex if local_ip is None: local_ip = self.find_local_ip(gateway_ip=gateway_ip) - self.xknx.logger.debug("Starting tunnel to %s:%s from %s", gateway_ip, gateway_port, local_ip) + self.xknx.logger.debug("Starting tunnel from %s to %s:%s", local_ip, gateway_ip, gateway_port) self.interface = Tunnel( self.xknx, self.xknx.own_address, From f48d936909e74bc24bab80feba58b8ec0388ce05 Mon Sep 17 00:00:00 2001 From: farmio Date: Sun, 7 Apr 2019 21:25:11 +0200 Subject: [PATCH 11/11] raise more beautiful (flake8) --- xknx/core/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xknx/core/config.py b/xknx/core/config.py index 95e7a2bfa..f22ce0686 100644 --- a/xknx/core/config.py +++ b/xknx/core/config.py @@ -52,7 +52,7 @@ def parse_connection(self, doc): try: if conn == "tunneling": if prefs is None or \ - not "gateway_ip" in prefs: + "gateway_ip" not in prefs: raise XKNXException("`gateway_ip` is required for tunneling connection.") conn_type = ConnectionType.TUNNELING elif conn == "routing":