[network] Cleanup code (#16235)

* Reuse ExpiringCacheAsync from Core
* Use Duration and Instant
* Replace Optional with null annotations
* Cleanup JavaDocs
* Improve logging
* Add missing null annotations
* Simplify code

Signed-off-by: Wouter Born <github@maindrain.net>
Signed-off-by: Ciprian Pascu <contact@ciprianpascu.ro>
This commit is contained in:
Wouter Born 2024-01-11 17:55:23 +01:00 committed by Ciprian Pascu
parent 589af9f813
commit b4974b3ffd
20 changed files with 688 additions and 986 deletions

View File

@ -47,7 +47,7 @@ public class NetworkBindingConfiguration {
this.preferResponseTimeAsLatency = newConfiguration.preferResponseTimeAsLatency; this.preferResponseTimeAsLatency = newConfiguration.preferResponseTimeAsLatency;
NetworkUtils networkUtils = new NetworkUtils(); NetworkUtils networkUtils = new NetworkUtils();
this.arpPingUtilMethod = networkUtils.determineNativeARPpingMethod(arpPingToolPath); this.arpPingUtilMethod = networkUtils.determineNativeArpPingMethod(arpPingToolPath);
notifyListeners(); notifyListeners();
} }

View File

@ -12,7 +12,6 @@
*/ */
package org.openhab.binding.network.internal; package org.openhab.binding.network.internal;
import java.util.HashSet;
import java.util.Set; import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
@ -36,10 +35,12 @@ public class NetworkBindingConstants {
public static final ThingTypeUID SERVICE_DEVICE = new ThingTypeUID(BINDING_ID, "servicedevice"); public static final ThingTypeUID SERVICE_DEVICE = new ThingTypeUID(BINDING_ID, "servicedevice");
public static final ThingTypeUID SPEEDTEST_DEVICE = new ThingTypeUID(BINDING_ID, "speedtest"); public static final ThingTypeUID SPEEDTEST_DEVICE = new ThingTypeUID(BINDING_ID, "speedtest");
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(BACKWARDS_COMPATIBLE_DEVICE, PING_DEVICE,
SERVICE_DEVICE, SPEEDTEST_DEVICE);
// List of all Channel ids // List of all Channel ids
public static final String CHANNEL_ONLINE = "online"; public static final String CHANNEL_ONLINE = "online";
public static final String CHANNEL_LATENCY = "latency"; public static final String CHANNEL_LATENCY = "latency";
public static final String CHANNEL_DEPRECATED_TIME = "time";
public static final String CHANNEL_LASTSEEN = "lastseen"; public static final String CHANNEL_LASTSEEN = "lastseen";
public static final String CHANNEL_TEST_ISRUNNING = "isRunning"; public static final String CHANNEL_TEST_ISRUNNING = "isRunning";
public static final String CHANNEL_TEST_PROGRESS = "progress"; public static final String CHANNEL_TEST_PROGRESS = "progress";
@ -60,13 +61,4 @@ public class NetworkBindingConstants {
public static final String PROPERTY_ICMP_STATE = "icmp_state"; public static final String PROPERTY_ICMP_STATE = "icmp_state";
public static final String PROPERTY_PRESENCE_DETECTION_TYPE = "presence_detection_type"; public static final String PROPERTY_PRESENCE_DETECTION_TYPE = "presence_detection_type";
public static final String PROPERTY_IOS_WAKEUP = "uses_ios_wakeup"; public static final String PROPERTY_IOS_WAKEUP = "uses_ios_wakeup";
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = new HashSet<>();
static {
SUPPORTED_THING_TYPES_UIDS.add(PING_DEVICE);
SUPPORTED_THING_TYPES_UIDS.add(SERVICE_DEVICE);
SUPPORTED_THING_TYPES_UIDS.add(BACKWARDS_COMPATIBLE_DEVICE);
SUPPORTED_THING_TYPES_UIDS.add(SPEEDTEST_DEVICE);
}
} }

View File

