added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.network-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||
|
||||
<feature name="openhab-binding-network" description="Network Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<feature>openhab-core-model-script</feature>
|
||||
<bundle dependency="true">mvn:commons-net/commons-net/3.6</bundle>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.network/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.network.internal.utils.NetworkUtils;
|
||||
import org.openhab.binding.network.internal.utils.NetworkUtils.ArpPingUtilEnum;
|
||||
|
||||
/**
|
||||
* Contains the binding configuration and default values. The field names represent the configuration names,
|
||||
* do not rename them if you don't intend to break the configuration interface.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NetworkBindingConfiguration {
|
||||
|
||||
public Boolean allowSystemPings = true;
|
||||
public Boolean allowDHCPlisten = true;
|
||||
public BigDecimal cacheDeviceStateTimeInMS = BigDecimal.valueOf(2000);
|
||||
public String arpPingToolPath = "arping";
|
||||
public @NonNullByDefault({}) ArpPingUtilEnum arpPingUtilMethod;
|
||||
// For backwards compatibility reasons, the default is to use the ping method execution time as latency value
|
||||
public boolean preferResponseTimeAsLatency = false;
|
||||
|
||||
private List<NetworkBindingConfigurationListener> listeners = new ArrayList<>();
|
||||
|
||||
public void update(NetworkBindingConfiguration newConfiguration) {
|
||||
this.allowSystemPings = newConfiguration.allowSystemPings;
|
||||
this.allowDHCPlisten = newConfiguration.allowDHCPlisten;
|
||||
this.cacheDeviceStateTimeInMS = newConfiguration.cacheDeviceStateTimeInMS;
|
||||
this.arpPingToolPath = newConfiguration.arpPingToolPath;
|
||||
this.preferResponseTimeAsLatency = newConfiguration.preferResponseTimeAsLatency;
|
||||
|
||||
NetworkUtils networkUtils = new NetworkUtils();
|
||||
this.arpPingUtilMethod = networkUtils.determineNativeARPpingMethod(arpPingToolPath);
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
public void addNetworkBindingConfigurationListener(NetworkBindingConfigurationListener listener) {
|
||||
listeners.add(listener);
|
||||
}
|
||||
|
||||
private void notifyListeners() {
|
||||
listeners.forEach(NetworkBindingConfigurationListener::bindingConfigurationChanged);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "NetworkBindingConfiguration{" + "allowSystemPings=" + allowSystemPings + ", allowDHCPlisten="
|
||||
+ allowDHCPlisten + ", cacheDeviceStateTimeInMS=" + cacheDeviceStateTimeInMS + ", arpPingToolPath='"
|
||||
+ arpPingToolPath + '\'' + ", arpPingUtilMethod=" + arpPingUtilMethod + ", preferResponseTimeAsLatency="
|
||||
+ preferResponseTimeAsLatency + '}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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;
|
||||
|
||||
/**
|
||||
* Listener for binding configuration changes.
|
||||
*
|
||||
* @author Andreas Hirsch - Initial contribution
|
||||
*/
|
||||
public interface NetworkBindingConfigurationListener {
|
||||
|
||||
void bindingConfigurationChanged();
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link NetworkBindingConstants} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Marc Mettke - Initial contribution
|
||||
* @author David Gräff - 2016, Add dhcp listen
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NetworkBindingConstants {
|
||||
|
||||
public static final String BINDING_ID = "network";
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID BACKWARDS_COMPATIBLE_DEVICE = new ThingTypeUID(BINDING_ID, "device");
|
||||
public static final ThingTypeUID PING_DEVICE = new ThingTypeUID(BINDING_ID, "pingdevice");
|
||||
public static final ThingTypeUID SERVICE_DEVICE = new ThingTypeUID(BINDING_ID, "servicedevice");
|
||||
public static final ThingTypeUID SPEEDTEST_DEVICE = new ThingTypeUID(BINDING_ID, "speedtest");
|
||||
|
||||
// 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 CHANNEL_RATE_UP = "rateUp";
|
||||
public static final String CHANNEL_RATE_DOWN = "rateDown";
|
||||
public static final String CHANNEL_TEST_START = "testStart";
|
||||
public static final String CHANNEL_TEST_END = "testEnd";
|
||||
|
||||
// List of all Parameters
|
||||
public static final String PARAMETER_HOSTNAME = "hostname";
|
||||
public static final String PARAMETER_RETRY = "retry";
|
||||
public static final String PARAMETER_TIMEOUT = "timeout";
|
||||
public static final String PARAMETER_REFRESH_INTERVAL = "refreshInterval";
|
||||
public static final String PARAMETER_PORT = "port";
|
||||
|
||||
public static final String PROPERTY_DHCP_STATE = "dhcp_state";
|
||||
public static final String PROPERTY_ARP_STATE = "arp_state";
|
||||
public static final String PROPERTY_ICMP_STATE = "icmp_state";
|
||||
public static final String PROPERTY_PRESENCE_DETECTION_TYPE = "presence_detection_type";
|
||||
public static final String PROPERTY_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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Contains the handler configuration and default values. The field names represent the configuration names,
|
||||
* do not rename them if you don't intend to break the configuration interface.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NetworkHandlerConfiguration {
|
||||
public String hostname = "";
|
||||
public String macAddress = "";
|
||||
public @Nullable Integer port;
|
||||
public Integer retry = 1;
|
||||
public Integer refreshInterval = 60000;
|
||||
public Integer timeout = 5000;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.network.internal.handler.NetworkHandler;
|
||||
import org.openhab.binding.network.internal.handler.SpeedTestHandler;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerFactory;
|
||||
import org.osgi.service.component.ComponentContext;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Deactivate;
|
||||
import org.osgi.service.component.annotations.Modified;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The handler factory retrieves the binding configuration and is responsible for creating
|
||||
* PING_DEVICE and SERVICE_DEVICE handlers.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.network")
|
||||
public class NetworkHandlerFactory extends BaseThingHandlerFactory {
|
||||
final NetworkBindingConfiguration configuration = new NetworkBindingConfiguration();
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(NetworkHandlerFactory.class);
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return NetworkBindingConstants.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
// The activate component call is used to access the bindings configuration
|
||||
@Activate
|
||||
protected void activate(ComponentContext componentContext, Map<String, Object> config) {
|
||||
super.activate(componentContext);
|
||||
modified(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deactivate
|
||||
protected void deactivate(ComponentContext componentContext) {
|
||||
super.deactivate(componentContext);
|
||||
}
|
||||
|
||||
@Modified
|
||||
protected void modified(Map<String, Object> config) {
|
||||
// We update instead of replace the configuration object, so that if the user updates the
|
||||
// configuration, the values are automatically available in all handlers. Because they all
|
||||
// share the same instance.
|
||||
configuration.update(new Configuration(config).as(NetworkBindingConfiguration.class));
|
||||
logger.debug("Updated binding configuration to {}", configuration);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (thingTypeUID.equals(NetworkBindingConstants.PING_DEVICE)
|
||||
|| thingTypeUID.equals(NetworkBindingConstants.BACKWARDS_COMPATIBLE_DEVICE)) {
|
||||
return new NetworkHandler(thing, false, configuration);
|
||||
} else if (thingTypeUID.equals(NetworkBindingConstants.SERVICE_DEVICE)) {
|
||||
return new NetworkHandler(thing, true, configuration);
|
||||
} else if (thingTypeUID.equals(NetworkBindingConstants.SPEEDTEST_DEVICE)) {
|
||||
return new SpeedTestHandler(thing);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,657 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.InetAddress;
|
||||
import java.net.SocketException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.network.internal.dhcp.DHCPListenService;
|
||||
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.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class PresenceDetection implements IPRequestReceivedCallback {
|
||||
|
||||
public static final double NOT_REACHABLE = -1;
|
||||
public static final int DESTINATION_TTL = 300 * 1000; // in ms, 300 s
|
||||
|
||||
NetworkUtils networkUtils = new NetworkUtils();
|
||||
private final Logger logger = LoggerFactory.getLogger(PresenceDetection.class);
|
||||
|
||||
/// Configuration variables
|
||||
private boolean useDHCPsniffing = false;
|
||||
private String arpPingState = "Disabled";
|
||||
private String ipPingState = "Disabled";
|
||||
protected String arpPingUtilPath = "";
|
||||
protected ArpPingUtilEnum arpPingMethod = ArpPingUtilEnum.UNKNOWN_TOOL;
|
||||
protected @Nullable IpPingMethodEnum pingMethod = null;
|
||||
private boolean iosDevice;
|
||||
private Set<Integer> tcpPorts = new HashSet<>();
|
||||
|
||||
private long refreshIntervalInMS = 60000;
|
||||
private int timeoutInMS = 5000;
|
||||
private long lastSeenInMS;
|
||||
|
||||
private @NonNullByDefault({}) String hostname;
|
||||
private @NonNullByDefault({}) ExpiringCache<@Nullable InetAddress> destination;
|
||||
private @Nullable InetAddress cachedDestination = null;
|
||||
|
||||
public boolean preferResponseTimeAsLatency;
|
||||
|
||||
/// State variables (cannot be final because of test dependency injections)
|
||||
ExpiringCacheAsync<PresenceDetectionValue> cache;
|
||||
private final PresenceDetectionListener updateListener;
|
||||
private @Nullable ScheduledFuture<?> refreshJob;
|
||||
protected @Nullable ExecutorService executorService;
|
||||
private String dhcpState = "off";
|
||||
Integer currentCheck = 0;
|
||||
int detectionChecks;
|
||||
|
||||
public PresenceDetection(final PresenceDetectionListener updateListener, int cacheDeviceStateTimeInMS)
|
||||
throws IllegalArgumentException {
|
||||
this.updateListener = updateListener;
|
||||
cache = new ExpiringCacheAsync<>(cacheDeviceStateTimeInMS, () -> {
|
||||
performPresenceDetection(false);
|
||||
});
|
||||
}
|
||||
|
||||
public @Nullable String getHostname() {
|
||||
return hostname;
|
||||
}
|
||||
|
||||
public Set<Integer> getServicePorts() {
|
||||
return tcpPorts;
|
||||
}
|
||||
|
||||
public long getRefreshInterval() {
|
||||
return refreshIntervalInMS;
|
||||
}
|
||||
|
||||
public int getTimeout() {
|
||||
return timeoutInMS;
|
||||
}
|
||||
|
||||
public void setHostname(String hostname) {
|
||||
this.hostname = hostname;
|
||||
this.destination = new ExpiringCache<>(DESTINATION_TTL, () -> {
|
||||
try {
|
||||
InetAddress destinationAddress = InetAddress.getByName(hostname);
|
||||
if (!destinationAddress.equals(cachedDestination)) {
|
||||
logger.trace("host name resolved to other address, (re-)setup presence detection");
|
||||
setUseArpPing(true, destinationAddress);
|
||||
if (useDHCPsniffing) {
|
||||
if (cachedDestination != null) {
|
||||
disableDHCPListen(cachedDestination);
|
||||
}
|
||||
enableDHCPListen(destinationAddress);
|
||||
}
|
||||
cachedDestination = destinationAddress;
|
||||
}
|
||||
return destinationAddress;
|
||||
} catch (UnknownHostException e) {
|
||||
logger.trace("hostname resolution failed");
|
||||
if (cachedDestination != null) {
|
||||
disableDHCPListen(cachedDestination);
|
||||
cachedDestination = null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void setServicePorts(Set<Integer> ports) {
|
||||
this.tcpPorts = ports;
|
||||
}
|
||||
|
||||
public void setUseDhcpSniffing(boolean enable) {
|
||||
this.useDHCPsniffing = enable;
|
||||
}
|
||||
|
||||
public void setRefreshInterval(long refreshInterval) {
|
||||
this.refreshIntervalInMS = refreshInterval;
|
||||
}
|
||||
|
||||
public void setTimeout(int timeout) {
|
||||
this.timeoutInMS = timeout;
|
||||
}
|
||||
|
||||
public void setPreferResponseTimeAsLatency(boolean preferResponseTimeAsLatency) {
|
||||
this.preferResponseTimeAsLatency = 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.
|
||||
*
|
||||
* @param useSystemPing Set to true to use a system ping method, false to use java ping and null to disable ICMP
|
||||
* pings.
|
||||
*/
|
||||
public void setUseIcmpPing(@Nullable Boolean useSystemPing) {
|
||||
if (useSystemPing == null) {
|
||||
ipPingState = "Disabled";
|
||||
pingMethod = null;
|
||||
} else if (useSystemPing) {
|
||||
final IpPingMethodEnum pingMethod = networkUtils.determinePingMethod();
|
||||
this.pingMethod = pingMethod;
|
||||
ipPingState = pingMethod == IpPingMethodEnum.JAVA_PING ? "System ping feature test failed. Using Java ping"
|
||||
: pingMethod.name();
|
||||
} else {
|
||||
pingMethod = IpPingMethodEnum.JAVA_PING;
|
||||
ipPingState = "Java ping";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables or disables ARP pings. Will be automatically disabled if the destination
|
||||
* is not an IPv4 address. If the feature test for the native arping utility fails,
|
||||
* it will be disabled as well.
|
||||
*
|
||||
* @param enable Enable or disable ARP ping
|
||||
* @param arpPingUtilPath c
|
||||
*/
|
||||
private void setUseArpPing(boolean enable, @Nullable InetAddress destinationAddress) {
|
||||
if (!enable || arpPingUtilPath.isEmpty()) {
|
||||
arpPingState = "Disabled";
|
||||
arpPingMethod = ArpPingUtilEnum.UNKNOWN_TOOL;
|
||||
return;
|
||||
} else if (destinationAddress == null || !(destinationAddress instanceof Inet4Address)) {
|
||||
arpPingState = "Destination is not a valid IPv4 address";
|
||||
arpPingMethod = ArpPingUtilEnum.UNKNOWN_TOOL;
|
||||
return;
|
||||
}
|
||||
|
||||
switch (arpPingMethod) {
|
||||
case UNKNOWN_TOOL: {
|
||||
arpPingState = "Unknown arping tool";
|
||||
break;
|
||||
}
|
||||
case THOMAS_HABERT_ARPING: {
|
||||
arpPingState = "Arping tool by Thomas Habets";
|
||||
break;
|
||||
}
|
||||
case THOMAS_HABERT_ARPING_WITHOUT_TIMEOUT: {
|
||||
arpPingState = "Arping tool by Thomas Habets (old version)";
|
||||
break;
|
||||
}
|
||||
case ELI_FULKERSON_ARP_PING_FOR_WINDOWS: {
|
||||
arpPingState = "Eli Fulkerson ARPing tool for Windows";
|
||||
break;
|
||||
}
|
||||
case IPUTILS_ARPING: {
|
||||
arpPingState = "Ipuitls Arping";
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* sets the path to arp ping
|
||||
*
|
||||
* @param enable Enable or disable ARP ping
|
||||
* @param arpPingUtilPath enableDHCPListen(useDHCPsniffing);
|
||||
*/
|
||||
public void setUseArpPing(boolean enable, String arpPingUtilPath, ArpPingUtilEnum arpPingUtilMethod) {
|
||||
setUseArpPing(enable, destination.getValue());
|
||||
this.arpPingUtilPath = arpPingUtilPath;
|
||||
this.arpPingMethod = arpPingUtilMethod;
|
||||
}
|
||||
|
||||
public String getArpPingState() {
|
||||
return arpPingState;
|
||||
}
|
||||
|
||||
public String getIPPingState() {
|
||||
return ipPingState;
|
||||
}
|
||||
|
||||
public String getDhcpState() {
|
||||
return dhcpState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true 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() {
|
||||
return iosDevice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to true 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) {
|
||||
iosDevice = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the last seen value in milliseconds based on {@link System.currentTimeMillis()} or 0 if not seen yet.
|
||||
*/
|
||||
public long getLastSeen() {
|
||||
return lastSeenInMS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return asynchronously the value of the presence detection as a PresenceDetectionValue.
|
||||
*
|
||||
* @param callback A callback with the 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);
|
||||
}
|
||||
|
||||
public ExecutorService getThreadsFor(int threadCount) {
|
||||
return Executors.newFixedThreadPool(threadCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* 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)
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
Set<String> interfaceNames = null;
|
||||
|
||||
currentCheck = 0;
|
||||
detectionChecks = tcpPorts.size();
|
||||
if (pingMethod != null) {
|
||||
detectionChecks += 1;
|
||||
}
|
||||
if (arpPingMethod != ArpPingUtilEnum.UNKNOWN_TOOL) {
|
||||
interfaceNames = networkUtils.getInterfaceNames();
|
||||
detectionChecks += interfaceNames.size();
|
||||
}
|
||||
|
||||
if (detectionChecks == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final ExecutorService executorService = getThreadsFor(detectionChecks);
|
||||
this.executorService = executorService;
|
||||
|
||||
for (Integer tcpPort : tcpPorts) {
|
||||
executorService.execute(() -> {
|
||||
Thread.currentThread().setName("presenceDetectionTCP_" + hostname + " " + String.valueOf(tcpPort));
|
||||
performServicePing(tcpPort);
|
||||
checkIfFinished();
|
||||
});
|
||||
}
|
||||
|
||||
// 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(() -> {
|
||||
Thread.currentThread().setName("presenceDetectionARP_" + hostname + " ");
|
||||
// arp-ping.exe tool capable of handling multiple interfaces by itself
|
||||
performARPping("");
|
||||
checkIfFinished();
|
||||
});
|
||||
} else if (interfaceNames != null) {
|
||||
for (final String interfaceName : interfaceNames) {
|
||||
executorService.execute(() -> {
|
||||
Thread.currentThread().setName("presenceDetectionARP_" + hostname + " " + interfaceName);
|
||||
performARPping(interfaceName);
|
||||
checkIfFinished();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ICMP ping
|
||||
if (pingMethod != null) {
|
||||
executorService.execute(() -> {
|
||||
if (pingMethod != IpPingMethodEnum.JAVA_PING) {
|
||||
Thread.currentThread().setName("presenceDetectionICMP_" + hostname);
|
||||
performSystemPing();
|
||||
} else {
|
||||
performJavaPing();
|
||||
}
|
||||
checkIfFinished();
|
||||
});
|
||||
}
|
||||
|
||||
if (waitForDetectionToFinish) {
|
||||
waitForPresenceDetection();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls updateListener.finalDetectionResult() with a final result value.
|
||||
* Safe to be called from different threads. After a call to this method,
|
||||
* the presence detection process is finished and all threads are forcefully
|
||||
* shut down.
|
||||
*/
|
||||
private synchronized void submitFinalResult() {
|
||||
// Do nothing if we are not in a detection process
|
||||
ExecutorService service = executorService;
|
||||
if (service == null) {
|
||||
return;
|
||||
}
|
||||
// Finish the detection process
|
||||
service.shutdownNow();
|
||||
executorService = null;
|
||||
detectionChecks = 0;
|
||||
|
||||
PresenceDetectionValue v;
|
||||
|
||||
// The cache will be expired by now if cache_time < timeoutInMS. But the device might be actually reachable.
|
||||
// Therefore use lastSeenInMS here and not cache.isExpired() to determine if we got a ping response.
|
||||
if (lastSeenInMS + timeoutInMS + 100 < System.currentTimeMillis()) {
|
||||
// We haven't seen the device in the detection process
|
||||
v = new PresenceDetectionValue(hostname, -1);
|
||||
} else {
|
||||
// Make the cache valid again and submit the value.
|
||||
v = cache.getExpiredValue();
|
||||
}
|
||||
cache.setValue(v);
|
||||
|
||||
if (!v.isReachable()) {
|
||||
// if target can't be reached, check if name resolution need to be updated
|
||||
destination.invalidateValue();
|
||||
}
|
||||
updateListener.finalDetectionResult(v);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called after each individual check and increases a check counter.
|
||||
* If the counter equals the total checks,the final result is submitted. This will
|
||||
* happen way before the "timeoutInMS", if all checks were successful.
|
||||
* Thread safe.
|
||||
*/
|
||||
private synchronized void checkIfFinished() {
|
||||
currentCheck += 1;
|
||||
if (currentCheck < detectionChecks) {
|
||||
return;
|
||||
}
|
||||
submitFinalResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the presence detection threads to finish. Returns immediately
|
||||
* if no presence detection is performed right now.
|
||||
*/
|
||||
public void waitForPresenceDetection() {
|
||||
ExecutorService service = executorService;
|
||||
if (service == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// We may get interrupted here by cancelRefreshJob().
|
||||
service.awaitTermination(timeoutInMS + 100, TimeUnit.MILLISECONDS);
|
||||
submitFinalResult();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt(); // Reset interrupt flag
|
||||
service.shutdownNow();
|
||||
executorService = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the cached PresenceDetectionValue has not expired yet, the cached version
|
||||
* is returned otherwise a new reachable PresenceDetectionValue is created with
|
||||
* a latency of 0.
|
||||
*
|
||||
* It is safe to call this method from multiple threads. The returned PresenceDetectionValue
|
||||
* might be still be altered in other threads though.
|
||||
*
|
||||
* @param type The detection type
|
||||
* @return The non expired or a new instance of PresenceDetectionValue.
|
||||
*/
|
||||
synchronized PresenceDetectionValue updateReachableValue(PresenceDetectionType type, double latency) {
|
||||
lastSeenInMS = System.currentTimeMillis();
|
||||
PresenceDetectionValue v;
|
||||
if (cache.isExpired()) {
|
||||
v = new PresenceDetectionValue(hostname, 0);
|
||||
} else {
|
||||
v = cache.getExpiredValue();
|
||||
}
|
||||
v.updateLatency(latency);
|
||||
v.addType(type);
|
||||
cache.setValue(v);
|
||||
return v;
|
||||
}
|
||||
|
||||
protected void performServicePing(int tcpPort) {
|
||||
logger.trace("Perform TCP presence detection for {} on port: {}", hostname, tcpPort);
|
||||
try {
|
||||
InetAddress destinationAddress = destination.getValue();
|
||||
if (destinationAddress != null) {
|
||||
networkUtils.servicePing(destinationAddress.getHostAddress(), tcpPort, timeoutInMS).ifPresent(o -> {
|
||||
if (o.isSuccess()) {
|
||||
PresenceDetectionValue v = updateReachableValue(PresenceDetectionType.TCP_CONNECTION,
|
||||
getLatency(o, preferResponseTimeAsLatency));
|
||||
v.addReachableTcpService(tcpPort);
|
||||
updateListener.partialDetectionResult(v);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// This should not happen and might be a user configuration issue, we log a warning message therefore.
|
||||
logger.warn("Could not create a socket connection", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs an "ARP ping" (ARP request) on the given interface.
|
||||
* If it is an iOS device, the {@see NetworkUtils.wakeUpIOS()} method is
|
||||
* called before performing the ARP ping.
|
||||
*
|
||||
* @param interfaceName The interface name. You can request a list of interface names
|
||||
* from {@see NetworkUtils.getInterfaceNames()} for example.
|
||||
*/
|
||||
protected void performARPping(String interfaceName) {
|
||||
try {
|
||||
logger.trace("Perform ARP ping presence detection for {} on interface: {}", hostname, interfaceName);
|
||||
InetAddress destinationAddress = destination.getValue();
|
||||
if (destinationAddress == null) {
|
||||
return;
|
||||
}
|
||||
if (iosDevice) {
|
||||
networkUtils.wakeUpIOS(destinationAddress);
|
||||
Thread.sleep(50);
|
||||
}
|
||||
|
||||
networkUtils.nativeARPPing(arpPingMethod, arpPingUtilPath, interfaceName,
|
||||
destinationAddress.getHostAddress(), timeoutInMS).ifPresent(o -> {
|
||||
if (o.isSuccess()) {
|
||||
PresenceDetectionValue v = updateReachableValue(PresenceDetectionType.ARP_PING,
|
||||
getLatency(o, preferResponseTimeAsLatency));
|
||||
updateListener.partialDetectionResult(v);
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
logger.trace("Failed to execute an arp ping for ip {}", hostname, e);
|
||||
} catch (InterruptedException ignored) {
|
||||
// This can be ignored, the thread will end anyway
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a java ping. It is not recommended to use this, as it is not interruptible,
|
||||
* and will not work on windows systems reliably and will fall back from ICMP pings to
|
||||
* the TCP echo service on port 7 which barely no device or server supports nowadays.
|
||||
* (http://docs.oracle.com/javase/7/docs/api/java/net/InetAddress.html#isReachable%28int%29)
|
||||
*/
|
||||
protected void performJavaPing() {
|
||||
logger.trace("Perform java ping presence detection for {}", hostname);
|
||||
|
||||
InetAddress destinationAddress = destination.getValue();
|
||||
if (destinationAddress == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
networkUtils.javaPing(timeoutInMS, destinationAddress).ifPresent(o -> {
|
||||
if (o.isSuccess()) {
|
||||
PresenceDetectionValue v = updateReachableValue(PresenceDetectionType.ICMP_PING,
|
||||
getLatency(o, preferResponseTimeAsLatency));
|
||||
updateListener.partialDetectionResult(v);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void performSystemPing() {
|
||||
try {
|
||||
logger.trace("Perform native ping presence detection for {}", hostname);
|
||||
InetAddress destinationAddress = destination.getValue();
|
||||
if (destinationAddress == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
networkUtils.nativePing(pingMethod, destinationAddress.getHostAddress(), timeoutInMS).ifPresent(o -> {
|
||||
if (o.isSuccess()) {
|
||||
PresenceDetectionValue v = updateReachableValue(PresenceDetectionType.ICMP_PING,
|
||||
getLatency(o, preferResponseTimeAsLatency));
|
||||
updateListener.partialDetectionResult(v);
|
||||
}
|
||||
});
|
||||
} 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
|
||||
}
|
||||
}
|
||||
|
||||
private double getLatency(PingResult pingResult, boolean preferResponseTimeAsLatency) {
|
||||
logger.debug("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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dhcpRequestReceived(String ipAddress) {
|
||||
PresenceDetectionValue v = updateReachableValue(PresenceDetectionType.DHCP_REQUEST, 0);
|
||||
updateListener.partialDetectionResult(v);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
ScheduledFuture<?> future = refreshJob;
|
||||
if (future != null && !future.isDone()) {
|
||||
future.cancel(true);
|
||||
}
|
||||
refreshJob = scheduledExecutorService.scheduleWithFixedDelay(() -> performPresenceDetection(true), 0,
|
||||
refreshIntervalInMS, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if automatic refreshing is enabled.
|
||||
*/
|
||||
public boolean isAutomaticRefreshing() {
|
||||
return refreshJob != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop automatic refreshing.
|
||||
*/
|
||||
public void stopAutomaticRefresh() {
|
||||
ScheduledFuture<?> future = refreshJob;
|
||||
if (future != null && !future.isDone()) {
|
||||
future.cancel(true);
|
||||
refreshJob = null;
|
||||
}
|
||||
if (cachedDestination != null) {
|
||||
disableDHCPListen(cachedDestination);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* listener if enabled and unregistered otherwise.
|
||||
*
|
||||
* @param destinationAddress the InetAddress to listen for.
|
||||
*/
|
||||
private void enableDHCPListen(InetAddress destinationAddress) {
|
||||
try {
|
||||
if (DHCPListenService.register(destinationAddress.getHostAddress(), this).isUseUnprevilegedPort()) {
|
||||
dhcpState = "No access right for port 67. Bound to port 6767 instead. Port forwarding necessary!";
|
||||
} else {
|
||||
dhcpState = "Running normally";
|
||||
}
|
||||
} catch (SocketException e) {
|
||||
logger.warn("Cannot use DHCP sniffing.", e);
|
||||
useDHCPsniffing = false;
|
||||
dhcpState = "Cannot use DHCP sniffing: " + e.getLocalizedMessage();
|
||||
}
|
||||
}
|
||||
|
||||
private void disableDHCPListen(@Nullable InetAddress destinationAddress) {
|
||||
if (destinationAddress != null) {
|
||||
DHCPListenService.unregister(destinationAddress.getHostAddress());
|
||||
dhcpState = "off";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Implement this callback to be notified of a presence detection result.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface PresenceDetectionListener {
|
||||
|
||||
/**
|
||||
* This method is called by the {@see PresenceDetectionService}
|
||||
* if a device is deemed to be reachable.
|
||||
*
|
||||
* @param value The partial result of the presence detection process.
|
||||
* A partial result always means that the device is reachable, but not
|
||||
* all methods have returned a value yet.
|
||||
*/
|
||||
public void partialDetectionResult(PresenceDetectionValue value);
|
||||
|
||||
/**
|
||||
* This method is called by the {@see PresenceDetectionService}
|
||||
* if a new device state is known. The device might be reachable by different means
|
||||
* or unreachable.
|
||||
*
|
||||
* @param value The final result of the presence detection process.
|
||||
*/
|
||||
public void finalDetectionResult(PresenceDetectionValue value);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* All the supported presence detection types of this binding.
|
||||
* Used by {@see PresenceDetectionValue}.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum PresenceDetectionType {
|
||||
ARP_PING,
|
||||
ICMP_PING,
|
||||
TCP_CONNECTION,
|
||||
DHCP_REQUEST
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Contains the result or partial result of a presence detection.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the ping latency in ms or -1 if not reachable. Can be 0 if
|
||||
* no specific latency is known but the device is still reachable.
|
||||
*/
|
||||
public double getLowestLatency() {
|
||||
return latency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a string of comma separated successful presence detection types.
|
||||
*/
|
||||
public String getSuccessfulDetectionTypes() {
|
||||
return reachableByType.stream().map(v -> v.name()).collect(Collectors.joining(", "));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the reachable tcp ports of the presence detection value.
|
||||
* Thread safe.
|
||||
*/
|
||||
public List<Integer> getReachableTCPports() {
|
||||
synchronized (tcpServiceReachable) {
|
||||
List<Integer> copy = new ArrayList<>();
|
||||
copy.addAll(tcpServiceReachable);
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the presence detection is fully completed (no running
|
||||
* threads anymore).
|
||||
*/
|
||||
public boolean isFinished() {
|
||||
return detectionIsFinished;
|
||||
}
|
||||
|
||||
////// Package private methods //////
|
||||
|
||||
/**
|
||||
* Create a new PresenceDetectionValue with an initial latency.
|
||||
*
|
||||
* @param hostAddress The target IP
|
||||
* @param latency The ping latency in ms. Can be <0 if the device is not reachable.
|
||||
*/
|
||||
PresenceDetectionValue(String hostAddress, double latency) {
|
||||
this.hostAddress = hostAddress;
|
||||
this.latency = latency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a successful PresenceDetectionType.
|
||||
*
|
||||
* @param type A PresenceDetectionType.
|
||||
*/
|
||||
void addType(PresenceDetectionType type) {
|
||||
reachableByType.add(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by {@see PresenceDetection} by all different means of presence detections.
|
||||
* If the given latency is lower than the already stored one, the stored one will be overwritten.
|
||||
*
|
||||
* @param newLatency The new latency.
|
||||
* @return Returns true if the latency was indeed lower and updated the stored one.
|
||||
*/
|
||||
boolean updateLatency(double newLatency) {
|
||||
if (newLatency < 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"Latency must be >=0. Create a new PresenceDetectionValue for a not reachable device!");
|
||||
}
|
||||
if (newLatency > 0 && (latency == 0 || newLatency < latency)) {
|
||||
latency = newLatency;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a reachable tcp port to this presence detection result value object.
|
||||
* Thread safe.
|
||||
*/
|
||||
void addReachableTcpService(int tcpPort) {
|
||||
synchronized (tcpServiceReachable) {
|
||||
tcpServiceReachable.add(tcpPort);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the result value as final. No modifications should occur after this call.
|
||||
*/
|
||||
void setDetectionIsFinished(boolean detectionIsFinished) {
|
||||
this.detectionIsFinished = detectionIsFinished;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the host address of the presence detection result object.
|
||||
*/
|
||||
public String getHostAddress() {
|
||||
return hostAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the target can be reached by ICMP or ARP pings.
|
||||
*/
|
||||
public boolean isPingReachable() {
|
||||
return reachableByType.contains(PresenceDetectionType.ARP_PING)
|
||||
|| reachableByType.contains(PresenceDetectionType.ICMP_PING);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the target provides open TCP ports.
|
||||
*/
|
||||
public boolean isTCPServiceReachable() {
|
||||
return reachableByType.contains(PresenceDetectionType.TCP_CONNECTION);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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;
|
||||
|
||||
/**
|
||||
* The {@link SpeedTestConfiguration} is the class used to match the
|
||||
* thing configuration.
|
||||
*
|
||||
* @author Gaël L'hopital - Initial contribution
|
||||
*/
|
||||
public class SpeedTestConfiguration {
|
||||
public Integer refreshInterval = 20;
|
||||
public Integer initialDelay = 5;
|
||||
public Integer uploadSize = 1000000;
|
||||
public Integer maxTimeout = 3;
|
||||
private String url;
|
||||
private String fileName;
|
||||
|
||||
public String getUploadURL() {
|
||||
return url + (url.endsWith("/") ? "" : "/");
|
||||
}
|
||||
|
||||
public String getDownloadURL() {
|
||||
return getUploadURL() + fileName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetAddress;
|
||||
import java.net.SocketException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.net.NetUtil;
|
||||
import org.openhab.core.util.HexUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link WakeOnLanPacketSender} broadcasts a magic packet to wake a device.
|
||||
*
|
||||
* @author Wouter Born - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class WakeOnLanPacketSender {
|
||||
|
||||
private static final int WOL_UDP_PORT = 9;
|
||||
|
||||
// Wake-on-LAN magic packet constants
|
||||
static final int PREFIX_BYTE_SIZE = 6;
|
||||
static final int MAC_REPETITIONS = 16;
|
||||
static final int MAC_BYTE_SIZE = 6;
|
||||
static final int MAGIC_PACKET_BYTE_SIZE = PREFIX_BYTE_SIZE + MAC_REPETITIONS * MAC_BYTE_SIZE;
|
||||
static final String[] MAC_SEPARATORS = new String[] { ":", "-" };
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(WakeOnLanPacketSender.class);
|
||||
|
||||
private final String macAddress;
|
||||
private byte @Nullable [] magicPacket;
|
||||
private final Consumer<byte[]> magicPacketSender;
|
||||
|
||||
public WakeOnLanPacketSender(String macAddress) {
|
||||
this.macAddress = macAddress;
|
||||
this.magicPacketSender = this::broadcastMagicPacket;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for testing only.
|
||||
*/
|
||||
WakeOnLanPacketSender(String macAddress, Consumer<byte[]> magicPacketSender) {
|
||||
this.macAddress = macAddress;
|
||||
this.magicPacketSender = magicPacketSender;
|
||||
}
|
||||
|
||||
public void sendPacket() {
|
||||
byte[] localMagicPacket = magicPacket;
|
||||
if (localMagicPacket == null) {
|
||||
localMagicPacket = createMagicPacket(createMacBytes(macAddress));
|
||||
magicPacket = localMagicPacket;
|
||||
}
|
||||
|
||||
magicPacketSender.accept(localMagicPacket);
|
||||
}
|
||||
|
||||
private byte[] createMacBytes(String macAddress) {
|
||||
String hexString = macAddress;
|
||||
for (String macSeparator : MAC_SEPARATORS) {
|
||||
hexString = hexString.replaceAll(macSeparator, "");
|
||||
}
|
||||
if (hexString.length() != 2 * MAC_BYTE_SIZE) {
|
||||
throw new IllegalStateException("Invalid MAC address: " + macAddress);
|
||||
}
|
||||
return HexUtils.hexToBytes(hexString);
|
||||
}
|
||||
|
||||
private byte[] createMagicPacket(byte[] macBytes) {
|
||||
byte[] bytes = new byte[MAGIC_PACKET_BYTE_SIZE];
|
||||
Arrays.fill(bytes, 0, PREFIX_BYTE_SIZE, (byte) 0xff);
|
||||
for (int i = PREFIX_BYTE_SIZE; i < MAGIC_PACKET_BYTE_SIZE; i += MAC_BYTE_SIZE) {
|
||||
System.arraycopy(macBytes, 0, bytes, i, macBytes.length);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private void broadcastMagicPacket(byte[] magicPacket) {
|
||||
try (DatagramSocket socket = new DatagramSocket()) {
|
||||
broadcastAddressStream().forEach(broadcastAddress -> {
|
||||
try {
|
||||
DatagramPacket packet = new DatagramPacket(magicPacket, MAGIC_PACKET_BYTE_SIZE, broadcastAddress,
|
||||
WOL_UDP_PORT);
|
||||
socket.send(packet);
|
||||
logger.debug("Wake-on-LAN packet sent (MAC address: {}, broadcast address: {})", macAddress,
|
||||
broadcastAddress.getHostAddress());
|
||||
} catch (IOException e) {
|
||||
logger.debug("Failed to send Wake-on-LAN packet (MAC address: {}, broadcast address: {})",
|
||||
macAddress, broadcastAddress.getHostAddress(), e);
|
||||
}
|
||||
});
|
||||
logger.info("Wake-on-LAN packets sent (MAC address: {})", macAddress);
|
||||
} catch (SocketException e) {
|
||||
logger.error("Failed to open Wake-on-LAN datagram socket", e);
|
||||
}
|
||||
}
|
||||
|
||||
private Stream<InetAddress> broadcastAddressStream() {
|
||||
return NetUtil.getAllBroadcastAddresses().stream().map(address -> {
|
||||
try {
|
||||
return InetAddress.getByName(address);
|
||||
} catch (UnknownHostException e) {
|
||||
logger.debug("Failed to get broadcast address '{}' by name", address, e);
|
||||
return null;
|
||||
}
|
||||
}).filter(Objects::nonNull);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.action;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link INetworkActions} defines the interface for all thing actions supported by the binding.
|
||||
*
|
||||
* @author Wouter Born - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface INetworkActions {
|
||||
|
||||
void sendWakeOnLanPacket();
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.action;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.network.internal.handler.NetworkHandler;
|
||||
import org.openhab.core.automation.annotation.RuleAction;
|
||||
import org.openhab.core.thing.binding.ThingActions;
|
||||
import org.openhab.core.thing.binding.ThingActionsScope;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The class is responsible to call corresponding actions on {@link NetworkHandler}.
|
||||
* <p>
|
||||
* <b>Note:</b>The static method <b>invokeMethodOf</b> handles the case where
|
||||
* the test <i>actions instanceof NetworkActions</i> fails. This test can fail
|
||||
* due to an issue in openHAB core v2.5.0 where the {@link NetworkActions} class
|
||||
* can be loaded by a different classloader than the <i>actions</i> instance.
|
||||
*
|
||||
* @author Wouter Born - Initial contribution
|
||||
*/
|
||||
@ThingActionsScope(name = "network")
|
||||
@NonNullByDefault
|
||||
public class NetworkActions implements ThingActions, INetworkActions {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(NetworkActions.class);
|
||||
|
||||
private @Nullable NetworkHandler handler;
|
||||
|
||||
@Override
|
||||
public void setThingHandler(@Nullable ThingHandler handler) {
|
||||
if (handler instanceof NetworkHandler) {
|
||||
this.handler = (NetworkHandler) handler;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable ThingHandler getThingHandler() {
|
||||
return handler;
|
||||
}
|
||||
|
||||
@Override
|
||||
@RuleAction(label = "Send WoL Packet", description = "Send a Wake-on-LAN packet to wake the device")
|
||||
public void sendWakeOnLanPacket() {
|
||||
NetworkHandler localHandler = handler;
|
||||
if (localHandler != null) {
|
||||
localHandler.sendWakeOnLanPacket();
|
||||
} else {
|
||||
logger.warn("Failed to send Wake-on-LAN packet (handler null)");
|
||||
}
|
||||
}
|
||||
|
||||
public static void sendWakeOnLanPacket(@Nullable ThingActions actions) {
|
||||
invokeMethodOf(actions).sendWakeOnLanPacket();
|
||||
}
|
||||
|
||||
private static INetworkActions invokeMethodOf(@Nullable ThingActions actions) {
|
||||
if (actions == null) {
|
||||
throw new IllegalArgumentException("actions cannot be null");
|
||||
}
|
||||
if (actions.getClass().getName().equals(NetworkActions.class.getName())) {
|
||||
if (actions instanceof INetworkActions) {
|
||||
return (INetworkActions) actions;
|
||||
} else {
|
||||
return (INetworkActions) Proxy.newProxyInstance(INetworkActions.class.getClassLoader(),
|
||||
new Class[] { INetworkActions.class }, (Object proxy, Method method, Object[] args) -> {
|
||||
Method m = actions.getClass().getDeclaredMethod(method.getName(),
|
||||
method.getParameterTypes());
|
||||
return m.invoke(actions, args);
|
||||
});
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Actions is not an instance of " + NetworkActions.class.getName());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.dhcp;
|
||||
|
||||
import java.net.SocketException;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* A singleton. IPRequestReceivedCallback objects can register and unregister.
|
||||
* If the first one is registered and there is no singleton instance, an instance will be created and the
|
||||
* receiver thread will be started. If the last IPRequestReceivedCallback is removed, the thread will be stopped
|
||||
* after the receive socket is closed.
|
||||
* IPRequestReceivedCallback will be called for the address that is registered and matches the
|
||||
* DHO_DHCP_REQUESTED_ADDRESS address field.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class DHCPListenService {
|
||||
static @Nullable DHCPPacketListenerServer instance;
|
||||
static Map<String, IPRequestReceivedCallback> registeredListeners = new TreeMap<>();
|
||||
static Logger logger = LoggerFactory.getLogger(DHCPListenService.class);
|
||||
|
||||
@SuppressWarnings({ "null", "unused" })
|
||||
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);
|
||||
if (listener != null) {
|
||||
listener.dhcpRequestReceived(ipAddress);
|
||||
} else {
|
||||
logger.trace("DHCP request for unknown address: {}", ipAddress);
|
||||
}
|
||||
});
|
||||
DHCPListenService.instance = instance;
|
||||
instance.start();
|
||||
}
|
||||
synchronized (registeredListeners) {
|
||||
registeredListeners.put(hostAddress, dhcpListener);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static void unregister(String hostAddress) {
|
||||
synchronized (registeredListeners) {
|
||||
registeredListeners.remove(hostAddress);
|
||||
if (!registeredListeners.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final DHCPPacketListenerServer instance = DHCPListenService.instance;
|
||||
if (instance != null) {
|
||||
instance.close();
|
||||
}
|
||||
DHCPListenService.instance = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.dhcp;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Parses a dhcp packet and extracts the OP code and all DHCP Options.
|
||||
*
|
||||
* Example:
|
||||
* DatagramSocket socket = new DatagramSocket(67);
|
||||
* while (true) {
|
||||
* DatagramPacket packet = new DatagramPacket(new byte[1500], 1500);
|
||||
* socket.receive(packet);
|
||||
* DHCPPacket dhcp = new DHCPPacket(packet);
|
||||
* InetAddress requestedAddress = dhcp.getRequestedIPAddress();
|
||||
* }
|
||||
*
|
||||
* If used this way, beware that a <tt>BadPacketExpcetion</tt> is thrown
|
||||
* if the datagram contains invalid DHCP data.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
class DHCPPacket {
|
||||
/** DHCP BOOTP CODES **/
|
||||
static final byte BOOTREQUEST = 1;
|
||||
static final byte BOOTREPLY = 2;
|
||||
|
||||
/** DHCP MESSAGE CODES **/
|
||||
static final byte DHCPDISCOVER = 1;
|
||||
|
||||
static final byte DHCPREQUEST = 3;
|
||||
static final byte DHCPDECLINE = 4;
|
||||
|
||||
static final byte DHCPRELEASE = 7;
|
||||
static final byte DHCPINFORM = 8;
|
||||
|
||||
/** DHCP OPTIONS CODE **/
|
||||
static final byte DHO_PAD = 0;
|
||||
|
||||
static final byte DHO_DHCP_REQUESTED_ADDRESS = 50;
|
||||
|
||||
static final byte DHO_DHCP_MESSAGE_TYPE = 53;
|
||||
|
||||
static final byte DHO_END = -1;
|
||||
|
||||
static final int BOOTP_ABSOLUTE_MIN_LEN = 236;
|
||||
static final int DHCP_MAX_MTU = 1500;
|
||||
|
||||
// Magic cookie
|
||||
static final int MAGIC_COOKIE = 0x63825363;
|
||||
|
||||
/**
|
||||
* If a DHCP datagram is malformed this Exception is thrown.
|
||||
*
|
||||
* It inherits from <tt>IllegalArgumentException</tt> and <tt>RuntimeException</tt>
|
||||
* so it doesn't need to be explicitly caught.
|
||||
*
|
||||
* @author David Gräff
|
||||
*/
|
||||
static class BadPacketException extends IllegalArgumentException {
|
||||
private static final long serialVersionUID = 5866225879843384688L;
|
||||
|
||||
BadPacketException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
BadPacketException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
private byte op;
|
||||
private Map<Byte, byte[]> options;
|
||||
|
||||
/**
|
||||
* Package private constructor for test suite.
|
||||
*/
|
||||
DHCPPacket(byte[] messageType, byte @Nullable [] requestedIP) {
|
||||
this.op = BOOTREQUEST;
|
||||
this.options = new LinkedHashMap<>();
|
||||
options.put(DHO_DHCP_MESSAGE_TYPE, messageType);
|
||||
if (requestedIP != null) {
|
||||
options.put(DHO_DHCP_REQUESTED_ADDRESS, requestedIP);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor for the <tt>DHCPPacket</tt> class. Parses the given datagram.
|
||||
*/
|
||||
public DHCPPacket(DatagramPacket datagram) throws BadPacketException, IOException {
|
||||
this.op = BOOTREPLY;
|
||||
this.options = new LinkedHashMap<>();
|
||||
|
||||
byte[] buffer = datagram.getData();
|
||||
int offset = datagram.getOffset();
|
||||
int length = datagram.getLength();
|
||||
|
||||
// absolute minimum size for a valid packet
|
||||
if (length < BOOTP_ABSOLUTE_MIN_LEN) {
|
||||
throw new BadPacketException(
|
||||
"DHCP Packet too small (" + length + ") absolute minimum is " + BOOTP_ABSOLUTE_MIN_LEN);
|
||||
}
|
||||
// maximum size for a valid DHCP packet
|
||||
if (length > DHCP_MAX_MTU) {
|
||||
throw new BadPacketException("DHCP Packet too big (" + length + ") max MTU is " + DHCP_MAX_MTU);
|
||||
}
|
||||
|
||||
// turn buffer into a readable stream
|
||||
ByteArrayInputStream inBStream = new ByteArrayInputStream(buffer, offset, length);
|
||||
DataInputStream inStream = new DataInputStream(inBStream);
|
||||
|
||||
byte[] dummy = new byte[128];
|
||||
|
||||
// parse static part of packet
|
||||
this.op = inStream.readByte();
|
||||
inStream.readByte(); // read hardware type (ETHERNET)
|
||||
inStream.readByte(); // read hardware address length (6 bytes)
|
||||
inStream.readByte(); // read hops
|
||||
inStream.readInt(); // read transaction id
|
||||
inStream.readShort(); // read secsonds elapsed
|
||||
inStream.readShort(); // read flags
|
||||
inStream.readFully(dummy, 0, 4); // ciaddr
|
||||
inStream.readFully(dummy, 0, 4); // yiaddr
|
||||
inStream.readFully(dummy, 0, 4); // siaddr
|
||||
inStream.readFully(dummy, 0, 4); // giaddr
|
||||
inStream.readFully(dummy, 0, 16); // chaddr
|
||||
inStream.readFully(dummy, 0, 64); // sname
|
||||
inStream.readFully(dummy, 0, 128); // file
|
||||
|
||||
// check for DHCP MAGIC_COOKIE
|
||||
inBStream.mark(4); // read ahead 4 bytes
|
||||
if (inStream.readInt() != MAGIC_COOKIE) {
|
||||
throw new BadPacketException("Packet seams to be truncated");
|
||||
}
|
||||
|
||||
// DHCP Packet: parsing options
|
||||
int type = 0;
|
||||
|
||||
while (true) {
|
||||
int r = inBStream.read();
|
||||
if (r < 0) {
|
||||
break;
|
||||
} // EOF
|
||||
|
||||
type = (byte) r;
|
||||
|
||||
if (type == DHO_PAD) {
|
||||
continue;
|
||||
} // skip Padding
|
||||
if (type == DHO_END) {
|
||||
break;
|
||||
} // break if end of options
|
||||
|
||||
r = inBStream.read();
|
||||
if (r < 0) {
|
||||
break;
|
||||
} // EOF
|
||||
|
||||
int len = Math.min(r, inBStream.available());
|
||||
byte[] unitOpt = new byte[len];
|
||||
inBStream.read(unitOpt);
|
||||
|
||||
this.options.put((byte) type, unitOpt);
|
||||
}
|
||||
if (type != DHO_END) {
|
||||
throw new BadPacketException("Packet seams to be truncated");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the op field (Message op code).
|
||||
*
|
||||
*
|
||||
* @return the op field.
|
||||
*/
|
||||
public byte getOp() {
|
||||
return this.op;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the DHCP Option Type.
|
||||
*
|
||||
* <p>
|
||||
* This is a short-cut for <tt>getOptionAsByte(DHO_DHCP_MESSAGE_TYPE)</tt>.
|
||||
*
|
||||
* @return option type, of <tt>null</tt> if not present.
|
||||
*/
|
||||
@SuppressWarnings({ "null", "unused" })
|
||||
public @Nullable Byte getDHCPMessageType() {
|
||||
byte[] opt = options.get(DHO_DHCP_MESSAGE_TYPE);
|
||||
if (opt == null) {
|
||||
return null;
|
||||
}
|
||||
if (opt.length != 1) {
|
||||
throw new BadPacketException(
|
||||
"option " + DHO_DHCP_MESSAGE_TYPE + " is wrong size:" + opt.length + " should be 1");
|
||||
}
|
||||
return opt[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the requested IP address of a BOOTREQUEST packet.
|
||||
*/
|
||||
@SuppressWarnings({ "null", "unused" })
|
||||
public @Nullable InetAddress getRequestedIPAddress() throws IllegalArgumentException, UnknownHostException {
|
||||
byte[] opt = options.get(DHO_DHCP_REQUESTED_ADDRESS);
|
||||
if (opt == null) {
|
||||
return null;
|
||||
}
|
||||
if (opt.length != 4) {
|
||||
throw new BadPacketException(
|
||||
"option " + DHO_DHCP_REQUESTED_ADDRESS + " is wrong size:" + opt.length + " should be 4");
|
||||
}
|
||||
return InetAddress.getByAddress(opt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.dhcp;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.BindException;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketException;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.network.internal.dhcp.DHCPPacket.BadPacketException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Receives UDP messages and try to parse them as DHCP messages.
|
||||
* First try to listen to the DHCP port 67 and if that failes because of missing access rights,
|
||||
* port 6767 will be opened instead (and the user is required to setup a port forarding).
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class DHCPPacketListenerServer extends Thread {
|
||||
private byte[] buffer = new byte[1024];
|
||||
protected @Nullable DatagramSocket dsocket;
|
||||
private DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
|
||||
boolean willbeclosed = false;
|
||||
Logger logger = LoggerFactory.getLogger(DHCPPacketListenerServer.class);
|
||||
private boolean useUnprevilegedPort = false;
|
||||
private final IPRequestReceivedCallback listener;
|
||||
|
||||
DHCPPacketListenerServer(IPRequestReceivedCallback listener) throws SocketException, BindException {
|
||||
this.listener = listener;
|
||||
try {
|
||||
bindSocketTo(67);
|
||||
} catch (SocketException e) {
|
||||
useUnprevilegedPort = true;
|
||||
bindSocketTo(6767);
|
||||
}
|
||||
}
|
||||
|
||||
protected void bindSocketTo(int port) throws SocketException {
|
||||
DatagramSocket dsocket = new DatagramSocket(null);
|
||||
dsocket.setReuseAddress(true);
|
||||
dsocket.setBroadcast(true);
|
||||
dsocket.bind(new InetSocketAddress(port));
|
||||
this.dsocket = dsocket;
|
||||
}
|
||||
|
||||
protected void receivePacket(DHCPPacket request, @Nullable InetAddress udpRemote)
|
||||
throws BadPacketException, UnknownHostException, IOException {
|
||||
if (request.getOp() != DHCPPacket.BOOTREQUEST) {
|
||||
return; // skipping non BOOTREQUEST message types
|
||||
}
|
||||
|
||||
Byte dhcpMessageType = request.getDHCPMessageType();
|
||||
|
||||
if (dhcpMessageType != DHCPPacket.DHCPREQUEST) {
|
||||
return; // skipping non DHCPREQUEST message types
|
||||
}
|
||||
|
||||
InetAddress requestedAddress = request.getRequestedIPAddress();
|
||||
if (requestedAddress == null) {
|
||||
// There is no requested address field. This may be a DHCPREQUEST message to renew
|
||||
// the lease. Let's deduct the IP by the IP/UDP src.
|
||||
requestedAddress = udpRemote;
|
||||
if (requestedAddress == null) {
|
||||
logger.warn("DHO_DHCP_REQUESTED_ADDRESS field is missing");
|
||||
return;
|
||||
}
|
||||
}
|
||||
listener.dhcpRequestReceived(requestedAddress.getHostAddress());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
logger.info("DHCP request packet listener online");
|
||||
while (!willbeclosed) {
|
||||
packet.setLength(buffer.length);
|
||||
DatagramSocket socket = dsocket;
|
||||
if (socket == null) {
|
||||
return;
|
||||
}
|
||||
socket.receive(packet);
|
||||
receivePacket(new DHCPPacket(packet), packet.getAddress());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
if (willbeclosed) {
|
||||
return;
|
||||
}
|
||||
logger.warn("{}", e.getLocalizedMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable DatagramSocket getSocket() {
|
||||
return dsocket;
|
||||
}
|
||||
|
||||
// Return true if the instance couldn't bind to port 67 and used port 6767 instead
|
||||
// to listen to DHCP traffic (port forwarding necessary).
|
||||
public boolean isUseUnprevilegedPort() {
|
||||
return useUnprevilegedPort;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the socket and waits for the receive thread to finish.
|
||||
* Does nothing if the receive thread is not running.
|
||||
*/
|
||||
public void close() {
|
||||
if (isAlive()) {
|
||||
willbeclosed = true;
|
||||
DatagramSocket socket = dsocket;
|
||||
if (socket != null) {
|
||||
socket.close();
|
||||
}
|
||||
try {
|
||||
join(1000);
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
interrupt();
|
||||
dsocket = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.dhcp;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Implement this interface to be notified of DHCP IP request messages
|
||||
* for a registered IP address. Register to {@see DHCPListenSingleton}.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface IPRequestReceivedCallback {
|
||||
/**
|
||||
* The {@see ReceiveDHCPRequestPackets} object could successfully identify
|
||||
* a DHCP request message on the network.
|
||||
*
|
||||
* @param ipAddress The requested IP address.
|
||||
*/
|
||||
void dhcpRequestReceived(String ipAddress);
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.discovery;
|
||||
|
||||
import static org.openhab.binding.network.internal.NetworkBindingConstants.*;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.network.internal.NetworkBindingConfiguration;
|
||||
import org.openhab.binding.network.internal.PresenceDetection;
|
||||
import org.openhab.binding.network.internal.PresenceDetectionListener;
|
||||
import org.openhab.binding.network.internal.PresenceDetectionValue;
|
||||
import org.openhab.binding.network.internal.utils.NetworkUtils;
|
||||
import org.openhab.core.config.core.Configuration;
|
||||
import org.openhab.core.config.discovery.AbstractDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.config.discovery.DiscoveryService;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Deactivate;
|
||||
import org.osgi.service.component.annotations.Modified;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link NetworkDiscoveryService} is responsible for discovering devices on
|
||||
* the current Network. It uses every Network Interface which is connected to a network.
|
||||
* It tries common TCP ports to connect to, ICMP pings and ARP pings.
|
||||
*
|
||||
* @author Marc Mettke - Initial contribution
|
||||
* @author David Graeff - Rewritten
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.network")
|
||||
public class NetworkDiscoveryService extends AbstractDiscoveryService implements PresenceDetectionListener {
|
||||
static final int PING_TIMEOUT_IN_MS = 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 Integer scannedIPcount = 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)),
|
||||
false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Activate
|
||||
public void activate(@Nullable Map<String, @Nullable Object> config) {
|
||||
super.activate(config);
|
||||
modified(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Modified
|
||||
protected void modified(@Nullable Map<String, @Nullable Object> config) {
|
||||
super.modified(config);
|
||||
// We update instead of replace the configuration object, so that if the user updates the
|
||||
// configuration, the values are automatically available in all handlers. Because they all
|
||||
// share the same instance.
|
||||
configuration.update(new Configuration(config).as(NetworkBindingConfiguration.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deactivate
|
||||
protected void deactivate() {
|
||||
if (executorService != null) {
|
||||
executorService.shutdown();
|
||||
}
|
||||
super.deactivate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void partialDetectionResult(PresenceDetectionValue value) {
|
||||
final String ip = value.getHostAddress();
|
||||
if (value.isPingReachable()) {
|
||||
newPingDevice(ip);
|
||||
} else if (value.isTCPServiceReachable()) {
|
||||
List<Integer> tcpServices = value.getReachableTCPports();
|
||||
for (int port : tcpServices) {
|
||||
newServiceDevice(ip, port);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finalDetectionResult(PresenceDetectionValue value) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the DiscoveryThread for each IP on each interface on the network
|
||||
*/
|
||||
@Override
|
||||
protected void startScan() {
|
||||
if (executorService == null) {
|
||||
executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2);
|
||||
}
|
||||
final ExecutorService service = executorService;
|
||||
if (service == null) {
|
||||
return;
|
||||
}
|
||||
removeOlderResults(getTimestampOfLastScan(), null);
|
||||
logger.trace("Starting Network Device Discovery");
|
||||
|
||||
final Set<String> networkIPs = networkUtils.getNetworkIPs(MAXIMUM_IPS_PER_INTERFACE);
|
||||
scannedIPcount = 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);
|
||||
// Ping devices
|
||||
s.setUseIcmpPing(true);
|
||||
s.setUseArpPing(true, configuration.arpPingToolPath, configuration.arpPingUtilMethod);
|
||||
// TCP devices
|
||||
s.setServicePorts(tcpServicePorts);
|
||||
|
||||
service.execute(() -> {
|
||||
Thread.currentThread().setName("Discovery thread " + ip);
|
||||
s.performPresenceDetection(true);
|
||||
synchronized (scannedIPcount) {
|
||||
scannedIPcount += 1;
|
||||
if (scannedIPcount == networkIPs.size()) {
|
||||
logger.trace("Scan of {} IPs successful", scannedIPcount);
|
||||
stopScan();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void stopScan() {
|
||||
super.stopScan();
|
||||
final ExecutorService service = executorService;
|
||||
if (service == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
service.awaitTermination(PING_TIMEOUT_IN_MS, TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt(); // Reset interrupt flag
|
||||
}
|
||||
service.shutdown();
|
||||
executorService = null;
|
||||
}
|
||||
|
||||
public static ThingUID createServiceUID(String ip, int tcpPort) {
|
||||
// uid must not contains dots
|
||||
return new ThingUID(SERVICE_DEVICE, ip.replace('.', '_') + "_" + String.valueOf(tcpPort));
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit newly discovered devices. This method is called by the spawned threads in {@link startScan}.
|
||||
*
|
||||
* @param ip The device IP
|
||||
* @param tcpPort The TCP port
|
||||
*/
|
||||
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";
|
||||
}
|
||||
label += " (" + ip + ":" + tcpPort + ")";
|
||||
|
||||
Map<String, Object> properties = new HashMap<>();
|
||||
properties.put(PARAMETER_HOSTNAME, ip);
|
||||
properties.put(PARAMETER_PORT, tcpPort);
|
||||
thingDiscovered(DiscoveryResultBuilder.create(createServiceUID(ip, tcpPort)).withTTL(DISCOVERY_RESULT_TTL)
|
||||
.withProperties(properties).withLabel(label).build());
|
||||
}
|
||||
|
||||
public static ThingUID createPingUID(String ip) {
|
||||
// uid must not contains dots
|
||||
return new ThingUID(PING_DEVICE, ip.replace('.', '_'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit newly discovered devices. This method is called by the spawned threads in {@link startScan}.
|
||||
*
|
||||
* @param ip The device IP
|
||||
*/
|
||||
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);
|
||||
thingDiscovered(DiscoveryResultBuilder.create(createPingUID(ip)).withTTL(DISCOVERY_RESULT_TTL)
|
||||
.withProperties(properties).withLabel("Network Device (" + ip + ")").build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.handler;
|
||||
|
||||
import static org.openhab.binding.network.internal.NetworkBindingConstants.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.network.internal.NetworkBindingConfiguration;
|
||||
import org.openhab.binding.network.internal.NetworkBindingConfigurationListener;
|
||||
import org.openhab.binding.network.internal.NetworkBindingConstants;
|
||||
import org.openhab.binding.network.internal.NetworkHandlerConfiguration;
|
||||
import org.openhab.binding.network.internal.PresenceDetection;
|
||||
import org.openhab.binding.network.internal.PresenceDetectionListener;
|
||||
import org.openhab.binding.network.internal.PresenceDetectionValue;
|
||||
import org.openhab.binding.network.internal.WakeOnLanPacketSender;
|
||||
import org.openhab.binding.network.internal.action.NetworkActions;
|
||||
import org.openhab.core.library.types.DateTimeType;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.unit.MetricPrefix;
|
||||
import org.openhab.core.library.unit.SmartHomeUnits;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerService;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* This handler is handling the CHANNEL_ONLINE (boolean) and CHANNEL_TIME (time in ms)
|
||||
* commands and is starting a {@see NetworkService} instance for the configured device.
|
||||
*
|
||||
* @author Marc Mettke - Initial contribution
|
||||
* @author David Graeff - Rewritten
|
||||
* @author Wouter Born - Add Wake-on-LAN thing action support
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NetworkHandler extends BaseThingHandler
|
||||
implements PresenceDetectionListener, NetworkBindingConfigurationListener {
|
||||
private final Logger logger = LoggerFactory.getLogger(NetworkHandler.class);
|
||||
private @NonNullByDefault({}) PresenceDetection presenceDetection;
|
||||
private @NonNullByDefault({}) WakeOnLanPacketSender wakeOnLanPacketSender;
|
||||
|
||||
private boolean isTCPServiceDevice;
|
||||
private NetworkBindingConfiguration configuration;
|
||||
|
||||
// How many retries before a device is deemed offline
|
||||
int retries;
|
||||
// Retry counter. Will be reset as soon as a device presence detection succeed.
|
||||
private int retryCounter = 0;
|
||||
private NetworkHandlerConfiguration handlerConfiguration = new NetworkHandlerConfiguration();
|
||||
|
||||
/**
|
||||
* Do not call this directly, but use the {@see NetworkHandlerBuilder} instead.
|
||||
*/
|
||||
public NetworkHandler(Thing thing, boolean isTCPServiceDevice, NetworkBindingConfiguration configuration) {
|
||||
super(thing);
|
||||
this.isTCPServiceDevice = isTCPServiceDevice;
|
||||
this.configuration = configuration;
|
||||
this.configuration.addNetworkBindingConfigurationListener(this);
|
||||
}
|
||||
|
||||
private void refreshValue(ChannelUID channelUID) {
|
||||
// We are not yet even initialized, don't do anything
|
||||
if (presenceDetection == null || !presenceDetection.isAutomaticRefreshing()) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (channelUID.getId()) {
|
||||
case CHANNEL_ONLINE:
|
||||
presenceDetection.getValue(
|
||||
value -> updateState(CHANNEL_ONLINE, value.isReachable() ? OnOffType.ON : OnOffType.OFF));
|
||||
break;
|
||||
case CHANNEL_LATENCY:
|
||||
case CHANNEL_DEPRECATED_TIME:
|
||||
presenceDetection.getValue(value -> {
|
||||
updateState(CHANNEL_LATENCY,
|
||||
new QuantityType<>(value.getLowestLatency(), MetricPrefix.MILLI(SmartHomeUnits.SECOND)));
|
||||
updateState(CHANNEL_DEPRECATED_TIME, new DecimalType(value.getLowestLatency()));
|
||||
});
|
||||
break;
|
||||
case CHANNEL_LASTSEEN:
|
||||
if (presenceDetection.getLastSeen() > 0) {
|
||||
Instant instant = Instant.ofEpochMilli(presenceDetection.getLastSeen());
|
||||
updateState(CHANNEL_LASTSEEN, new DateTimeType(
|
||||
ZonedDateTime.ofInstant(instant, TimeZone.getDefault().toZoneId()).withFixedOffsetZone()));
|
||||
} else {
|
||||
updateState(CHANNEL_LASTSEEN, UnDefType.UNDEF);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
logger.debug("Command received for an unknown channel: {}", channelUID.getId());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
refreshValue(channelUID);
|
||||
} else {
|
||||
logger.debug("Command {} is not supported for channel: {}", command, channelUID.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void partialDetectionResult(PresenceDetectionValue value) {
|
||||
updateState(CHANNEL_ONLINE, OnOffType.ON);
|
||||
updateState(CHANNEL_LATENCY,
|
||||
new QuantityType<>(value.getLowestLatency(), MetricPrefix.MILLI(SmartHomeUnits.SECOND)));
|
||||
updateState(CHANNEL_DEPRECATED_TIME, new DecimalType(value.getLowestLatency()));
|
||||
}
|
||||
|
||||
@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;
|
||||
|
||||
if (retryCounter >= this.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());
|
||||
updateState(CHANNEL_LASTSEEN, new DateTimeType(
|
||||
ZonedDateTime.ofInstant(instant, TimeZone.getDefault().toZoneId()).withFixedOffsetZone()));
|
||||
}
|
||||
|
||||
updateNetworkProperties();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
PresenceDetection detection = presenceDetection;
|
||||
if (detection != null) {
|
||||
detection.stopAutomaticRefresh();
|
||||
}
|
||||
presenceDetection = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize with a presenceDetection object.
|
||||
* Used by testing for injecting.
|
||||
*/
|
||||
void initialize(PresenceDetection presenceDetection) {
|
||||
handlerConfiguration = getConfigAs(NetworkHandlerConfiguration.class);
|
||||
|
||||
this.presenceDetection = presenceDetection;
|
||||
presenceDetection.setHostname(handlerConfiguration.hostname);
|
||||
presenceDetection.setPreferResponseTimeAsLatency(configuration.preferResponseTimeAsLatency);
|
||||
|
||||
if (isTCPServiceDevice) {
|
||||
Integer port = handlerConfiguration.port;
|
||||
if (port == null) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No port configured!");
|
||||
return;
|
||||
}
|
||||
presenceDetection.setServicePorts(Collections.singleton(port));
|
||||
} else {
|
||||
// It does not harm to send an additional UDP packet to a device,
|
||||
// therefore we assume all ping devices are iOS devices. If this
|
||||
// does not work for all users for some obscure reason, we can make
|
||||
// this a thing configuration variable.
|
||||
presenceDetection.setIOSDevice(true);
|
||||
// Hand over binding configurations to the network service
|
||||
presenceDetection.setUseDhcpSniffing(configuration.allowDHCPlisten);
|
||||
presenceDetection.setUseIcmpPing(configuration.allowSystemPings);
|
||||
presenceDetection.setUseArpPing(true, configuration.arpPingToolPath, configuration.arpPingUtilMethod);
|
||||
}
|
||||
|
||||
this.retries = handlerConfiguration.retry.intValue();
|
||||
presenceDetection.setRefreshInterval(handlerConfiguration.refreshInterval.longValue());
|
||||
presenceDetection.setTimeout(handlerConfiguration.timeout.intValue());
|
||||
|
||||
wakeOnLanPacketSender = new WakeOnLanPacketSender(handlerConfiguration.macAddress);
|
||||
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
presenceDetection.startAutomaticRefresh(scheduler);
|
||||
|
||||
updateNetworkProperties();
|
||||
}
|
||||
|
||||
private void updateNetworkProperties() {
|
||||
// Update properties (after startAutomaticRefresh, to get the correct dhcp state)
|
||||
Map<String, String> properties = editProperties();
|
||||
properties.put(NetworkBindingConstants.PROPERTY_ARP_STATE, presenceDetection.getArpPingState());
|
||||
properties.put(NetworkBindingConstants.PROPERTY_ICMP_STATE, presenceDetection.getIPPingState());
|
||||
properties.put(NetworkBindingConstants.PROPERTY_PRESENCE_DETECTION_TYPE, "");
|
||||
properties.put(NetworkBindingConstants.PROPERTY_IOS_WAKEUP, presenceDetection.isIOSdevice() ? "Yes" : "No");
|
||||
properties.put(NetworkBindingConstants.PROPERTY_DHCP_STATE, presenceDetection.getDhcpState());
|
||||
updateProperties(properties);
|
||||
}
|
||||
|
||||
// Create a new network service and apply all configurations.
|
||||
@Override
|
||||
public void initialize() {
|
||||
initialize(new PresenceDetection(this, configuration.cacheDeviceStateTimeInMS.intValue()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this handler is for a TCP service device.
|
||||
*/
|
||||
public boolean isTCPServiceDevice() {
|
||||
return isTCPServiceDevice;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindingConfigurationChanged() {
|
||||
// Make sure that changed binding configuration is reflected
|
||||
presenceDetection.setPreferResponseTimeAsLatency(configuration.preferResponseTimeAsLatency);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Class<? extends ThingHandlerService>> getServices() {
|
||||
return Collections.singletonList(NetworkActions.class);
|
||||
}
|
||||
|
||||
public void sendWakeOnLanPacket() {
|
||||
if (handlerConfiguration.macAddress.isEmpty()) {
|
||||
throw new IllegalStateException(
|
||||
"Cannot send WoL packet because the 'macAddress' is not configured for " + thing.getUID());
|
||||
}
|
||||
wakeOnLanPacketSender.sendPacket();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.handler;
|
||||
|
||||
import static org.openhab.binding.network.internal.NetworkBindingConstants.*;
|
||||
import static org.openhab.core.library.unit.SmartHomeUnits.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.network.internal.SpeedTestConfiguration;
|
||||
import org.openhab.core.library.dimension.DataTransferRate;
|
||||
import org.openhab.core.library.types.DateTimeType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.unit.SmartHomeUnits;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import fr.bmartel.speedtest.SpeedTestReport;
|
||||
import fr.bmartel.speedtest.SpeedTestSocket;
|
||||
import fr.bmartel.speedtest.inter.ISpeedTestListener;
|
||||
import fr.bmartel.speedtest.model.SpeedTestError;
|
||||
|
||||
/**
|
||||
* The {@link SpeedTestHandler } is responsible for launching bandwidth
|
||||
* measurements at a given interval and for given file / size
|
||||
*
|
||||
* @author Gaël L'hopital - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SpeedTestHandler extends BaseThingHandler implements ISpeedTestListener {
|
||||
private final Logger logger = LoggerFactory.getLogger(SpeedTestHandler.class);
|
||||
private @Nullable SpeedTestSocket speedTestSocket;
|
||||
private @NonNullByDefault({}) ScheduledFuture<?> refreshTask;
|
||||
private @NonNullByDefault({}) SpeedTestConfiguration configuration;
|
||||
private State bufferedProgress = UnDefType.UNDEF;
|
||||
private int timeouts;
|
||||
|
||||
public SpeedTestHandler(Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
configuration = getConfigAs(SpeedTestConfiguration.class);
|
||||
startRefreshTask();
|
||||
}
|
||||
|
||||
private synchronized void startSpeedTest() {
|
||||
if (speedTestSocket == null) {
|
||||
logger.debug("Network speedtest started");
|
||||
final SpeedTestSocket socket = new SpeedTestSocket(1500);
|
||||
speedTestSocket = socket;
|
||||
socket.addSpeedTestListener(this);
|
||||
updateState(CHANNEL_TEST_ISRUNNING, OnOffType.ON);
|
||||
updateState(CHANNEL_TEST_START, new DateTimeType());
|
||||
updateState(CHANNEL_TEST_END, UnDefType.NULL);
|
||||
updateProgress(new QuantityType<>(0, SmartHomeUnits.PERCENT));
|
||||
socket.startDownload(configuration.getDownloadURL());
|
||||
} else {
|
||||
logger.info("A speedtest is already in progress, will retry on next refresh");
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void stopSpeedTest() {
|
||||
updateState(CHANNEL_TEST_ISRUNNING, OnOffType.OFF);
|
||||
updateProgress(UnDefType.NULL);
|
||||
updateState(CHANNEL_TEST_END, new DateTimeType());
|
||||
if (speedTestSocket != null) {
|
||||
SpeedTestSocket socket = speedTestSocket;
|
||||
socket.closeSocket();
|
||||
socket.removeSpeedTestListener(this);
|
||||
socket = null;
|
||||
speedTestSocket = null;
|
||||
logger.debug("Network speedtest finished");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompletion(final @Nullable SpeedTestReport testReport) {
|
||||
timeouts = configuration.maxTimeout;
|
||||
if (testReport != null) {
|
||||
BigDecimal rate = testReport.getTransferRateBit();
|
||||
QuantityType<DataTransferRate> quantity = new QuantityType<>(rate, BIT_PER_SECOND)
|
||||
.toUnit(MEGABIT_PER_SECOND);
|
||||
if (quantity != null) {
|
||||
switch (testReport.getSpeedTestMode()) {
|
||||
case DOWNLOAD:
|
||||
updateState(CHANNEL_RATE_DOWN, quantity);
|
||||
if (speedTestSocket != null && configuration != null) {
|
||||
speedTestSocket.startUpload(configuration.getUploadURL(), configuration.uploadSize);
|
||||
}
|
||||
break;
|
||||
case UPLOAD:
|
||||
updateState(CHANNEL_RATE_UP, quantity);
|
||||
stopSpeedTest();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(final @Nullable SpeedTestError testError, final @Nullable String errorMessage) {
|
||||
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) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Max timeout count reached");
|
||||
freeRefreshTask();
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgress(float percent, @Nullable SpeedTestReport testReport) {
|
||||
updateProgress(new QuantityType<>(Math.round(percent), SmartHomeUnits.PERCENT));
|
||||
}
|
||||
|
||||
private void updateProgress(State state) {
|
||||
if (!state.toString().equals(bufferedProgress.toString())) {
|
||||
bufferedProgress = state;
|
||||
updateState(CHANNEL_TEST_PROGRESS, bufferedProgress);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (command == OnOffType.ON
|
||||
&& ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR == getThing().getStatusInfo().getStatusDetail()) {
|
||||
logger.debug("Speedtest was offline, restarting it upon command to do so");
|
||||
startRefreshTask();
|
||||
} else {
|
||||
if (CHANNEL_TEST_ISRUNNING.equals(channelUID.getId())) {
|
||||
if (command == OnOffType.ON) {
|
||||
startSpeedTest();
|
||||
} else if (command == OnOffType.OFF) {
|
||||
stopSpeedTest();
|
||||
}
|
||||
} else {
|
||||
logger.debug("Command {} is not supported for channel: {}.", command, channelUID.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
freeRefreshTask();
|
||||
}
|
||||
|
||||
private void freeRefreshTask() {
|
||||
stopSpeedTest();
|
||||
if (refreshTask != null) {
|
||||
refreshTask.cancel(true);
|
||||
refreshTask = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void startRefreshTask() {
|
||||
logger.info("Speedtests starts in {} minutes, then refreshes every {} minutes", configuration.initialDelay,
|
||||
configuration.refreshInterval);
|
||||
refreshTask = scheduler.scheduleWithFixedDelay(this::startSpeedTest, configuration.initialDelay,
|
||||
configuration.refreshInterval, TimeUnit.MINUTES);
|
||||
timeouts = configuration.maxTimeout;
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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> {
|
||||
final long expiry;
|
||||
ExpiringCacheUpdate cacheUpdater;
|
||||
long expiresAt = 0;
|
||||
boolean refreshRequested = false;
|
||||
V value;
|
||||
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 <=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.
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
public 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.utils;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Examines output lines of the ping command and tries to extract the contained latency value.
|
||||
*
|
||||
* @author Andreas Hirsch - Initial contribution
|
||||
*/
|
||||
public class LatencyParser {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(LatencyParser.class);
|
||||
|
||||
// This is how the input looks like on Mac and Linux:
|
||||
// ping -c 1 192.168.1.1
|
||||
// PING 192.168.1.1 (192.168.1.1): 56 data bytes
|
||||
// 64 bytes from 192.168.1.1: icmp_seq=0 ttl=64 time=1.225 ms
|
||||
//
|
||||
// --- 192.168.1.1 ping statistics ---
|
||||
// 1 packets transmitted, 1 packets received, 0.0% packet loss
|
||||
// round-trip min/avg/max/stddev = 1.225/1.225/1.225/0.000 ms
|
||||
|
||||
/**
|
||||
* 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
|
||||
* latency value which matches the known patterns.
|
||||
*/
|
||||
public Optional<Double> parseLatency(String inputLine) {
|
||||
logger.debug("Parsing latency from input {}", inputLine);
|
||||
|
||||
String pattern = ".*time=(.*) ms";
|
||||
Matcher m = Pattern.compile(pattern).matcher(inputLine);
|
||||
if (m.find() && m.groupCount() == 1) {
|
||||
return Optional.of(Double.parseDouble(m.group(1)));
|
||||
}
|
||||
|
||||
logger.debug("Did not find a latency value");
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.utils;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.*;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang.StringUtils;
|
||||
import org.apache.commons.lang.SystemUtils;
|
||||
import org.apache.commons.net.util.SubnetUtils;
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.io.net.exec.ExecUtil;
|
||||
import org.openhab.core.net.CidrAddress;
|
||||
import org.openhab.core.net.NetUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Network utility functions for pinging and for determining all interfaces and assigned IP addresses.
|
||||
*
|
||||
* @author David Graeff - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class NetworkUtils {
|
||||
private final Logger logger = LoggerFactory.getLogger(NetworkUtils.class);
|
||||
|
||||
private LatencyParser latencyParser = new LatencyParser();
|
||||
|
||||
/**
|
||||
* Gets every IPv4 Address on each Interface except the loopback
|
||||
* The Address format is ip/subnet
|
||||
*
|
||||
* @return The collected IPv4 Addresses
|
||||
*/
|
||||
public Set<CidrAddress> getInterfaceIPs() {
|
||||
return NetUtil.getAllInterfaceAddresses().stream().filter(a -> a.getAddress() instanceof Inet4Address)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a set of all interface names.
|
||||
*
|
||||
* @return Set of interface names
|
||||
*/
|
||||
public Set<String> getInterfaceNames() {
|
||||
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.
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines every IP which can be assigned on all available interfaces
|
||||
*
|
||||
* @param maximumPerInterface The maximum of IP addresses per interface or 0 to get all.
|
||||
* @return Every single IP which can be assigned on the Networks the computer is connected to
|
||||
*/
|
||||
public Set<String> getNetworkIPs(int maximumPerInterface) {
|
||||
return getNetworkIPs(getInterfaceIPs(), maximumPerInterface);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes the interfaceIPs and fetches every IP which can be assigned on their network
|
||||
*
|
||||
* @param interfaceIPs The IPs which are assigned to the Network Interfaces
|
||||
* @param maximumPerInterface The maximum of IP addresses per interface or 0 to get all.
|
||||
* @return Every single IP which can be assigned on the Networks the computer is connected to
|
||||
*/
|
||||
public Set<String> getNetworkIPs(Set<CidrAddress> interfaceIPs, int maximumPerInterface) {
|
||||
LinkedHashSet<String> networkIPs = new LinkedHashSet<>();
|
||||
|
||||
short minCidrPrefixLength = 8; // historic Class A network, addresses = 16777214
|
||||
if (maximumPerInterface != 0) {
|
||||
// calculate minimum CIDR prefix length from maximumPerInterface
|
||||
// (equals leading unset bits (Integer has 32 bits)
|
||||
minCidrPrefixLength = (short) Integer.numberOfLeadingZeros(maximumPerInterface);
|
||||
if (Integer.bitCount(maximumPerInterface) == 1) {
|
||||
// if only the highest is set, decrease prefix by 1 to cover all addresses
|
||||
minCidrPrefixLength--;
|
||||
}
|
||||
}
|
||||
logger.trace("set minCidrPrefixLength to {}, maximumPerInterface is {}", minCidrPrefixLength,
|
||||
maximumPerInterface);
|
||||
|
||||
for (CidrAddress cidrNotation : interfaceIPs) {
|
||||
if (cidrNotation.getPrefix() < minCidrPrefixLength) {
|
||||
logger.info(
|
||||
"CIDR prefix is smaller than /{} on interface with address {}, truncating to /{}, some addresses might be lost",
|
||||
minCidrPrefixLength, cidrNotation, minCidrPrefixLength);
|
||||
cidrNotation = new CidrAddress(cidrNotation.getAddress(), minCidrPrefixLength);
|
||||
}
|
||||
|
||||
SubnetUtils utils = new SubnetUtils(cidrNotation.toString());
|
||||
String[] addresses = utils.getInfo().getAllAddresses();
|
||||
int len = addresses.length;
|
||||
if (maximumPerInterface != 0 && maximumPerInterface < len) {
|
||||
len = maximumPerInterface;
|
||||
}
|
||||
for (int i = 0; i < len; i++) {
|
||||
networkIPs.add(addresses[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return networkIPs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to establish a tcp connection to the given port. Returns false if a timeout occurred
|
||||
* or the connection was denied.
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
public Optional<PingResult> servicePing(String host, int port, int timeout) throws IOException {
|
||||
double execStartTimeInMS = System.currentTimeMillis();
|
||||
|
||||
SocketAddress socketAddress = new InetSocketAddress(host, port);
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the working method for the native system ping. If no native ping
|
||||
* works JavaPing is returned.
|
||||
*/
|
||||
public IpPingMethodEnum determinePingMethod() {
|
||||
IpPingMethodEnum method;
|
||||
if (SystemUtils.IS_OS_WINDOWS) {
|
||||
method = IpPingMethodEnum.WINDOWS_PING;
|
||||
} else if (SystemUtils.IS_OS_MAC) {
|
||||
method = IpPingMethodEnum.MAC_OS_PING;
|
||||
} else if (SystemUtils.IS_OS_UNIX) {
|
||||
method = IpPingMethodEnum.IPUTILS_LINUX_PING;
|
||||
} else {
|
||||
// We cannot estimate the command line for any other operating system and just return false
|
||||
return IpPingMethodEnum.JAVA_PING;
|
||||
}
|
||||
|
||||
try {
|
||||
Optional<PingResult> pingResult = nativePing(method, "127.0.0.1", 1000);
|
||||
if (pingResult.isPresent() && pingResult.get().isSuccess()) {
|
||||
return method;
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt(); // Reset interrupt flag
|
||||
}
|
||||
return IpPingMethodEnum.JAVA_PING;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the external arp ping utility (arping) is available and executable on the given path.
|
||||
*/
|
||||
public ArpPingUtilEnum determineNativeARPpingMethod(String arpToolPath) {
|
||||
String result = ExecUtil.executeCommandLineAndWaitResponse(arpToolPath + " --help", 100);
|
||||
if (StringUtils.isBlank(result)) {
|
||||
return ArpPingUtilEnum.UNKNOWN_TOOL;
|
||||
} else if (result.contains("Thomas Habets")) {
|
||||
if (result.matches("(?s)(.*)w sec Specify a timeout(.*)")) {
|
||||
return ArpPingUtilEnum.THOMAS_HABERT_ARPING;
|
||||
} else {
|
||||
return ArpPingUtilEnum.THOMAS_HABERT_ARPING_WITHOUT_TIMEOUT;
|
||||
}
|
||||
} else if (result.contains("-w timeout")) {
|
||||
return ArpPingUtilEnum.IPUTILS_ARPING;
|
||||
} else if (result.contains("Usage: arp-ping.exe")) {
|
||||
return ArpPingUtilEnum.ELI_FULKERSON_ARP_PING_FOR_WINDOWS;
|
||||
}
|
||||
return ArpPingUtilEnum.UNKNOWN_TOOL;
|
||||
}
|
||||
|
||||
public enum IpPingMethodEnum {
|
||||
JAVA_PING,
|
||||
WINDOWS_PING,
|
||||
IPUTILS_LINUX_PING,
|
||||
MAC_OS_PING
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @throws IOException The ping command could probably not be found
|
||||
*/
|
||||
public Optional<PingResult> nativePing(@Nullable IpPingMethodEnum method, String hostname, int timeoutInMS)
|
||||
throws IOException, InterruptedException {
|
||||
double execStartTimeInMS = System.currentTimeMillis();
|
||||
|
||||
Process proc;
|
||||
if (method == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
// 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)
|
||||
.start();
|
||||
break;
|
||||
case MAC_OS_PING:
|
||||
proc = new ProcessBuilder("ping", "-t", String.valueOf(timeoutInMS / 1000), "-c", "1", hostname)
|
||||
.start();
|
||||
break;
|
||||
case WINDOWS_PING:
|
||||
proc = new ProcessBuilder("ping", "-w", String.valueOf(timeoutInMS), "-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();
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Exception: return code is also 0 in Windows for all requests on the local subnet.
|
||||
// see https://superuser.com/questions/403905/ping-from-windows-7-get-no-reply-but-sets-errorlevel-to-0
|
||||
|
||||
int result = proc.waitFor();
|
||||
if (result != 0) {
|
||||
return Optional.of(new PingResult(false, System.currentTimeMillis() - execStartTimeInMS));
|
||||
}
|
||||
|
||||
try (BufferedReader r = new BufferedReader(new InputStreamReader(proc.getInputStream()))) {
|
||||
String line = r.readLine();
|
||||
if (line == null) {
|
||||
throw new IOException("Received no output from ping process.");
|
||||
}
|
||||
do {
|
||||
// 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);
|
||||
}
|
||||
line = r.readLine();
|
||||
} while (line != null);
|
||||
|
||||
return Optional.of(new PingResult(false, System.currentTimeMillis() - execStartTimeInMS));
|
||||
}
|
||||
}
|
||||
|
||||
public enum ArpPingUtilEnum {
|
||||
UNKNOWN_TOOL,
|
||||
IPUTILS_ARPING,
|
||||
THOMAS_HABERT_ARPING,
|
||||
THOMAS_HABERT_ARPING_WITHOUT_TIMEOUT,
|
||||
ELI_FULKERSON_ARP_PING_FOR_WINDOWS
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @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.
|
||||
* @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();
|
||||
|
||||
if (arpUtilPath == null || arpingTool == null || arpingTool == ArpPingUtilEnum.UNKNOWN_TOOL) {
|
||||
return Optional.empty();
|
||||
}
|
||||
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",
|
||||
interfaceName, ipV4address).start();
|
||||
} else if (arpingTool == ArpPingUtilEnum.ELI_FULKERSON_ARP_PING_FOR_WINDOWS) {
|
||||
proc = new ProcessBuilder(arpUtilPath, "-w", String.valueOf(timeoutInMS), "-x", ipV4address).start();
|
||||
} else {
|
||||
proc = new ProcessBuilder(arpUtilPath, "-w", String.valueOf(timeoutInMS / 1000), "-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));
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a Java ping.
|
||||
*
|
||||
* @param timeoutInMS A timeout in milliseconds
|
||||
* @param destinationAddress The address to check
|
||||
* @return Ping result information. Optional is empty if ping command was not executed.
|
||||
*/
|
||||
public Optional<PingResult> javaPing(int timeoutInMS, InetAddress destinationAddress) {
|
||||
double execStartTimeInMS = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
if (destinationAddress.isReachable(timeoutInMS)) {
|
||||
return Optional.of(new PingResult(true, System.currentTimeMillis() - execStartTimeInMS));
|
||||
} else {
|
||||
return Optional.of(new PingResult(false, System.currentTimeMillis() - execStartTimeInMS));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
return Optional.of(new PingResult(false, System.currentTimeMillis() - execStartTimeInMS));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public void wakeUpIOS(InetAddress address) throws IOException {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.utils;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Information about the ping result.
|
||||
*
|
||||
* @author Andreas Hirsch - Initial contribution
|
||||
*/
|
||||
public class PingResult {
|
||||
|
||||
private boolean success;
|
||||
private Double responseTimeInMS;
|
||||
private double executionTimeInMS;
|
||||
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
public PingResult(boolean success, double executionTimeInMS) {
|
||||
this.success = success;
|
||||
this.executionTimeInMS = executionTimeInMS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return <code>true</code> if the device was reachable, <code>false</code> if not.
|
||||
*/
|
||||
public boolean isSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Response time in ms which was returned by the ping command. Optional is empty if response time provided
|
||||
* by ping command is not available.
|
||||
*/
|
||||
public Optional<Double> getResponseTimeInMS() {
|
||||
return responseTimeInMS == null ? Optional.empty() : Optional.of(responseTimeInMS);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param responseTimeInMS Response time in ms which was returned by the ping command.
|
||||
*/
|
||||
public void setResponseTimeInMS(double responseTimeInMS) {
|
||||
this.responseTimeInMS = responseTimeInMS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PingResult{" + "success=" + success + ", responseTimeInMS=" + responseTimeInMS + ", executionTimeInMS="
|
||||
+ executionTimeInMS + '}';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Execution time of the ping command in ms.
|
||||
*/
|
||||
public double getExecutionTimeInMS() {
|
||||
return executionTimeInMS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<binding:binding id="network" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
|
||||
|
||||
<name>Network Binding</name>
|
||||
<description>The Network Binding can be used for device presence detection and to determine network device health</description>
|
||||
<author>Marc Mettke, David Graeff</author>
|
||||
<config-description>
|
||||
<parameter name="allowSystemPings" type="boolean">
|
||||
<default>true</default>
|
||||
<label>Allow System Pings</label>
|
||||
<description>Allows or disallows to use system pings next to the java integrated ping functionality.
|
||||
On windows the
|
||||
system ping works more reliable most of the time.</description>
|
||||
</parameter>
|
||||
<parameter name="allowDHCPlisten" type="boolean">
|
||||
<default>true</default>
|
||||
<label>Listen for DHCP Requests</label>
|
||||
<description>Usually a device requests an IP address in an IPv4 network with the help of DHCP as soon as it enters a
|
||||
network. If we listen to those
|
||||
packets, we can detect a device presence even faster. You need elevated access rights
|
||||
(see readme) for this to work.</description>
|
||||
</parameter>
|
||||
<parameter name="cacheDeviceStateTimeInMS" type="integer" unit="ms">
|
||||
<default>2000</default>
|
||||
<label>Cache Time</label>
|
||||
<description>The result of a device presence detection is cached for a small amount of time. Be aware that no new
|
||||
pings will be issued within this time frame, even if explicitly requested.</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="arpPingToolPath" type="text">
|
||||
<default>arping</default>
|
||||
<label>ARP Ping Tool Path</label>
|
||||
<description>If your arp ping tool is not called arping and cannot be found in the PATH environment, you can
|
||||
configure the absolute path / tool name here.</description>
|
||||
</parameter>
|
||||
<parameter name="preferResponseTimeAsLatency" type="boolean">
|
||||
<default>false</default>
|
||||
<label>Use Response Time as Latency</label>
|
||||
<description>If enabled, an attempt will be made to extract the latency from the output of the ping command. If no
|
||||
such latency value is found in the ping command output, the time to execute the ping command is used as fallback
|
||||
latency. If disabled, the time to execute the ping command is always used as latency value.</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</binding:binding>
|
||||
@@ -0,0 +1,46 @@
|
||||
# binding
|
||||
binding.network.name = Network Binding
|
||||
binding.network.description = Das Network Binding überprüft, ob sich ein Gerät aktuell im Netzwerk befindet oder nicht.
|
||||
binding.config.network.allow_system_pings.label = Erlaubt System Pings
|
||||
binding.config.network.allow_system_pings.description = Nutzt das Ping Programm des Systems zusätzlich zum Java ping.
|
||||
binding.config.network.allow_dhcp_listen.label = Erlaubt DHCP Sniffing
|
||||
binding.config.network.allow_dhcp_listen.description = Lauscht auf DHCP Pakete, welche beim Eintritt von Geräten in das Netzwerk gesendet werden, um die Verfügbarkeit eines Gerätes in beinahe Echtzeit mitzuteilen.
|
||||
binding.config.network.cache_device_state.label = Cache Zeitlimit
|
||||
binding.config.network.cache_device_state.description = Die Geräte Verfügbarkeit wird für eine geringe Zeit in Millisekunden zwischengespeichert.
|
||||
binding.config.network.arp_ping_tool_path.label = ARP Ping Pfad
|
||||
binding.config.network.arp_ping_tool_path.description = Wenn arping nicht in der %PATH% Umgebung aufgefunden werden kann, muss der absolute Pfad inklusive Toolname hier angegeben werden.
|
||||
|
||||
# thing types
|
||||
thing-type.network.pingdevice.label = Pingable Netzwerkgerät
|
||||
thing-type.network.pingdevice.description = Die Verfügbarkeit des Geräts wird mit ICMP Ping, ARP Ping und DHCP Paketen festgestellt.
|
||||
thing-type.config.network.pingdevice.hostname.label = Hostname oder IP
|
||||
thing-type.config.network.pingdevice.hostname.description = Hostname oder IP des Netzwerkgerätes
|
||||
thing-type.config.network.pingdevice.retry.label = Wiederholen
|
||||
thing-type.config.network.pingdevice.retry.description = Gibt an, wie oft der PING wiederholt werden soll, bevor das Gerät als offline markiert wird.
|
||||
thing-type.config.network.pingdevice.timeout.label = Zeitlimit
|
||||
thing-type.config.network.pingdevice.timeout.description = Gibt an, wie lange maximal gewartet wird (in ms), bevor ein Gerät als nicht vorhanden gekennzeichnet wird.
|
||||
thing-type.config.network.pingdevice.refreshInterval.label = Aktualisierungsintervall
|
||||
thing-type.config.network.pingdevice.refreshInterval.description = Spezifiziert den Aktualisierungsintervall (in ms)
|
||||
|
||||
thing-type.network.servicedevice.label = Netzwerkgerät mit Dienst
|
||||
thing-type.network.servicedevice.description = Die Verfügbarkeit des Geräts wird durch einen Verbindungsversuch mit dem angegeben TCP Dienst festgestellt.
|
||||
thing-type.config.network.servicedevice.hostname.label = Hostname oder IP
|
||||
thing-type.config.network.servicedevice.hostname.description = Hostname oder IP des Netzwerkgerätes
|
||||
thing-type.config.network.servicedevice.retry.label = Wiederholen
|
||||
thing-type.config.network.servicedevice.retry.description = Gibt an, wie oft der Verbindungsversuch wiederholt werden soll, bevor das Gerät als offline markiert wird.
|
||||
thing-type.config.network.servicedevice.timeout.label = Zeitlimit
|
||||
thing-type.config.network.servicedevice.timeout.description = Gibt an, wie lange maximal gewartet wird (in ms), bevor ein Gerät als nicht vorhanden gekennzeichnet wird.
|
||||
thing-type.config.network.servicedevice.refreshInterval.label = Aktualisierungsintervall
|
||||
thing-type.config.network.servicedevice.refreshInterval.description = Spezifiziert das Aktualisierungsintervall (in ms)
|
||||
thing-type.config.network.servicedevice.port.label = Port
|
||||
thing-type.config.network.servicedevice.port.description = Der TCP Port an dem das Gerät erreichbar ist. Muss größer 0 sein.
|
||||
|
||||
# channel types
|
||||
channel-type.network.online.label = Online
|
||||
channel-type.network.online.description = Gibt an ob das Gerät aktuell online oder offline ist.
|
||||
|
||||
channel-type.network.latency.label = Pingzeit
|
||||
channel-type.network.latency.description = Gibt an wie lange ein Ping in Millisekunden an das Gerät dauert.
|
||||
|
||||
channel-type.network.lastseen.label = Zuletzt gesehen
|
||||
channel-type.network.lastseen.description = Gibt Zeit/Datum an wann das Gerät zuletzt gesehen wurde.
|
||||
@@ -0,0 +1,222 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="network"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd"><!--Network Binding -->
|
||||
<thing-type id="pingdevice">
|
||||
<label>Pingable Network Device</label>
|
||||
<description>The presence detection is performed by using ICMP and, if available, ARP pings.
|
||||
You can change the arping
|
||||
tool path in the binding configuration.
|
||||
DHCP sniffing is performed for faster network reentry discovery.</description>
|
||||
|
||||
<channels>
|
||||
<channel id="online" typeId="online"/>
|
||||
<channel id="latency" typeId="latency"/>
|
||||
<channel id="lastseen" typeId="lastseen"/>
|
||||
</channels>
|
||||
|
||||
<properties>
|
||||
<property name="arp_state">-</property>
|
||||
<property name="dhcp_state">-</property>
|
||||
<property name="icmp_state">-</property>
|
||||
<property name="presence_detection_type">-</property>
|
||||
<property name="uses_ios_wakeup">-</property>
|
||||
</properties>
|
||||
|
||||
<config-description>
|
||||
<parameter name="hostname" type="text" required="true">
|
||||
<label>Hostname or IP</label>
|
||||
<description>Hostname or IP of the device</description>
|
||||
</parameter>
|
||||
|
||||
<parameter name="macAddress" type="text" pattern="([0-9A-Fa-f]{2}[:-]?){5}([0-9A-Fa-f]{2})">
|
||||
<label>MAC Address</label>
|
||||
<description>MAC address used for waking the device by the Wake-on-LAN action</description>
|
||||
</parameter>
|
||||
|
||||
<parameter name="refreshInterval" type="integer">
|
||||
<label>Refresh Interval</label>
|
||||
<description>States how long to wait after a device state update before the next refresh shall occur (in ms)</description>
|
||||
<default>60000</default>
|
||||
</parameter>
|
||||
|
||||
<parameter name="retry" type="integer">
|
||||
<label>Retry</label>
|
||||
<description>How many refresh interval cycles should a presence detection should take place, before the device is
|
||||
stated as offline</description>
|
||||
<default>1</default>
|
||||
</parameter>
|
||||
|
||||
<parameter name="timeout" type="integer" unit="ms">
|
||||
<label>Timeout</label>
|
||||
<description>States how long to wait for a response (in ms), before if a device is stated as offline</description>
|
||||
<default>5000</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<thing-type id="servicedevice">
|
||||
<label>Network Device with Running Service</label>
|
||||
<description>A device which reachable state is detected by connecting to a TCP port.
|
||||
DHCP sniffing is performed for
|
||||
faster network reentry discovery.</description>
|
||||
|
||||
<channels>
|
||||
<channel id="online" typeId="online"/>
|
||||
<channel id="latency" typeId="latency"/>
|
||||
<channel id="lastseen" typeId="lastseen"/>
|
||||
</channels>
|
||||
|
||||
<properties>
|
||||
<property name="arp_state">-</property>
|
||||
<property name="dhcp_state">-</property>
|
||||
<property name="icmp_state">-</property>
|
||||
<property name="presence_detection_type">-</property>
|
||||
<property name="uses_ios_wakeup">-</property>
|
||||
</properties>
|
||||
|
||||
<config-description>
|
||||
<parameter name="hostname" type="text" required="true">
|
||||
<label>Hostname or IP</label>
|
||||
<description>Hostname or IP of the device</description>
|
||||
</parameter>
|
||||
|
||||
<parameter name="port" type="integer" required="true">
|
||||
<label>Port</label>
|
||||
<description>The port on which the device can be accessed. Windows systems usually have the 445 port open.
|
||||
Webservers are on port 80.</description>
|
||||
<default>80</default>
|
||||
</parameter>
|
||||
|
||||
<parameter name="macAddress" type="text" pattern="([0-9A-Fa-f]{2}[:-]?){5}([0-9A-Fa-f]{2})">
|
||||
<label>MAC Address</label>
|
||||
<description>MAC address used for waking the device by the Wake-on-LAN action</description>
|
||||
</parameter>
|
||||
|
||||
<parameter name="retry" type="integer">
|
||||
<label>Retry</label>
|
||||
<description>Defines how many times a connection attempt shall occur, before the device is stated as offline</description>
|
||||
<default>1</default>
|
||||
</parameter>
|
||||
|
||||
<parameter name="timeout" type="integer">
|
||||
<label>Timeout</label>
|
||||
<description>States how long to wait for a response (in ms), before if a device is stated as offline</description>
|
||||
<default>5000</default>
|
||||
</parameter>
|
||||
|
||||
<parameter name="refreshInterval" type="integer">
|
||||
<label>Refresh Interval</label>
|
||||
<description>States how long to wait after a device state update before the next refresh shall occur (in ms)</description>
|
||||
<default>60000</default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<thing-type id="speedtest">
|
||||
<label>SpeedTest</label>
|
||||
<description>Provides information about bandwidth speed.</description>
|
||||
|
||||
<channels>
|
||||
<channel id="isRunning" typeId="isRunning"/>
|
||||
<channel id="progress" typeId="progress"/>
|
||||
<channel id="rateUp" typeId="rateUp"/>
|
||||
<channel id="rateDown" typeId="rateDown"/>
|
||||
<channel id="testStart" typeId="Timestamp">
|
||||
<label>Test Start</label>
|
||||
</channel>
|
||||
<channel id="testEnd" typeId="Timestamp">
|
||||
<label>Test End</label>
|
||||
</channel>
|
||||
</channels>
|
||||
|
||||
<config-description>
|
||||
<parameter name="refreshInterval" type="integer" min="2">
|
||||
<label>Refresh Time Interval</label>
|
||||
<description>Refresh time interval in minutes.</description>
|
||||
<default>20</default>
|
||||
</parameter>
|
||||
<parameter name="initialDelay" type="integer" min="2">
|
||||
<label>Initial Delay</label>
|
||||
<description>Delay before starting the first speed test (minutes) after initialization of the binding.</description>
|
||||
<default>5</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
<parameter name="uploadSize" type="integer" min="5000">
|
||||
<label>Upload Size</label>
|
||||
<description>Size of the file to be uploaded (bytes).</description>
|
||||
<default>1000000</default>
|
||||
</parameter>
|
||||
<parameter name="url" type="text" required="true">
|
||||
<label>Test Server URL</label>
|
||||
<description>Url of the speed test server</description>
|
||||
</parameter>
|
||||
<parameter name="fileName" type="text" required="true">
|
||||
<label>File Name</label>
|
||||
<description>Name of the file to download from test server</description>
|
||||
</parameter>
|
||||
<parameter name="maxTimeout" type="integer">
|
||||
<label>Timeouts</label>
|
||||
<description>Number of timeout that can happend before the device is stated as offline</description>
|
||||
<default>3</default>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
<channel-type id="isRunning">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Test Running</label>
|
||||
<description>Indicates if a test is currently ongoing</description>
|
||||
<state readOnly="false"></state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="progress">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>Progress</label>
|
||||
<description>Current Test progression</description>
|
||||
<state readOnly="true" min="0" max="100" pattern="%.0f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="rateUp">
|
||||
<item-type>Number:DataTransferRate</item-type>
|
||||
<label>Upload Rate</label>
|
||||
<description>Current upload rate</description>
|
||||
<state readOnly="true" pattern="%d %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="rateDown">
|
||||
<item-type>Number:DataTransferRate</item-type>
|
||||
<label>Download Rate</label>
|
||||
<description>Current download rate</description>
|
||||
<state readOnly="true" pattern="%d %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="Timestamp">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>Timestamp</label>
|
||||
<description>Status timestamp</description>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="online">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Online</label>
|
||||
<description>States whether a device is online or offline</description>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
<channel-type id="latency">
|
||||
<item-type>Number:Time</item-type>
|
||||
<label>Latency</label>
|
||||
<description>States the latency time</description>
|
||||
<state readOnly="true" pattern="%d %unit%"></state>
|
||||
</channel-type>
|
||||
<channel-type id="lastseen">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>Last Seen</label>
|
||||
<description>States the last seen date/time</description>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
</thing:thing-descriptions>
|
||||
Reference in New Issue
Block a user