added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -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>

View File

@@ -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 + '}';
}
}

View File

@@ -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();
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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";
}
}
}

View File

@@ -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);
}

View File

@@ -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
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}

View File

@@ -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());
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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
}
}
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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.

View File

@@ -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>