Skip to content
This repository has been archived by the owner on May 7, 2020. It is now read-only.

Implemented Option to choose default network interface #3930

Merged
merged 8 commits into from Aug 11, 2017
Expand Up @@ -4,12 +4,13 @@ Bundle-Name: Eclipse SmartHome Config Core
Bundle-SymbolicName: org.eclipse.smarthome.config.core
Bundle-Version: 0.9.0.qualifier
Bundle-Vendor: Eclipse.org/SmartHome
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Import-Package: com.google.common.base,
com.google.common.collect,
com.google.gson,
org.apache.commons.lang.reflect,
org.eclipse.jdt.annotation;resolution:=optional,
org.apache.commons.net.util,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We imho must not introduce this dependency to the ESH core bundles.

org.eclipse.smarthome.config.core,
org.eclipse.smarthome.config.core.dto,
org.eclipse.smarthome.config.core.i18n,
Expand Down
@@ -1 +1,2 @@
/org.eclipse.smarthome.config.core.internal.i18n.I18nConfigOptionsProvider.xml
/org.eclipse.smarthome.config.core.net.internal.NetworkConfigOptionProvider.xml
@@ -0,0 +1,99 @@
/**
* Copyright (c) 2014-2017 by the respective copyright holders.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.smarthome.config.core.net.internal;

import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;

import org.apache.commons.net.util.SubnetUtils;
import org.eclipse.smarthome.config.core.ConfigOptionProvider;
import org.eclipse.smarthome.config.core.ParameterOption;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Provides a list of IPv4 addresses of the local machine and shows the user which interface belongs to which IP address
*
* @author Stefan Triller - initial contribution
*
*/
@Component
public class NetworkConfigOptionProvider implements ConfigOptionProvider {

static final URI CONFIG_URI = URI.create("system:network");
static final String PARAM_PRIMARY_ADDRESS = "primaryAddress";

private final Logger logger = LoggerFactory.getLogger(NetworkConfigOptionProvider.class);

@Override
public Collection<ParameterOption> getParameterOptions(URI uri, String param, Locale locale) {
if (!uri.equals(CONFIG_URI)) {
return null;
}

if (param.equals(PARAM_PRIMARY_ADDRESS)) {
return getIPv4Addresses();
}
return null;
}

private List<ParameterOption> getIPv4Addresses() {
ArrayList<ParameterOption> interfaceOptions = new ArrayList<>();

HashSet<String> subnets = new HashSet<>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change first occurrence to Set


try {
final Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
final NetworkInterface current = interfaces.nextElement();
if (!current.isUp() || current.isLoopback() || current.isVirtual()) {
continue;
}

for (InterfaceAddress ifAddr : current.getInterfaceAddresses()) {
InetAddress addr = ifAddr.getAddress();

if (addr.isLoopbackAddress() || (addr instanceof Inet6Address)) {
continue;
}

String ipv4Address = addr.getHostAddress();

SubnetUtils su = new SubnetUtils(
ipv4Address + "/" + String.valueOf(ifAddr.getNetworkPrefixLength()));
String subNetString = su.getInfo().getNetworkAddress() + "/"
+ String.valueOf(ifAddr.getNetworkPrefixLength());

subnets.add(subNetString);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although you didn't add any annotation to NetUtil.getIpv4NetAddress, I assume that subNetString can be null here.

}
}
} catch (SocketException ex) {
logger.error("Could not retrieve network interface: {}", ex.getMessage(), ex);
return null;
}

for (String subnet : subnets) {
ParameterOption po = new ParameterOption(subnet, subnet);
interfaceOptions.add(po);
}

return interfaceOptions;
}

}
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="http://eclipse.org/smarthome/schemas/config-description/v1.0.0"
xsi:schemaLocation="http://eclipse.org/smarthome/schemas/config-description/v1.0.0
http://eclipse.org/smarthome/schemas/config-description-1.0.0.xsd">

<config-description uri="system:network">
<parameter name="primaryAddress" type="text">
<label>Primary Address</label>
<description><![CDATA[<p>The primary network to be used</p>
<p>Alternative: Enter an IP address (XXX.XXX.XXX.XXX) manually</p>]]></description>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You do not mention the alternative to the alternative :-)

Better say:

A subnet (e.g. 192.168.1.0/24) or an IP address (e.g. 192.168.1.5)

<limitToOptions>false</limitToOptions>
</parameter>
</config-description>

