this.preferResponseTimeAsLatency = newConfiguration.preferResponseTimeAsLatency;
NetworkUtils networkUtils = new NetworkUtils();
- this.arpPingUtilMethod = networkUtils.determineNativeARPpingMethod(arpPingToolPath);
+ this.arpPingUtilMethod = networkUtils.determineNativeArpPingMethod(arpPingToolPath);
notifyListeners();
}
*/
package org.openhab.binding.network.internal;
-import java.util.HashSet;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
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";
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);
- }
}
*/
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;
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);
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() {
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) {
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) {
}
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);
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) {
}
/**
- * 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) {
}
/**
- * 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) {
}
/**
- * 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() {
}
/**
- * 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) {
}
/**
- * 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 @Nullable Instant getLastSeen() {
+ return lastSeen;
+ }
+
+ /**
+ * Gets the presence detection value synchronously as a {@link PresenceDetectionValue}.
+ * <p>
+ * The value is only updated if the cached value has not expired.
*/
- public long getLastSeen() {
- return lastSeenInMS;
+ public PresenceDetectionValue getValue() throws InterruptedException, ExecutionException {
+ return cache.getValue(this::performPresenceDetection).get();
}
/**
- * Return asynchronously the value of the presence detection as a PresenceDetectionValue.
+ * 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;
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();
+ }
- /**
- * 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);
+ logger.debug("Sending listener final result: {}", pdv);
+ updateListener.finalDetectionResult(pdv);
- if (!v.isReachable()) {
- // if target can't be reached, check if name resolution need to be updated
- destination.invalidateValue();
- }
- updateListener.finalDetectionResult(v);
- }
+ detectionExecutorService.shutdownNow();
+ this.detectionExecutorService = null;
+ detectionChecks = 0;
- /**
- * 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();
+ return pdv;
+ }, scheduledExecutorService);
}
/**
- * Waits for the presence detection threads to finish. Returns immediately
- * if no presence detection is performed right now.
+ * 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.
+ *
+ * @param type the detection type
+ * @param latency the latency
*/
- 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;
- }
+ synchronized PresenceDetectionValue updateReachable(PresenceDetectionType type, Duration latency) {
+ PresenceDetectionValue pdv = new PresenceDetectionValue(hostname, latency);
+ updateReachable(pdv, type, latency);
+ return pdv;
}
/**
- * If the cached PresenceDetectionValue has not expired yet, the cached version
- * is returned otherwise a new reachable PresenceDetectionValue is created with
- * a latency of 0.
- *
- * It is safe to call this method from multiple threads. The returned PresenceDetectionValue
- * might be still be altered in other threads though.
+ * 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 type The detection type
- * @return The non expired or a new instance of PresenceDetectionValue.
+ * @param pdv the {@link PresenceDetectionValue} to update
+ * @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();
+ 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);
}
- v.updateLatency(latency);
- v.addType(type);
- cache.setValue(v);
- return v;
+ logger.debug("Sending listener partial result: {}", pdv);
+ updateListener.partialDetectionResult(pdv);
}
- protected void performServicePing(int tcpPort) {
+ 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);
- }
- });
+
+ 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);
}
- } 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
+ * If it is an iOS device, the {@link NetworkUtils#wakeUpIOS(InetAddress)} 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.
+ * @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(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);
- }
+ protected void performArpPing(PresenceDetectionValue pdv, String interfaceName) {
+ logger.trace("Perform ARP ping presence detection for {} on interface: {}", hostname, interfaceName);
- 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
- }
+ withDestinationAddress(destinationAddress -> {
+ try {
+ if (iosDevice) {
+ networkUtils.wakeUpIOS(destinationAddress);
+ Thread.sleep(50);
+ }
+
+ 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
+ }
+ });
}
/**
- * 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
+ * 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)
+ *
+ * @see InetAddress#isReachable(int)
*/
- protected void performJavaPing() {
- logger.trace("Perform java ping presence detection for {}", hostname);
-
- InetAddress destinationAddress = destination.getValue();
- if (destinationAddress == null) {
- return;
- }
+ protected void performJavaPing(PresenceDetectionValue pdv) {
+ logger.trace("Perform Java ping presence detection for {}", hostname);
- networkUtils.javaPing(timeoutInMS, destinationAddress).ifPresent(o -> {
- if (o.isSuccess()) {
- PresenceDetectionValue v = updateReachableValue(PresenceDetectionType.ICMP_PING,
- getLatency(o, preferResponseTimeAsLatency));
- updateListener.partialDetectionResult(v);
+ withDestinationAddress(destinationAddress -> {
+ PingResult pingResult = networkUtils.javaPing(timeout, destinationAddress);
+ if (pingResult.isSuccess()) {
+ updateReachable(pdv, ICMP_PING, getLatency(pingResult));
}
});
}
- protected void performSystemPing() {
- try {
- logger.trace("Perform native ping presence detection for {}", hostname);
- InetAddress destinationAddress = destination.getValue();
- if (destinationAddress == null) {
- return;
- }
+ protected void performSystemPing(PresenceDetectionValue pdv) {
+ logger.trace("Perform native ping presence detection for {}", hostname);
- 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 {
+ 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 ip {}", hostname, e);
- } catch (InterruptedException e) {
- // This can be ignored, the thread will end anyway
- }
+ } 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 double getLatency(PingResult pingResult, boolean preferResponseTimeAsLatency) {
- logger.debug("Getting latency from ping result {} using latency mode {}", pingResult,
+ 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;
}
/**
- * 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);
*/
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;
*/
@NonNullByDefault
public class PresenceDetectionValue {
- private double latency;
- private boolean detectionIsFinished;
- private final Set<PresenceDetectionType> reachableByType = new TreeSet<>();
- private final List<Integer> tcpServiceReachable = new ArrayList<>();
- private final String hostAddress;
- /**
- * Returns true if the target is reachable by any means.
- */
- public boolean isReachable() {
- return latency >= 0;
- }
+ public static final Duration UNREACHABLE = Duration.ofMillis(-1);
- /**
- * 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;
- }
+ private final String hostAddress;
+ private Duration latency;
- /**
- * Return a string of comma separated successful presence detection types.
- */
- public String getSuccessfulDetectionTypes() {
- return reachableByType.stream().map(v -> v.name()).collect(Collectors.joining(", "));
- }
+ private final Set<PresenceDetectionType> reachableDetectionTypes = new TreeSet<>();
+ private final List<Integer> reachableTcpPorts = new ArrayList<>();
/**
- * Return the reachable tcp ports of the presence detection value.
- * Thread safe.
+ * Create a new {@link PresenceDetectionValue} with an initial latency.
+ *
+ * @param hostAddress The target IP
+ * @param latency The ping latency. Can be <0 if the device is not reachable.
*/
- public List<Integer> getReachableTCPports() {
- synchronized (tcpServiceReachable) {
- List<Integer> copy = new ArrayList<>();
- copy.addAll(tcpServiceReachable);
- return copy;
- }
+ PresenceDetectionValue(String hostAddress, Duration latency) {
+ this.hostAddress = hostAddress;
+ this.latency = latency;
}
/**
- * Return true if the presence detection is fully completed (no running
- * threads anymore).
+ * Return the host address of the presence detection result object.
*/
- public boolean isFinished() {
- return detectionIsFinished;
+ public String getHostAddress() {
+ return hostAddress;
}
- ////// Package private methods //////
-
/**
- * Create a new 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.
+ * Return the ping latency, {@value #UNREACHABLE} if not reachable. Can be 0 if
+ * no specific latency is known but the device is still reachable.
*/
- PresenceDetectionValue(String hostAddress, double latency) {
- this.hostAddress = hostAddress;
- this.latency = latency;
+ public Duration getLowestLatency() {
+ return latency;
}
/**
- * Add a successful PresenceDetectionType.
- *
- * @param type A PresenceDetectionType.
+ * Returns <code>true</code> if the target is reachable by any means.
*/
- void addType(PresenceDetectionType type) {
- reachableByType.add(type);
+ public boolean isReachable() {
+ return !UNREACHABLE.equals(latency);
}
/**
* 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.
+ * @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(double newLatency) {
- if (newLatency < 0) {
+ boolean updateLatency(Duration newLatency) {
+ if (newLatency.isNegative()) {
throw new IllegalArgumentException(
"Latency must be >=0. Create a new PresenceDetectionValue for a not reachable device!");
- }
- if (newLatency > 0 && (latency == 0 || newLatency < latency)) {
+ } else if (newLatency.isZero()) {
+ return false;
+ } else if (!isReachable() || latency.isZero() || newLatency.compareTo(latency) < 0) {
latency = newLatency;
return true;
}
}
/**
- * Add a reachable tcp port to this presence detection result value object.
- * Thread safe.
+ * Add a successful {@link PresenceDetectionType}.
+ *
+ * @param type a {@link PresenceDetectionType}.
*/
- void addReachableTcpService(int tcpPort) {
- synchronized (tcpServiceReachable) {
- tcpServiceReachable.add(tcpPort);
- }
+ void addReachableDetectionType(PresenceDetectionType type) {
+ reachableDetectionTypes.add(type);
}
/**
- * Mark the result value as final. No modifications should occur after this call.
+ * Return true if the target can be reached by ICMP or ARP pings.
*/
- void setDetectionIsFinished(boolean detectionIsFinished) {
- this.detectionIsFinished = detectionIsFinished;
+ public boolean isPingReachable() {
+ return reachableDetectionTypes.contains(PresenceDetectionType.ARP_PING)
+ || reachableDetectionTypes.contains(PresenceDetectionType.ICMP_PING);
}
/**
- * Return the host address of the presence detection result object.
+ * Return true if the target provides open TCP ports.
*/
- public String getHostAddress() {
- return hostAddress;
+ public boolean isTcpServiceReachable() {
+ return reachableDetectionTypes.contains(PresenceDetectionType.TCP_CONNECTION);
}
/**
- * Return true if the target can be reached by ICMP or ARP pings.
+ * Return a string of comma-separated successful presence detection types.
*/
- public boolean isPingReachable() {
- return reachableByType.contains(PresenceDetectionType.ARP_PING)
- || reachableByType.contains(PresenceDetectionType.ICMP_PING);
+ public String getSuccessfulDetectionTypes() {
+ return reachableDetectionTypes.stream().map(PresenceDetectionType::name).collect(Collectors.joining(", "));
}
/**
- * Return true if the target provides open TCP ports.
+ * 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.
*/
- public boolean isTCPServiceReachable() {
- return reachableByType.contains(PresenceDetectionType.TCP_CONNECTION);
+ 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
+ + "]";
}
}
@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;
}
}
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;
}
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
}
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;
@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);
// 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);
}
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);
}
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);
}
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
}
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<>();
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());
}
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;
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;
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);
}
@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();
}
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();
}
// 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())));
}
/**
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) {
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);
+++ /dev/null
-/**
- * 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;
- }
-}
*/
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;
* 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;
}
}
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;
*/
@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();
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;
}
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;
* @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) {
}
/**
- * 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()));
}
/**
}
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
}
}
/**
- * 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(.*)")) {
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 {
* 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
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()))) {
// 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()));
}
}
/**
* 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);
}
}
}
*/
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.
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;
}
/**
}
/**
- * @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;
}
}
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;
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;
*/
@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));
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());
+
+ doReturn(detectionExecutorService).when(subject).getThreadsFor(anyInt());
- assertTrue(subject.performPresenceDetection(false));
- subject.waitForPresenceDetection();
+ subject.performPresenceDetection();
- verify(subject, times(0)).performJavaPing();
- verify(subject).performSystemPing();
- verify(subject).performARPping(any());
- verify(subject).performServicePing(anyInt());
+ 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.
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));
- }
}
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;
/**
*
* @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"));
}
}
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;
*/
@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");
}
// 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();
// 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();
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;
*/
@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() {
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
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());
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
// 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");
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());
}
+++ /dev/null
-/**
- * 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);
- }
-}
+++ /dev/null
-/**
- * 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;
- }
-}
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;
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
for (String inputLine : inputLines) {
// Act
- Optional<Double> resultLatency = latencyParser.parseLatency(inputLine);
+ Duration resultLatency = latencyParser.parseLatency(inputLine);
// Assert
- assertFalse(resultLatency.isPresent());
+ assertNull(resultLatency);
}
}
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);
}
}