From 7c02d52138d5fbd86aa526f18f6f0b9fa28c1285 Mon Sep 17 00:00:00 2001 From: Wouter Born Date: Wed, 3 Jan 2024 19:28:23 +0100 Subject: [PATCH] [network] Add parameter for limiting network interfaces to fix performance issues (#16145) * [network] Add parameter for limiting network interfaces to fix performance issues * Use last known reachable interface --------- Signed-off-by: Wouter Born Signed-off-by: Ciprian Pascu --- bundles/org.openhab.binding.network/README.md | 27 ++++++----- .../internal/NetworkHandlerConfiguration.java | 3 ++ .../network/internal/PresenceDetection.java | 25 ++++++++-- .../internal/WakeOnLanPacketSender.java | 48 +++++++++++++++++-- .../discovery/NetworkDiscoveryService.java | 4 +- .../internal/handler/NetworkHandler.java | 3 +- .../resources/OH-INF/i18n/network.properties | 4 ++ .../resources/OH-INF/thing/thing-types.xml | 16 +++++++ .../internal/WakeOnLanPacketSenderTest.java | 3 +- 9 files changed, 109 insertions(+), 24 deletions(-) diff --git a/bundles/org.openhab.binding.network/README.md b/bundles/org.openhab.binding.network/README.md index 7598735f98b..f6be56dfa22 100644 --- a/bundles/org.openhab.binding.network/README.md +++ b/bundles/org.openhab.binding.network/README.md @@ -11,7 +11,7 @@ The binding has the following configuration options: - **allowSystemPings:** Use the external ICMP ping program of the operating system instead of the Java ping. Useful if the devices cannot be reached by Java ping. Default is true. - **allowDHCPlisten:** If devices leave and reenter a network, they usually request their last IPv4 address by using DHCP requests. By listening for those messages, the status update can be more "real-time" without having to wait for the next refresh cycle. Default is true. -- **arpPingToolPath:** If the arp ping tool is not called `arping` and cannot be found in the PATH environment variable, the absolute path can be configured here. Default is `arping`. +- **arpPingToolPath:** If the ARP ping tool is not called `arping` and cannot be found in the PATH environment variable, the absolute path can be configured here. Default is `arping`. - **cacheDeviceStateTimeInMS:** The result of a device presence detection is cached for a small amount of time. Set this time here in milliseconds. Be aware that no new pings will be issued within this time frame, even if explicitly requested. Default is 2000. - **preferResponseTimeAsLatency:** If enabled, an attempt will be made to extract the latency from the output of the ping command. If no such latency value is found in the ping command output, the time to execute the ping command is used as fallback latency. If disabled, the time to execute the ping command is always used as latency value. This is disabled by default to be backwards-compatible and to not break statistics and monitoring which existed before this feature. @@ -26,7 +26,7 @@ binding.network:cacheDeviceStateTimeInMS=2000 ## Supported Things -- **pingdevice:** Detects device presence by using ICMP pings, arp pings and dhcp packet sniffing. +- **pingdevice:** Detects device presence by using ICMP pings, ARP pings and DHCP packet sniffing. - **servicedevice:** Detects device presence by scanning for a specific open tcp port. - **speedtest:** Monitors available bandwidth for upload and download. @@ -41,7 +41,7 @@ Please note: things discovered by the network binding will be provided with a ti ```java network:pingdevice:one_device [ hostname="192.168.0.64" ] -network:pingdevice:second_device [ hostname="192.168.0.65", macAddress="6f:70:65:6e:48:41", retry=1, timeout=5000, refreshInterval=60000 ] +network:pingdevice:second_device [ hostname="192.168.0.65", macAddress="6f:70:65:6e:48:41", retry=1, timeout=5000, refreshInterval=60000, networkInterfaceNames="eth0","wlan0" ] network:servicedevice:important_server [ hostname="192.168.0.62", port=1234 ] network:speedtest:local "SpeedTest 50Mo" @ "Internet" [refreshInterval=20, uploadSize=1000000, url="https://bouygues.testdebit.info/", fileName="50M.iso"] ``` @@ -53,6 +53,9 @@ Use the following options for a **network:pingdevice**: - **retry:** After how many refresh interval cycles the device will be assumed to be offline. Default: `1`. - **timeout:** How long the ping will wait for an answer, in milliseconds. Default: `5000` (5 seconds). - **refreshInterval:** How often the device will be checked, in milliseconds. Default: `60000` (one minute). +- **networkInterfaceNames:** The network interface names used for communicating with the device. + Limiting the network interfaces reduces the load when arping and Wake-on-LAN are used. + Use comma separated values when using textual config. Default: empty (all network interfaces). Use the following additional options for a **network:servicedevice**: @@ -62,10 +65,10 @@ Use the following options for a **network:speedtest**: - **refreshInterval:** Interval between each test execution, in minutes. Default: `20`. - **uploadSize:** Size of the file to be uploaded in bytes. Default: `1000000`. -- **url:** Url of the speed test server. +- **url:** URL of the speed test server. - **fileName:** Name of the file to download from test server. - **initialDelay:** Delay (in minutes) before starting the first speed test (can help avoid flooding your server at startup). Default: `5`. -- **maxTimeout:** Number of timeout events that can happend (resetted at success) before setting the thing offline. Default: `3`. +- **maxTimeout:** Number of timeout events that can happend (reset when successful) before setting the thing offline. Default: `3`. ## Presence detection - Configure target device @@ -81,15 +84,15 @@ Windows 10 must be configured to allow "Echo Request for ICMPv4" so that it can Because mobile devices put themselves in a deep sleep mode after some inactivity, they do not react to normal ICMP pings. Configure ARP ping to realize presence detection for those devices. -This only works if the devices have WIFI enabled, have been configured to use the WIFI network, and have the option "Disable wifi in standby" disabled (default). -Use DHCP listen for an almost immediate presence detection for phones and tablets when they (re)join the home Wifi network. +This only works if the devices have Wi-Fi enabled, have been configured to use the Wi-Fi network, and have the option "Disable Wi-Fi in standby" disabled (default). +Use DHCP listen for an almost immediate presence detection for phones and tablets when they (re)join the home Wi-Fi network. ### iPhones, iPads Apple iOS devices are usually in a deep sleep mode and do not respond to ARP pings under all conditions, but to Bonjour service discovery messages (UDP port 5353). -Therefore first a Bonjour message is sent, before the ARP presence detection is performed. +Therefore, first a Bonjour message is sent, before the ARP presence detection is performed. The binding automatically figures out if the target device is an iOS device. -To check if the binding has correctly recognised a device, have a look at the _uses_ios_wakeup_ property of the THING. +To check if the binding has correctly recognised a device, have a look at the _uses_ios_wakeup_ property of the Thing. ### Use open TCP ports @@ -115,7 +118,7 @@ Nmap done: 1 IP address (1 host up) scanned in 106.17 seconds In this example, there are four suitable ports to use. The port 554 (Windows network file sharing service) is open on most Windows PCs and Windows compatible Linux systems. -Port 1025 (MS RPC) is open on XBox systems. Port 548 (Apple Filing Protocol (AFP)) is open on Mac OS X systems. +Port 1025 (MS RPC) is open on XBox systems. Port 548 (Apple Filing Protocol (AFP)) is open on macOS systems. Please don't forget to open the required ports in the system's firewall setup. @@ -125,7 +128,7 @@ Because external tools are used for some of the presence detection mechanism or ### Arping -For arp pings to work, a separate tool called "arping" is used. +For ARP pings to work, a separate tool called "arping" is used. Linux has three different tools: - arp-scan (not yet supported by this binding) @@ -144,7 +147,7 @@ Just test the executable on the command line; if `sudo` is required, grant eleva Some operating systems such as Linux restrict applications to only use ports >= 1024 without elevated privileges. If the binding is not able to use port 67 (DHCP) because of such a restriction, or because the same system is used as a DHCP server, port 6767 will be used instead. -Check the property _dhcp_state_ on the THING for such a hint. In this case, establish port forwarding: +Check the property _dhcp_state_ on the Thing for such a hint. In this case, establish port forwarding: ```shell sysctl -w net.ipv4.ip_forward=1 diff --git a/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/NetworkHandlerConfiguration.java b/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/NetworkHandlerConfiguration.java index 8bb722d7fc1..9c2f529f497 100644 --- a/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/NetworkHandlerConfiguration.java +++ b/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/NetworkHandlerConfiguration.java @@ -12,6 +12,8 @@ */ package org.openhab.binding.network.internal; +import java.util.Set; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -29,4 +31,5 @@ public class NetworkHandlerConfiguration { public Integer retry = 1; public Integer refreshInterval = 60000; public Integer timeout = 5000; + public Set networkInterfaceNames = Set.of(); } diff --git a/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/PresenceDetection.java b/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/PresenceDetection.java index 6972ad649ce..54d8f47e4dd 100644 --- a/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/PresenceDetection.java +++ b/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/PresenceDetection.java @@ -77,18 +77,19 @@ public class PresenceDetection implements IPRequestReceivedCallback { /// State variables (cannot be final because of test dependency injections) ExpiringCacheAsync cache; private final PresenceDetectionListener updateListener; + + private Set networkInterfaceNames = Set.of(); private @Nullable ScheduledFuture refreshJob; protected @Nullable ExecutorService executorService; private String dhcpState = "off"; private Integer currentCheck = 0; int detectionChecks; + private String lastReachableNetworkInterfaceName = ""; public PresenceDetection(final PresenceDetectionListener updateListener, int cacheDeviceStateTimeInMS) throws IllegalArgumentException { this.updateListener = updateListener; - cache = new ExpiringCacheAsync<>(cacheDeviceStateTimeInMS, () -> { - performPresenceDetection(false); - }); + cache = new ExpiringCacheAsync<>(cacheDeviceStateTimeInMS, () -> performPresenceDetection(false)); } public @Nullable String getHostname() { @@ -137,6 +138,10 @@ public class PresenceDetection implements IPRequestReceivedCallback { }); } + public void setNetworkInterfaceNames(Set networkInterfaceNames) { + this.networkInterfaceNames = networkInterfaceNames; + } + public void setServicePorts(Set ports) { this.tcpPorts = ports; } @@ -293,7 +298,13 @@ public class PresenceDetection implements IPRequestReceivedCallback { detectionChecks += 1; } if (arpPingMethod.canProceed) { - interfaceNames = networkUtils.getInterfaceNames(); + if (!lastReachableNetworkInterfaceName.isEmpty()) { + interfaceNames = Set.of(lastReachableNetworkInterfaceName); + } else if (!networkInterfaceNames.isEmpty()) { + interfaceNames = networkInterfaceNames; + } else { + interfaceNames = networkUtils.getInterfaceNames(); + } detectionChecks += interfaceNames.size(); } @@ -306,7 +317,7 @@ public class PresenceDetection implements IPRequestReceivedCallback { for (Integer tcpPort : tcpPorts) { executorService.execute(() -> { - Thread.currentThread().setName("presenceDetectionTCP_" + hostname + " " + String.valueOf(tcpPort)); + Thread.currentThread().setName("presenceDetectionTCP_" + hostname + " " + tcpPort); performServicePing(tcpPort); checkIfFinished(); }); @@ -493,6 +504,10 @@ public class PresenceDetection implements IPRequestReceivedCallback { PresenceDetectionValue v = updateReachableValue(PresenceDetectionType.ARP_PING, getLatency(o, preferResponseTimeAsLatency)); updateListener.partialDetectionResult(v); + lastReachableNetworkInterfaceName = interfaceName; + } else if (lastReachableNetworkInterfaceName.equals(interfaceName)) { + logger.trace("{} is no longer reachable on network interface: {}", hostname, interfaceName); + lastReachableNetworkInterfaceName = ""; } }); } catch (IOException e) { diff --git a/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/WakeOnLanPacketSender.java b/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/WakeOnLanPacketSender.java index 7fed28f4c75..a9775c42731 100644 --- a/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/WakeOnLanPacketSender.java +++ b/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/WakeOnLanPacketSender.java @@ -17,11 +17,16 @@ import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.net.InetSocketAddress; +import java.net.InterfaceAddress; +import java.net.NetworkInterface; import java.net.SocketAddress; import java.net.SocketException; import java.net.UnknownHostException; import java.util.Arrays; +import java.util.Enumeration; +import java.util.HashSet; import java.util.Objects; +import java.util.Set; import java.util.function.Consumer; import java.util.stream.Stream; @@ -55,15 +60,19 @@ public class WakeOnLanPacketSender { private final @Nullable String hostname; private final @Nullable Integer port; + private final Set networkInterfaceNames; private final Consumer magicPacketMacSender; private final Consumer magicPacketIpSender; - public WakeOnLanPacketSender(String macAddress, @Nullable String hostname, @Nullable Integer port) { - logger.debug("initialized WOL Packet Sender (mac: {}, hostname: {}, port: {}", macAddress, hostname, port); + public WakeOnLanPacketSender(String macAddress, @Nullable String hostname, @Nullable Integer port, + Set networkInterfaceNames) { + logger.debug("initialized WOL Packet Sender (mac: {}, hostname: {}, port: {}, networkInterfaceNames: {})", + macAddress, hostname, port, networkInterfaceNames); this.macAddress = macAddress; this.hostname = hostname; this.port = port; + this.networkInterfaceNames = networkInterfaceNames; this.magicPacketMacSender = this::sendMagicPacketViaMac; this.magicPacketIpSender = this::sendMagicPacketViaIp; } @@ -72,10 +81,11 @@ public class WakeOnLanPacketSender { * Used for testing only. */ public WakeOnLanPacketSender(String macAddress) { - logger.debug("initialized WOL Packet Sender (mac: {}", macAddress); + logger.debug("initialized WOL Packet Sender (mac: {})", macAddress); this.macAddress = macAddress; this.hostname = null; this.port = null; + this.networkInterfaceNames = Set.of(); this.magicPacketMacSender = this::sendMagicPacketViaMac; this.magicPacketIpSender = this::sendMagicPacketViaIp; } @@ -87,6 +97,7 @@ public class WakeOnLanPacketSender { this.macAddress = macAddress; this.hostname = null; this.port = null; + this.networkInterfaceNames = Set.of(); this.magicPacketMacSender = magicPacketSender; this.magicPacketIpSender = this::sendMagicPacketViaIp; } @@ -172,8 +183,39 @@ public class WakeOnLanPacketSender { logger.info("Wake-on-LAN packets sent (IP address: {})", ip); } + private Set configuredBroadcastAddresses() { + if (networkInterfaceNames.isEmpty()) { + return Set.of(); + } + + try { + Enumeration niEnum = NetworkInterface.getNetworkInterfaces(); + Set broadcastAddresses = new HashSet<>(); + while (niEnum.hasMoreElements()) { + NetworkInterface networkInterface = niEnum.nextElement(); + if (networkInterfaceNames.contains(networkInterface.getName())) { + for (InterfaceAddress interfaceAddress : networkInterface.getInterfaceAddresses()) { + InetAddress broadcast = interfaceAddress.getBroadcast(); + if (broadcast != null) { + broadcastAddresses.add(broadcast.getHostAddress()); + } + } + } + } + return broadcastAddresses; + } catch (SocketException e) { + logger.debug("Unable to determine configured broadcast addresses", e); + return Set.of(); + } + } + private Stream broadcastAddressStream() { + Set configuredBroadcastAddresses = configuredBroadcastAddresses(); return NetUtil.getAllBroadcastAddresses().stream().map(address -> { + if (!networkInterfaceNames.isEmpty() && !configuredBroadcastAddresses.contains(address)) { + logger.debug("'{}' is not a broadcast address of the configured network interfaces", address); + return null; + } try { return InetAddress.getByName(address); } catch (UnknownHostException e) { diff --git a/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/discovery/NetworkDiscoveryService.java b/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/discovery/NetworkDiscoveryService.java index ddbdfe30a61..7c4287c5be5 100644 --- a/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/discovery/NetworkDiscoveryService.java +++ b/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/discovery/NetworkDiscoveryService.java @@ -185,7 +185,7 @@ public class NetworkDiscoveryService extends AbstractDiscoveryService implements } /** - * Submit newly discovered devices. This method is called by the spawned threads in {@link startScan}. + * Submit newly discovered devices. This method is called by the spawned threads in {@link #startScan()}. * * @param ip The device IP * @param tcpPort The TCP port @@ -228,7 +228,7 @@ public class NetworkDiscoveryService extends AbstractDiscoveryService implements } /** - * Submit newly discovered devices. This method is called by the spawned threads in {@link startScan}. + * Submit newly discovered devices. This method is called by the spawned threads in {@link #startScan()}. * * @param ip The device IP */ diff --git a/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/handler/NetworkHandler.java b/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/handler/NetworkHandler.java index 2f4db0d287e..10d7780fbb7 100644 --- a/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/handler/NetworkHandler.java +++ b/bundles/org.openhab.binding.network/src/main/java/org/openhab/binding/network/internal/handler/NetworkHandler.java @@ -173,6 +173,7 @@ public class NetworkHandler extends BaseThingHandler this.presenceDetection = presenceDetection; presenceDetection.setHostname(handlerConfiguration.hostname); + presenceDetection.setNetworkInterfaceNames(handlerConfiguration.networkInterfaceNames); presenceDetection.setPreferResponseTimeAsLatency(configuration.preferResponseTimeAsLatency); if (isTCPServiceDevice) { @@ -199,7 +200,7 @@ public class NetworkHandler extends BaseThingHandler presenceDetection.setTimeout(handlerConfiguration.timeout.intValue()); wakeOnLanPacketSender = new WakeOnLanPacketSender(handlerConfiguration.macAddress, - handlerConfiguration.hostname, handlerConfiguration.port); + handlerConfiguration.hostname, handlerConfiguration.port, handlerConfiguration.networkInterfaceNames); updateStatus(ThingStatus.ONLINE); presenceDetection.startAutomaticRefresh(scheduler); diff --git a/bundles/org.openhab.binding.network/src/main/resources/OH-INF/i18n/network.properties b/bundles/org.openhab.binding.network/src/main/resources/OH-INF/i18n/network.properties index 63752d76e35..ba06fcd5bc3 100644 --- a/bundles/org.openhab.binding.network/src/main/resources/OH-INF/i18n/network.properties +++ b/bundles/org.openhab.binding.network/src/main/resources/OH-INF/i18n/network.properties @@ -33,6 +33,8 @@ thing-type.config.network.pingdevice.hostname.label = Hostname or IP thing-type.config.network.pingdevice.hostname.description = Hostname or IP of the device thing-type.config.network.pingdevice.macAddress.label = MAC Address thing-type.config.network.pingdevice.macAddress.description = MAC address used for waking the device by the Wake-on-LAN action +thing-type.config.network.pingdevice.networkInterfaceNames.label = Network Interface Names +thing-type.config.network.pingdevice.networkInterfaceNames.description = The network interface names used for communicating with the device. Limiting the network interfaces reduces the load when arping and Wake-on-LAN are used. All interfaces are used when left empty. thing-type.config.network.pingdevice.refreshInterval.label = Refresh Interval thing-type.config.network.pingdevice.refreshInterval.description = States how long to wait after a device state update before the next refresh shall occur (in ms) thing-type.config.network.pingdevice.retry.label = Retry @@ -43,6 +45,8 @@ thing-type.config.network.servicedevice.hostname.label = Hostname or IP thing-type.config.network.servicedevice.hostname.description = Hostname or IP of the device thing-type.config.network.servicedevice.macAddress.label = MAC Address thing-type.config.network.servicedevice.macAddress.description = MAC address used for waking the device by the Wake-on-LAN action +thing-type.config.network.servicedevice.networkInterfaceNames.label = Network Interface Names +thing-type.config.network.servicedevice.networkInterfaceNames.description = The network interface names used for communicating with the device. Limiting the network interfaces reduces the load when arping and Wake-on-LAN are used. All interfaces are used when left empty. thing-type.config.network.servicedevice.port.label = Port thing-type.config.network.servicedevice.port.description = The port on which the device can be accessed. Windows systems usually have the 445 port open. Webservers are on port 80. thing-type.config.network.servicedevice.refreshInterval.label = Refresh Interval diff --git a/bundles/org.openhab.binding.network/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.network/src/main/resources/OH-INF/thing/thing-types.xml index 23cd4cc0bdc..007e3e72730 100644 --- a/bundles/org.openhab.binding.network/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.network/src/main/resources/OH-INF/thing/thing-types.xml @@ -54,6 +54,14 @@ 5000 true + + + + network-interface + The network interface names used for communicating with the device. Limiting the network interfaces + reduces the load when arping and Wake-on-LAN are used. All interfaces are used when left empty. + true + @@ -112,6 +120,14 @@ States how long to wait after a device state update before the next refresh shall occur (in ms) 60000 + + + + network-interface + The network interface names used for communicating with the device. Limiting the network interfaces + reduces the load when arping and Wake-on-LAN are used. All interfaces are used when left empty. + true + diff --git a/bundles/org.openhab.binding.network/src/test/java/org/openhab/binding/network/internal/WakeOnLanPacketSenderTest.java b/bundles/org.openhab.binding.network/src/test/java/org/openhab/binding/network/internal/WakeOnLanPacketSenderTest.java index a1a07250175..88dfb1d1ae9 100644 --- a/bundles/org.openhab.binding.network/src/test/java/org/openhab/binding/network/internal/WakeOnLanPacketSenderTest.java +++ b/bundles/org.openhab.binding.network/src/test/java/org/openhab/binding/network/internal/WakeOnLanPacketSenderTest.java @@ -21,6 +21,7 @@ import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; import java.util.Arrays; +import java.util.Set; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -117,7 +118,7 @@ public class WakeOnLanPacketSenderTest { } try { - WakeOnLanPacketSender sender = new WakeOnLanPacketSender("6f70656e4841", hostname, port); + WakeOnLanPacketSender sender = new WakeOnLanPacketSender("6f70656e4841", hostname, port, Set.of()); sender.sendWakeOnLanPacketViaIp(); // This Test is only applicable for IP Requests