Skip to content

Commit

Permalink
Merge 949c0ea into 5dc9299
Browse files Browse the repository at this point in the history
  • Loading branch information
DrMurx committed Feb 19, 2018
2 parents 5dc9299 + 949c0ea commit d01879a
Show file tree
Hide file tree
Showing 9 changed files with 210 additions and 120 deletions.
12 changes: 7 additions & 5 deletions examples/example_disconnect.py
Expand Up @@ -9,20 +9,22 @@ async def main():
"""Search for a Tunelling device, walk through all possible channels and disconnect them."""
xknx = XKNX()
gatewayscanner = GatewayScanner(xknx)
await gatewayscanner.start()
gateways = await gatewayscanner.scan()

if not gatewayscanner.found:
if len(gateways) == 0:
print("No Gateways found")
return

if not gatewayscanner.supports_tunneling:
gateway = gateways[0]

if not gateway.supports_tunnelling:
print("Gateway does not support tunneling")
return

udp_client = UDPClient(
xknx,
(gatewayscanner.found_local_ip, 0),
(gatewayscanner.found_ip_addr, gatewayscanner.found_port))
(gateway.local_ip, 0),
(gateway.ip, gateway.port))

await udp_client.connect()

Expand Down
25 changes: 12 additions & 13 deletions examples/example_gatewayscanner.py
Expand Up @@ -9,23 +9,22 @@ 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)
await gatewayscanner.start()
gateways = await gatewayscanner.scan()

if not gatewayscanner.found:
if len(gateways) == 0:
print("No Gateways found")

else:
print("Gateway found: {0} / {1}:{2}".format(
gatewayscanner.found_name,
gatewayscanner.found_ip_addr,
gatewayscanner.found_port))
if gatewayscanner.supports_tunneling:
print("- Device supports tunneling")
if gatewayscanner.supports_routing:
print("- Device supports routing, connecting via {0}".format(
gatewayscanner.found_local_ip))

await gatewayscanner.stop()
for gateway in gateways:
print("Gateway found: {0} / {1}:{2}".format(
gateway.name,
gateway.ip,
gateway.port))
if gateway.supports_tunnelling:
print("- Device supports tunneling")
if gateway.supports_routing:
print("- Device supports routing, connecting via {0}".format(
gateway.local_ip))

# pylint: disable=invalid-name
loop = asyncio.get_event_loop()
Expand Down
17 changes: 9 additions & 8 deletions examples/example_tunnel.py
Expand Up @@ -10,25 +10,26 @@ async def main():
"""Connect to a tunnel, send 2 telegrams and disconnect."""
xknx = XKNX()
gatewayscanner = GatewayScanner(xknx)
await gatewayscanner.start()
gateways = await gatewayscanner.scan()

if not gatewayscanner.found:
if len(gateways) == 0:
print("No Gateways found")
return

gateway = gateways[0]
src_address = PhysicalAddress("15.15.249")

print("Connecting to {}:{} from {}".format(
gatewayscanner.found_ip_addr,
gatewayscanner.found_port,
gatewayscanner.found_local_ip))
gateway.ip,
gateway.port,
gateway.local_ip))

tunnel = Tunnel(
xknx,
src_address,
local_ip=gatewayscanner.found_local_ip,
gateway_ip=gatewayscanner.found_ip_addr,
gateway_port=gatewayscanner.found_port)
local_ip=gateway.local_ip,
gateway_ip=gateway.ip,
gateway_port=gateway.port)

