[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;
NetworkUtils networkUtils = new NetworkUtils();
this.arpPingUtilMethod = networkUtils.determineNativeARPpingMethod(arpPingToolPath);
this.arpPingUtilMethod = networkUtils.determineNativeArpPingMethod(arpPingToolPath);
notifyListeners();
}

View File

@ -12,7 +12,6 @@
*/
package org.openhab.binding.network.internal;
import java.util.HashSet;
import java.util.Set;
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 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
public static final String CHANNEL_ONLINE = "online";
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_TEST_ISRUNNING = "isRunning";
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_PRESENCE_DETECTION_TYPE = "presence_detection_type";
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;
import static org.openhab.binding.network.internal.PresenceDetectionType.*;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
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.DHCPPacketListenerServer;
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.ArpPingUtilEnum;
import org.openhab.binding.network.internal.utils.NetworkUtils.IpPingMethodEnum;
import org.openhab.binding.network.internal.utils.PingResult;
import org.openhab.core.cache.ExpiringCache;
import org.openhab.core.cache.ExpiringCacheAsync;
import org.slf4j.Logger;
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 David Gräff, 2017 - Rewritten
* @author Jan N. Klug - refactored host name resolution
* @author Wouter Born - Reuse ExpiringCacheAsync from Core
*/
@NonNullByDefault
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();
private final Logger logger = LoggerFactory.getLogger(PresenceDetection.class);
@ -64,32 +73,35 @@ public class PresenceDetection implements IPRequestReceivedCallback {
private boolean iosDevice;
private Set<Integer> tcpPorts = new HashSet<>();
private long refreshIntervalInMS = 60000;
private int timeoutInMS = 5000;
private long lastSeenInMS;
private Duration refreshInterval = Duration.ofMinutes(1);
private Duration timeout = Duration.ofSeconds(5);
private @Nullable Instant lastSeen;
private @NonNullByDefault({}) String hostname;
private @NonNullByDefault({}) ExpiringCache<@Nullable InetAddress> destination;
private @Nullable InetAddress cachedDestination = null;
private @Nullable InetAddress cachedDestination;
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;
private final PresenceDetectionListener updateListener;
private ScheduledExecutorService scheduledExecutorService;
private Set<String> networkInterfaceNames = Set.of();
private @Nullable ScheduledFuture<?> refreshJob;
protected @Nullable ExecutorService executorService;
protected @Nullable ExecutorService detectionExecutorService;
private String dhcpState = "off";
private Integer currentCheck = 0;
int detectionChecks;
private String lastReachableNetworkInterfaceName = "";
public PresenceDetection(final PresenceDetectionListener updateListener, int cacheDeviceStateTimeInMS)
public PresenceDetection(final PresenceDetectionListener updateListener,
ScheduledExecutorService scheduledExecutorService, Duration cacheDeviceStateTime)
throws IllegalArgumentException {
this.updateListener = updateListener;
cache = new ExpiringCacheAsync<>(cacheDeviceStateTimeInMS, () -> performPresenceDetection(false));
this.scheduledExecutorService = scheduledExecutorService;
cache = new ExpiringCacheAsync<>(cacheDeviceStateTime);
}
public @Nullable String getHostname() {
@ -100,12 +112,12 @@ public class PresenceDetection implements IPRequestReceivedCallback {
return tcpPorts;
}
public long getRefreshInterval() {
return refreshIntervalInMS;
public Duration getRefreshInterval() {
return refreshInterval;
}
public int getTimeout() {
return timeoutInMS;
public Duration getTimeout() {
return timeout;
}
public void setHostname(String hostname) {
@ -115,7 +127,8 @@ public class PresenceDetection implements IPRequestReceivedCallback {
InetAddress destinationAddress = InetAddress.getByName(hostname);
InetAddress cached = cachedDestination;
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);
if (useDHCPsniffing) {
if (cached != null) {
@ -127,7 +140,7 @@ public class PresenceDetection implements IPRequestReceivedCallback {
}
return destinationAddress;
} catch (UnknownHostException e) {
logger.trace("hostname resolution failed");
logger.trace("Hostname resolution for {} failed", hostname);
InetAddress cached = cachedDestination;
if (cached != null) {
disableDHCPListen(cached);
@ -150,12 +163,12 @@ public class PresenceDetection implements IPRequestReceivedCallback {
this.useDHCPsniffing = enable;
}
public void setRefreshInterval(long refreshInterval) {
this.refreshIntervalInMS = refreshInterval;
public void setRefreshInterval(Duration refreshInterval) {
this.refreshInterval = refreshInterval;
}
public void setTimeout(int timeout) {
this.timeoutInMS = timeout;
public void setTimeout(Duration timeout) {
this.timeout = timeout;
}
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
* does not work on this system, JAVA_PING will be used instead.
* Sets the ping method. This method will perform a feature test. If {@link IpPingMethodEnum#SYSTEM_PING}
* 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
* pings.
* @param useSystemPing Set to <code>true</code> to use a system ping method, <code>false</code> to use Java ping
* and <code>null</code> to disable ICMP pings.
*/
public void setUseIcmpPing(@Nullable Boolean useSystemPing) {
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);
*/
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.
*/
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.
*/
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() {
return lastSeenInMS;
public @Nullable Instant getLastSeen() {
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
* discovery took place.
*/
public void getValue(Consumer<PresenceDetectionValue> callback) {
cache.getValue(callback);
cache.getValue(this::performPresenceDetection).thenAccept(callback);
}
public ExecutorService getThreadsFor(int 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
* TCP connection attempts simultaneously. A fixed thread pool will be created with as many
* 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.
* Perform a presence detection with ICMP-, ARP ping and TCP connection attempts simultaneously.
* A fixed thread pool will be created with as many threads as necessary to perform all tests at once.
*
* Please be aware of the following restrictions:
* - ARP pings are only executed on IPv4 addresses.
* - Non system / Java pings are not recommended at all
* (not interruptible, useless TCP echo service fall back)
* <ul>
* <li>ARP pings are only executed on IPv4 addresses.
* <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 Return true if a presence detection is performed and false otherwise.
* @return a {@link CompletableFuture} for obtaining the {@link PresenceDetectionValue}
*/
public boolean performPresenceDetection(boolean waitForDetectionToFinish) {
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;
}
public CompletableFuture<PresenceDetectionValue> performPresenceDetection() {
Set<String> interfaceNames = null;
currentCheck = 0;
detectionChecks = tcpPorts.size();
if (pingMethod != null) {
detectionChecks += 1;
@ -308,295 +325,240 @@ public class PresenceDetection implements IPRequestReceivedCallback {
detectionChecks += interfaceNames.size();
}
logger.trace("Performing {} presence detection checks for {}", detectionChecks, hostname);
PresenceDetectionValue pdv = new PresenceDetectionValue(hostname, PresenceDetectionValue.UNREACHABLE);
if (detectionChecks == 0) {
return false;
return CompletableFuture.completedFuture(pdv);
}
final ExecutorService executorService = getThreadsFor(detectionChecks);
this.executorService = executorService;
ExecutorService detectionExecutorService = getThreadsFor(detectionChecks);
this.detectionExecutorService = detectionExecutorService;
List<CompletableFuture<Void>> completableFutures = new ArrayList<>();
for (Integer tcpPort : tcpPorts) {
executorService.execute(() -> {
completableFutures.add(CompletableFuture.runAsync(() -> {
Thread.currentThread().setName("presenceDetectionTCP_" + hostname + " " + tcpPort);
performServicePing(tcpPort);
checkIfFinished();
});
performServicePing(pdv, tcpPort);
}, detectionExecutorService));
}
// ARP ping for IPv4 addresses. Use single executor for Windows tool and
// each own executor for each network interface for other tools
if (arpPingMethod == ArpPingUtilEnum.ELI_FULKERSON_ARP_PING_FOR_WINDOWS) {
executorService.execute(() -> {
completableFutures.add(CompletableFuture.runAsync(() -> {
Thread.currentThread().setName("presenceDetectionARP_" + hostname + " ");
// arp-ping.exe tool capable of handling multiple interfaces by itself
performARPping("");
checkIfFinished();
});
performArpPing(pdv, "");
}, detectionExecutorService));
} else if (interfaceNames != null) {
for (final String interfaceName : interfaceNames) {
executorService.execute(() -> {
completableFutures.add(CompletableFuture.runAsync(() -> {
Thread.currentThread().setName("presenceDetectionARP_" + hostname + " " + interfaceName);
performARPping(interfaceName);
checkIfFinished();
});
performArpPing(pdv, interfaceName);
}, detectionExecutorService));
}
}
// ICMP ping
if (pingMethod != null) {
executorService.execute(() -> {
if (pingMethod != IpPingMethodEnum.JAVA_PING) {
Thread.currentThread().setName("presenceDetectionICMP_" + hostname);
performSystemPing();
completableFutures.add(CompletableFuture.runAsync(() -> {
Thread.currentThread().setName("presenceDetectionICMP_" + hostname);
if (pingMethod == IpPingMethodEnum.JAVA_PING) {
performJavaPing(pdv);
} else {
performJavaPing();
performSystemPing(pdv);
}
checkIfFinished();
});
}, detectionExecutorService));
}
if (waitForDetectionToFinish) {
waitForPresenceDetection();
}
return CompletableFuture.supplyAsync(() -> {
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.
* Safe to be called from different threads. After a call to this method,
* the presence detection process is finished and all threads are forcefully
* shut down.
*/
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.
* Creates a new {@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.
*
* It is safe to call this method from multiple threads. The returned PresenceDetectionValue
* might be still be altered in other threads though.
*
* @param type The detection type
* @return The non expired or a new instance of PresenceDetectionValue.
* @param type the detection type
* @param latency the latency
*/
synchronized PresenceDetectionValue updateReachableValue(PresenceDetectionType type, double latency) {
lastSeenInMS = System.currentTimeMillis();
PresenceDetectionValue v;
if (cache.isExpired()) {
v = new PresenceDetectionValue(hostname, 0);
} else {
v = cache.getExpiredValue();
}
v.updateLatency(latency);
v.addType(type);
cache.setValue(v);
return v;
synchronized PresenceDetectionValue updateReachable(PresenceDetectionType type, Duration latency) {
PresenceDetectionValue pdv = new PresenceDetectionValue(hostname, latency);
updateReachable(pdv, type, latency);
return pdv;
}
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);
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);
}
}
/**
* Performs an "ARP ping" (ARP request) on the given interface.
* If it is an iOS device, the {@see NetworkUtils.wakeUpIOS()} method is
* called before performing the ARP ping.
*
* @param interfaceName The interface name. You can request a list of interface names
* from {@see NetworkUtils.getInterfaceNames()} for example.
*/
protected void performARPping(String interfaceName) {
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);
withDestinationAddress(destinationAddress -> {
try {
PingResult pingResult = networkUtils.servicePing(destinationAddress.getHostAddress(), tcpPort, timeout);
if (pingResult.isSuccess()) {
updateReachable(pdv, TCP_CONNECTION, getLatency(pingResult), tcpPort);
}
} 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);
}
});
}
protected void performSystemPing() {
try {
logger.trace("Perform native ping presence detection for {}", hostname);
InetAddress destinationAddress = destination.getValue();
if (destinationAddress == null) {
return;
}
/**
* Performs an "ARP ping" (ARP request) on the given interface.
* If it is an iOS device, the {@link NetworkUtils#wakeUpIOS(InetAddress)} method is
* called before performing the ARP ping.
*
* @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 -> {
if (o.isSuccess()) {
PresenceDetectionValue v = updateReachableValue(PresenceDetectionType.ICMP_PING,
getLatency(o, preferResponseTimeAsLatency));
updateListener.partialDetectionResult(v);
withDestinationAddress(destinationAddress -> {
try {
if (iosDevice) {
networkUtils.wakeUpIOS(destinationAddress);
Thread.sleep(50);
}
});
} catch (IOException e) {
logger.trace("Failed to execute a native ping for ip {}", hostname, e);
} catch (InterruptedException e) {
// This can be ignored, the thread will end anyway
}
PingResult pingResult = networkUtils.nativeArpPing(arpPingMethod, arpPingUtilPath, interfaceName,
destinationAddress.getHostAddress(), timeout);
if (pingResult != null) {
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);
// Execution time is always set and this value is also the default. So lets use it first.
double latency = pingResult.getExecutionTimeInMS();
if (preferResponseTimeAsLatency && pingResult.getResponseTimeInMS().isPresent()) {
latency = pingResult.getResponseTimeInMS().get();
}
return latency;
Duration executionTime = pingResult.getExecutionTime();
Duration responseTime = pingResult.getResponseTime();
return preferResponseTimeAsLatency && responseTime != null ? responseTime : executionTime;
}
@Override
public void dhcpRequestReceived(String ipAddress) {
PresenceDetectionValue v = updateReachableValue(PresenceDetectionType.DHCP_REQUEST, 0);
updateListener.partialDetectionResult(v);
updateReachable(DHCP_REQUEST, Duration.ZERO);
}
/**
* 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;
if (future != null && !future.isDone()) {
future.cancel(true);
}
refreshJob = scheduledExecutorService.scheduleWithFixedDelay(() -> performPresenceDetection(true), 0,
refreshIntervalInMS, TimeUnit.MILLISECONDS);
refreshJob = scheduledExecutorService.scheduleWithFixedDelay(() -> {
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() {
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
* for iOS devices. The hostname of this network service object will be registered to the dhcp request packet
* 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
* 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) {
try {
DHCPPacketListenerServer listener = DHCPListenService.register(destinationAddress.getHostAddress(), this);
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) {
dhcpState = String.format("Cannot use DHCP sniffing: %s", e.getMessage());
logger.warn("{}", dhcpState);

View File

@ -12,6 +12,9 @@
*/
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.List;
import java.util.Set;
@ -27,112 +30,26 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
*/
@NonNullByDefault
public class PresenceDetectionValue {
private double latency;
private boolean detectionIsFinished;
private final Set<PresenceDetectionType> reachableByType = new TreeSet<>();
private final List<Integer> tcpServiceReachable = new ArrayList<>();
public static final Duration UNREACHABLE = Duration.ofMillis(-1);
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.
*/
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.
* Create a new {@link PresenceDetectionValue} with an initial latency.
*
* @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.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.
*/
@ -140,18 +57,97 @@ public class PresenceDetectionValue {
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.
*/
public boolean isPingReachable() {
return reachableByType.contains(PresenceDetectionType.ARP_PING)
|| reachableByType.contains(PresenceDetectionType.ICMP_PING);
return reachableDetectionTypes.contains(PresenceDetectionType.ARP_PING)
|| reachableDetectionTypes.contains(PresenceDetectionType.ICMP_PING);
}
/**
* Return true if the target provides open TCP ports.
*/
public boolean isTCPServiceReachable() {
return reachableByType.contains(PresenceDetectionType.TCP_CONNECTION);
public boolean isTcpServiceReachable() {
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
public class DHCPListenService {
static @Nullable DHCPPacketListenerServer instance;
private static Map<String, IPRequestReceivedCallback> registeredListeners = new TreeMap<>();
private static Logger logger = LoggerFactory.getLogger(DHCPListenService.class);
private static final Map<String, IPRequestReceivedCallback> REGISTERED_LISTENERS = new TreeMap<>();
private static final Logger LOGGER = LoggerFactory.getLogger(DHCPListenService.class);
public static synchronized DHCPPacketListenerServer register(String hostAddress,
IPRequestReceivedCallback dhcpListener) throws SocketException {
DHCPPacketListenerServer instance = DHCPListenService.instance;
if (instance == null) {
instance = new DHCPPacketListenerServer((String ipAddress) -> {
IPRequestReceivedCallback listener = registeredListeners.get(ipAddress);
instance = new DHCPPacketListenerServer(ipAddress -> {
IPRequestReceivedCallback listener = REGISTERED_LISTENERS.get(ipAddress);
if (listener != null) {
listener.dhcpRequestReceived(ipAddress);
} else {
logger.trace("DHCP request for unknown address: {}", ipAddress);
LOGGER.trace("DHCP request for unknown address: {}", ipAddress);
}
});
DHCPListenService.instance = instance;
instance.start();
}
synchronized (registeredListeners) {
registeredListeners.put(hostAddress, dhcpListener);
synchronized (REGISTERED_LISTENERS) {
REGISTERED_LISTENERS.put(hostAddress, dhcpListener);
}
return instance;
}
public static void unregister(String hostAddress) {
synchronized (registeredListeners) {
registeredListeners.remove(hostAddress);
if (!registeredListeners.isEmpty()) {
synchronized (REGISTERED_LISTENERS) {
REGISTERED_LISTENERS.remove(hostAddress);
if (!REGISTERED_LISTENERS.isEmpty()) {
return;
}
}

View File

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

View File

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

View File

@ -13,7 +13,9 @@
package org.openhab.binding.network.internal.handler;
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.ZonedDateTime;
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.action.NetworkActions;
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.QuantityType;
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())));
break;
case CHANNEL_LATENCY:
case CHANNEL_DEPRECATED_TIME:
presenceDetection.getValue(value -> {
updateState(CHANNEL_LATENCY,
new QuantityType<>(value.getLowestLatency(), MetricPrefix.MILLI(Units.SECOND)));
updateState(CHANNEL_DEPRECATED_TIME, new DecimalType(value.getLowestLatency()));
double latencyMs = durationToMillis(value.getLowestLatency());
updateState(CHANNEL_LATENCY, new QuantityType<>(latencyMs, MetricPrefix.MILLI(Units.SECOND)));
});
break;
case CHANNEL_LASTSEEN:
if (presenceDetection.getLastSeen() > 0) {
Instant instant = Instant.ofEpochMilli(presenceDetection.getLastSeen());
Instant lastSeen = presenceDetection.getLastSeen();
if (lastSeen != null) {
updateState(CHANNEL_LASTSEEN, new DateTimeType(
ZonedDateTime.ofInstant(instant, TimeZone.getDefault().toZoneId()).withFixedOffsetZone()));
ZonedDateTime.ofInstant(lastSeen, TimeZone.getDefault().toZoneId()).withFixedOffsetZone()));
} else {
updateState(CHANNEL_LASTSEEN, UnDefType.UNDEF);
}
@ -128,28 +127,29 @@ public class NetworkHandler extends BaseThingHandler
@Override
public void partialDetectionResult(PresenceDetectionValue value) {
double latencyMs = durationToMillis(value.getLowestLatency());
updateState(CHANNEL_ONLINE, OnOffType.ON);
updateState(CHANNEL_LATENCY, new QuantityType<>(value.getLowestLatency(), MetricPrefix.MILLI(Units.SECOND)));
updateState(CHANNEL_DEPRECATED_TIME, new DecimalType(value.getLowestLatency()));
updateState(CHANNEL_LATENCY, new QuantityType<>(latencyMs, MetricPrefix.MILLI(Units.SECOND)));
}
@Override
public void finalDetectionResult(PresenceDetectionValue value) {
// We do not notify the framework immediately if a device presence detection failed and
// 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_LATENCY, UnDefType.UNDEF);
updateState(CHANNEL_DEPRECATED_TIME, UnDefType.UNDEF);
retryCounter = 0;
}
if (value.isReachable()) {
Instant instant = Instant.ofEpochMilli(presenceDetection.getLastSeen());
Instant lastSeen = presenceDetection.getLastSeen();
if (value.isReachable() && lastSeen != null) {
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();
@ -196,14 +196,14 @@ public class NetworkHandler extends BaseThingHandler
}
this.retries = handlerConfiguration.retry.intValue();
presenceDetection.setRefreshInterval(handlerConfiguration.refreshInterval.longValue());
presenceDetection.setTimeout(handlerConfiguration.timeout.intValue());
presenceDetection.setRefreshInterval(Duration.ofMillis(handlerConfiguration.refreshInterval));
presenceDetection.setTimeout(Duration.ofMillis(handlerConfiguration.timeout));
wakeOnLanPacketSender = new WakeOnLanPacketSender(handlerConfiguration.macAddress,
handlerConfiguration.hostname, handlerConfiguration.port, handlerConfiguration.networkInterfaceNames);
updateStatus(ThingStatus.ONLINE);
presenceDetection.startAutomaticRefresh(scheduler);
presenceDetection.startAutomaticRefresh();
updateNetworkProperties();
}
@ -222,7 +222,8 @@ public class NetworkHandler extends BaseThingHandler
// Create a new network service and apply all configurations.
@Override
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)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, errorMessage);
freeRefreshTask();
return;
} else if (SpeedTestError.SOCKET_TIMEOUT.equals(testError)) {
timeouts--;
if (timeouts <= 0) {
@ -141,12 +140,10 @@ public class SpeedTestHandler extends BaseThingHandler implements ISpeedTestList
logger.warn("Speedtest timed out, {} attempts left. Message '{}'", timeouts, errorMessage);
stopSpeedTest();
}
return;
} else if (SpeedTestError.SOCKET_ERROR.equals(testError)
|| SpeedTestError.INVALID_HTTP_RESPONSE.equals(testError)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, errorMessage);
freeRefreshTask();
return;
} else {
stopSpeedTest();
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;
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.Pattern;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
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.
*
* @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.
*/
public Optional<Double> parseLatency(String inputLine) {
public @Nullable Duration parseLatency(String inputLine) {
logger.debug("Parsing latency from input {}", inputLine);
Matcher m = LATENCY_PATTERN.matcher(inputLine);
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");
return Optional.empty();
return null;
}
}

View File

@ -25,17 +25,16 @@ import java.net.NetworkInterface;
import java.net.NoRouteToHostException;
import java.net.PortUnreachableException;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
@ -54,6 +53,37 @@ import org.slf4j.LoggerFactory;
*/
@NonNullByDefault
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 LatencyParser latencyParser = new LatencyParser();
@ -93,7 +123,7 @@ public class NetworkUtils {
result.add(InetAddress.getByAddress(segments).getHostAddress());
}
} catch (UnknownHostException e) {
logger.debug("Could not build net ip address.", e);
logger.trace("Could not build net IP address.", e);
}
return result;
}
@ -107,15 +137,14 @@ public class NetworkUtils {
Set<String> result = new HashSet<>();
try {
// For each interface ...
for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements();) {
NetworkInterface networkInterface = en.nextElement();
if (!networkInterface.isLoopback()) {
result.add(networkInterface.getName());
}
}
} catch (SocketException ignored) {
// If we are not allowed to enumerate, we return an empty result set.
} catch (SocketException e) {
logger.trace("Could not get network interfaces", e);
}
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
*/
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
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
* or the connection was denied.
* Try to establish a TCP connection to the given port.
*
* @param host The IP or hostname
* @param port The tcp port. Must be not 0.
* @param timeout Timeout in ms
* @return Ping result information. Optional is empty if ping command was not executed.
* @throws IOException
* @param host the IP or hostname
* @param port the TCP port. Must be not 0.
* @param timeout the timeout before the call aborts
* @return the {@link PingResult} of connecting to the given port
* @throws IOException if an error occurs during the connection
*/
public Optional<PingResult> servicePing(String host, int port, int timeout) throws IOException {
double execStartTimeInMS = System.currentTimeMillis();
SocketAddress socketAddress = new InetSocketAddress(host, port);
public PingResult servicePing(String host, int port, Duration timeout) throws IOException {
Instant execStartTime = Instant.now();
boolean success = false;
try (Socket socket = new Socket()) {
socket.connect(socketAddress, timeout);
return Optional.of(new PingResult(true, System.currentTimeMillis() - execStartTimeInMS));
} catch (ConnectException | SocketTimeoutException | NoRouteToHostException ignored) {
return Optional.of(new PingResult(false, System.currentTimeMillis() - execStartTimeInMS));
socket.connect(new InetSocketAddress(host, port), (int) timeout.toMillis());
success = true;
} catch (ConnectException | SocketTimeoutException | NoRouteToHostException e) {
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 {
Optional<PingResult> pingResult = nativePing(method, "127.0.0.1", 1000);
if (pingResult.isPresent() && pingResult.get().isSuccess()) {
PingResult pingResult = nativePing(method, "127.0.0.1", Duration.ofSeconds(1));
if (pingResult != null && pingResult.isSuccess()) {
return method;
}
} catch (IOException ignored) {
} catch (IOException e) {
logger.trace("Native ping to 127.0.0.1 failed", e);
} catch (InterruptedException e) {
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");
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;
} else if (result.contains("Thomas Habets")) {
if (result.matches("(?s)(.*)w sec Specify a timeout(.*)")) {
@ -249,8 +279,10 @@ public class NetworkUtils {
return ArpPingUtilEnum.IPUTILS_ARPING;
} else if (result.contains("Usage: arp-ping.exe")) {
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 {
@ -264,35 +296,36 @@ public class NetworkUtils {
* 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 timeoutInMS Timeout in milliseconds. Be aware that DNS resolution is not part of this timeout.
* @return Ping result information. Optional is empty if ping command was not executed.
* @param timeout the timeout before the call aborts. Be aware that DNS resolution is not part of this timeout.
* @return Ping result information. <code>null</code> if ping command was not executed.
* @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 {
double execStartTimeInMS = System.currentTimeMillis();
Instant execStartTime = Instant.now();
Process proc;
if (method == null) {
return Optional.empty();
return null;
}
// Yes, all supported operating systems have their own ping utility with a different command line
switch (method) {
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();
break;
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();
break;
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;
case JAVA_PING:
default:
// We cannot estimate the command line for any other operating system and just return false
return Optional.empty();
// We cannot estimate the command line for any other operating system and just return null
return null;
}
// 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();
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()))) {
@ -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
// this specific string is contained in the output
if (line.contains("TTL=") || line.contains("ttl=")) {
PingResult pingResult = new PingResult(true, System.currentTimeMillis() - execStartTimeInMS);
latencyParser.parseLatency(line).ifPresent(pingResult::setResponseTimeInMS);
return Optional.of(pingResult);
PingResult pingResult = new PingResult(true, Duration.between(execStartTime, Instant.now()));
pingResult.setResponseTime(latencyParser.parseLatency(line));
return pingResult;
}
line = r.readLine();
} 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).
* There exist two different arping utils with the same name unfortunatelly.
* * iputils arping which is sometimes preinstalled on fedora/ubuntu and the
* * https://github.com/ThomasHabets/arping which also works on Windows and MacOS.
* There exist two different arping utils with the same name unfortunately.
* <ul>
* <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
* "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 ipV4address The ipV4 address. Must not be null.
* @param timeoutInMS A timeout in milliseconds
* @return Ping result information. Optional is empty if ping command was not executed.
* @param timeout the timeout before the call aborts
* @return Ping result information. <code>null</code> if ping command was not executed.
* @throws IOException The ping command could probably not be found
*/
public Optional<PingResult> nativeARPPing(@Nullable ArpPingUtilEnum arpingTool, @Nullable String arpUtilPath,
String interfaceName, String ipV4address, int timeoutInMS) throws IOException, InterruptedException {
double execStartTimeInMS = System.currentTimeMillis();
public @Nullable PingResult nativeArpPing(@Nullable ArpPingUtilEnum arpingTool, @Nullable String arpUtilPath,
String interfaceName, String ipV4address, Duration timeout) throws IOException, InterruptedException {
if (arpUtilPath == null || arpingTool == null || !arpingTool.canProceed) {
return Optional.empty();
return null;
}
Instant execStartTime = Instant.now();
Process proc;
if (arpingTool == ArpPingUtilEnum.THOMAS_HABERT_ARPING_WITHOUT_TIMEOUT) {
proc = new ProcessBuilder(arpUtilPath, "-c", "1", "-i", interfaceName, ipV4address).start();
} 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();
} 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 {
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();
}
// 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.
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.
*
* @param timeoutInMS A timeout in milliseconds
* @param timeout the timeout before the call aborts
* @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) {
double execStartTimeInMS = System.currentTimeMillis();
public PingResult javaPing(Duration timeout, InetAddress destinationAddress) {
Instant execStartTime = Instant.now();
boolean success = false;
try {
if (destinationAddress.isReachable(timeoutInMS)) {
return Optional.of(new PingResult(true, System.currentTimeMillis() - execStartTimeInMS));
} else {
return Optional.of(new PingResult(false, System.currentTimeMillis() - execStartTimeInMS));
if (destinationAddress.isReachable((int) timeout.toMillis())) {
success = true;
}
} 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
* 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 {
int port = 5353;
try (DatagramSocket s = new DatagramSocket()) {
byte[] buffer = new byte[0];
s.send(new DatagramPacket(buffer, buffer.length, address, 5353));
} catch (PortUnreachableException ignored) {
// We ignore the port unreachable error
s.send(new DatagramPacket(buffer, buffer.length, address, port));
logger.trace("Sent packet to {}:{} to wake up iOS device", address, port);
} 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;
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.Nullable;
/**
* Information about the ping result.
@ -25,16 +28,16 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
public class PingResult {
private boolean success;
private Optional<Double> responseTimeInMS = Optional.empty();
private double executionTimeInMS;
private @Nullable Duration responseTime;
private Duration executionTime;
/**
* @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.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.
*/
public Optional<Double> getResponseTimeInMS() {
return responseTimeInMS;
public @Nullable Duration getResponseTime() {
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) {
this.responseTimeInMS = Optional.of(responseTimeInMS);
public void setResponseTime(@Nullable Duration responseTime) {
this.responseTime = responseTime;
}
@Override
public String toString() {
return "PingResult{" + "success=" + success + ", responseTimeInMS=" + responseTimeInMS + ", executionTimeInMS="
+ executionTimeInMS + '}';
Duration responseTime = this.responseTime;
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() {
return executionTimeInMS;
public Duration getExecutionTime() {
return executionTime;
}
}

View File

@ -19,14 +19,13 @@ import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.io.IOException;
import java.net.UnknownHostException;
import java.util.Optional;
import java.time.Duration;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ScheduledExecutorService;
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.Test;
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.MockitoSettings;
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.ArpPingUtilEnum;
import org.openhab.binding.network.internal.utils.NetworkUtils.IpPingMethodEnum;
@ -49,32 +46,30 @@ import org.openhab.binding.network.internal.utils.PingResult;
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
@NonNullByDefault
public class PresenceDetectionTest {
private static final long CACHETIME = 2000L;
private PresenceDetection subject;
private @NonNullByDefault({}) PresenceDetection subject;
private @Mock Consumer<PresenceDetectionValue> callback;
private @Mock ExecutorService executorService;
private @Mock PresenceDetectionListener listener;
private @Mock NetworkUtils networkUtils;
private @Mock @NonNullByDefault({}) Consumer<PresenceDetectionValue> callback;
private @Mock @NonNullByDefault({}) ExecutorService detectionExecutorService;
private @Mock @NonNullByDefault({}) ScheduledExecutorService scheduledExecutorService;
private @Mock @NonNullByDefault({}) PresenceDetectionListener listener;
private @Mock @NonNullByDefault({}) NetworkUtils networkUtils;
@BeforeEach
public void setUp() throws UnknownHostException {
public void setUp() {
// Mock an interface
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();
subject = spy(new PresenceDetection(listener, (int) CACHETIME));
subject = spy(new PresenceDetection(listener, scheduledExecutorService, Duration.ofSeconds(2)));
subject.networkUtils = networkUtils;
subject.cache = spy(new ExpiringCacheAsync<>(CACHETIME, () -> {
subject.performPresenceDetection(false);
}));
// Set a useful configuration. The default presenceDetection is a no-op.
subject.setHostname("127.0.0.1");
subject.setTimeout(300);
subject.setTimeout(Duration.ofMillis(300));
subject.setUseDhcpSniffing(false);
subject.setIOSDevice(true);
subject.setServicePorts(Set.of(1010));
@ -84,83 +79,103 @@ public class PresenceDetectionTest {
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.
// We will check if they spawn and return in time.
@Test
public void threadCountTest() {
assertNull(subject.executorService);
assertNull(subject.detectionExecutorService);
doNothing().when(subject).performARPping(any());
doNothing().when(subject).performJavaPing();
doNothing().when(subject).performSystemPing();
doNothing().when(subject).performServicePing(anyInt());
doNothing().when(subject).performArpPing(any(), any());
doNothing().when(subject).performJavaPing(any());
doNothing().when(subject).performSystemPing(any());
doNothing().when(subject).performServicePing(any(), anyInt());
subject.performPresenceDetection(false);
subject.getValue(callback -> {
});
// Thread count: ARP + ICMP + 1*TCP
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));
assertNull(subject.executorService);
assertNull(subject.detectionExecutorService);
}
@Test
public void partialAndFinalCallbackTests() throws InterruptedException, IOException {
doReturn(Optional.of(new PingResult(true, 10))).when(networkUtils).nativePing(eq(IpPingMethodEnum.WINDOWS_PING),
anyString(), anyInt());
doReturn(Optional.of(new PingResult(true, 10))).when(networkUtils)
.nativeARPPing(eq(ArpPingUtilEnum.IPUTILS_ARPING), anyString(), anyString(), any(), anyInt());
doReturn(Optional.of(new PingResult(true, 10))).when(networkUtils).servicePing(anyString(), anyInt(), anyInt());
PingResult pingResult = new PingResult(true, Duration.ofMillis(10));
doReturn(pingResult).when(networkUtils).nativePing(eq(IpPingMethodEnum.WINDOWS_PING), anyString(), any());
doReturn(pingResult).when(networkUtils).nativeArpPing(eq(ArpPingUtilEnum.IPUTILS_ARPING), anyString(),
anyString(), any(), any());
doReturn(pingResult).when(networkUtils).servicePing(anyString(), anyInt(), any());
assertTrue(subject.performPresenceDetection(false));
subject.waitForPresenceDetection();
doReturn(detectionExecutorService).when(subject).getThreadsFor(anyInt());
verify(subject, times(0)).performJavaPing();
verify(subject).performSystemPing();
verify(subject).performARPping(any());
verify(subject).performServicePing(anyInt());
subject.performPresenceDetection();
assertThat(subject.detectionChecks, is(3));
// 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());
ArgumentCaptor<PresenceDetectionValue> capture = ArgumentCaptor.forClass(PresenceDetectionValue.class);
verify(listener, times(1)).finalDetectionResult(capture.capture());
ArgumentCaptor<PresenceDetectionValue> pdvCapture = ArgumentCaptor.forClass(PresenceDetectionValue.class);
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
public void cacheTest() throws InterruptedException, IOException {
doReturn(Optional.of(new PingResult(true, 10))).when(networkUtils).nativePing(eq(IpPingMethodEnum.WINDOWS_PING),
anyString(), anyInt());
doReturn(Optional.of(new PingResult(true, 10))).when(networkUtils)
.nativeARPPing(eq(ArpPingUtilEnum.IPUTILS_ARPING), anyString(), anyString(), any(), anyInt());
doReturn(Optional.of(new PingResult(true, 10))).when(networkUtils).servicePing(anyString(), anyInt(), anyInt());
PingResult pingResult = new PingResult(true, Duration.ofMillis(10));
doReturn(pingResult).when(networkUtils).nativePing(eq(IpPingMethodEnum.WINDOWS_PING), anyString(), any());
doReturn(pingResult).when(networkUtils).nativeArpPing(eq(ArpPingUtilEnum.IPUTILS_ARPING), anyString(),
anyString(), any(), any());
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
assertTrue(subject.cache.isExpired());
// Get value will issue a PresenceDetection internally.
subject.getValue(callback);
verify(subject).performPresenceDetection(eq(false));
assertNotNull(subject.executorService);
verify(subject).performPresenceDetection();
assertNotNull(subject.detectionExecutorService);
// There should be no straight callback yet
verify(callback, times(0)).accept(any());
// Perform the different presence detection threads now
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()) {
r.run();
}
// "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,
// the getValue() consumers get the fastest response possible, and only once.
@ -175,34 +190,4 @@ public class PresenceDetectionTest {
subject.getValue(callback);
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.junit.jupiter.api.Assertions.*;
import java.time.Duration;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.Test;
/**
@ -23,47 +26,40 @@ import org.junit.jupiter.api.Test;
*
* @author David Graeff - Initial contribution
*/
@NonNullByDefault
public class PresenceDetectionValuesTest {
@Test
public void updateLatencyTests() {
PresenceDetectionValue value = new PresenceDetectionValue("127.0.0.1", 10.0);
assertThat(value.getLowestLatency(), is(10.0));
value.updateLatency(20.0);
assertThat(value.getLowestLatency(), is(10.0));
value.updateLatency(0.0);
assertThat(value.getLowestLatency(), is(10.0));
value.updateLatency(5.0);
assertThat(value.getLowestLatency(), is(5.0));
PresenceDetectionValue value = new PresenceDetectionValue("127.0.0.1", Duration.ofMillis(10));
assertThat(value.getLowestLatency(), is(Duration.ofMillis(10)));
value.updateLatency(Duration.ofMillis(20));
assertThat(value.getLowestLatency(), is(Duration.ofMillis(10)));
value.updateLatency(Duration.ofMillis(0));
assertThat(value.getLowestLatency(), is(Duration.ofMillis(10)));
value.updateLatency(Duration.ofMillis(5));
assertThat(value.getLowestLatency(), is(Duration.ofMillis(5)));
}
@Test
public void tcpTests() {
PresenceDetectionValue value = new PresenceDetectionValue("127.0.0.1", 10.0);
assertFalse(value.isTCPServiceReachable());
value.addReachableTcpService(1010);
assertThat(value.getReachableTCPports(), hasItem(1010));
value.addType(PresenceDetectionType.TCP_CONNECTION);
assertTrue(value.isTCPServiceReachable());
PresenceDetectionValue value = new PresenceDetectionValue("127.0.0.1", Duration.ofMillis(10));
assertFalse(value.isTcpServiceReachable());
value.addReachableTcpPort(1010);
assertThat(value.getReachableTcpPorts(), hasItem(1010));
value.addReachableDetectionType(PresenceDetectionType.TCP_CONNECTION);
assertTrue(value.isTcpServiceReachable());
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
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());
value.addType(PresenceDetectionType.ICMP_PING);
value.addReachableDetectionType(PresenceDetectionType.ICMP_PING);
assertTrue(value.isPingReachable());
value.addType(PresenceDetectionType.ARP_PING);
value.addType(PresenceDetectionType.TCP_CONNECTION);
value.addReachableDetectionType(PresenceDetectionType.ARP_PING);
value.addReachableDetectionType(PresenceDetectionType.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.Mockito.*;
import java.time.Duration;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ -39,16 +41,17 @@ import org.openhab.core.config.discovery.DiscoveryResult;
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
@NonNullByDefault
public class DiscoveryTest {
private final String ip = "127.0.0.1";
private @Mock PresenceDetectionValue value;
private @Mock DiscoveryListener listener;
private @Mock @NonNullByDefault({}) PresenceDetectionValue value;
private @Mock @NonNullByDefault({}) DiscoveryListener listener;
@BeforeEach
public void setUp() {
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.getSuccessfulDetectionTypes()).thenReturn("TESTMETHOD");
}
@ -62,7 +65,7 @@ public class DiscoveryTest {
// Ping device
when(value.isPingReachable()).thenReturn(true);
when(value.isTCPServiceReachable()).thenReturn(false);
when(value.isTcpServiceReachable()).thenReturn(false);
d.partialDetectionResult(value);
verify(listener).thingDiscovered(any(), result.capture());
DiscoveryResult dresult = result.getValue();
@ -79,8 +82,8 @@ public class DiscoveryTest {
// TCP device
when(value.isPingReachable()).thenReturn(false);
when(value.isTCPServiceReachable()).thenReturn(true);
when(value.getReachableTCPports()).thenReturn(List.of(1010));
when(value.isTcpServiceReachable()).thenReturn(true);
when(value.getReachableTcpPorts()).thenReturn(List.of(1010));
d.partialDetectionResult(value);
verify(listener).thingDiscovered(any(), result.capture());
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.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.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ -50,11 +55,13 @@ import org.openhab.core.thing.binding.ThingHandlerCallback;
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
@NonNullByDefault
public class NetworkHandlerTest extends JavaTest {
private ThingUID thingUID = new ThingUID("network", "ttype", "ping");
private @Mock ThingHandlerCallback callback;
private @Mock Thing thing;
private @Mock @NonNullByDefault({}) ThingHandlerCallback callback;
private @Mock @NonNullByDefault({}) ScheduledExecutorService scheduledExecutorService;
private @Mock @NonNullByDefault({}) Thing thing;
@BeforeEach
public void setUp() {
@ -76,17 +83,18 @@ public class NetworkHandlerTest extends JavaTest {
conf.put(NetworkBindingConstants.PARAMETER_TIMEOUT, 1234);
return conf;
});
PresenceDetection presenceDetection = spy(new PresenceDetection(handler, 2000));
PresenceDetection presenceDetection = spy(
new PresenceDetection(handler, scheduledExecutorService, Duration.ofSeconds(2)));
// Mock start/stop automatic refresh
doNothing().when(presenceDetection).startAutomaticRefresh(any());
doNothing().when(presenceDetection).startAutomaticRefresh();
doNothing().when(presenceDetection).stopAutomaticRefresh();
handler.initialize(presenceDetection);
assertThat(handler.retries, is(10));
assertThat(presenceDetection.getHostname(), is("127.0.0.1"));
assertThat(presenceDetection.getServicePorts().iterator().next(), is(8080));
assertThat(presenceDetection.getRefreshInterval(), is(101010L));
assertThat(presenceDetection.getTimeout(), is(1234));
assertThat(presenceDetection.getRefreshInterval(), is(Duration.ofMillis(101010)));
assertThat(presenceDetection.getTimeout(), is(Duration.ofMillis(1234)));
}
@Test
@ -101,7 +109,7 @@ public class NetworkHandlerTest extends JavaTest {
conf.put(NetworkBindingConstants.PARAMETER_HOSTNAME, "127.0.0.1");
return conf;
});
handler.initialize(new PresenceDetection(handler, 2000));
handler.initialize(new PresenceDetection(handler, scheduledExecutorService, Duration.ofSeconds(2)));
// Check that we are offline
ArgumentCaptor<ThingStatusInfo> statusInfoCaptor = ArgumentCaptor.forClass(ThingStatusInfo.class);
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");
return conf;
});
PresenceDetection presenceDetection = spy(new PresenceDetection(handler, 2000));
PresenceDetection presenceDetection = spy(
new PresenceDetection(handler, scheduledExecutorService, Duration.ofSeconds(2)));
// Mock start/stop automatic refresh
doNothing().when(presenceDetection).startAutomaticRefresh(any());
doNothing().when(presenceDetection).startAutomaticRefresh();
doNothing().when(presenceDetection).stopAutomaticRefresh();
doReturn(Instant.now()).when(presenceDetection).getLastSeen();
handler.initialize(presenceDetection);
// Check that we are online
@ -133,7 +143,7 @@ public class NetworkHandlerTest extends JavaTest {
// Mock result value
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.getSuccessfulDetectionTypes()).thenReturn("TESTMETHOD");
@ -146,7 +156,6 @@ public class NetworkHandlerTest extends JavaTest {
eq(new QuantityType<>("10.0 ms")));
// Final result affects the LASTSEEN channel
when(value.isFinished()).thenReturn(true);
handler.finalDetectionResult(value);
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;
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.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";
// Act
Optional<Double> resultLatency = latencyParser.parseLatency(input);
Duration resultLatency = latencyParser.parseLatency(input);
// Assert
assertTrue(resultLatency.isPresent());
assertEquals(1.225, resultLatency.get(), 0);
assertNotNull(resultLatency);
assertEquals(1.225, durationToMillis(resultLatency), 0);
}
@Test
@ -54,10 +55,10 @@ public class LatencyParserTest {
for (String inputLine : inputLines) {
// Act
Optional<Double> resultLatency = latencyParser.parseLatency(inputLine);
Duration resultLatency = latencyParser.parseLatency(inputLine);
// 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";
// Act
Optional<Double> resultLatency = latencyParser.parseLatency(input);
Duration resultLatency = latencyParser.parseLatency(input);
// Assert
assertTrue(resultLatency.isPresent());
assertEquals(2, resultLatency.get(), 0);
assertNotNull(resultLatency);
assertEquals(2, durationToMillis(resultLatency), 0);
}
}