"""Helper functions for XKNX io module.""" from __future__ import annotations import asyncio import ipaddress import logging import socket from typing import cast import ifaddr from xknx.exceptions import XKNXException from .const import DEFAULT_MCAST_GRP, DEFAULT_MCAST_PORT logger = logging.getLogger("xknx.log") async def get_default_local_ip(remote_ip: str = DEFAULT_MCAST_GRP) -> str | None: """Return the local ip used for communication with remote_ip.""" with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: sock.setblocking(False) # must be non-blocking for async loop = asyncio.get_running_loop() try: await loop.sock_connect(sock, (remote_ip, DEFAULT_MCAST_PORT)) local_ip = sock.getsockname()[0] logger.debug("Using local ip: %s", local_ip) return local_ip # type: ignore[no-any-return] except Exception: # pylint: disable=broad-except logger.warning( "The system could not auto detect the source ip for %s on your operating system", remote_ip, ) return None def get_local_ips() -> list[ifaddr.IP]: """Return list of local IPv4 addresses.""" return [ip for iface in ifaddr.get_adapters() for ip in iface.ips if ip.is_IPv4] def get_local_interface_name(local_ip: str) -> str: """Return the name of the interface with the given ip.""" return next((link.nice_name for link in get_local_ips() if link.ip == local_ip), "") def get_ip_for_adapter_name(name: str) -> str | None: """Return the ip for the given interface name.""" return next( ( ip.ip for iface in ifaddr.get_adapters() if name in (iface.name, iface.nice_name) for ip in iface.ips if isinstance(ip.ip, str) # IPv6 would return tuple ), None, ) def find_local_ip(gateway_ip: str) -> str | None: """Find local IP address on same subnet as gateway.""" gateway = ipaddress.IPv4Address(gateway_ip) for link in get_local_ips(): network = ipaddress.IPv4Network((link.ip, link.network_prefix), strict=False) if gateway in network: logger.debug("Using interface: %s", link.nice_name) return cast(str, link.ip) logger.debug("No interface on same subnet as gateway found.") return None async def validate_ip(address: str, address_name: str = "IP address") -> str: """ Return IPv4 address parsed or resolved as a string. Valid addresses are IPv4 strings, adapter names or hostnames. Raises XKNXException if address is not a valid IPv4 address or cannot be resolved. """ try: ipaddress.IPv4Address(address) return address except ipaddress.AddressValueError as ex: logger.debug( "%s is not a valid IPv4 address: %s. Trying to resolve...", address_name, ex, ) if adapter_ip := get_ip_for_adapter_name(address): return adapter_ip try: return await asyncio.to_thread(socket.gethostbyname, address) except socket.gaierror as ex: raise XKNXException(f"Could not resolve {address_name}: {address}") from ex