await tunnel.connect_udp()
await tunnel.connect()
Expand Down
2 changes: 1 addition & 1 deletion xknx/io/__init__.py
Expand Up @@ -9,7 +9,7 @@
# flake8: noqa
from .request_response import RequestResponse
from .knxip_interface import KNXIPInterface, ConnectionType, ConnectionConfig
from .gateway_scanner import GatewayScanner
from .gateway_scanner import GatewayScanner, GatewayScanFilter
from .routing import Routing
from .tunnel import Tunnel
from .disconnect import Disconnect
Expand Down
208 changes: 138 additions & 70 deletions xknx/io/gateway_scanner.py
Expand Up @@ -7,6 +7,7 @@
"""

import asyncio
from typing import List

import netifaces
from xknx.knxip import (HPAI, DIBServiceFamily, DIBSuppSVCFamilies, KNXIPFrame,
Expand All @@ -16,104 +17,171 @@
from .udp_client import UDPClient


class GatewayDescriptor:
"""Used to return infos about the discovered gateways"""

# pylint: disable=too-few-public-methods

def __init__(self,
name: str,
ip: str,
port: int,
local_interface: str,
local_ip: str,
supports_tunnelling: bool = False,
supports_routing: bool = False) -> None:
"""Initialize GatewayDescriptor class."""
# pylint: disable=too-many-arguments
self.name = name
self.ip = ip
self.port = port
self.local_interface = local_interface
self.local_ip = local_ip
self.supports_routing = supports_routing
self.supports_tunnelling = supports_tunnelling

def __str__(self):
"""Return object as readable string."""
return '<GatewayDescriptor name="{0}" addr="{1}:{2}" local="{3}@{4}" routing="{5}" tunnelling="{6} />'.format(
self.name,
self.ip,
self.port,
self.local_ip,
self.local_interface,
self.supports_routing,
self.supports_tunnelling)


class GatewayScanFilter:
"""Filter to limit scan attempts"""

# pylint: disable=too-few-public-methods

def __init__(self,
name: str = None,
tunnelling: bool = None,
routing: bool = None) -> None:
"""Initialize GatewayScanFilter class."""
self.name = name
self.tunnelling = tunnelling
self.routing = routing

def match(self, gateway: GatewayDescriptor) -> bool:
if self.name is not None and self.name != gateway.name:
return False
if self.tunnelling is not None and self.tunnelling != gateway.supports_tunnelling:
return False
if self.routing is not None and self.routing != gateway.supports_routing:
return False
return True


class GatewayScanner():
"""Class for searching KNX/IP devices."""

# pylint: disable=too-few-public-methods
# pylint: disable=too-many-instance-attributes
def __init__(self, xknx, timeout_in_seconds=4):
def __init__(self,
xknx,
timeout_in_seconds: int = 4,
stop_on_found: int = 1,
scan_filter: GatewayScanFilter = GatewayScanFilter()) -> None:
"""Initialize GatewayScanner class."""
self.xknx = xknx
self.response_received_or_timeout = asyncio.Event()
self.found = False
self.found_ip_addr = None
self.found_port = None
self.found_name = None
self.found_local_ip = None
self.supports_routing = False
self.supports_tunneling = False
self.udpclients = []
self.timeout_in_seconds = timeout_in_seconds
self.timeout_callback = None
self.timeout_handle = None

def response_rec_callback(self, knxipframe, udp_client):
"""Verify and handle knxipframe. Callback from internal udpclient."""
if not isinstance(knxipframe.body, SearchResponse):
self.xknx.logger.warning("Cant understand knxipframe")
return

if not self.found:
self.found_ip_addr = knxipframe.body.control_endpoint.ip_addr
self.found_port = knxipframe.body.control_endpoint.port
self.found_name = knxipframe.body.device_name

for dib in knxipframe.body.dibs:
if isinstance(dib, DIBSuppSVCFamilies):
self.supports_routing = dib.supports(DIBServiceFamily.ROUTING)
self.supports_tunneling = dib.supports(DIBServiceFamily.TUNNELING)

(self.found_local_ip, _) = udp_client.getsockname()

self.response_received_or_timeout.set()
self.found = True

async def start(self):
"""Start searching."""
await self.send_search_requests()
await self.start_timeout()
await self.response_received_or_timeout.wait()
await self.stop()
await self.stop_timeout()

async def stop(self):
self.stop_on_found = stop_on_found
self.scan_filter = scan_filter
self.found_gateways = [] # List[GatewayDescriptor]
self._udp_clients = []
self._response_received_or_timeout = asyncio.Event()
self._timeout_callback = None
self._timeout_handle = None

async def scan(self) -> List[GatewayDescriptor]:
"""Scans and returns a list of GatewayDescriptors on success."""
await self._send_search_requests()
await self._start_timeout()
await self._response_received_or_timeout.wait()
await self._stop()
await self._stop_timeout()
return self.found_gateways

async def _stop(self):
"""Stop tearing down udpclient."""
for udpclient in self.udpclients:
await udpclient.stop()
for udp_client in self._udp_clients:
await udp_client.stop()

async def send_search_requests(self):
async def _send_search_requests(self):
"""Send search requests on all connected interfaces."""
# pylint: disable=no-member
for interface in netifaces.interfaces():
try:
af_inet = netifaces.ifaddresses(interface)[netifaces.AF_INET]
ip_addr = af_inet[0]["addr"]
await self.search_interface(interface, ip_addr)
await self._search_interface(interface, ip_addr)
except KeyError:
self.xknx.logger.info("Could not connect to an KNX/IP device on %s", interface)
continue

async def search_interface(self, interface, ip_addr):
async def _search_interface(self, interface, ip_addr):
"""Search on a specific interface."""
self.xknx.logger.debug("Searching on %s / %s", interface, ip_addr)

udpclient = UDPClient(self.xknx,
(ip_addr, 0),
(DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT),
multicast=True)
udp_client = UDPClient(self.xknx,
(ip_addr, 0, interface),
(DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT),
multicast=True)

udpclient.register_callback(
self.response_rec_callback, [KNXIPServiceType.SEARCH_RESPONSE])
await udpclient.connect()
udp_client.register_callback(
self._response_rec_callback, [KNXIPServiceType.SEARCH_RESPONSE])
await udp_client.connect()

self.udpclients.append(udpclient)
self._udp_clients.append(udp_client)

(local_addr, local_port) = udpclient.getsockname()
knxipframe = KNXIPFrame(self.xknx)
knxipframe.init(KNXIPServiceType.SEARCH_REQUEST)
knxipframe.body.discovery_endpoint = \
(local_addr, local_port) = udp_client.getsockname()
knx_ip_frame = KNXIPFrame(self.xknx)
knx_ip_frame.init(KNXIPServiceType.SEARCH_REQUEST)
knx_ip_frame.body.discovery_endpoint = \
HPAI(ip_addr=local_addr, port=local_port)
knxipframe.normalize()
udpclient.send(knxipframe)
knx_ip_frame.normalize()
udp_client.send(knx_ip_frame)

def timeout(self):
"""Handle timeout for not having received a SearchResponse."""
self.response_received_or_timeout.set()
def _response_rec_callback(self, knx_ip_frame: KNXIPFrame, udp_client: UDPClient) -> None:
"""Verify and handle knxipframe. Callback from internal udpclient."""
if not isinstance(knx_ip_frame.body, SearchResponse):
self.xknx.logger.warning("Cant understand knxipframe")
return

async def start_timeout(self):
(found_local_ip, _) = udp_client.getsockname()
gateway = GatewayDescriptor(name=knx_ip_frame.body.device_name,
ip=knx_ip_frame.body.control_endpoint.ip_addr,
port=knx_ip_frame.body.control_endpoint.port,
local_interface=udp_client.local_addr[2],
local_ip=found_local_ip)
try:
dib = knx_ip_frame.body[DIBSuppSVCFamilies]
gateway.supports_routing = dib.supports(DIBServiceFamily.ROUTING)
gateway.supports_tunnelling = dib.supports(DIBServiceFamily.TUNNELING)
except IndexError:
pass

self._add_found_gateway(gateway)

def _add_found_gateway(self, gateway):
if self.scan_filter.match(gateway):
self.found_gateways.append(gateway)
if len(self.found_gateways) >= self.stop_on_found:
self._response_received_or_timeout.set()

def _timeout(self):
"""Handle timeout for not having received enough SearchResponse."""
self._response_received_or_timeout.set()

async def _start_timeout(self):
"""Start time out."""
self.timeout_handle = self.xknx.loop.call_later(
self.timeout_in_seconds, self.timeout)
self._timeout_handle = self.xknx.loop.call_later(
self.timeout_in_seconds, self._timeout)

async def stop_timeout(self):
async def _stop_timeout(self):
"""Stop/cancel timeout."""
self.timeout_handle.cancel()
self._timeout_handle.cancel()

0 comments on commit d01879a

Please sign in to comment.