</config-description:config-descriptions>
3 changes: 2 additions & 1 deletion bundles/core/org.eclipse.smarthome.core/META-INF/MANIFEST.MF
Expand Up @@ -28,7 +28,7 @@ Ignore-Package: org.eclipse.smarthome.core.internal.items,org.eclipse.smarthome.
nal,org.eclipse.smarthome.core.internal.events,org.eclipse.smarthome.core.internal.loggin
g
Bundle-Name: Eclipse SmartHome Core
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Bundle-RequiredExecutionEnvironment: JavaSE-1.8
Bundle-Vendor: Eclipse.org/SmartHome
Bundle-Version: 0.9.0.qualifier
Bundle-ManifestVersion: 2
Expand All @@ -39,6 +39,7 @@ Import-Package: com.google.common.base,
org.apache.commons.io,
org.apache.commons.lang,
org.eclipse.jdt.annotation;resolution:=optional,
org.apache.commons.net.util,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We imho must not introduce this dependency to the ESH core bundles.

org.eclipse.smarthome.core.auth,
org.eclipse.smarthome.core.binding,
org.eclipse.smarthome.core.binding.dto,
Expand Down
@@ -0,0 +1 @@
/org.eclipse.smarthome.network.xml
Expand Up @@ -13,10 +13,16 @@
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.apache.commons.net.util.SubnetUtils;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -25,12 +31,51 @@
*
* @author Markus Rathgeb - Initial contribution and API
* @author Mark Herwege - Added methods to find broadcast address(es)
* @author Stefan Triller - Converted to OSGi service with primary ipv4 conf
*/
public class NetUtil {
@Component(name = "org.eclipse.smarthome.network", property = { "service.config.description.uri=system:network",
"service.config.label=Network Settings", "service.config.category=system" })
public class NetUtil implements NetworkAddressProvider {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, just realised your name choice now: "Provider" has a very special meaning in many contexts in ESH. NetworkAddressService would probably a more neutral choice.


private static final String PRIMARY_ADDRESS = "primaryAddress";
private static final Logger LOGGER = LoggerFactory.getLogger(NetUtil.class);

private NetUtil() {
private String primaryAddress;

@SuppressWarnings("unchecked")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why that?

protected void activate(ComponentContext componentContext) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of pulling the properties out of the context, why don't you simply implement activate(Map<String, Object> props)?

Dictionary<String, Object> props = componentContext.getProperties();
modified((Map<String, Object>) props);
}

@Modified
public synchronized void modified(Map<String, Object> config) {
String defaultInterfaceConfig = (String) config.get(PRIMARY_ADDRESS);
if (defaultInterfaceConfig == null || defaultInterfaceConfig.equals("")) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

better use .isEmpty()

// if none is specified we return the default one for backward compatibility
primaryAddress = NetUtil.getLocalIpv4HostAddress();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are calling a deprecated method here. Should be avoided :-)

} else {
String primaryAddressConf = (String) config.get(PRIMARY_ADDRESS);

String[] addrString = primaryAddressConf.split("/");
if (addrString.length > 1) {
String ip = getIPv4inSubnet(primaryAddressConf);
if (ip == null) {
// an error has occurred, used first interface like nothing has been configured
LOGGER.warn("Error in IP configuration, will continue to use first interface");
NetUtil.getLocalIpv4HostAddress();
} else {
primaryAddress = ip;
}
} else {
primaryAddress = addrString[0];
}
}
}

@Override
public String getPrimaryIpv4HostAddress() {
return primaryAddress;
}

