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

Commit

Permalink
[Tradfri] General improvements (#4486)
Browse files Browse the repository at this point in the history
* [Tradfri] Adapt binding to use DTLS identities #4479

Signed-off-by: Ivaylo Ivanov <ivivanov.bg@gmail.com>
  • Loading branch information
ivivanov-bg authored and kaikreuzer committed Nov 10, 2017
1 parent 448003d commit 8baff74
Show file tree
Hide file tree
Showing 10 changed files with 270 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import static org.eclipse.smarthome.binding.tradfri.TradfriBindingConstants.*;
import static org.eclipse.smarthome.binding.tradfri.internal.config.TradfriDeviceConfig.*;
import static org.eclipse.smarthome.binding.tradfri.internal.config.TradfriGatewayConfig.*;

import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;
Expand Down Expand Up @@ -49,8 +49,9 @@ public void setUp() {
managedThingProvider = getService(ThingProvider.class, ManagedThingProvider.class);

Map<String, Object> properties = new HashMap<>();
properties.put(CONFIG_HOST, "1.2.3.4");
properties.put(CONFIG_CODE, "abc");
properties.put(GATEWAY_CONFIG_HOST, "1.2.3.4");
properties.put(GATEWAY_CONFIG_IDENTITY, "identity");
properties.put(GATEWAY_CONFIG_PRE_SHARED_KEY, "pre-shared-secret-key");
bridge = BridgeBuilder.create(GATEWAY_TYPE_UID, "1").withLabel("My Gateway")
.withConfiguration(new Configuration(properties)).build();

Expand Down Expand Up @@ -80,8 +81,9 @@ private void configurationOfTradfriGatewayHandler() {
Configuration configuration = bridge.getConfiguration();
assertThat(configuration, is(notNullValue()));

assertThat(configuration.get(CONFIG_HOST), is("1.2.3.4"));
assertThat(configuration.get(CONFIG_CODE), is("abc"));
assertThat(configuration.get(GATEWAY_CONFIG_HOST), is("1.2.3.4"));
assertThat(configuration.get(GATEWAY_CONFIG_IDENTITY), is("identity"));
assertThat(configuration.get(GATEWAY_CONFIG_PRE_SHARED_KEY), is("pre-shared-secret-key"));
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
package org.eclipse.smarthome.binding.tradfri.discovery;

import static org.eclipse.smarthome.binding.tradfri.TradfriBindingConstants.*;
import static org.eclipse.smarthome.binding.tradfri.internal.config.TradfriGatewayConfig.*;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.when;
Expand Down Expand Up @@ -68,8 +67,32 @@ public void correctSupportedTypes() {

@Test
public void correctThingUID() {
when(tradfriGateway.getName()).thenReturn("gw:12-34-56-78-90-ab");
assertThat(discoveryParticipant.getThingUID(tradfriGateway),
is(new ThingUID("tradfri:gateway:gw1234567890ab")));

when(tradfriGateway.getName()).thenReturn("gw:1234567890ab");
assertThat(discoveryParticipant.getThingUID(tradfriGateway),
is(new ThingUID("tradfri:gateway:gw1234567890ab")));

when(tradfriGateway.getName()).thenReturn("gw-12-34-56-78-90-ab");
assertThat(discoveryParticipant.getThingUID(tradfriGateway),
is(new ThingUID("tradfri:gateway:gw1234567890ab")));

when(tradfriGateway.getName()).thenReturn("gw:1234567890ab");
assertThat(discoveryParticipant.getThingUID(tradfriGateway),
is(new ThingUID("tradfri:gateway:gw1234567890ab")));

when(tradfriGateway.getName()).thenReturn("gw:1234567890abServiceInfo");
assertThat(discoveryParticipant.getThingUID(tradfriGateway),
is(new ThingUID("tradfri:gateway:gw1234567890ab")));

when(tradfriGateway.getName()).thenReturn("gw:12-34-56-78-90-ab-service-info");
assertThat(discoveryParticipant.getThingUID(tradfriGateway),
is(new ThingUID("tradfri:gateway:gw1234567890ab")));

// restore original value
when(tradfriGateway.getName()).thenReturn("gw:12-34-56-78-90-ab");
}

@Test
Expand All @@ -83,14 +106,27 @@ public void validDiscoveryResult() {
assertThat(result.getThingTypeUID(), is(GATEWAY_TYPE_UID));
assertThat(result.getBridgeUID(), is(nullValue()));
assertThat(result.getProperties().get(Thing.PROPERTY_VENDOR), is("IKEA of Sweden"));
assertThat(result.getProperties().get(CONFIG_HOST), is("192.168.0.5"));
assertThat(result.getProperties().get(CONFIG_PORT), is(1234));
assertThat(result.getRepresentationProperty(), is(CONFIG_HOST));
assertThat(result.getProperties().get(GATEWAY_CONFIG_HOST), is("192.168.0.5"));
assertThat(result.getProperties().get(GATEWAY_CONFIG_PORT), is(1234));
assertThat(result.getRepresentationProperty(), is(GATEWAY_CONFIG_HOST));
}

@Test
public void noThingUIDForUnknownDevice() {
when(otherDevice.getName()).thenReturn("something");
assertThat(discoveryParticipant.getThingUID(otherDevice), is(nullValue()));

when(otherDevice.getName()).thenReturn("gw_1234567890ab");
assertThat(discoveryParticipant.getThingUID(otherDevice), is(nullValue()));

when(otherDevice.getName()).thenReturn("gw:12-3456--7890-ab");
assertThat(discoveryParticipant.getThingUID(otherDevice), is(nullValue()));

when(otherDevice.getName()).thenReturn("gw1234567890ab");
assertThat(discoveryParticipant.getThingUID(otherDevice), is(nullValue()));

// restore original value
when(otherDevice.getName()).thenReturn("something");
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,22 @@
<advanced>true</advanced>
<default>5684</default>
</parameter>
<parameter name="code" type="text" required="true">
<parameter name="code" type="text" required="false">
<context>password</context>
<label>Security Code</label>
<description>Security code printed on the label underneath the gateway.</description>
</parameter>
<parameter name="identity" type="text" required="false">
<advanced>true</advanced>
<label>Identity</label>
<description>Unique identity used for communication with the gateway</description>
</parameter>
<parameter name="preSharedKey" type="text" required="false">
<advanced>true</advanced>
<context>password</context>
<label>Pre-Shared security key</label>
<description>Security key obtained during first initialization of the gateway</description>
</parameter>
</config-description>

<config-description uri="thing-type:tradfri:device">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ The following matrix lists the capabilities (channels) for each of the supported

## Thing Configuration

The gateway requires a `host` parameter for the hostname or IP address and a `code`, which is the security code that is printed on the bottom of the gateway. Optionally, a `port` can be configured, but any standard gateway uses the default port 5684.
For first pairing - the gateway requires a `host` parameter for the hostname or IP address and a `code`, which is the security code that is printed on the bottom of the gateway.
Optionally, a `port` can be configured, but any standard gateway uses the default port 5684.

The `code` is used during the initialization for retrieving unique identity and pre-shared key from the gateway (fw version 1.2.0042 onwards) and then it's discarded from the configuration. The newly created authentication data is stored in advanced parameters `identity` and `preSharedKey`.
On each initialization if the code is present in the thing configuration - the `identity` and `preSharedKey` are recreated and the `code` is again discarded.

The devices require only a single (integer) parameter, which is their instance id. Unfortunately, this is not displayed anywhere in the IKEA app, but it seems that they are sequentially numbered starting with 65537 for the first device. If in doubt, use the auto-discovered things to find out the correct instance ids.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ public class TradfriBindingConstants {
.unmodifiableSet(Stream.of(THING_TYPE_DIMMABLE_LIGHT, THING_TYPE_COLOR_TEMP_LIGHT, THING_TYPE_COLOR_LIGHT)
.collect(Collectors.toSet()));

// List of all Gateway Configuration Properties
public static final String GATEWAY_CONFIG_HOST = "host";
public static final String GATEWAY_CONFIG_PORT = "port";
public static final String GATEWAY_CONFIG_CODE = "code";
public static final String GATEWAY_CONFIG_IDENTITY = "identity";
public static final String GATEWAY_CONFIG_PRE_SHARED_KEY = "preSharedKey";

// Not yet used - included for future support
public static final Set<ThingTypeUID> SUPPORTED_CONTROLLER_TYPES_UIDS = Collections.unmodifiableSet(Stream
.of(THING_TYPE_DIMMER, THING_TYPE_REMOTE_CONTROL, THING_TYPE_MOTION_SENSOR).collect(Collectors.toSet()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,26 @@
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import org.eclipse.californium.core.CoapClient;
import org.eclipse.californium.core.CoapResponse;
import org.eclipse.californium.core.network.CoapEndpoint;
import org.eclipse.californium.core.network.config.NetworkConfig;
import org.eclipse.californium.scandium.DTLSConnector;
import org.eclipse.californium.scandium.config.DtlsConnectorConfig;
import org.eclipse.californium.scandium.dtls.pskstore.StaticPskStore;
import org.eclipse.smarthome.binding.tradfri.TradfriBindingConstants;
import org.eclipse.smarthome.binding.tradfri.internal.CoapCallback;
import org.eclipse.smarthome.binding.tradfri.internal.DeviceUpdateListener;
import org.eclipse.smarthome.binding.tradfri.internal.TradfriCoapClient;
import org.eclipse.smarthome.binding.tradfri.internal.TradfriCoapEndpoint;
import org.eclipse.smarthome.binding.tradfri.internal.TradfriCoapHandler;
import org.eclipse.smarthome.binding.tradfri.internal.config.TradfriGatewayConfig;
import org.eclipse.smarthome.config.core.Configuration;
import org.eclipse.smarthome.core.thing.Bridge;
import org.eclipse.smarthome.core.thing.ChannelUID;
import org.eclipse.smarthome.core.thing.Thing;
Expand All @@ -42,6 +47,7 @@
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;

Expand All @@ -53,7 +59,9 @@
*/
public class TradfriGatewayHandler extends BaseBridgeHandler implements CoapCallback {

private final Logger logger = LoggerFactory.getLogger(TradfriGatewayHandler.class);
protected final Logger logger = LoggerFactory.getLogger(getClass());

private static final String MIN_SUPPORTED_VERSION = "1.2.42";

private TradfriCoapClient deviceClient;
private String gatewayURI;
Expand All @@ -77,40 +85,131 @@ public void handleCommand(ChannelUID channelUID, Command command) {
@Override
public void initialize() {
TradfriGatewayConfig configuration = getConfigAs(TradfriGatewayConfig.class);
if (configuration.host == null || configuration.host.isEmpty()) {

if (isNullOrEmpty(configuration.host)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Host must be specified in the configuration!");
return;
}
if (configuration.code == null || configuration.code.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Security code must be provided in the configuration!");
return;
if (isNullOrEmpty(configuration.code)) {
if (isNullOrEmpty(configuration.identity) || isNullOrEmpty(configuration.preSharedKey)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Either security code or identity and pre-shared key must be provided in the configuration!");
return;
} else {
setupCoapClient();
}
} else {
if (isOldFirmware()) {
/*
* older firmware - fall back to authentication with security code
* in this case the Thing configuration will not be persisted
*/
logger.warn("Gateway with old firmware - please consider upgrading to the latest version.");

Configuration editedConfig = editConfiguration();
editedConfig.put(TradfriBindingConstants.GATEWAY_CONFIG_IDENTITY, "");
editedConfig.put(TradfriBindingConstants.GATEWAY_CONFIG_PRE_SHARED_KEY, configuration.code);
updateConfiguration(editedConfig);

setupCoapClient();
} else {
/*
* Running async operation to retrieve new <'identity','key'> pair
*/
scheduler.execute(this::obtainIdentityAndPreSharedKey);
}
}
}

private void setupCoapClient() {
TradfriGatewayConfig configuration = getConfigAs(TradfriGatewayConfig.class);

this.gatewayURI = "coaps://" + configuration.host + ":" + configuration.port + "/" + DEVICES;
this.gatewayInfoURI = "coaps://" + configuration.host + ":" + configuration.port + "/" + GATEWAY + "/"
+ GATEWAY_DETAILS;
this.gatewayInfoURI = "coaps://" + configuration.host + ":" + configuration.port + "/" + GATEWAY + "/" + GATEWAY_DETAILS;
try {
URI uri = new URI(gatewayURI);
deviceClient = new TradfriCoapClient(uri);
} catch (URISyntaxException e) {
logger.debug("Illegal gateway URI `{}`: {}", gatewayURI, e.getMessage());
logger.error("Illegal gateway URI '{}': {}", gatewayURI, e.getMessage());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
return;
}

DtlsConnectorConfig.Builder builder = new DtlsConnectorConfig.Builder(new InetSocketAddress(0));
builder.setPskStore(new StaticPskStore("", configuration.code.getBytes()));
builder.setPskStore(new StaticPskStore(configuration.identity, configuration.preSharedKey.getBytes()));
dtlsConnector = new DTLSConnector(builder.build());
endPoint = new TradfriCoapEndpoint(dtlsConnector, NetworkConfig.getStandard());
deviceClient.setEndpoint(endPoint);
updateStatus(ThingStatus.UNKNOWN);

// schedule a new scan every minute
scanJob = scheduler.scheduleWithFixedDelay(() -> {
startScan();
}, 0, 1, TimeUnit.MINUTES);
scanJob = scheduler.scheduleWithFixedDelay(this::startScan, 0, 1, TimeUnit.MINUTES);
}

protected void obtainIdentityAndPreSharedKey() {
TradfriGatewayConfig configuration = getConfigAs(TradfriGatewayConfig.class);

String identity = UUID.randomUUID().toString().replace("-", "");
String preSharedKey = null;

CoapResponse gatewayResponse;
String authUrl = null;
String responseText = null;
try {
DtlsConnectorConfig.Builder builder = new DtlsConnectorConfig.Builder(new InetSocketAddress(0));
builder.setPskStore(new StaticPskStore("Client_identity", configuration.code.getBytes()));

DTLSConnector dtlsConnector = new DTLSConnector(builder.build());
CoapEndpoint authEndpoint = new CoapEndpoint(dtlsConnector, NetworkConfig.getStandard());
authUrl = "coaps://" + configuration.host + ":" + configuration.port + "/15011/9063";

CoapClient deviceClient = new CoapClient(new URI(authUrl));
deviceClient.setEndpoint(authEndpoint);

JsonObject json = new JsonObject();
json.addProperty(CLIENT_IDENTITY_PROPOSED, identity);

gatewayResponse = deviceClient.post(json.toString(), 0);

authEndpoint.destroy();
deviceClient.shutdown();

if (gatewayResponse.isSuccess()) {
responseText = gatewayResponse.getResponseText();
json = new JsonParser().parse(responseText).getAsJsonObject();
preSharedKey = json.get(NEW_PSK_BY_GW).getAsString();

if (isNullOrEmpty(preSharedKey)) {
logger.error("Pre-shared key was not obtain successfully");
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Pre-shared key was not obtain successfully");
} else {
logger.info("Recieved pre-shared key for gateway '{}'", configuration.host);
logger.debug("Using identity '{}' with pre-shared key '{}'. Code can be discarded now", identity, preSharedKey);

Configuration editedConfig = editConfiguration();
editedConfig.put(TradfriBindingConstants.GATEWAY_CONFIG_CODE, null);
editedConfig.put(TradfriBindingConstants.GATEWAY_CONFIG_IDENTITY, identity);
editedConfig.put(TradfriBindingConstants.GATEWAY_CONFIG_PRE_SHARED_KEY, preSharedKey);
updateConfiguration(editedConfig);

setupCoapClient();
}
} else {
logger.warn("Failed obtaining pre-shared key for identity '{}' (response code '{}', response text '{}')",
identity, gatewayResponse.getCode(),
isNullOrEmpty(gatewayResponse.getResponseText()) ? "<empty>" : gatewayResponse.getResponseText());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
String.format("Failed obtaining pre-shared key with status code '%s'", gatewayResponse.getCode()));
}
} catch (URISyntaxException e) {
logger.error("Illegal gateway URI '{}'", authUrl, e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
} catch (JsonParseException e) {
logger.warn("Invalid response recieved from gateway '{}'", responseText, e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
String.format("Invalid response recieved from gateway '%s'", responseText));
}
}

@Override
Expand Down Expand Up @@ -233,4 +332,28 @@ public void registerDeviceUpdateListener(DeviceUpdateListener listener) {
public void unregisterDeviceUpdateListener(DeviceUpdateListener listener) {
this.deviceUpdateListeners.remove(listener);
}

private boolean isNullOrEmpty(String string) {
return string == null || string.isEmpty();
}

/**
* Checks current firmware in the thing properties and compares it with the value of {@link #MIN_SUPPORTED_VERSION}
*
* @return true if current firmware is older than {@value #MIN_SUPPORTED_VERSION}
*/
private boolean isOldFirmware() {
String currentFirmware = thing.getProperties().get(Thing.PROPERTY_FIRMWARE_VERSION);
return currentFirmware == null || MIN_SUPPORTED_VERSION.compareTo(currentFirmware) > 0;
}

@Override
public void thingUpdated(Thing thing) {
super.thingUpdated(thing);

logger.info("Bridge configuration updated. Updating paired things (if any).");
for (Thing t : ((Bridge) thing).getThings()) {
t.getHandler().thingUpdated(t);
}
}
}
Loading

0 comments on commit 8baff74

Please sign in to comment.