@ -12,13 +12,21 @@
*/ */
package org.openhab.binding.network.internal; package org.openhab.binding.network.internal;
import static org.openhab.binding.network.internal.PresenceDetectionType.*;
import java.io.IOException; import java.io.IOException;
import java.net.Inet4Address; import java.net.Inet4Address;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.SocketException; import java.net.SocketException;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
@ -31,26 +39,27 @@ import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.network.internal.dhcp.DHCPListenService; import org.openhab.binding.network.internal.dhcp.DHCPListenService;
import org.openhab.binding.network.internal.dhcp.DHCPPacketListenerServer; import org.openhab.binding.network.internal.dhcp.DHCPPacketListenerServer;
import org.openhab.binding.network.internal.dhcp.IPRequestReceivedCallback; import org.openhab.binding.network.internal.dhcp.IPRequestReceivedCallback;
import org.openhab.binding.network.internal.toberemoved.cache.ExpiringCacheAsync;
import org.openhab.binding.network.internal.utils.NetworkUtils; import org.openhab.binding.network.internal.utils.NetworkUtils;
import org.openhab.binding.network.internal.utils.NetworkUtils.ArpPingUtilEnum; import org.openhab.binding.network.internal.utils.NetworkUtils.ArpPingUtilEnum;
import org.openhab.binding.network.internal.utils.NetworkUtils.IpPingMethodEnum; import org.openhab.binding.network.internal.utils.NetworkUtils.IpPingMethodEnum;
import org.openhab.binding.network.internal.utils.PingResult; import org.openhab.binding.network.internal.utils.PingResult;
import org.openhab.core.cache.ExpiringCache; import org.openhab.core.cache.ExpiringCache;
import org.openhab.core.cache.ExpiringCacheAsync;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
/** /**
* The {@link PresenceDetection} handles the connection to the Device * The {@link PresenceDetection} handles the connection to the Device.
* *
* @author Marc Mettke - Initial contribution * @author Marc Mettke - Initial contribution
* @author David Gräff, 2017 - Rewritten * @author David Gräff, 2017 - Rewritten
* @author Jan N. Klug - refactored host name resolution * @author Jan N. Klug - refactored host name resolution
* @author Wouter Born - Reuse ExpiringCacheAsync from Core
*/ */
@NonNullByDefault @NonNullByDefault
public class PresenceDetection implements IPRequestReceivedCallback { public class PresenceDetection implements IPRequestReceivedCallback {
private static final int DESTINATION_TTL = 300 * 1000; // in ms, 300 s private static final Duration DESTINATION_TTL = Duration.ofMinutes(5);
NetworkUtils networkUtils = new NetworkUtils(); NetworkUtils networkUtils = new NetworkUtils();
private final Logger logger = LoggerFactory.getLogger(PresenceDetection.class); private final Logger logger = LoggerFactory.getLogger(PresenceDetection.class);
@ -64,32 +73,35 @@ public class PresenceDetection implements IPRequestReceivedCallback {
private boolean iosDevice; private boolean iosDevice;
private Set<Integer> tcpPorts = new HashSet<>(); private Set<Integer> tcpPorts = new HashSet<>();
private long refreshIntervalInMS = 60000; private Duration refreshInterval = Duration.ofMinutes(1);
private int timeoutInMS = 5000; private Duration timeout = Duration.ofSeconds(5);
private long lastSeenInMS; private @Nullable Instant lastSeen;
private @NonNullByDefault({}) String hostname; private @NonNullByDefault({}) String hostname;
private @NonNullByDefault({}) ExpiringCache<@Nullable InetAddress> destination; private @NonNullByDefault({}) ExpiringCache<@Nullable InetAddress> destination;
private @Nullable InetAddress cachedDestination = null; private @Nullable InetAddress cachedDestination;
private boolean preferResponseTimeAsLatency; private boolean preferResponseTimeAsLatency;
/// State variables (cannot be final because of test dependency injections) // State variables (cannot be final because of test dependency injections)
ExpiringCacheAsync<PresenceDetectionValue> cache; ExpiringCacheAsync<PresenceDetectionValue> cache;
private final PresenceDetectionListener updateListener; private final PresenceDetectionListener updateListener;
private ScheduledExecutorService scheduledExecutorService;
private Set<String> networkInterfaceNames = Set.of(); private Set<String> networkInterfaceNames = Set.of();
private @Nullable ScheduledFuture<?> refreshJob; private @Nullable ScheduledFuture<?> refreshJob;
protected @Nullable ExecutorService executorService; protected @Nullable ExecutorService detectionExecutorService;
private String dhcpState = "off"; private String dhcpState = "off";
private Integer currentCheck = 0;
int detectionChecks; int detectionChecks;
private String lastReachableNetworkInterfaceName = ""; private String lastReachableNetworkInterfaceName = "";
public PresenceDetection(final PresenceDetectionListener updateListener, int cacheDeviceStateTimeInMS) public PresenceDetection(final PresenceDetectionListener updateListener,
ScheduledExecutorService scheduledExecutorService, Duration cacheDeviceStateTime)
throws IllegalArgumentException { throws IllegalArgumentException {
this.updateListener = updateListener; this.updateListener = updateListener;
cache = new ExpiringCacheAsync<>(cacheDeviceStateTimeInMS, () -> performPresenceDetection(false)); this.scheduledExecutorService = scheduledExecutorService;
cache = new ExpiringCacheAsync<>(cacheDeviceStateTime);
} }
public @Nullable String getHostname() { public @Nullable String getHostname() {
@ -100,12 +112,12 @@ public class PresenceDetection implements IPRequestReceivedCallback {
return tcpPorts; return tcpPorts;
} }
public long getRefreshInterval() { public Duration getRefreshInterval() {
return refreshIntervalInMS; return refreshInterval;
} }
public int getTimeout() { public Duration getTimeout() {
return timeoutInMS; return timeout;
} }
public void setHostname(String hostname) { public void setHostname(String hostname) {
@ -115,7 +127,8 @@ public class PresenceDetection implements IPRequestReceivedCallback {
InetAddress destinationAddress = InetAddress.getByName(hostname); InetAddress destinationAddress = InetAddress.getByName(hostname);
InetAddress cached = cachedDestination; InetAddress cached = cachedDestination;
if (!destinationAddress.equals(cached)) { if (!destinationAddress.equals(cached)) {
logger.trace("host name resolved to other address, (re-)setup presence detection"); logger.trace("Hostname {} resolved to other address {}, (re-)setup presence detection", hostname,
destinationAddress);
setUseArpPing(true, destinationAddress); setUseArpPing(true, destinationAddress);
if (useDHCPsniffing) { if (useDHCPsniffing) {
if (cached != null) { if (cached != null) {
@ -127,7 +140,7 @@ public class PresenceDetection implements IPRequestReceivedCallback {
} }
return destinationAddress; return destinationAddress;
} catch (UnknownHostException e) { } catch (UnknownHostException e) {
logger.trace("hostname resolution failed"); logger.trace("Hostname resolution for {} failed", hostname);
InetAddress cached = cachedDestination; InetAddress cached = cachedDestination;
if (cached != null) { if (cached != null) {
disableDHCPListen(cached); disableDHCPListen(cached);
@ -150,12 +163,12 @@ public class PresenceDetection implements IPRequestReceivedCallback {
this.useDHCPsniffing = enable; this.useDHCPsniffing = enable;
} }
public void setRefreshInterval(long refreshInterval) { public void setRefreshInterval(Duration refreshInterval) {
this.refreshIntervalInMS = refreshInterval; this.refreshInterval = refreshInterval;
} }
public void setTimeout(int timeout) { public void setTimeout(Duration timeout) {
this.timeoutInMS = timeout; this.timeout = timeout;
} }
public void setPreferResponseTimeAsLatency(boolean preferResponseTimeAsLatency) { public void setPreferResponseTimeAsLatency(boolean preferResponseTimeAsLatency) {
@ -163,11 +176,11 @@ public class PresenceDetection implements IPRequestReceivedCallback {
} }
/** /**
* Sets the ping method. This method will perform a feature test. If SYSTEM_PING * Sets the ping method. This method will perform a feature test. If {@link IpPingMethodEnum#SYSTEM_PING}
* does not work on this system, JAVA_PING will be used instead. * does not work on this system, {@link IpPingMethodEnum#JAVA_PING} will be used instead.
* *
* @param useSystemPing Set to true to use a system ping method, false to use java ping and null to disable ICMP * @param useSystemPing Set to <code>true</code> to use a system ping method, <code>false</code> to use Java ping
* pings. * and <code>null</code> to disable ICMP pings.
*/ */
public void setUseIcmpPing(@Nullable Boolean useSystemPing) { public void setUseIcmpPing(@Nullable Boolean useSystemPing) {
if (useSystemPing == null) { if (useSystemPing == null) {
@ -201,9 +214,9 @@ public class PresenceDetection implements IPRequestReceivedCallback {
} }
/** /**
* sets the path to arp ping * Sets the path to ARP ping.
* *
* @param enable Enable or disable ARP ping * @param enable enable or disable ARP ping
* @param arpPingUtilPath enableDHCPListen(useDHCPsniffing); * @param arpPingUtilPath enableDHCPListen(useDHCPsniffing);
*/ */
public void setUseArpPing(boolean enable, String arpPingUtilPath, ArpPingUtilEnum arpPingUtilMethod) { public void setUseArpPing(boolean enable, String arpPingUtilPath, ArpPingUtilEnum arpPingUtilMethod) {
@ -225,7 +238,7 @@ public class PresenceDetection implements IPRequestReceivedCallback {
} }
/** /**
* Return true if the device presence detection is performed for an iOS device * Return <code>true</code> if the device presence detection is performed for an iOS device
* like iPhone or iPads. An additional port knock is performed before a ping. * like iPhone or iPads. An additional port knock is performed before a ping.
*/ */
public boolean isIOSdevice() { public boolean isIOSdevice() {
@ -233,7 +246,7 @@ public class PresenceDetection implements IPRequestReceivedCallback {
} }
/** /**
* Set to true if the device presence detection should be performed for an iOS device * Set to <code>true</code> if the device presence detection should be performed for an iOS device
* like iPhone or iPads. An additional port knock is performed before a ping. * like iPhone or iPads. An additional port knock is performed before a ping.
*/ */
public void setIOSDevice(boolean value) { public void setIOSDevice(boolean value) {
@ -241,58 +254,62 @@ public class PresenceDetection implements IPRequestReceivedCallback {
} }
/** /**
* Return the last seen value in milliseconds based on {@link System#currentTimeMillis()} or 0 if not seen yet. * Return the last seen value as an {@link Instant} or <code>null</code> if not yet seen.
*/ */
public long getLastSeen() { public @Nullable Instant getLastSeen() {
return lastSeenInMS; return lastSeen;
} }
/** /**
* Return asynchronously the value of the presence detection as a PresenceDetectionValue. * Gets the presence detection value synchronously as a {@link PresenceDetectionValue}.
* <p>
* The value is only updated if the cached value has not expired.
*/
public PresenceDetectionValue getValue() throws InterruptedException, ExecutionException {
return cache.getValue(this::performPresenceDetection).get();
}
/**
* Gets the presence detection value asynchronously as a {@link PresenceDetectionValue}.
* <p>
* The value is only updated if the cached value has not expired.
* *
* @param callback A callback with the PresenceDetectionValue. The callback may * @param callback a callback with the {@link PresenceDetectionValue}. The callback may
* not happen immediately if the cached value expired, but as soon as a new * not happen immediately if the cached value expired, but as soon as a new
* discovery took place. * discovery took place.
*/ */
public void getValue(Consumer<PresenceDetectionValue> callback) { public void getValue(Consumer<PresenceDetectionValue> callback) {
cache.getValue(callback); cache.getValue(this::performPresenceDetection).thenAccept(callback);
} }
public ExecutorService getThreadsFor(int threadCount) { public ExecutorService getThreadsFor(int threadCount) {
return Executors.newFixedThreadPool(threadCount); return Executors.newFixedThreadPool(threadCount);
} }
private void withDestinationAddress(Consumer<InetAddress> consumer) {
InetAddress destinationAddress = destination.getValue();
if (destinationAddress == null) {
logger.trace("The destinationAddress for {} is null", hostname);
} else {
consumer.accept(destinationAddress);
}
}
/** /**
* Perform a presence detection with ICMP-, ARP ping and * Perform a presence detection with ICMP-, ARP ping and TCP connection attempts simultaneously.
* TCP connection attempts simultaneously. A fixed thread pool will be created with as many * A fixed thread pool will be created with as many threads as necessary to perform all tests at once.
* thread as necessary to perform all tests at once.
*
* This is a NO-OP, if there is already an ongoing detection or if the cached value
* is not expired yet.
* *
* Please be aware of the following restrictions: * Please be aware of the following restrictions:
* - ARP pings are only executed on IPv4 addresses. * <ul>
* - Non system / Java pings are not recommended at all * <li>ARP pings are only executed on IPv4 addresses.
* (not interruptible, useless TCP echo service fall back) * <li>Non system / Java pings are not recommended at all (not interruptible, useless TCP echo service fall back)
* </ul>
* *
* @param waitForDetectionToFinish If you want to synchronously wait for the result, set this to true * @return a {@link CompletableFuture} for obtaining the {@link PresenceDetectionValue}
* @return Return true if a presence detection is performed and false otherwise.
*/ */
public boolean performPresenceDetection(boolean waitForDetectionToFinish) { public CompletableFuture<PresenceDetectionValue> performPresenceDetection() {
if (executorService != null) {
logger.debug(
"There is already an ongoing presence discovery for {} and a new one was issued by the scheduler! TCP Port {}",
hostname, tcpPorts);
return false;
}
if (!cache.isExpired()) {
return false;
}
Set<String> interfaceNames = null; Set<String> interfaceNames = null;
currentCheck = 0;
detectionChecks = tcpPorts.size(); detectionChecks = tcpPorts.size();
if (pingMethod != null) { if (pingMethod != null) {
detectionChecks += 1; detectionChecks += 1;
@ -308,295 +325,240 @@ public class PresenceDetection implements IPRequestReceivedCallback {
detectionChecks += interfaceNames.size(); detectionChecks += interfaceNames.size();
} }
logger.trace("Performing {} presence detection checks for {}", detectionChecks, hostname);
PresenceDetectionValue pdv = new PresenceDetectionValue(hostname, PresenceDetectionValue.UNREACHABLE);
if (detectionChecks == 0) { if (detectionChecks == 0) {
return false; return CompletableFuture.completedFuture(pdv);
} }
final ExecutorService executorService = getThreadsFor(detectionChecks); ExecutorService detectionExecutorService = getThreadsFor(detectionChecks);
this.executorService = executorService; this.detectionExecutorService = detectionExecutorService;
List<CompletableFuture<Void>> completableFutures = new ArrayList<>();
for (Integer tcpPort : tcpPorts) { for (Integer tcpPort : tcpPorts) {
executorService.execute(() -> { completableFutures.add(CompletableFuture.runAsync(() -> {
Thread.currentThread().setName("presenceDetectionTCP_" + hostname + " " + tcpPort); Thread.currentThread().setName("presenceDetectionTCP_" + hostname + " " + tcpPort);
performServicePing(tcpPort); performServicePing(pdv, tcpPort);
checkIfFinished(); }, detectionExecutorService));
});
} }
// ARP ping for IPv4 addresses. Use single executor for Windows tool and // ARP ping for IPv4 addresses. Use single executor for Windows tool and
// each own executor for each network interface for other tools // each own executor for each network interface for other tools
if (arpPingMethod == ArpPingUtilEnum.ELI_FULKERSON_ARP_PING_FOR_WINDOWS) { if (arpPingMethod == ArpPingUtilEnum.ELI_FULKERSON_ARP_PING_FOR_WINDOWS) {
executorService.execute(() -> { completableFutures.add(CompletableFuture.runAsync(() -> {
Thread.currentThread().setName("presenceDetectionARP_" + hostname + " "); Thread.currentThread().setName("presenceDetectionARP_" + hostname + " ");
// arp-ping.exe tool capable of handling multiple interfaces by itself // arp-ping.exe tool capable of handling multiple interfaces by itself
performARPping(""); performArpPing(pdv, "");
checkIfFinished(); }, detectionExecutorService));
});
} else if (interfaceNames != null) { } else if (interfaceNames != null) {
for (final String interfaceName : interfaceNames) { for (final String interfaceName : interfaceNames) {
executorService.execute(() -> { completableFutures.add(CompletableFuture.runAsync(() -> {
Thread.currentThread().setName("presenceDetectionARP_" + hostname + " " + interfaceName); Thread.currentThread().setName("presenceDetectionARP_" + hostname + " " + interfaceName);
performARPping(interfaceName); performArpPing(pdv, interfaceName);
checkIfFinished(); }, detectionExecutorService));
});
} }
} }
// ICMP ping // ICMP ping
if (pingMethod != null) { if (pingMethod != null) {
executorService.execute(() -> { completableFutures.add(CompletableFuture.runAsync(() -> {
if (pingMethod != IpPingMethodEnum.JAVA_PING) { Thread.currentThread().setName("presenceDetectionICMP_" + hostname);
Thread.currentThread().setName("presenceDetectionICMP_" + hostname); if (pingMethod == IpPingMethodEnum.JAVA_PING) {
performSystemPing(); performJavaPing(pdv);
} else { } else {
performJavaPing(); performSystemPing(pdv);
} }
checkIfFinished(); }, detectionExecutorService));
});
} }
if (waitForDetectionToFinish) { return CompletableFuture.supplyAsync(() -> {
waitForPresenceDetection(); logger.debug("Waiting for {} detection futures for {} to complete", completableFutures.size(), hostname);
} completableFutures.forEach(CompletableFuture::join);
logger.debug("All {} detection futures for {} have completed", completableFutures.size(), hostname);
return true; if (!pdv.isReachable()) {
logger.debug("{} is unreachable, invalidating destination value", hostname);
destination.invalidateValue();
}
logger.debug("Sending listener final result: {}", pdv);
updateListener.finalDetectionResult(pdv);
detectionExecutorService.shutdownNow();
this.detectionExecutorService = null;
detectionChecks = 0;
return pdv;
}, scheduledExecutorService);
} }
/** /**
* Calls updateListener.finalDetectionResult() with a final result value. * Creates a new {@link PresenceDetectionValue} when a host is reachable. Also updates the {@link #lastSeen}
* Safe to be called from different threads. After a call to this method, * value and sends a partial detection result to the {@link #updateListener}.
* the presence detection process is finished and all threads are forcefully * <p>
* shut down. * It is safe to call this method from multiple threads.
*/
private synchronized void submitFinalResult() {
// Do nothing if we are not in a detection process
ExecutorService service = executorService;
if (service == null) {
return;
}
// Finish the detection process
service.shutdownNow();
executorService = null;
detectionChecks = 0;
PresenceDetectionValue v;
// The cache will be expired by now if cache_time < timeoutInMS. But the device might be actually reachable.
// Therefore use lastSeenInMS here and not cache.isExpired() to determine if we got a ping response.
if (lastSeenInMS + timeoutInMS + 100 < System.currentTimeMillis()) {
// We haven't seen the device in the detection process
v = new PresenceDetectionValue(hostname, -1);
} else {
// Make the cache valid again and submit the value.
v = cache.getExpiredValue();
}
cache.setValue(v);
if (!v.isReachable()) {
// if target can't be reached, check if name resolution need to be updated
destination.invalidateValue();
}
updateListener.finalDetectionResult(v);
}
/**
* This method is called after each individual check and increases a check counter.
* If the counter equals the total checks,the final result is submitted. This will
* happen way before the "timeoutInMS", if all checks were successful.
* Thread safe.
*/
private synchronized void checkIfFinished() {
currentCheck += 1;
if (currentCheck < detectionChecks) {
return;
}
submitFinalResult();
}
/**
* Waits for the presence detection threads to finish. Returns immediately
* if no presence detection is performed right now.
*/
public void waitForPresenceDetection() {
ExecutorService service = executorService;
if (service == null) {
return;
}
try {
// We may get interrupted here by cancelRefreshJob().
service.awaitTermination(timeoutInMS + 100, TimeUnit.MILLISECONDS);
submitFinalResult();
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // Reset interrupt flag
service.shutdownNow();
executorService = null;
}
}
/**
* If the cached PresenceDetectionValue has not expired yet, the cached version
* is returned otherwise a new reachable PresenceDetectionValue is created with
* a latency of 0.
* *
* It is safe to call this method from multiple threads. The returned PresenceDetectionValue * @param type the detection type
* might be still be altered in other threads though. * @param latency the latency
*
* @param type The detection type
* @return The non expired or a new instance of PresenceDetectionValue.
*/ */
synchronized PresenceDetectionValue updateReachableValue(PresenceDetectionType type, double latency) { synchronized PresenceDetectionValue updateReachable(PresenceDetectionType type, Duration latency) {
lastSeenInMS = System.currentTimeMillis(); PresenceDetectionValue pdv = new PresenceDetectionValue(hostname, latency);
PresenceDetectionValue v; updateReachable(pdv, type, latency);
if (cache.isExpired()) { return pdv;
v = new PresenceDetectionValue(hostname, 0);
} else {
v = cache.getExpiredValue();
}
v.updateLatency(latency);
v.addType(type);
cache.setValue(v);
return v;
} }
protected void performServicePing(int tcpPort) { /**
* Updates the given {@link PresenceDetectionValue} when a host is reachable. Also updates the {@link #lastSeen}
* value and sends a partial detection result to the {@link #updateListener}.
* <p>
* It is safe to call this method from multiple threads.
*
* @param pdv the {@link PresenceDetectionValue} to update
* @param type the detection type
* @param latency the latency
*/
synchronized void updateReachable(PresenceDetectionValue pdv, PresenceDetectionType type, Duration latency) {
updateReachable(pdv, type, latency, -1);
}
synchronized void updateReachable(PresenceDetectionValue pdv, PresenceDetectionType type, Duration latency,
int tcpPort) {
lastSeen = Instant.now();
pdv.addReachableDetectionType(type);
pdv.updateLatency(latency);
if (0 <= tcpPort) {
pdv.addReachableTcpPort(tcpPort);
}
logger.debug("Sending listener partial result: {}", pdv);
updateListener.partialDetectionResult(pdv);
}
protected void performServicePing(PresenceDetectionValue pdv, int tcpPort) {
logger.trace("Perform TCP presence detection for {} on port: {}", hostname, tcpPort); logger.trace("Perform TCP presence detection for {} on port: {}", hostname, tcpPort);
try {
InetAddress destinationAddress = destination.getValue();
if (destinationAddress != null) {
networkUtils.servicePing(destinationAddress.getHostAddress(), tcpPort, timeoutInMS).ifPresent(o -> {
if (o.isSuccess()) {
PresenceDetectionValue v = updateReachableValue(PresenceDetectionType.TCP_CONNECTION,
getLatency(o, preferResponseTimeAsLatency));
v.addReachableTcpService(tcpPort);
updateListener.partialDetectionResult(v);
}
});
}
} catch (IOException e) {
// This should not happen and might be a user configuration issue, we log a warning message therefore.
logger.warn("Could not create a socket connection", e);
}
}
/** withDestinationAddress(destinationAddress -> {
* Performs an "ARP ping" (ARP request) on the given interface. try {
* If it is an iOS device, the {@see NetworkUtils.wakeUpIOS()} method is PingResult pingResult = networkUtils.servicePing(destinationAddress.getHostAddress(), tcpPort, timeout);
* called before performing the ARP ping. if (pingResult.isSuccess()) {
* updateReachable(pdv, TCP_CONNECTION, getLatency(pingResult), tcpPort);
* @param interfaceName The interface name. You can request a list of interface names }
* from {@see NetworkUtils.getInterfaceNames()} for example. } catch (IOException e) {
*/ // This should not happen and might be a user configuration issue, we log a warning message therefore.
protected void performARPping(String interfaceName) { logger.warn("Could not create a socket connection", e);
try {
logger.trace("Perform ARP ping presence detection for {} on interface: {}", hostname, interfaceName);
InetAddress destinationAddress = destination.getValue();
if (destinationAddress == null) {
return;
}
if (iosDevice) {
networkUtils.wakeUpIOS(destinationAddress);
Thread.sleep(50);
}
networkUtils.nativeARPPing(arpPingMethod, arpPingUtilPath, interfaceName,
destinationAddress.getHostAddress(), timeoutInMS).ifPresent(o -> {
if (o.isSuccess()) {
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) {
logger.trace("Failed to execute an arp ping for ip {}", hostname, e);
} catch (InterruptedException ignored) {
// This can be ignored, the thread will end anyway
}
}
/**
* Performs a java ping. It is not recommended to use this, as it is not interruptible,
* and will not work on windows systems reliably and will fall back from ICMP pings to
* the TCP echo service on port 7 which barely no device or server supports nowadays.
* (https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/net/InetAddress.html#isReachable%28int%29)
*/
protected void performJavaPing() {
logger.trace("Perform java ping presence detection for {}", hostname);
InetAddress destinationAddress = destination.getValue();
if (destinationAddress == null) {
return;
}
networkUtils.javaPing(timeoutInMS, destinationAddress).ifPresent(o -> {
if (o.isSuccess()) {
PresenceDetectionValue v = updateReachableValue(PresenceDetectionType.ICMP_PING,
getLatency(o, preferResponseTimeAsLatency));
updateListener.partialDetectionResult(v);
} }
}); });
} }
protected void performSystemPing() { /**
try { * Performs an "ARP ping" (ARP request) on the given interface.
logger.trace("Perform native ping presence detection for {}", hostname); * If it is an iOS device, the {@link NetworkUtils#wakeUpIOS(InetAddress)} method is
InetAddress destinationAddress = destination.getValue(); * called before performing the ARP ping.
if (destinationAddress == null) { *
return; * @param pdv the {@link PresenceDetectionValue} to update
} * @param interfaceName the interface name. You can request a list of interface names
* from {@link NetworkUtils#getInterfaceNames()} for example.
*/
protected void performArpPing(PresenceDetectionValue pdv, String interfaceName) {
logger.trace("Perform ARP ping presence detection for {} on interface: {}", hostname, interfaceName);
networkUtils.nativePing(pingMethod, destinationAddress.getHostAddress(), timeoutInMS).ifPresent(o -> { withDestinationAddress(destinationAddress -> {
if (o.isSuccess()) { try {
PresenceDetectionValue v = updateReachableValue(PresenceDetectionType.ICMP_PING, if (iosDevice) {
getLatency(o, preferResponseTimeAsLatency)); networkUtils.wakeUpIOS(destinationAddress);
updateListener.partialDetectionResult(v); Thread.sleep(50);
} }
});
} catch (IOException e) { PingResult pingResult = networkUtils.nativeArpPing(arpPingMethod, arpPingUtilPath, interfaceName,
logger.trace("Failed to execute a native ping for ip {}", hostname, e); destinationAddress.getHostAddress(), timeout);
} catch (InterruptedException e) { if (pingResult != null) {
// This can be ignored, the thread will end anyway if (pingResult.isSuccess()) {
} updateReachable(pdv, ARP_PING, getLatency(pingResult));
lastReachableNetworkInterfaceName = interfaceName;
} else if (lastReachableNetworkInterfaceName.equals(interfaceName)) {
logger.trace("{} is no longer reachable on network interface: {}", hostname, interfaceName);
lastReachableNetworkInterfaceName = "";
}
}
} catch (IOException e) {
logger.trace("Failed to execute an ARP ping for {}", hostname, e);
} catch (InterruptedException ignored) {
// This can be ignored, the thread will end anyway
}
});
} }
private double getLatency(PingResult pingResult, boolean preferResponseTimeAsLatency) { /**
logger.debug("Getting latency from ping result {} using latency mode {}", pingResult, * Performs a Java ping. It is not recommended to use this, as it is not interruptible,
* and will not work on Windows systems reliably and will fall back from ICMP pings to
* the TCP echo service on port 7 which barely no device or server supports nowadays.
*
* @see InetAddress#isReachable(int)
*/
protected void performJavaPing(PresenceDetectionValue pdv) {
logger.trace("Perform Java ping presence detection for {}", hostname);
withDestinationAddress(destinationAddress -> {
PingResult pingResult = networkUtils.javaPing(timeout, destinationAddress);
if (pingResult.isSuccess()) {
updateReachable(pdv, ICMP_PING, getLatency(pingResult));
}
});
}
protected void performSystemPing(PresenceDetectionValue pdv) {
logger.trace("Perform native ping presence detection for {}", hostname);
withDestinationAddress(destinationAddress -> {
try {
PingResult pingResult = networkUtils.nativePing(pingMethod, destinationAddress.getHostAddress(),
timeout);
if (pingResult != null && pingResult.isSuccess()) {
updateReachable(pdv, ICMP_PING, getLatency(pingResult));
}
} catch (IOException e) {
logger.trace("Failed to execute a native ping for {}", hostname, e);
} catch (InterruptedException e) {
// This can be ignored, the thread will end anyway
}
});
}
private Duration getLatency(PingResult pingResult) {
logger.trace("Getting latency from ping result {} using latency mode {}", pingResult,
preferResponseTimeAsLatency); preferResponseTimeAsLatency);
// Execution time is always set and this value is also the default. So lets use it first. Duration executionTime = pingResult.getExecutionTime();
double latency = pingResult.getExecutionTimeInMS(); Duration responseTime = pingResult.getResponseTime();
return preferResponseTimeAsLatency && responseTime != null ? responseTime : executionTime;
if (preferResponseTimeAsLatency && pingResult.getResponseTimeInMS().isPresent()) {
latency = pingResult.getResponseTimeInMS().get();
}
return latency;
} }
@Override @Override
public void dhcpRequestReceived(String ipAddress) { public void dhcpRequestReceived(String ipAddress) {
PresenceDetectionValue v = updateReachableValue(PresenceDetectionType.DHCP_REQUEST, 0); updateReachable(DHCP_REQUEST, Duration.ZERO);
updateListener.partialDetectionResult(v);
} }
/** /**
* Start/Restart a fixed scheduled runner to update the devices reach-ability state. * Start/Restart a fixed scheduled runner to update the devices reach-ability state.
*
* @param scheduledExecutorService A scheduler to run pings periodically.
*/ */
public void startAutomaticRefresh(ScheduledExecutorService scheduledExecutorService) { public void startAutomaticRefresh() {
ScheduledFuture<?> future = refreshJob; ScheduledFuture<?> future = refreshJob;
if (future != null && !future.isDone()) { if (future != null && !future.isDone()) {
future.cancel(true); future.cancel(true);
} }
refreshJob = scheduledExecutorService.scheduleWithFixedDelay(() -> performPresenceDetection(true), 0, refreshJob = scheduledExecutorService.scheduleWithFixedDelay(() -> {
refreshIntervalInMS, TimeUnit.MILLISECONDS); try {
logger.debug("Refreshing {} reachability state", hostname);
getValue();
} catch (InterruptedException | ExecutionException e) {
logger.debug("Failed to refresh {} presence detection", hostname, e);
}
}, 0, refreshInterval.toMillis(), TimeUnit.MILLISECONDS);
} }
/** /**
* Return true if automatic refreshing is enabled. * Return <code>true</code> if automatic refreshing is enabled.
*/ */
public boolean isAutomaticRefreshing() { public boolean isAutomaticRefreshing() {
return refreshJob != null; return refreshJob != null;
@ -618,17 +580,17 @@ public class PresenceDetection implements IPRequestReceivedCallback {
} }
/** /**
* Enables listing for dhcp packets to figure out if devices have entered the network. This does not work * Enables listening for DHCP packets to figure out if devices have entered the network. This does not work
* for iOS devices. The hostname of this network service object will be registered to the dhcp request packet * for iOS devices. The hostname of this network service object will be registered to the DHCP request packet
* listener if enabled and unregistered otherwise. * listener if enabled and unregistered otherwise.
* *
* @param destinationAddress the InetAddress to listen for. * @param destinationAddress the {@link InetAddress} to listen for.
*/ */
private void enableDHCPListen(InetAddress destinationAddress) { private void enableDHCPListen(InetAddress destinationAddress) {
try { try {
DHCPPacketListenerServer listener = DHCPListenService.register(destinationAddress.getHostAddress(), this); DHCPPacketListenerServer listener = DHCPListenService.register(destinationAddress.getHostAddress(), this);
dhcpState = String.format("Bound to port %d - %s", listener.getCurrentPort(), dhcpState = String.format("Bound to port %d - %s", listener.getCurrentPort(),
(listener.usingPrivilegedPort() ? "Running normally" : "Port forwarding necessary !")); (listener.usingPrivilegedPort() ? "Running normally" : "Port forwarding necessary!"));
} catch (SocketException e) { } catch (SocketException e) {
dhcpState = String.format("Cannot use DHCP sniffing: %s", e.getMessage()); dhcpState = String.format("Cannot use DHCP sniffing: %s", e.getMessage());
logger.warn("{}", dhcpState); logger.warn("{}", dhcpState);

View File

@ -12,6 +12,9 @@
*/ */
package org.openhab.binding.network.internal; package org.openhab.binding.network.internal;
import static org.openhab.binding.network.internal.utils.NetworkUtils.durationToMillis;
import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -27,112 +30,26 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
*/ */
@NonNullByDefault @NonNullByDefault
public class PresenceDetectionValue { public class PresenceDetectionValue {
private double latency;
private boolean detectionIsFinished; public static final Duration UNREACHABLE = Duration.ofMillis(-1);
private final Set<PresenceDetectionType> reachableByType = new TreeSet<>();
private final List<Integer> tcpServiceReachable = new ArrayList<>();
private final String hostAddress; private final String hostAddress;
private Duration latency;
private final Set<PresenceDetectionType> reachableDetectionTypes = new TreeSet<>();
private final List<Integer> reachableTcpPorts = new ArrayList<>();
/** /**
* Returns true if the target is reachable by any means. * Create a new {@link PresenceDetectionValue} with an initial latency.
*/
public boolean isReachable() {
return latency >= 0;
}
/**
* Return the ping latency in ms or -1 if not reachable. Can be 0 if
* no specific latency is known but the device is still reachable.
*/
public double getLowestLatency() {
return latency;
}
/**
* Return a string of comma separated successful presence detection types.
*/
public String getSuccessfulDetectionTypes() {
return reachableByType.stream().map(v -> v.name()).collect(Collectors.joining(", "));
}
/**
* Return the reachable tcp ports of the presence detection value.
* Thread safe.
*/
public List<Integer> getReachableTCPports() {
synchronized (tcpServiceReachable) {
List<Integer> copy = new ArrayList<>();
copy.addAll(tcpServiceReachable);
return copy;
}
}
/**
* Return true if the presence detection is fully completed (no running
* threads anymore).
*/
public boolean isFinished() {
return detectionIsFinished;
}
////// Package private methods //////
/**
* Create a new PresenceDetectionValue with an initial latency.
* *
* @param hostAddress The target IP * @param hostAddress The target IP
* @param latency The ping latency in ms. Can be <0 if the device is not reachable. * @param latency The ping latency. Can be <0 if the device is not reachable.
*/ */
PresenceDetectionValue(String hostAddress, double latency) { PresenceDetectionValue(String hostAddress, Duration latency) {
this.hostAddress = hostAddress; this.hostAddress = hostAddress;
this.latency = latency; this.latency = latency;
} }
/**
* Add a successful PresenceDetectionType.
*
* @param type A PresenceDetectionType.
*/
void addType(PresenceDetectionType type) {
reachableByType.add(type);
}
/**
* Called by {@see PresenceDetection} by all different means of presence detections.
* If the given latency is lower than the already stored one, the stored one will be overwritten.
*
* @param newLatency The new latency.
* @return Returns true if the latency was indeed lower and updated the stored one.
*/
boolean updateLatency(double newLatency) {
if (newLatency < 0) {
throw new IllegalArgumentException(
"Latency must be >=0. Create a new PresenceDetectionValue for a not reachable device!");
}
if (newLatency > 0 && (latency == 0 || newLatency < latency)) {
latency = newLatency;
return true;
}
return false;
}
/**
* Add a reachable tcp port to this presence detection result value object.
* Thread safe.
*/
void addReachableTcpService(int tcpPort) {
synchronized (tcpServiceReachable) {
tcpServiceReachable.add(tcpPort);
}
}
/**
* Mark the result value as final. No modifications should occur after this call.
*/
void setDetectionIsFinished(boolean detectionIsFinished) {
this.detectionIsFinished = detectionIsFinished;
}
/** /**
* Return the host address of the presence detection result object. * Return the host address of the presence detection result object.
*/ */
@ -140,18 +57,97 @@ public class PresenceDetectionValue {
return hostAddress; return hostAddress;
} }
/**
* Return the ping latency, {@value #UNREACHABLE} if not reachable. Can be 0 if
* no specific latency is known but the device is still reachable.
*/
public Duration getLowestLatency() {
return latency;
}
/**
* Returns <code>true</code> if the target is reachable by any means.
*/
public boolean isReachable() {
return !UNREACHABLE.equals(latency);
}
/**
* Called by {@see PresenceDetection} by all different means of presence detections.
* If the given latency is lower than the already stored one, the stored one will be overwritten.
*
* @param newLatency The new latency.
* @return Returns <code>true</code> if the latency was indeed lower and updated the stored one.
* @throws IllegalArgumentException when {@code newLatency} is negative
*/
boolean updateLatency(Duration newLatency) {
if (newLatency.isNegative()) {
throw new IllegalArgumentException(
"Latency must be >=0. Create a new PresenceDetectionValue for a not reachable device!");
} else if (newLatency.isZero()) {
return false;
} else if (!isReachable() || latency.isZero() || newLatency.compareTo(latency) < 0) {
latency = newLatency;
return true;
}
return false;
}
/**
* Add a successful {@link PresenceDetectionType}.
*
* @param type a {@link PresenceDetectionType}.
*/
void addReachableDetectionType(PresenceDetectionType type) {
reachableDetectionTypes.add(type);
}
/** /**
* Return true if the target can be reached by ICMP or ARP pings. * Return true if the target can be reached by ICMP or ARP pings.
*/ */
public boolean isPingReachable() { public boolean isPingReachable() {
return reachableByType.contains(PresenceDetectionType.ARP_PING) return reachableDetectionTypes.contains(PresenceDetectionType.ARP_PING)
|| reachableByType.contains(PresenceDetectionType.ICMP_PING); || reachableDetectionTypes.contains(PresenceDetectionType.ICMP_PING);
} }
/** /**
* Return true if the target provides open TCP ports. * Return true if the target provides open TCP ports.
*/ */
public boolean isTCPServiceReachable() { public boolean isTcpServiceReachable() {
return reachableByType.contains(PresenceDetectionType.TCP_CONNECTION); return reachableDetectionTypes.contains(PresenceDetectionType.TCP_CONNECTION);
}
/**
* Return a string of comma-separated successful presence detection types.
*/
public String getSuccessfulDetectionTypes() {
return reachableDetectionTypes.stream().map(PresenceDetectionType::name).collect(Collectors.joining(", "));
}
/**
* Return the reachable TCP ports of the presence detection value.
* Thread safe.
*/
public List<Integer> getReachableTcpPorts() {
synchronized (reachableTcpPorts) {
return new ArrayList<>(reachableTcpPorts);
}
}
/**
* Add a reachable TCP port to this presence detection result value object.
* Thread safe.
*/
void addReachableTcpPort(int tcpPort) {
synchronized (reachableTcpPorts) {
reachableTcpPorts.add(tcpPort);
}
}
@Override
public String toString() {
return "PresenceDetectionValue [hostAddress=" + hostAddress + ", latency=" + durationToMillis(latency)
+ "ms, reachableDetectionTypes=" + reachableDetectionTypes + ", reachableTcpPorts=" + reachableTcpPorts
+ "]";
} }
} }

View File

@ -34,34 +34,34 @@ import org.slf4j.LoggerFactory;
@NonNullByDefault @NonNullByDefault
public class DHCPListenService { public class DHCPListenService {
static @Nullable DHCPPacketListenerServer instance; static @Nullable DHCPPacketListenerServer instance;
private static Map<String, IPRequestReceivedCallback> registeredListeners = new TreeMap<>(); private static final Map<String, IPRequestReceivedCallback> REGISTERED_LISTENERS = new TreeMap<>();
private static Logger logger = LoggerFactory.getLogger(DHCPListenService.class); private static final Logger LOGGER = LoggerFactory.getLogger(DHCPListenService.class);
public static synchronized DHCPPacketListenerServer register(String hostAddress, public static synchronized DHCPPacketListenerServer register(String hostAddress,
IPRequestReceivedCallback dhcpListener) throws SocketException { IPRequestReceivedCallback dhcpListener) throws SocketException {
DHCPPacketListenerServer instance = DHCPListenService.instance; DHCPPacketListenerServer instance = DHCPListenService.instance;
if (instance == null) { if (instance == null) {
instance = new DHCPPacketListenerServer((String ipAddress) -> { instance = new DHCPPacketListenerServer(ipAddress -> {
IPRequestReceivedCallback listener = registeredListeners.get(ipAddress); IPRequestReceivedCallback listener = REGISTERED_LISTENERS.get(ipAddress);
if (listener != null) { if (listener != null) {
listener.dhcpRequestReceived(ipAddress); listener.dhcpRequestReceived(ipAddress);
} else { } else {
logger.trace("DHCP request for unknown address: {}", ipAddress); LOGGER.trace("DHCP request for unknown address: {}", ipAddress);
} }
}); });
DHCPListenService.instance = instance; DHCPListenService.instance = instance;
instance.start(); instance.start();
} }
synchronized (registeredListeners) { synchronized (REGISTERED_LISTENERS) {
registeredListeners.put(hostAddress, dhcpListener); REGISTERED_LISTENERS.put(hostAddress, dhcpListener);
} }
return instance; return instance;
} }
public static void unregister(String hostAddress) { public static void unregister(String hostAddress) {
synchronized (registeredListeners) { synchronized (REGISTERED_LISTENERS) {
registeredListeners.remove(hostAddress); REGISTERED_LISTENERS.remove(hostAddress);
if (!registeredListeners.isEmpty()) { if (!REGISTERED_LISTENERS.isEmpty()) {
return; return;
} }
} }

View File

@ -18,7 +18,6 @@ import java.net.DatagramSocket;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.net.SocketException; import java.net.SocketException;
import java.net.UnknownHostException;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
@ -64,7 +63,7 @@ public class DHCPPacketListenerServer extends Thread {
} }
protected void receivePacket(DHCPPacket request, @Nullable InetAddress udpRemote) protected void receivePacket(DHCPPacket request, @Nullable InetAddress udpRemote)
throws BadPacketException, UnknownHostException, IOException { throws BadPacketException, IOException {
if (request.getOp() != DHCPPacket.BOOTREQUEST) { if (request.getOp() != DHCPPacket.BOOTREQUEST) {
return; // skipping non BOOTREQUEST message types return; // skipping non BOOTREQUEST message types
} }

View File

@ -13,18 +13,18 @@
package org.openhab.binding.network.internal.discovery; package org.openhab.binding.network.internal.discovery;
import static org.openhab.binding.network.internal.NetworkBindingConstants.*; import static org.openhab.binding.network.internal.NetworkBindingConstants.*;
import static org.openhab.binding.network.internal.utils.NetworkUtils.durationToMillis;
import java.util.Collections; import java.time.Duration;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
@ -56,7 +56,7 @@ import org.slf4j.LoggerFactory;
@NonNullByDefault @NonNullByDefault
@Component(service = DiscoveryService.class, configurationPid = "discovery.network") @Component(service = DiscoveryService.class, configurationPid = "discovery.network")
public class NetworkDiscoveryService extends AbstractDiscoveryService implements PresenceDetectionListener { public class NetworkDiscoveryService extends AbstractDiscoveryService implements PresenceDetectionListener {
static final int PING_TIMEOUT_IN_MS = 500; static final Duration PING_TIMEOUT = Duration.ofMillis(500);
static final int MAXIMUM_IPS_PER_INTERFACE = 255; static final int MAXIMUM_IPS_PER_INTERFACE = 255;
private static final long DISCOVERY_RESULT_TTL = TimeUnit.MINUTES.toSeconds(10); private static final long DISCOVERY_RESULT_TTL = TimeUnit.MINUTES.toSeconds(10);
private final Logger logger = LoggerFactory.getLogger(NetworkDiscoveryService.class); private final Logger logger = LoggerFactory.getLogger(NetworkDiscoveryService.class);
@ -64,16 +64,16 @@ public class NetworkDiscoveryService extends AbstractDiscoveryService implements
// TCP port 548 (Apple Filing Protocol (AFP)) // TCP port 548 (Apple Filing Protocol (AFP))
// TCP port 554 (Windows share / Linux samba) // TCP port 554 (Windows share / Linux samba)
// TCP port 1025 (Xbox / MS-RPC) // TCP port 1025 (Xbox / MS-RPC)
private Set<Integer> tcpServicePorts = Collections private Set<Integer> tcpServicePorts = Set.of(80, 548, 554, 1025);
.unmodifiableSet(Stream.of(80, 548, 554, 1025).collect(Collectors.toSet()));
private AtomicInteger scannedIPcount = new AtomicInteger(0); private AtomicInteger scannedIPcount = new AtomicInteger(0);
private @Nullable ExecutorService executorService = null; private @Nullable ExecutorService executorService = null;
private final NetworkBindingConfiguration configuration = new NetworkBindingConfiguration(); private final NetworkBindingConfiguration configuration = new NetworkBindingConfiguration();
private final NetworkUtils networkUtils = new NetworkUtils(); private final NetworkUtils networkUtils = new NetworkUtils();
public NetworkDiscoveryService() { public NetworkDiscoveryService() {
super(SUPPORTED_THING_TYPES_UIDS, (int) Math.round( super(SUPPORTED_THING_TYPES_UIDS,
new NetworkUtils().getNetworkIPs(MAXIMUM_IPS_PER_INTERFACE).size() * (PING_TIMEOUT_IN_MS / 1000.0)), (int) Math.round(new NetworkUtils().getNetworkIPs(MAXIMUM_IPS_PER_INTERFACE).size()
* (durationToMillis(PING_TIMEOUT) / 1000.0)),
false); false);
} }
@ -108,8 +108,8 @@ public class NetworkDiscoveryService extends AbstractDiscoveryService implements
final String ip = value.getHostAddress(); final String ip = value.getHostAddress();
if (value.isPingReachable()) { if (value.isPingReachable()) {
newPingDevice(ip); newPingDevice(ip);
} else if (value.isTCPServiceReachable()) { } else if (value.isTcpServiceReachable()) {
List<Integer> tcpServices = value.getReachableTCPports(); List<Integer> tcpServices = value.getReachableTcpPorts();
for (int port : tcpServices) { for (int port : tcpServices) {
newServiceDevice(ip, port); newServiceDevice(ip, port);
} }
@ -139,20 +139,24 @@ public class NetworkDiscoveryService extends AbstractDiscoveryService implements
scannedIPcount.set(0); scannedIPcount.set(0);
for (String ip : networkIPs) { for (String ip : networkIPs) {
final PresenceDetection s = new PresenceDetection(this, 2000); final PresenceDetection pd = new PresenceDetection(this, scheduler, Duration.ofSeconds(2));
s.setHostname(ip); pd.setHostname(ip);
s.setIOSDevice(true); pd.setIOSDevice(true);
s.setUseDhcpSniffing(false); pd.setUseDhcpSniffing(false);
s.setTimeout(PING_TIMEOUT_IN_MS); pd.setTimeout(PING_TIMEOUT);
// Ping devices // Ping devices
s.setUseIcmpPing(true); pd.setUseIcmpPing(true);
s.setUseArpPing(true, configuration.arpPingToolPath, configuration.arpPingUtilMethod); pd.setUseArpPing(true, configuration.arpPingToolPath, configuration.arpPingUtilMethod);
// TCP devices // TCP devices
s.setServicePorts(tcpServicePorts); pd.setServicePorts(tcpServicePorts);
service.execute(() -> { service.execute(() -> {
Thread.currentThread().setName("Discovery thread " + ip); Thread.currentThread().setName("Discovery thread " + ip);
s.performPresenceDetection(true); try {
pd.getValue();
} catch (ExecutionException | InterruptedException e) {
stopScan();
}
int count = scannedIPcount.incrementAndGet(); int count = scannedIPcount.incrementAndGet();
if (count == networkIPs.size()) { if (count == networkIPs.size()) {
logger.trace("Scan of {} IPs successful", scannedIPcount); logger.trace("Scan of {} IPs successful", scannedIPcount);
@ -171,7 +175,7 @@ public class NetworkDiscoveryService extends AbstractDiscoveryService implements
} }
try { try {
service.awaitTermination(PING_TIMEOUT_IN_MS, TimeUnit.MILLISECONDS); service.awaitTermination(PING_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS);
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); // Reset interrupt flag Thread.currentThread().interrupt(); // Reset interrupt flag
} }
@ -193,26 +197,16 @@ public class NetworkDiscoveryService extends AbstractDiscoveryService implements
public void newServiceDevice(String ip, int tcpPort) { public void newServiceDevice(String ip, int tcpPort) {
logger.trace("Found reachable service for device with IP address {} on port {}", ip, tcpPort); logger.trace("Found reachable service for device with IP address {} on port {}", ip, tcpPort);
String label;
// TCP port 548 (Apple Filing Protocol (AFP)) // TCP port 548 (Apple Filing Protocol (AFP))
// TCP port 554 (Windows share / Linux samba) // TCP port 554 (Windows share / Linux samba)
// TCP port 1025 (Xbox / MS-RPC) // TCP port 1025 (Xbox / MS-RPC)
switch (tcpPort) { String label = switch (tcpPort) {
case 80: case 80 -> "Device providing a Webserver";
label = "Device providing a Webserver"; case 548 -> "Device providing the Apple AFP Service";
break; case 554 -> "Device providing Network/Samba Shares";
case 548: case 1025 -> "Device providing Xbox/MS-RPC Capability";
label = "Device providing the Apple AFP Service"; default -> "Network Device";
break; };
case 554:
label = "Device providing Network/Samba Shares";
break;
case 1025:
label = "Device providing Xbox/MS-RPC Capability";
break;
default:
label = "Network Device";
}
label += " (" + ip + ":" + tcpPort + ")"; label += " (" + ip + ":" + tcpPort + ")";
Map<String, Object> properties = new HashMap<>(); Map<String, Object> properties = new HashMap<>();
@ -235,8 +229,7 @@ public class NetworkDiscoveryService extends AbstractDiscoveryService implements
public void newPingDevice(String ip) { public void newPingDevice(String ip) {
logger.trace("Found pingable network device with IP address {}", ip); logger.trace("Found pingable network device with IP address {}", ip);
Map<String, Object> properties = new HashMap<>(); Map<String, Object> properties = Map.of(PARAMETER_HOSTNAME, ip);
properties.put(PARAMETER_HOSTNAME, ip);
thingDiscovered(DiscoveryResultBuilder.create(createPingUID(ip)).withTTL(DISCOVERY_RESULT_TTL) thingDiscovered(DiscoveryResultBuilder.create(createPingUID(ip)).withTTL(DISCOVERY_RESULT_TTL)
.withProperties(properties).withLabel("Network Device (" + ip + ")").build()); .withProperties(properties).withLabel("Network Device (" + ip + ")").build());
} }

View File

@ -13,7 +13,9 @@
package org.openhab.binding.network.internal.handler; package org.openhab.binding.network.internal.handler;
import static org.openhab.binding.network.internal.NetworkBindingConstants.*; import static org.openhab.binding.network.internal.NetworkBindingConstants.*;
import static org.openhab.binding.network.internal.utils.NetworkUtils.durationToMillis;
import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.Collection; import java.util.Collection;
@ -33,7 +35,6 @@ import org.openhab.binding.network.internal.PresenceDetectionValue;
import org.openhab.binding.network.internal.WakeOnLanPacketSender; import org.openhab.binding.network.internal.WakeOnLanPacketSender;
import org.openhab.binding.network.internal.action.NetworkActions; import org.openhab.binding.network.internal.action.NetworkActions;
import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.MetricPrefix; import org.openhab.core.library.unit.MetricPrefix;
@ -95,18 +96,16 @@ public class NetworkHandler extends BaseThingHandler
presenceDetection.getValue(value -> updateState(CHANNEL_ONLINE, OnOffType.from(value.isReachable()))); presenceDetection.getValue(value -> updateState(CHANNEL_ONLINE, OnOffType.from(value.isReachable())));
break; break;
case CHANNEL_LATENCY: case CHANNEL_LATENCY:
case CHANNEL_DEPRECATED_TIME:
presenceDetection.getValue(value -> { presenceDetection.getValue(value -> {
updateState(CHANNEL_LATENCY, double latencyMs = durationToMillis(value.getLowestLatency());
new QuantityType<>(value.getLowestLatency(), MetricPrefix.MILLI(Units.SECOND))); updateState(CHANNEL_LATENCY, new QuantityType<>(latencyMs, MetricPrefix.MILLI(Units.SECOND)));
updateState(CHANNEL_DEPRECATED_TIME, new DecimalType(value.getLowestLatency()));
}); });
break; break;
case CHANNEL_LASTSEEN: case CHANNEL_LASTSEEN:
if (presenceDetection.getLastSeen() > 0) { Instant lastSeen = presenceDetection.getLastSeen();
Instant instant = Instant.ofEpochMilli(presenceDetection.getLastSeen()); if (lastSeen != null) {
updateState(CHANNEL_LASTSEEN, new DateTimeType( updateState(CHANNEL_LASTSEEN, new DateTimeType(
ZonedDateTime.ofInstant(instant, TimeZone.getDefault().toZoneId()).withFixedOffsetZone())); ZonedDateTime.ofInstant(lastSeen, TimeZone.getDefault().toZoneId()).withFixedOffsetZone()));
} else { } else {
updateState(CHANNEL_LASTSEEN, UnDefType.UNDEF); updateState(CHANNEL_LASTSEEN, UnDefType.UNDEF);
} }
@ -128,28 +127,29 @@ public class NetworkHandler extends BaseThingHandler
@Override @Override
public void partialDetectionResult(PresenceDetectionValue value) { public void partialDetectionResult(PresenceDetectionValue value) {
double latencyMs = durationToMillis(value.getLowestLatency());
updateState(CHANNEL_ONLINE, OnOffType.ON); updateState(CHANNEL_ONLINE, OnOffType.ON);
updateState(CHANNEL_LATENCY, new QuantityType<>(value.getLowestLatency(), MetricPrefix.MILLI(Units.SECOND))); updateState(CHANNEL_LATENCY, new QuantityType<>(latencyMs, MetricPrefix.MILLI(Units.SECOND)));
updateState(CHANNEL_DEPRECATED_TIME, new DecimalType(value.getLowestLatency()));
} }
@Override @Override
public void finalDetectionResult(PresenceDetectionValue value) { public void finalDetectionResult(PresenceDetectionValue value) {
// We do not notify the framework immediately if a device presence detection failed and // We do not notify the framework immediately if a device presence detection failed and
// the user configured retries to be > 1. // the user configured retries to be > 1.
retryCounter = !value.isReachable() ? retryCounter + 1 : 0; retryCounter = value.isReachable() ? 0 : retryCounter + 1;
if (retryCounter >= this.retries) { if (retryCounter >= retries) {
updateState(CHANNEL_ONLINE, OnOffType.OFF); updateState(CHANNEL_ONLINE, OnOffType.OFF);
updateState(CHANNEL_LATENCY, UnDefType.UNDEF); updateState(CHANNEL_LATENCY, UnDefType.UNDEF);
updateState(CHANNEL_DEPRECATED_TIME, UnDefType.UNDEF);
retryCounter = 0; retryCounter = 0;
} }
if (value.isReachable()) { Instant lastSeen = presenceDetection.getLastSeen();
Instant instant = Instant.ofEpochMilli(presenceDetection.getLastSeen()); if (value.isReachable() && lastSeen != null) {
updateState(CHANNEL_LASTSEEN, new DateTimeType( updateState(CHANNEL_LASTSEEN, new DateTimeType(
ZonedDateTime.ofInstant(instant, TimeZone.getDefault().toZoneId()).withFixedOffsetZone())); ZonedDateTime.ofInstant(lastSeen, TimeZone.getDefault().toZoneId()).withFixedOffsetZone()));
} else if (!value.isReachable() && lastSeen == null) {
updateState(CHANNEL_LASTSEEN, UnDefType.UNDEF);
} }
updateNetworkProperties(); updateNetworkProperties();
@ -196,14 +196,14 @@ public class NetworkHandler extends BaseThingHandler
} }
this.retries = handlerConfiguration.retry.intValue(); this.retries = handlerConfiguration.retry.intValue();
presenceDetection.setRefreshInterval(handlerConfiguration.refreshInterval.longValue()); presenceDetection.setRefreshInterval(Duration.ofMillis(handlerConfiguration.refreshInterval));
presenceDetection.setTimeout(handlerConfiguration.timeout.intValue()); presenceDetection.setTimeout(Duration.ofMillis(handlerConfiguration.timeout));
wakeOnLanPacketSender = new WakeOnLanPacketSender(handlerConfiguration.macAddress, wakeOnLanPacketSender = new WakeOnLanPacketSender(handlerConfiguration.macAddress,
handlerConfiguration.hostname, handlerConfiguration.port, handlerConfiguration.networkInterfaceNames); handlerConfiguration.hostname, handlerConfiguration.port, handlerConfiguration.networkInterfaceNames);
updateStatus(ThingStatus.ONLINE); updateStatus(ThingStatus.ONLINE);
presenceDetection.startAutomaticRefresh(scheduler); presenceDetection.startAutomaticRefresh();
updateNetworkProperties(); updateNetworkProperties();
} }
@ -222,7 +222,8 @@ public class NetworkHandler extends BaseThingHandler
// Create a new network service and apply all configurations. // Create a new network service and apply all configurations.
@Override @Override
public void initialize() { public void initialize() {
initialize(new PresenceDetection(this, configuration.cacheDeviceStateTimeInMS.intValue())); initialize(new PresenceDetection(this, scheduler,
Duration.ofMillis(configuration.cacheDeviceStateTimeInMS.intValue())));
} }
/** /**

View File

@ -131,7 +131,6 @@ public class SpeedTestHandler extends BaseThingHandler implements ISpeedTestList
if (SpeedTestError.UNSUPPORTED_PROTOCOL.equals(testError) || SpeedTestError.MALFORMED_URI.equals(testError)) { if (SpeedTestError.UNSUPPORTED_PROTOCOL.equals(testError) || SpeedTestError.MALFORMED_URI.equals(testError)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, errorMessage); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, errorMessage);
freeRefreshTask(); freeRefreshTask();
return;
} else if (SpeedTestError.SOCKET_TIMEOUT.equals(testError)) { } else if (SpeedTestError.SOCKET_TIMEOUT.equals(testError)) {
timeouts--; timeouts--;
if (timeouts <= 0) { if (timeouts <= 0) {
@ -141,12 +140,10 @@ public class SpeedTestHandler extends BaseThingHandler implements ISpeedTestList
logger.warn("Speedtest timed out, {} attempts left. Message '{}'", timeouts, errorMessage); logger.warn("Speedtest timed out, {} attempts left. Message '{}'", timeouts, errorMessage);
stopSpeedTest(); stopSpeedTest();
} }
return;
} else if (SpeedTestError.SOCKET_ERROR.equals(testError) } else if (SpeedTestError.SOCKET_ERROR.equals(testError)
|| SpeedTestError.INVALID_HTTP_RESPONSE.equals(testError)) { || SpeedTestError.INVALID_HTTP_RESPONSE.equals(testError)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, errorMessage); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, errorMessage);
freeRefreshTask(); freeRefreshTask();
return;
} else { } else {
stopSpeedTest(); stopSpeedTest();
logger.warn("Speedtest failed: {}", errorMessage); logger.warn("Speedtest failed: {}", errorMessage);

View File

@ -1,143 +0,0 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.network.internal.toberemoved.cache;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* Complementary class to {@link org.openhab.core.cache.ExpiringCache}, implementing an async variant
* of an expiring cache. Returns the cached value immediately to the callback if not expired yet, otherwise issue
* a fetch and notify callback implementors asynchronously.
*
* @author David Graeff - Initial contribution
*
* @param <V> the type of the cached value
*/
@NonNullByDefault
public class ExpiringCacheAsync<V> {
private final long expiry;
private ExpiringCacheUpdate cacheUpdater;
long expiresAt = 0;
private boolean refreshRequested = false;
private V value;
private final List<Consumer<V>> waitingCacheCallbacks = new LinkedList<>();
/**
* Implement the requestCacheUpdate method which will be called when the cache
* needs an updated value. Call {@see setValue} to update the cached value.
*/
public static interface ExpiringCacheUpdate {
void requestCacheUpdate();
}
/**
* Create a new instance.
*
* @param expiry the duration in milliseconds for how long the value stays valid. Must be greater than 0.
* @param cacheUpdater The cache will use this callback if a new value is needed. Must not be null.
* @throws IllegalArgumentException For an expire value {@literal <=0} or a null cacheUpdater.
*/
public ExpiringCacheAsync(long expiry, @Nullable ExpiringCacheUpdate cacheUpdater) throws IllegalArgumentException {
if (expiry <= 0) {
throw new IllegalArgumentException("Cache expire time must be greater than 0");
}
if (cacheUpdater == null) {
throw new IllegalArgumentException("A cache updater is necessary");
}
this.expiry = TimeUnit.MILLISECONDS.toNanos(expiry);
this.cacheUpdater = cacheUpdater;
}
/**
* Returns the value - possibly from the cache, if it is still valid.
*
* @param callback callback to return the value
*/
public void getValue(Consumer<V> callback) {
if (isExpired()) {
refreshValue(callback);
} else {
callback.accept(value);
}
}
/**
* Invalidates the value in the cache.
*/
public void invalidateValue() {
expiresAt = 0;
}
/**
* Updates the cached value with the given one.
*
* @param newValue The new value. All listeners, registered by getValueAsync() and refreshValue(), will be notified
* of the new value.
*/
public void setValue(V newValue) {
refreshRequested = false;
value = newValue;
expiresAt = getCurrentNanoTime() + expiry;
// Inform all callback handlers of the new value and clear the list
for (Consumer<V> callback : waitingCacheCallbacks) {
callback.accept(value);
}
waitingCacheCallbacks.clear();
}
/**
* Returns an arbitrary time reference in nanoseconds.
* This is used for the cache to determine if a value has expired.
*/
public long getCurrentNanoTime() {
return System.nanoTime();
}
/**
* Refreshes and returns the value asynchronously.
*
* @return the new value
*/
private void refreshValue(Consumer<V> callback) {
waitingCacheCallbacks.add(callback);
if (refreshRequested) {
return;
}
refreshRequested = true;
expiresAt = 0;
cacheUpdater.requestCacheUpdate();
}
/**
* Checks if the value is expired.
*
* @return true if the value is expired
*/
public boolean isExpired() {
return expiresAt < getCurrentNanoTime();
}
/**
* Return the raw value, no matter if it is already
* expired or still valid.
*/
public V getExpiredValue() {
return value;
}
}

View File

@ -12,11 +12,14 @@
*/ */
package org.openhab.binding.network.internal.utils; package org.openhab.binding.network.internal.utils;
import java.util.Optional; import static org.openhab.binding.network.internal.utils.NetworkUtils.millisToDuration;
import java.time.Duration;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -44,18 +47,18 @@ public class LatencyParser {
* Examine a single ping command output line and try to extract the latency value if it is contained. * Examine a single ping command output line and try to extract the latency value if it is contained.
* *
* @param inputLine Single output line of the ping command. * @param inputLine Single output line of the ping command.
* @return Latency value provided by the ping command. Optional is empty if the provided line did not contain a * @return Latency value provided by the ping command. <code>null</code> if the provided line did not contain a
* latency value which matches the known patterns. * latency value which matches the known patterns.
*/ */
public Optional<Double> parseLatency(String inputLine) { public @Nullable Duration parseLatency(String inputLine) {
logger.debug("Parsing latency from input {}", inputLine); logger.debug("Parsing latency from input {}", inputLine);
Matcher m = LATENCY_PATTERN.matcher(inputLine); Matcher m = LATENCY_PATTERN.matcher(inputLine);
if (m.find() && m.groupCount() == 1) { if (m.find() && m.groupCount() == 1) {
return Optional.of(Double.parseDouble(m.group(1))); return millisToDuration(Double.parseDouble(m.group(1)));
} }
logger.debug("Did not find a latency value"); logger.debug("Did not find a latency value");
return Optional.empty(); return null;
} }
} }

View File

@ -25,17 +25,16 @@ import java.net.NetworkInterface;
import java.net.NoRouteToHostException; import java.net.NoRouteToHostException;
import java.net.PortUnreachableException; import java.net.PortUnreachableException;
import java.net.Socket; import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketException; import java.net.SocketException;
import java.net.SocketTimeoutException; import java.net.SocketTimeoutException;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.time.Duration; import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -54,6 +53,37 @@ import org.slf4j.LoggerFactory;
*/ */
@NonNullByDefault @NonNullByDefault
public class NetworkUtils { public class NetworkUtils {
/**
* Nanos per millisecond.
*/
private static final long NANOS_PER_MILLI = 1000_000L;
/**
* Converts a {@link Duration} to milliseconds.
* <p>
* The result has a greater than millisecond precision compared to {@link Duration#toMillis()} which drops excess
* precision information.
*
* @param duration the {@link Duration} to be converted
* @return the equivalent milliseconds of the given {@link Duration}
*/
public static double durationToMillis(Duration duration) {
return (double) duration.toNanos() / NANOS_PER_MILLI;
}
/**
* Converts a double representing milliseconds to a {@link Duration} instance.
* <p>
* The result has a greater than millisecond precision compared to {@link Duration#ofMillis(long)}.
*
* @param millis the milliseconds to be converted
* @return a {@link Duration} instance representing the given milliseconds
*/
public static Duration millisToDuration(double millis) {
return Duration.ofNanos((long) (millis * NANOS_PER_MILLI));
}
private final Logger logger = LoggerFactory.getLogger(NetworkUtils.class); private final Logger logger = LoggerFactory.getLogger(NetworkUtils.class);
private LatencyParser latencyParser = new LatencyParser(); private LatencyParser latencyParser = new LatencyParser();
@ -93,7 +123,7 @@ public class NetworkUtils {
result.add(InetAddress.getByAddress(segments).getHostAddress()); result.add(InetAddress.getByAddress(segments).getHostAddress());
} }
} catch (UnknownHostException e) { } catch (UnknownHostException e) {
logger.debug("Could not build net ip address.", e); logger.trace("Could not build net IP address.", e);
} }
return result; return result;
} }
@ -107,15 +137,14 @@ public class NetworkUtils {
Set<String> result = new HashSet<>(); Set<String> result = new HashSet<>();
try { try {
// For each interface ...
for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements();) { for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements();) {
NetworkInterface networkInterface = en.nextElement(); NetworkInterface networkInterface = en.nextElement();
if (!networkInterface.isLoopback()) { if (!networkInterface.isLoopback()) {
result.add(networkInterface.getName()); result.add(networkInterface.getName());
} }
} }
} catch (SocketException ignored) { } catch (SocketException e) {
// If we are not allowed to enumerate, we return an empty result set. logger.trace("Could not get network interfaces", e);
} }
return result; return result;
@ -139,7 +168,7 @@ public class NetworkUtils {
* @return Every single IP which can be assigned on the Networks the computer is connected to * @return Every single IP which can be assigned on the Networks the computer is connected to
*/ */
private Set<String> getNetworkIPs(Set<CidrAddress> interfaceIPs, int maximumPerInterface) { private Set<String> getNetworkIPs(Set<CidrAddress> interfaceIPs, int maximumPerInterface) {
LinkedHashSet<String> networkIPs = new LinkedHashSet<>(); Set<String> networkIPs = new LinkedHashSet<>();
short minCidrPrefixLength = 8; // historic Class A network, addresses = 16777214 short minCidrPrefixLength = 8; // historic Class A network, addresses = 16777214
if (maximumPerInterface != 0) { if (maximumPerInterface != 0) {
@ -176,25 +205,24 @@ public class NetworkUtils {
} }
/** /**
* Try to establish a tcp connection to the given port. Returns false if a timeout occurred * Try to establish a TCP connection to the given port.
* or the connection was denied.
* *
* @param host The IP or hostname * @param host the IP or hostname
* @param port The tcp port. Must be not 0. * @param port the TCP port. Must be not 0.
* @param timeout Timeout in ms * @param timeout the timeout before the call aborts
* @return Ping result information. Optional is empty if ping command was not executed. * @return the {@link PingResult} of connecting to the given port
* @throws IOException * @throws IOException if an error occurs during the connection
*/ */
public Optional<PingResult> servicePing(String host, int port, int timeout) throws IOException { public PingResult servicePing(String host, int port, Duration timeout) throws IOException {
double execStartTimeInMS = System.currentTimeMillis(); Instant execStartTime = Instant.now();
boolean success = false;
SocketAddress socketAddress = new InetSocketAddress(host, port);
try (Socket socket = new Socket()) { try (Socket socket = new Socket()) {
socket.connect(socketAddress, timeout); socket.connect(new InetSocketAddress(host, port), (int) timeout.toMillis());
return Optional.of(new PingResult(true, System.currentTimeMillis() - execStartTimeInMS)); success = true;
} catch (ConnectException | SocketTimeoutException | NoRouteToHostException ignored) { } catch (ConnectException | SocketTimeoutException | NoRouteToHostException e) {
return Optional.of(new PingResult(false, System.currentTimeMillis() - execStartTimeInMS)); logger.trace("Could not connect to {}:{}", host, port, e);
} }
return new PingResult(success, Duration.between(execStartTime, Instant.now()));
} }
/** /**
@ -221,11 +249,12 @@ public class NetworkUtils {
} }
try { try {
Optional<PingResult> pingResult = nativePing(method, "127.0.0.1", 1000); PingResult pingResult = nativePing(method, "127.0.0.1", Duration.ofSeconds(1));
if (pingResult.isPresent() && pingResult.get().isSuccess()) { if (pingResult != null && pingResult.isSuccess()) {
return method; return method;
} }
} catch (IOException ignored) { } catch (IOException e) {
logger.trace("Native ping to 127.0.0.1 failed", e);
} catch (InterruptedException e) { } catch (InterruptedException e) {
Thread.currentThread().interrupt(); // Reset interrupt flag Thread.currentThread().interrupt(); // Reset interrupt flag
} }
@ -233,11 +262,12 @@ public class NetworkUtils {
} }
/** /**
* Return true if the external arp ping utility (arping) is available and executable on the given path. * Return true if the external ARP ping utility (arping) is available and executable on the given path.
*/ */
public ArpPingUtilEnum determineNativeARPpingMethod(String arpToolPath) { public ArpPingUtilEnum determineNativeArpPingMethod(String arpToolPath) {
String result = ExecUtil.executeCommandLineAndWaitResponse(Duration.ofMillis(100), arpToolPath, "--help"); String result = ExecUtil.executeCommandLineAndWaitResponse(Duration.ofMillis(100), arpToolPath, "--help");
if (result == null || result.isBlank()) { if (result == null || result.isBlank()) {
logger.trace("The command did not return a response due to an error or timeout");
return ArpPingUtilEnum.DISABLED_UNKNOWN_TOOL; return ArpPingUtilEnum.DISABLED_UNKNOWN_TOOL;
} else if (result.contains("Thomas Habets")) { } else if (result.contains("Thomas Habets")) {
if (result.matches("(?s)(.*)w sec Specify a timeout(.*)")) { if (result.matches("(?s)(.*)w sec Specify a timeout(.*)")) {
@ -249,8 +279,10 @@ public class NetworkUtils {
return ArpPingUtilEnum.IPUTILS_ARPING; return ArpPingUtilEnum.IPUTILS_ARPING;
} else if (result.contains("Usage: arp-ping.exe")) { } else if (result.contains("Usage: arp-ping.exe")) {
return ArpPingUtilEnum.ELI_FULKERSON_ARP_PING_FOR_WINDOWS; return ArpPingUtilEnum.ELI_FULKERSON_ARP_PING_FOR_WINDOWS;
} else {
logger.trace("The command output did not match any known output");
return ArpPingUtilEnum.DISABLED_UNKNOWN_TOOL;
} }
return ArpPingUtilEnum.DISABLED_UNKNOWN_TOOL;
} }
public enum IpPingMethodEnum { public enum IpPingMethodEnum {
@ -264,35 +296,36 @@ public class NetworkUtils {
* Use the native ping utility of the operating system to detect device presence. * Use the native ping utility of the operating system to detect device presence.
* *
* @param hostname The DNS name, IPv4 or IPv6 address. Must not be null. * @param hostname The DNS name, IPv4 or IPv6 address. Must not be null.
* @param timeoutInMS Timeout in milliseconds. Be aware that DNS resolution is not part of this timeout. * @param timeout the timeout before the call aborts. Be aware that DNS resolution is not part of this timeout.
* @return Ping result information. Optional is empty if ping command was not executed. * @return Ping result information. <code>null</code> if ping command was not executed.
* @throws IOException The ping command could probably not be found * @throws IOException The ping command could probably not be found
*/ */
public Optional<PingResult> nativePing(@Nullable IpPingMethodEnum method, String hostname, int timeoutInMS) public @Nullable PingResult nativePing(@Nullable IpPingMethodEnum method, String hostname, Duration timeout)
throws IOException, InterruptedException { throws IOException, InterruptedException {
double execStartTimeInMS = System.currentTimeMillis(); Instant execStartTime = Instant.now();
Process proc; Process proc;
if (method == null) { if (method == null) {
return Optional.empty(); return null;
} }
// Yes, all supported operating systems have their own ping utility with a different command line // Yes, all supported operating systems have their own ping utility with a different command line
switch (method) { switch (method) {
case IPUTILS_LINUX_PING: case IPUTILS_LINUX_PING:
proc = new ProcessBuilder("ping", "-w", String.valueOf(timeoutInMS / 1000), "-c", "1", hostname) proc = new ProcessBuilder("ping", "-w", String.valueOf(timeout.toSeconds()), "-c", "1", hostname)
.start(); .start();
break; break;
case MAC_OS_PING: case MAC_OS_PING:
proc = new ProcessBuilder("ping", "-t", String.valueOf(timeoutInMS / 1000), "-c", "1", hostname) proc = new ProcessBuilder("ping", "-t", String.valueOf(timeout.toSeconds()), "-c", "1", hostname)
.start(); .start();
break; break;
case WINDOWS_PING: case WINDOWS_PING:
proc = new ProcessBuilder("ping", "-w", String.valueOf(timeoutInMS), "-n", "1", hostname).start(); proc = new ProcessBuilder("ping", "-w", String.valueOf(timeout.toMillis()), "-n", "1", hostname)
.start();
break; break;
case JAVA_PING: case JAVA_PING:
default: default:
// We cannot estimate the command line for any other operating system and just return false // We cannot estimate the command line for any other operating system and just return null
return Optional.empty(); return null;
} }
// The return code is 0 for a successful ping, 1 if device didn't // The return code is 0 for a successful ping, 1 if device didn't
@ -303,7 +336,7 @@ public class NetworkUtils {
int result = proc.waitFor(); int result = proc.waitFor();
if (result != 0) { if (result != 0) {
return Optional.of(new PingResult(false, System.currentTimeMillis() - execStartTimeInMS)); return new PingResult(false, Duration.between(execStartTime, Instant.now()));
} }
try (BufferedReader r = new BufferedReader(new InputStreamReader(proc.getInputStream()))) { try (BufferedReader r = new BufferedReader(new InputStreamReader(proc.getInputStream()))) {
@ -315,14 +348,14 @@ public class NetworkUtils {
// Because of the Windows issue, we need to check this. We assume that the ping was successful whenever // Because of the Windows issue, we need to check this. We assume that the ping was successful whenever
// this specific string is contained in the output // this specific string is contained in the output
if (line.contains("TTL=") || line.contains("ttl=")) { if (line.contains("TTL=") || line.contains("ttl=")) {
PingResult pingResult = new PingResult(true, System.currentTimeMillis() - execStartTimeInMS); PingResult pingResult = new PingResult(true, Duration.between(execStartTime, Instant.now()));
latencyParser.parseLatency(line).ifPresent(pingResult::setResponseTimeInMS); pingResult.setResponseTime(latencyParser.parseLatency(line));
return Optional.of(pingResult); return pingResult;
} }
line = r.readLine(); line = r.readLine();
} while (line != null); } while (line != null);
return Optional.of(new PingResult(false, System.currentTimeMillis() - execStartTimeInMS)); return new PingResult(false, Duration.between(execStartTime, Instant.now()));
} }
} }
@ -346,76 +379,78 @@ public class NetworkUtils {
/** /**
* Execute the arping tool to perform an ARP ping (only for IPv4 addresses). * Execute the arping tool to perform an ARP ping (only for IPv4 addresses).
* There exist two different arping utils with the same name unfortunatelly. * There exist two different arping utils with the same name unfortunately.
* * iputils arping which is sometimes preinstalled on fedora/ubuntu and the * <ul>
* * https://github.com/ThomasHabets/arping which also works on Windows and MacOS. * <li>iputils arping which is sometimes preinstalled on Fedora/Ubuntu and the
* <li>https://github.com/ThomasHabets/arping which also works on Windows and macOS.
* </ul>
* *
* @param arpUtilPath The arping absolute path including filename. Example: "arping" or "/usr/bin/arping" or * @param arpUtilPath The arping absolute path including filename. Example: "arping" or "/usr/bin/arping" or
* "C:\something\arping.exe" or "arp-ping.exe" * "C:\something\arping.exe" or "arp-ping.exe"
* @param interfaceName An interface name, on linux for example "wlp58s0", shown by ifconfig. Must not be null. * @param interfaceName An interface name, on linux for example "wlp58s0", shown by ifconfig. Must not be null.
* @param ipV4address The ipV4 address. Must not be null. * @param ipV4address The ipV4 address. Must not be null.
* @param timeoutInMS A timeout in milliseconds * @param timeout the timeout before the call aborts
* @return Ping result information. Optional is empty if ping command was not executed. * @return Ping result information. <code>null</code> if ping command was not executed.
* @throws IOException The ping command could probably not be found * @throws IOException The ping command could probably not be found
*/ */
public Optional<PingResult> nativeARPPing(@Nullable ArpPingUtilEnum arpingTool, @Nullable String arpUtilPath, public @Nullable PingResult nativeArpPing(@Nullable ArpPingUtilEnum arpingTool, @Nullable String arpUtilPath,
String interfaceName, String ipV4address, int timeoutInMS) throws IOException, InterruptedException { String interfaceName, String ipV4address, Duration timeout) throws IOException, InterruptedException {
double execStartTimeInMS = System.currentTimeMillis();
if (arpUtilPath == null || arpingTool == null || !arpingTool.canProceed) { if (arpUtilPath == null || arpingTool == null || !arpingTool.canProceed) {
return Optional.empty(); return null;
} }
Instant execStartTime = Instant.now();
Process proc; Process proc;
if (arpingTool == ArpPingUtilEnum.THOMAS_HABERT_ARPING_WITHOUT_TIMEOUT) { if (arpingTool == ArpPingUtilEnum.THOMAS_HABERT_ARPING_WITHOUT_TIMEOUT) {
proc = new ProcessBuilder(arpUtilPath, "-c", "1", "-i", interfaceName, ipV4address).start(); proc = new ProcessBuilder(arpUtilPath, "-c", "1", "-i", interfaceName, ipV4address).start();
} else if (arpingTool == ArpPingUtilEnum.THOMAS_HABERT_ARPING) { } else if (arpingTool == ArpPingUtilEnum.THOMAS_HABERT_ARPING) {
proc = new ProcessBuilder(arpUtilPath, "-w", String.valueOf(timeoutInMS / 1000), "-C", "1", "-i", proc = new ProcessBuilder(arpUtilPath, "-w", String.valueOf(timeout.toSeconds()), "-C", "1", "-i",
interfaceName, ipV4address).start(); interfaceName, ipV4address).start();
} else if (arpingTool == ArpPingUtilEnum.ELI_FULKERSON_ARP_PING_FOR_WINDOWS) { } else if (arpingTool == ArpPingUtilEnum.ELI_FULKERSON_ARP_PING_FOR_WINDOWS) {
proc = new ProcessBuilder(arpUtilPath, "-w", String.valueOf(timeoutInMS), "-x", ipV4address).start(); proc = new ProcessBuilder(arpUtilPath, "-w", String.valueOf(timeout.toMillis()), "-x", ipV4address).start();
} else { } else {
proc = new ProcessBuilder(arpUtilPath, "-w", String.valueOf(timeoutInMS / 1000), "-c", "1", "-I", proc = new ProcessBuilder(arpUtilPath, "-w", String.valueOf(timeout.toSeconds()), "-c", "1", "-I",
interfaceName, ipV4address).start(); interfaceName, ipV4address).start();
} }
// The return code is 0 for a successful ping. 1 if device didn't respond and 2 if there is another error like // The return code is 0 for a successful ping. 1 if device didn't respond and 2 if there is another error like
// network interface not ready. // network interface not ready.
return Optional.of(new PingResult(proc.waitFor() == 0, System.currentTimeMillis() - execStartTimeInMS)); return new PingResult(proc.waitFor() == 0, Duration.between(execStartTime, Instant.now()));
} }
/** /**
* Execute a Java ping. * Execute a Java ping.
* *
* @param timeoutInMS A timeout in milliseconds * @param timeout the timeout before the call aborts
* @param destinationAddress The address to check * @param destinationAddress The address to check
* @return Ping result information. Optional is empty if ping command was not executed. * @return Ping result information
*/ */
public Optional<PingResult> javaPing(int timeoutInMS, InetAddress destinationAddress) { public PingResult javaPing(Duration timeout, InetAddress destinationAddress) {
double execStartTimeInMS = System.currentTimeMillis(); Instant execStartTime = Instant.now();
boolean success = false;
try { try {
if (destinationAddress.isReachable(timeoutInMS)) { if (destinationAddress.isReachable((int) timeout.toMillis())) {
return Optional.of(new PingResult(true, System.currentTimeMillis() - execStartTimeInMS)); success = true;
} else {
return Optional.of(new PingResult(false, System.currentTimeMillis() - execStartTimeInMS));
} }
} catch (IOException e) { } catch (IOException e) {
return Optional.of(new PingResult(false, System.currentTimeMillis() - execStartTimeInMS)); logger.trace("Could not connect to {}", destinationAddress, e);
} }
return new PingResult(success, Duration.between(execStartTime, Instant.now()));
} }
/** /**
* iOS devices are in a deep sleep mode, where they only listen to UDP traffic on port 5353 (Bonjour service * iOS devices are in a deep sleep mode, where they only listen to UDP traffic on port 5353 (Bonjour service
* discovery). A packet on port 5353 will wake up the network stack to respond to ARP pings at least. * discovery). A packet on port 5353 will wake up the network stack to respond to ARP pings at least.
* *
* @throws IOException * @throws IOException if an error occurs during the connection
*/ */
public void wakeUpIOS(InetAddress address) throws IOException { public void wakeUpIOS(InetAddress address) throws IOException {
int port = 5353;
try (DatagramSocket s = new DatagramSocket()) { try (DatagramSocket s = new DatagramSocket()) {
byte[] buffer = new byte[0]; byte[] buffer = new byte[0];
s.send(new DatagramPacket(buffer, buffer.length, address, 5353)); s.send(new DatagramPacket(buffer, buffer.length, address, port));
} catch (PortUnreachableException ignored) { logger.trace("Sent packet to {}:{} to wake up iOS device", address, port);
// We ignore the port unreachable error } catch (PortUnreachableException e) {
logger.trace("Unable to send packet to wake up iOS device at {}:{}", address, port, e);
} }
} }
} }

View File

@ -12,9 +12,12 @@
*/ */
package org.openhab.binding.network.internal.utils; package org.openhab.binding.network.internal.utils;
import java.util.Optional; import static org.openhab.binding.network.internal.utils.NetworkUtils.durationToMillis;
import java.time.Duration;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/** /**
* Information about the ping result. * Information about the ping result.
@ -25,16 +28,16 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
public class PingResult { public class PingResult {
private boolean success; private boolean success;
private Optional<Double> responseTimeInMS = Optional.empty(); private @Nullable Duration responseTime;
private double executionTimeInMS; private Duration executionTime;
/** /**
* @param success <code>true</code> if the device was reachable, <code>false</code> if not. * @param success <code>true</code> if the device was reachable, <code>false</code> if not.
* @param executionTimeInMS Execution time of the ping command in ms. * @param executionTime execution time of the ping command.
*/ */
public PingResult(boolean success, double executionTimeInMS) { public PingResult(boolean success, Duration executionTime) {
this.success = success; this.success = success;
this.executionTimeInMS = executionTimeInMS; this.executionTime = executionTime;
} }
/** /**
@ -45,30 +48,32 @@ public class PingResult {
} }
/** /**
* @return Response time in ms which was returned by the ping command. Optional is empty if response time provided * @return response time which was returned by the ping command. <code>null</code> if response time provided
* by ping command is not available. * by ping command is not available.
*/ */
public Optional<Double> getResponseTimeInMS() { public @Nullable Duration getResponseTime() {
return responseTimeInMS; return responseTime;
} }
/** /**
* @param responseTimeInMS Response time in ms which was returned by the ping command. * @param responseTime the response time which was returned by the ping command.
*/ */
public void setResponseTimeInMS(double responseTimeInMS) { public void setResponseTime(@Nullable Duration responseTime) {
this.responseTimeInMS = Optional.of(responseTimeInMS); this.responseTime = responseTime;
} }
@Override @Override
public String toString() { public String toString() {
return "PingResult{" + "success=" + success + ", responseTimeInMS=" + responseTimeInMS + ", executionTimeInMS=" Duration responseTime = this.responseTime;
+ executionTimeInMS + '}'; String rt = responseTime == null ? "null" : durationToMillis(responseTime) + "ms";
String et = durationToMillis(executionTime) + "ms";
return "PingResult{success=" + success + ", responseTime=" + rt + ", executionTime=" + et + "}";
} }
/** /**
* @return Execution time of the ping command in ms. * @return Execution time of the ping command.
*/ */
public double getExecutionTimeInMS() { public Duration getExecutionTime() {
return executionTimeInMS; return executionTime;
} }
} }

View File

@ -19,14 +19,13 @@ import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
import java.io.IOException; import java.io.IOException;
import java.net.UnknownHostException; import java.time.Duration;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Consumer; import java.util.function.Consumer;
import org.junit.jupiter.api.AfterEach; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
@ -35,8 +34,6 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness; import org.mockito.quality.Strictness;
import org.openhab.binding.network.internal.toberemoved.cache.ExpiringCacheAsync;
import org.openhab.binding.network.internal.toberemoved.cache.ExpiringCacheHelper;
import org.openhab.binding.network.internal.utils.NetworkUtils; import org.openhab.binding.network.internal.utils.NetworkUtils;
import org.openhab.binding.network.internal.utils.NetworkUtils.ArpPingUtilEnum; import org.openhab.binding.network.internal.utils.NetworkUtils.ArpPingUtilEnum;
import org.openhab.binding.network.internal.utils.NetworkUtils.IpPingMethodEnum; import org.openhab.binding.network.internal.utils.NetworkUtils.IpPingMethodEnum;
@ -49,32 +46,30 @@ import org.openhab.binding.network.internal.utils.PingResult;
*/ */
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT) @MockitoSettings(strictness = Strictness.LENIENT)
@NonNullByDefault
public class PresenceDetectionTest { public class PresenceDetectionTest {
private static final long CACHETIME = 2000L;
private PresenceDetection subject; private @NonNullByDefault({}) PresenceDetection subject;
private @Mock Consumer<PresenceDetectionValue> callback; private @Mock @NonNullByDefault({}) Consumer<PresenceDetectionValue> callback;
private @Mock ExecutorService executorService; private @Mock @NonNullByDefault({}) ExecutorService detectionExecutorService;
private @Mock PresenceDetectionListener listener; private @Mock @NonNullByDefault({}) ScheduledExecutorService scheduledExecutorService;
private @Mock NetworkUtils networkUtils; private @Mock @NonNullByDefault({}) PresenceDetectionListener listener;
private @Mock @NonNullByDefault({}) NetworkUtils networkUtils;
@BeforeEach @BeforeEach
public void setUp() throws UnknownHostException { public void setUp() {
// Mock an interface // Mock an interface
when(networkUtils.getInterfaceNames()).thenReturn(Set.of("TESTinterface")); when(networkUtils.getInterfaceNames()).thenReturn(Set.of("TESTinterface"));
doReturn(ArpPingUtilEnum.IPUTILS_ARPING).when(networkUtils).determineNativeARPpingMethod(anyString()); doReturn(ArpPingUtilEnum.IPUTILS_ARPING).when(networkUtils).determineNativeArpPingMethod(anyString());
doReturn(IpPingMethodEnum.WINDOWS_PING).when(networkUtils).determinePingMethod(); doReturn(IpPingMethodEnum.WINDOWS_PING).when(networkUtils).determinePingMethod();
subject = spy(new PresenceDetection(listener, (int) CACHETIME)); subject = spy(new PresenceDetection(listener, scheduledExecutorService, Duration.ofSeconds(2)));
subject.networkUtils = networkUtils; subject.networkUtils = networkUtils;
subject.cache = spy(new ExpiringCacheAsync<>(CACHETIME, () -> {
subject.performPresenceDetection(false);
}));
// Set a useful configuration. The default presenceDetection is a no-op. // Set a useful configuration. The default presenceDetection is a no-op.
subject.setHostname("127.0.0.1"); subject.setHostname("127.0.0.1");
subject.setTimeout(300); subject.setTimeout(Duration.ofMillis(300));
subject.setUseDhcpSniffing(false); subject.setUseDhcpSniffing(false);
subject.setIOSDevice(true); subject.setIOSDevice(true);
subject.setServicePorts(Set.of(1010)); subject.setServicePorts(Set.of(1010));
@ -84,83 +79,103 @@ public class PresenceDetectionTest {
assertThat(subject.pingMethod, is(IpPingMethodEnum.WINDOWS_PING)); assertThat(subject.pingMethod, is(IpPingMethodEnum.WINDOWS_PING));
} }
@AfterEach
public void shutDown() {
subject.waitForPresenceDetection();
}
// Depending on the amount of test methods an according amount of threads is spawned. // Depending on the amount of test methods an according amount of threads is spawned.
// We will check if they spawn and return in time. // We will check if they spawn and return in time.
@Test @Test
public void threadCountTest() { public void threadCountTest() {
assertNull(subject.executorService); assertNull(subject.detectionExecutorService);
doNothing().when(subject).performARPping(any()); doNothing().when(subject).performArpPing(any(), any());
doNothing().when(subject).performJavaPing(); doNothing().when(subject).performJavaPing(any());
doNothing().when(subject).performSystemPing(); doNothing().when(subject).performSystemPing(any());
doNothing().when(subject).performServicePing(anyInt()); doNothing().when(subject).performServicePing(any(), anyInt());
subject.performPresenceDetection(false); subject.getValue(callback -> {
});
// Thread count: ARP + ICMP + 1*TCP // Thread count: ARP + ICMP + 1*TCP
assertThat(subject.detectionChecks, is(3)); assertThat(subject.detectionChecks, is(3));
assertNotNull(subject.executorService); assertNotNull(subject.detectionExecutorService);
// "Wait" for the presence detection to finish
ArgumentCaptor<Runnable> runnableCapture = ArgumentCaptor.forClass(Runnable.class);
verify(scheduledExecutorService, times(1)).execute(runnableCapture.capture());
runnableCapture.getValue().run();
subject.waitForPresenceDetection();
assertThat(subject.detectionChecks, is(0)); assertThat(subject.detectionChecks, is(0));
assertNull(subject.executorService); assertNull(subject.detectionExecutorService);
} }
@Test @Test
public void partialAndFinalCallbackTests() throws InterruptedException, IOException { public void partialAndFinalCallbackTests() throws InterruptedException, IOException {
doReturn(Optional.of(new PingResult(true, 10))).when(networkUtils).nativePing(eq(IpPingMethodEnum.WINDOWS_PING), PingResult pingResult = new PingResult(true, Duration.ofMillis(10));
anyString(), anyInt()); doReturn(pingResult).when(networkUtils).nativePing(eq(IpPingMethodEnum.WINDOWS_PING), anyString(), any());
doReturn(Optional.of(new PingResult(true, 10))).when(networkUtils) doReturn(pingResult).when(networkUtils).nativeArpPing(eq(ArpPingUtilEnum.IPUTILS_ARPING), anyString(),
.nativeARPPing(eq(ArpPingUtilEnum.IPUTILS_ARPING), anyString(), anyString(), any(), anyInt()); anyString(), any(), any());
doReturn(Optional.of(new PingResult(true, 10))).when(networkUtils).servicePing(anyString(), anyInt(), anyInt()); doReturn(pingResult).when(networkUtils).servicePing(anyString(), anyInt(), any());
assertTrue(subject.performPresenceDetection(false)); doReturn(detectionExecutorService).when(subject).getThreadsFor(anyInt());
subject.waitForPresenceDetection();
verify(subject, times(0)).performJavaPing(); subject.performPresenceDetection();
verify(subject).performSystemPing();
verify(subject).performARPping(any()); assertThat(subject.detectionChecks, is(3));
verify(subject).performServicePing(anyInt());
// Perform the different presence detection threads now
ArgumentCaptor<Runnable> capture = ArgumentCaptor.forClass(Runnable.class);
verify(detectionExecutorService, times(3)).execute(capture.capture());
for (Runnable r : capture.getAllValues()) {
r.run();
}
// "Wait" for the presence detection to finish
ArgumentCaptor<Runnable> runnableCapture = ArgumentCaptor.forClass(Runnable.class);
verify(scheduledExecutorService, times(1)).execute(runnableCapture.capture());
runnableCapture.getValue().run();
assertThat(subject.detectionChecks, is(0));
verify(subject, times(0)).performJavaPing(any());
verify(subject).performSystemPing(any());
verify(subject).performArpPing(any(), any());
verify(subject).performServicePing(any(), anyInt());
verify(listener, times(3)).partialDetectionResult(any()); verify(listener, times(3)).partialDetectionResult(any());
ArgumentCaptor<PresenceDetectionValue> capture = ArgumentCaptor.forClass(PresenceDetectionValue.class); ArgumentCaptor<PresenceDetectionValue> pdvCapture = ArgumentCaptor.forClass(PresenceDetectionValue.class);
verify(listener, times(1)).finalDetectionResult(capture.capture()); verify(listener, times(1)).finalDetectionResult(pdvCapture.capture());
assertThat(capture.getValue().getSuccessfulDetectionTypes(), is("ARP_PING, ICMP_PING, TCP_CONNECTION")); assertThat(pdvCapture.getValue().getSuccessfulDetectionTypes(), is("ARP_PING, ICMP_PING, TCP_CONNECTION"));
} }
@Test @Test
public void cacheTest() throws InterruptedException, IOException { public void cacheTest() throws InterruptedException, IOException {
doReturn(Optional.of(new PingResult(true, 10))).when(networkUtils).nativePing(eq(IpPingMethodEnum.WINDOWS_PING), PingResult pingResult = new PingResult(true, Duration.ofMillis(10));
anyString(), anyInt()); doReturn(pingResult).when(networkUtils).nativePing(eq(IpPingMethodEnum.WINDOWS_PING), anyString(), any());
doReturn(Optional.of(new PingResult(true, 10))).when(networkUtils) doReturn(pingResult).when(networkUtils).nativeArpPing(eq(ArpPingUtilEnum.IPUTILS_ARPING), anyString(),
.nativeARPPing(eq(ArpPingUtilEnum.IPUTILS_ARPING), anyString(), anyString(), any(), anyInt()); anyString(), any(), any());
doReturn(Optional.of(new PingResult(true, 10))).when(networkUtils).servicePing(anyString(), anyInt(), anyInt()); doReturn(pingResult).when(networkUtils).servicePing(anyString(), anyInt(), any());
doReturn(executorService).when(subject).getThreadsFor(anyInt()); doReturn(detectionExecutorService).when(subject).getThreadsFor(anyInt());
// We expect no valid value // We expect no valid value
assertTrue(subject.cache.isExpired()); assertTrue(subject.cache.isExpired());
// Get value will issue a PresenceDetection internally. // Get value will issue a PresenceDetection internally.
subject.getValue(callback); subject.getValue(callback);
verify(subject).performPresenceDetection(eq(false)); verify(subject).performPresenceDetection();
assertNotNull(subject.executorService); assertNotNull(subject.detectionExecutorService);
// There should be no straight callback yet // There should be no straight callback yet
verify(callback, times(0)).accept(any()); verify(callback, times(0)).accept(any());
// Perform the different presence detection threads now // Perform the different presence detection threads now
ArgumentCaptor<Runnable> capture = ArgumentCaptor.forClass(Runnable.class); ArgumentCaptor<Runnable> capture = ArgumentCaptor.forClass(Runnable.class);
verify(executorService, times(3)).execute(capture.capture()); verify(detectionExecutorService, times(3)).execute(capture.capture());
for (Runnable r : capture.getAllValues()) { for (Runnable r : capture.getAllValues()) {
r.run(); r.run();
} }
// "Wait" for the presence detection to finish // "Wait" for the presence detection to finish
subject.waitForPresenceDetection(); capture = ArgumentCaptor.forClass(Runnable.class);
verify(scheduledExecutorService, times(1)).execute(capture.capture());
capture.getValue().run();
// Although there are multiple partial results and a final result, // Although there are multiple partial results and a final result,
// the getValue() consumers get the fastest response possible, and only once. // the getValue() consumers get the fastest response possible, and only once.
@ -175,34 +190,4 @@ public class PresenceDetectionTest {
subject.getValue(callback); subject.getValue(callback);
verify(callback, times(2)).accept(any()); verify(callback, times(2)).accept(any());
} }
@Test
public void reuseValueTests() throws InterruptedException, IOException {
final long startTime = 1000L;
when(subject.cache.getCurrentNanoTime()).thenReturn(TimeUnit.MILLISECONDS.toNanos(startTime));
// The PresenceDetectionValue.getLowestLatency() should return the smallest latency
PresenceDetectionValue v = subject.updateReachableValue(PresenceDetectionType.ICMP_PING, 20);
PresenceDetectionValue v2 = subject.updateReachableValue(PresenceDetectionType.ICMP_PING, 19);
assertEquals(v, v2);
assertThat(v.getLowestLatency(), is(19.0));
// Advance in time but not expire the cache (1ms left)
final long almostExpire = startTime + CACHETIME - 1;
when(subject.cache.getCurrentNanoTime()).thenReturn(TimeUnit.MILLISECONDS.toNanos(almostExpire));
// Updating should reset the expire timer of the cache
v2 = subject.updateReachableValue(PresenceDetectionType.ICMP_PING, 28);
assertEquals(v, v2);
assertThat(v2.getLowestLatency(), is(19.0));
assertThat(ExpiringCacheHelper.expireTime(subject.cache),
is(TimeUnit.MILLISECONDS.toNanos(almostExpire + CACHETIME)));
// Cache expire. A new PresenceDetectionValue instance will be returned
when(subject.cache.getCurrentNanoTime())
.thenReturn(TimeUnit.MILLISECONDS.toNanos(almostExpire + CACHETIME + CACHETIME + 1));
v2 = subject.updateReachableValue(PresenceDetectionType.ICMP_PING, 25);
assertNotEquals(v, v2);
assertThat(v2.getLowestLatency(), is(25.0));
}
} }

View File

@ -16,6 +16,9 @@ import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
import java.time.Duration;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
/** /**
@ -23,47 +26,40 @@ import org.junit.jupiter.api.Test;
* *
* @author David Graeff - Initial contribution * @author David Graeff - Initial contribution
*/ */
@NonNullByDefault
public class PresenceDetectionValuesTest { public class PresenceDetectionValuesTest {
@Test @Test
public void updateLatencyTests() { public void updateLatencyTests() {
PresenceDetectionValue value = new PresenceDetectionValue("127.0.0.1", 10.0); PresenceDetectionValue value = new PresenceDetectionValue("127.0.0.1", Duration.ofMillis(10));
assertThat(value.getLowestLatency(), is(10.0)); assertThat(value.getLowestLatency(), is(Duration.ofMillis(10)));
value.updateLatency(20.0); value.updateLatency(Duration.ofMillis(20));
assertThat(value.getLowestLatency(), is(10.0)); assertThat(value.getLowestLatency(), is(Duration.ofMillis(10)));
value.updateLatency(0.0); value.updateLatency(Duration.ofMillis(0));
assertThat(value.getLowestLatency(), is(10.0)); assertThat(value.getLowestLatency(), is(Duration.ofMillis(10)));
value.updateLatency(5.0); value.updateLatency(Duration.ofMillis(5));
assertThat(value.getLowestLatency(), is(5.0)); assertThat(value.getLowestLatency(), is(Duration.ofMillis(5)));
} }
@Test @Test
public void tcpTests() { public void tcpTests() {
PresenceDetectionValue value = new PresenceDetectionValue("127.0.0.1", 10.0); PresenceDetectionValue value = new PresenceDetectionValue("127.0.0.1", Duration.ofMillis(10));
assertFalse(value.isTCPServiceReachable()); assertFalse(value.isTcpServiceReachable());
value.addReachableTcpService(1010); value.addReachableTcpPort(1010);
assertThat(value.getReachableTCPports(), hasItem(1010)); assertThat(value.getReachableTcpPorts(), hasItem(1010));
value.addType(PresenceDetectionType.TCP_CONNECTION); value.addReachableDetectionType(PresenceDetectionType.TCP_CONNECTION);
assertTrue(value.isTCPServiceReachable()); assertTrue(value.isTcpServiceReachable());
assertThat(value.getSuccessfulDetectionTypes(), is("TCP_CONNECTION")); assertThat(value.getSuccessfulDetectionTypes(), is("TCP_CONNECTION"));
} }
@Test
public void isFinishedTests() {
PresenceDetectionValue value = new PresenceDetectionValue("127.0.0.1", 10.0);
assertFalse(value.isFinished());
value.setDetectionIsFinished(true);
assertTrue(value.isFinished());
}
@Test @Test
public void pingTests() { public void pingTests() {
PresenceDetectionValue value = new PresenceDetectionValue("127.0.0.1", 10.0); PresenceDetectionValue value = new PresenceDetectionValue("127.0.0.1", Duration.ofMillis(10));
assertFalse(value.isPingReachable()); assertFalse(value.isPingReachable());
value.addType(PresenceDetectionType.ICMP_PING); value.addReachableDetectionType(PresenceDetectionType.ICMP_PING);
assertTrue(value.isPingReachable()); assertTrue(value.isPingReachable());
value.addType(PresenceDetectionType.ARP_PING); value.addReachableDetectionType(PresenceDetectionType.ARP_PING);
value.addType(PresenceDetectionType.TCP_CONNECTION); value.addReachableDetectionType(PresenceDetectionType.TCP_CONNECTION);
assertThat(value.getSuccessfulDetectionTypes(), is("ARP_PING, ICMP_PING, TCP_CONNECTION")); assertThat(value.getSuccessfulDetectionTypes(), is("ARP_PING, ICMP_PING, TCP_CONNECTION"));
} }
} }

View File

@ -17,8 +17,10 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
import java.time.Duration;
import java.util.List; import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
@ -39,16 +41,17 @@ import org.openhab.core.config.discovery.DiscoveryResult;
*/ */
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT) @MockitoSettings(strictness = Strictness.LENIENT)
@NonNullByDefault
public class DiscoveryTest { public class DiscoveryTest {
private final String ip = "127.0.0.1"; private final String ip = "127.0.0.1";
private @Mock PresenceDetectionValue value; private @Mock @NonNullByDefault({}) PresenceDetectionValue value;
private @Mock DiscoveryListener listener; private @Mock @NonNullByDefault({}) DiscoveryListener listener;
@BeforeEach @BeforeEach
public void setUp() { public void setUp() {
when(value.getHostAddress()).thenReturn(ip); when(value.getHostAddress()).thenReturn(ip);
when(value.getLowestLatency()).thenReturn(10.0); when(value.getLowestLatency()).thenReturn(Duration.ofMillis(10));
when(value.isReachable()).thenReturn(true); when(value.isReachable()).thenReturn(true);
when(value.getSuccessfulDetectionTypes()).thenReturn("TESTMETHOD"); when(value.getSuccessfulDetectionTypes()).thenReturn("TESTMETHOD");
} }
@ -62,7 +65,7 @@ public class DiscoveryTest {
// Ping device // Ping device
when(value.isPingReachable()).thenReturn(true); when(value.isPingReachable()).thenReturn(true);
when(value.isTCPServiceReachable()).thenReturn(false); when(value.isTcpServiceReachable()).thenReturn(false);
d.partialDetectionResult(value); d.partialDetectionResult(value);
verify(listener).thingDiscovered(any(), result.capture()); verify(listener).thingDiscovered(any(), result.capture());
DiscoveryResult dresult = result.getValue(); DiscoveryResult dresult = result.getValue();
@ -79,8 +82,8 @@ public class DiscoveryTest {
// TCP device // TCP device
when(value.isPingReachable()).thenReturn(false); when(value.isPingReachable()).thenReturn(false);
when(value.isTCPServiceReachable()).thenReturn(true); when(value.isTcpServiceReachable()).thenReturn(true);
when(value.getReachableTCPports()).thenReturn(List.of(1010)); when(value.getReachableTcpPorts()).thenReturn(List.of(1010));
d.partialDetectionResult(value); d.partialDetectionResult(value);
verify(listener).thingDiscovered(any(), result.capture()); verify(listener).thingDiscovered(any(), result.capture());
DiscoveryResult dresult = result.getValue(); DiscoveryResult dresult = result.getValue();

View File

@ -19,6 +19,11 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.ScheduledExecutorService;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
@ -50,11 +55,13 @@ import org.openhab.core.thing.binding.ThingHandlerCallback;
*/ */
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT) @MockitoSettings(strictness = Strictness.LENIENT)
@NonNullByDefault
public class NetworkHandlerTest extends JavaTest { public class NetworkHandlerTest extends JavaTest {
private ThingUID thingUID = new ThingUID("network", "ttype", "ping"); private ThingUID thingUID = new ThingUID("network", "ttype", "ping");
private @Mock ThingHandlerCallback callback; private @Mock @NonNullByDefault({}) ThingHandlerCallback callback;
private @Mock Thing thing; private @Mock @NonNullByDefault({}) ScheduledExecutorService scheduledExecutorService;
private @Mock @NonNullByDefault({}) Thing thing;
@BeforeEach @BeforeEach
public void setUp() { public void setUp() {
@ -76,17 +83,18 @@ public class NetworkHandlerTest extends JavaTest {
conf.put(NetworkBindingConstants.PARAMETER_TIMEOUT, 1234); conf.put(NetworkBindingConstants.PARAMETER_TIMEOUT, 1234);
return conf; return conf;
}); });
PresenceDetection presenceDetection = spy(new PresenceDetection(handler, 2000)); PresenceDetection presenceDetection = spy(
new PresenceDetection(handler, scheduledExecutorService, Duration.ofSeconds(2)));
// Mock start/stop automatic refresh // Mock start/stop automatic refresh
doNothing().when(presenceDetection).startAutomaticRefresh(any()); doNothing().when(presenceDetection).startAutomaticRefresh();
doNothing().when(presenceDetection).stopAutomaticRefresh(); doNothing().when(presenceDetection).stopAutomaticRefresh();
handler.initialize(presenceDetection); handler.initialize(presenceDetection);
assertThat(handler.retries, is(10)); assertThat(handler.retries, is(10));
assertThat(presenceDetection.getHostname(), is("127.0.0.1")); assertThat(presenceDetection.getHostname(), is("127.0.0.1"));
assertThat(presenceDetection.getServicePorts().iterator().next(), is(8080)); assertThat(presenceDetection.getServicePorts().iterator().next(), is(8080));
assertThat(presenceDetection.getRefreshInterval(), is(101010L)); assertThat(presenceDetection.getRefreshInterval(), is(Duration.ofMillis(101010)));
assertThat(presenceDetection.getTimeout(), is(1234)); assertThat(presenceDetection.getTimeout(), is(Duration.ofMillis(1234)));
} }
@Test @Test
@ -101,7 +109,7 @@ public class NetworkHandlerTest extends JavaTest {
conf.put(NetworkBindingConstants.PARAMETER_HOSTNAME, "127.0.0.1"); conf.put(NetworkBindingConstants.PARAMETER_HOSTNAME, "127.0.0.1");
return conf; return conf;
}); });
handler.initialize(new PresenceDetection(handler, 2000)); handler.initialize(new PresenceDetection(handler, scheduledExecutorService, Duration.ofSeconds(2)));
// Check that we are offline // Check that we are offline
ArgumentCaptor<ThingStatusInfo> statusInfoCaptor = ArgumentCaptor.forClass(ThingStatusInfo.class); ArgumentCaptor<ThingStatusInfo> statusInfoCaptor = ArgumentCaptor.forClass(ThingStatusInfo.class);
verify(callback).statusUpdated(eq(thing), statusInfoCaptor.capture()); verify(callback).statusUpdated(eq(thing), statusInfoCaptor.capture());
@ -120,10 +128,12 @@ public class NetworkHandlerTest extends JavaTest {
conf.put(NetworkBindingConstants.PARAMETER_HOSTNAME, "127.0.0.1"); conf.put(NetworkBindingConstants.PARAMETER_HOSTNAME, "127.0.0.1");
return conf; return conf;
}); });
PresenceDetection presenceDetection = spy(new PresenceDetection(handler, 2000)); PresenceDetection presenceDetection = spy(
new PresenceDetection(handler, scheduledExecutorService, Duration.ofSeconds(2)));
// Mock start/stop automatic refresh // Mock start/stop automatic refresh
doNothing().when(presenceDetection).startAutomaticRefresh(any()); doNothing().when(presenceDetection).startAutomaticRefresh();
doNothing().when(presenceDetection).stopAutomaticRefresh(); doNothing().when(presenceDetection).stopAutomaticRefresh();
doReturn(Instant.now()).when(presenceDetection).getLastSeen();
handler.initialize(presenceDetection); handler.initialize(presenceDetection);
// Check that we are online // Check that we are online
@ -133,7 +143,7 @@ public class NetworkHandlerTest extends JavaTest {
// Mock result value // Mock result value
PresenceDetectionValue value = mock(PresenceDetectionValue.class); PresenceDetectionValue value = mock(PresenceDetectionValue.class);
when(value.getLowestLatency()).thenReturn(10.0); when(value.getLowestLatency()).thenReturn(Duration.ofMillis(10));
when(value.isReachable()).thenReturn(true); when(value.isReachable()).thenReturn(true);
when(value.getSuccessfulDetectionTypes()).thenReturn("TESTMETHOD"); when(value.getSuccessfulDetectionTypes()).thenReturn("TESTMETHOD");
@ -146,7 +156,6 @@ public class NetworkHandlerTest extends JavaTest {
eq(new QuantityType<>("10.0 ms"))); eq(new QuantityType<>("10.0 ms")));
// Final result affects the LASTSEEN channel // Final result affects the LASTSEEN channel
when(value.isFinished()).thenReturn(true);
handler.finalDetectionResult(value); handler.finalDetectionResult(value);
verify(callback).stateUpdated(eq(new ChannelUID(thingUID, NetworkBindingConstants.CHANNEL_LASTSEEN)), any()); verify(callback).stateUpdated(eq(new ChannelUID(thingUID, NetworkBindingConstants.CHANNEL_LASTSEEN)), any());
} }

View File

@ -1,105 +0,0 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.network.internal.toberemoved.cache;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.function.Consumer;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.openhab.binding.network.internal.toberemoved.cache.ExpiringCacheAsync.ExpiringCacheUpdate;
/**
* Tests cases for {@see ExpiringAsyncCache}
*
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class ExpiringCacheAsyncTest {
@Test
public void testConstructorWrongCacheTime() {
assertThrows(IllegalArgumentException.class, () ->
// Fail if cache time is <= 0
new ExpiringCacheAsync<>(0, () -> {
}));
}
@Test
public void testConstructorNoRefrehCommand() {
assertThrows(IllegalArgumentException.class, () -> new ExpiringCacheAsync<>(2000, null));
}
@Test
public void testFetchValue() {
ExpiringCacheUpdate u = mock(ExpiringCacheUpdate.class);
ExpiringCacheAsync<Double> t = new ExpiringCacheAsync<>(2000, u);
assertTrue(t.isExpired());
// Request a value
@SuppressWarnings("unchecked")
Consumer<Double> consumer = mock(Consumer.class);
t.getValue(consumer);
// We expect a call to the updater object
verify(u).requestCacheUpdate();
// Update the value now
t.setValue(10.0);
// The value should be valid
assertFalse(t.isExpired());
// We expect a call to the consumer
ArgumentCaptor<Double> valueCaptor = ArgumentCaptor.forClass(Double.class);
verify(consumer).accept(valueCaptor.capture());
assertEquals(10.0, valueCaptor.getValue(), 0);
}
@Test
public void testExpiring() {
ExpiringCacheUpdate u = mock(ExpiringCacheUpdate.class);
@SuppressWarnings("unchecked")
Consumer<Double> consumer = mock(Consumer.class);
ExpiringCacheAsync<Double> t = new ExpiringCacheAsync<>(100, u);
t.setValue(10.0);
assertFalse(t.isExpired());
// Request a value
t.getValue(consumer);
// There should be no call to update the cache
verify(u, times(0)).requestCacheUpdate();
// Wait
try {
Thread.sleep(101);
} catch (InterruptedException ignored) {
return;
}
// Request a value two times
t.getValue(consumer);
t.getValue(consumer);
// There should be one call to update the cache
verify(u, times(1)).requestCacheUpdate();
assertTrue(t.isExpired());
}
@Test
public void testFetchExpiredValue() {
ExpiringCacheUpdate u = mock(ExpiringCacheUpdate.class);
ExpiringCacheAsync<Double> t = new ExpiringCacheAsync<>(2000, u);
t.setValue(10.0);
// We should always be able to get the raw value, expired or not
assertEquals(10.0, t.getExpiredValue(), 0);
t.invalidateValue();
assertTrue(t.isExpired());
assertEquals(10.0, t.getExpiredValue(), 0);
}
}

View File

@ -1,27 +0,0 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.network.internal.toberemoved.cache;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Helper class to make the package private cacheUpdater field available for tests.
*
* @author David Graeff - Initial Contribution
*/
@NonNullByDefault
public class ExpiringCacheHelper {
public static long expireTime(@SuppressWarnings("rawtypes") ExpiringCacheAsync cache) {
return cache.expiresAt;
}
}

View File

@ -13,8 +13,9 @@
package org.openhab.binding.network.internal.utils; package org.openhab.binding.network.internal.utils;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
import static org.openhab.binding.network.internal.utils.NetworkUtils.durationToMillis;
import java.util.Optional; import java.time.Duration;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -34,11 +35,11 @@ public class LatencyParserTest {
String input = "64 bytes from 192.168.1.1: icmp_seq=0 ttl=64 time=1.225 ms"; String input = "64 bytes from 192.168.1.1: icmp_seq=0 ttl=64 time=1.225 ms";
// Act // Act
Optional<Double> resultLatency = latencyParser.parseLatency(input); Duration resultLatency = latencyParser.parseLatency(input);
// Assert // Assert
assertTrue(resultLatency.isPresent()); assertNotNull(resultLatency);
assertEquals(1.225, resultLatency.get(), 0); assertEquals(1.225, durationToMillis(resultLatency), 0);
} }
@Test @Test
@ -54,10 +55,10 @@ public class LatencyParserTest {
for (String inputLine : inputLines) { for (String inputLine : inputLines) {
// Act // Act
Optional<Double> resultLatency = latencyParser.parseLatency(inputLine); Duration resultLatency = latencyParser.parseLatency(inputLine);
// Assert // Assert
assertFalse(resultLatency.isPresent()); assertNull(resultLatency);
} }
} }
@ -68,10 +69,10 @@ public class LatencyParserTest {
String input = "Reply from 192.168.178.207: bytes=32 time=2ms TTL=64"; String input = "Reply from 192.168.178.207: bytes=32 time=2ms TTL=64";
// Act // Act
Optional<Double> resultLatency = latencyParser.parseLatency(input); Duration resultLatency = latencyParser.parseLatency(input);
// Assert // Assert
assertTrue(resultLatency.isPresent()); assertNotNull(resultLatency);
assertEquals(2, resultLatency.get(), 0); assertEquals(2, durationToMillis(resultLatency), 0);
} }
} }