/**
Expand Down Expand Up @@ -104,4 +149,40 @@ public static String getBroadcastAddress() {
}
}

private String getIPv4inSubnet(String subnet) {
try {
final Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
while (interfaces.hasMoreElements()) {
final NetworkInterface current = interfaces.nextElement();
if (!current.isUp() || current.isLoopback() || current.isVirtual()) {
continue;
}

for (InterfaceAddress ifAddr : current.getInterfaceAddresses()) {
InetAddress addr = ifAddr.getAddress();

if (addr.isLoopbackAddress() || (addr instanceof Inet6Address)) {
continue;
}

String ipv4Address = addr.getHostAddress();

SubnetUtils su = new SubnetUtils(
ipv4Address + "/" + String.valueOf(ifAddr.getNetworkPrefixLength()));
String subNetString = su.getInfo().getNetworkAddress() + "/"
+ String.valueOf(ifAddr.getNetworkPrefixLength());

// use first IP within this subnet
if (subNetString.equals(subnet)) {
return ipv4Address;
}
}
}
} catch (SocketException ex) {
LOGGER.error("Could not retrieve network interface: {}", ex.getMessage(), ex);
return null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this line is superfluous.

}
return null;
}

}
@@ -0,0 +1,19 @@
/**
* Copyright (c) 2014-2017 by the respective copyright holders.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.smarthome.core.net;

/**
* Interface that provides access to configured network addresses
*
* @author Stefan Triller - initial contribution
*
*/
public interface NetworkAddressProvider {

public String getPrimaryIpv4HostAddress();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about adding a NonNull annotation? :-)
On an interface, you can remove the "public" keyword.

}
21 changes: 21 additions & 0 deletions docs/documentation/development/bindings/thing-handler.md
Expand Up @@ -180,6 +180,27 @@ Furthermore bindings can specify a localized description of the thing status by
rate_limit=Device is blocked by remote service for {0} minutes. Maximum limit of {1} configuration changes per {2} has been exceeded. For further info please refer to device vendor.
```

## Offering a callback URL
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure whether this is the correct place for the documentation of this feature as the feature is completely independent of bindings. It is often used for the implementation of AudioSinks and potentially other extensions. We should rather have a place for such general system services.


Some things might require a `callback` URL which should be bound to a certain network interface. A user can configure his default network address via Paper UI under `Configuration -> System -> Network Settings`. To obtain this configured address the `ThingHandlerFactory` needs a `service reference` to the `NetworkInterfaceService` in its `OSGI-INF/MyHandlerFactory.xml`:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a new line for every sentence - this makes diffing and reviews easier.

The correct name seems to be NetworkAddressProvider.
The example is not good, because we by now ask people to use DS annotations, not XML files.


```
<reference bind="setNetworkAddressprovider" cardinality="1..1" interface="org.eclipse.smarthome.core.net.NetworkAddressProvider" name="NetworkAddressprovider" policy="static" unbind="unsetNetworkAddressprovider"/>
```

Inside `MyHandlerFactory.java` two methods are required:

```java
protected void setNetworkAddressprovider(NetworkAddressprovider networkAddressprovider) {
this.networkAddressprovider = networkInterfaceService;
}
protected void unsetNetworkAddressprovider(NetworkAddressprovider networkAddressprovider) {
this.networkAddressprovider = null;
}
```

Now the `MyHandlerFactory` can obtain the configured IP address via `networkAddressprovider.getPrimaryIpv4HostAddress()`. This IP address can be used in callback URL offered to a device.

## Channel Links

Some bindings might want to start specific functionality for a channel only if an item is linked to the channel. The `ThingHandler` has two callback methods `channelLinked(ChannelUID channelUID)` and `channelUnlinked(ChannelUID channelUID)`, which are called for every link that is added or removed to/from a channel. So please be aware of the fact that both methods can be called multiple times.
Expand Down
Expand Up @@ -18,4 +18,5 @@
<provide interface="org.eclipse.smarthome.core.thing.binding.ThingHandlerFactory"/>
</service>
<reference bind="setAudioHTTPServer" cardinality="1..1" interface="org.eclipse.smarthome.core.audio.AudioHTTPServer" name="AudioHTTPServer" policy="static" unbind="unsetAudioHTTPServer"/>
<reference bind="setNetworkAddressProvider" cardinality="1..1" interface="org.eclipse.smarthome.core.net.NetworkAddressProvider" name="NetworkAddressProvider" policy="static" unbind="unsetNetworkAddressProvider"/>
</scr:component>
Expand Up @@ -21,7 +21,7 @@
import org.eclipse.smarthome.core.audio.AudioHTTPServer;
import org.eclipse.smarthome.core.audio.AudioSink;
import org.eclipse.smarthome.core.net.HttpServiceUtil;
import org.eclipse.smarthome.core.net.NetUtil;
import org.eclipse.smarthome.core.net.NetworkAddressProvider;
import org.eclipse.smarthome.core.thing.Thing;
import org.eclipse.smarthome.core.thing.ThingTypeUID;
import org.eclipse.smarthome.core.thing.ThingUID;
Expand All @@ -46,6 +46,7 @@ public class SonosHandlerFactory extends BaseThingHandlerFactory {
private UpnpIOService upnpIOService;
private DiscoveryServiceRegistry discoveryServiceRegistry;
private AudioHTTPServer audioHTTPServer;
private NetworkAddressProvider networkAddressprovider;

private Map<String, ServiceRegistration<AudioSink>> audioSinkRegistrations = new ConcurrentHashMap<>();

Expand Down Expand Up @@ -110,7 +111,7 @@ private String createCallbackUrl() {
if (callbackUrl != null) {
return callbackUrl;
} else {
final String ipAddress = NetUtil.getLocalIpv4HostAddress();
final String ipAddress = networkAddressprovider.getPrimaryIpv4HostAddress();
if (ipAddress == null) {
logger.warn("No network interface could be found.");
return null;
Expand Down Expand Up @@ -171,4 +172,12 @@ protected void unsetAudioHTTPServer(AudioHTTPServer audioHTTPServer) {
this.audioHTTPServer = null;
}

protected void setNetworkAddressProvider(NetworkAddressProvider networkUtil) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change networkUtil to networkAddressProvider

this.networkAddressprovider = networkUtil;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change networkAddressprovider to networkAddressProvider

}

protected void unsetNetworkAddressProvider(NetworkAddressProvider networkUtil) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dito

this.networkAddressprovider = null;
}

}