Skip to content

Commit

Permalink
issue #27 initial version of device discovery
Browse files Browse the repository at this point in the history
Signed-off-by: Gerd Zanker <[email protected]>
  • Loading branch information
GerdZanker committed May 2, 2021
1 parent bd1cfef commit aa4fefd
Show file tree
Hide file tree
Showing 10 changed files with 318 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
@NonNullByDefault
public class BoschSHCBindingConstants {

private static final String BINDING_ID = "boschshc";
public static final String BINDING_ID = "boschshc";

// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_SHC = new ThingTypeUID(BINDING_ID, "shc");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,10 @@ public class BoschHttpClient extends HttpClient {
private final Logger logger = LoggerFactory.getLogger(BoschHttpClient.class);

private final String ipAddress;
private final String systemPassword;

public BoschHttpClient(String ipAddress, String systemPassword, SslContextFactory sslContextFactory) {
public BoschHttpClient(String ipAddress, SslContextFactory sslContextFactory) {
super(sslContextFactory);
this.ipAddress = ipAddress;
this.systemPassword = systemPassword;
}

/**
Expand Down Expand Up @@ -181,7 +179,7 @@ public boolean isAccessPossible() throws InterruptedException {
* @return true if pairing was successful, otherwise false
* @throws InterruptedException in case of an interrupt
*/
public boolean doPairing() throws InterruptedException {
public boolean doPairing(String systemPassword) throws InterruptedException {
logger.trace("Starting pairing openHAB Client with Bosch Smart Home Controller!");
logger.trace("Please press the Bosch Smart Home Controller button until LED starts blinking");

Expand All @@ -201,7 +199,7 @@ public boolean doPairing() throws InterruptedException {

String url = this.getPairingUrl();
Request request = this.createRequest(url, HttpMethod.POST, items).header("Systempassword",
Base64.getEncoder().encodeToString(this.systemPassword.getBytes(StandardCharsets.UTF_8)));
Base64.getEncoder().encodeToString(systemPassword.getBytes(StandardCharsets.UTF_8)));

contentResponse = request.send();

Expand Down Expand Up @@ -290,7 +288,8 @@ public <TContent> TContent sendRequest(Request request, Class<TContent> response
if (errorResponseHandler != null) {
throw errorResponseHandler.apply(statusCode, textContent);
} else {
throw new ExecutionException(String.format("Send request failed with status code %s", statusCode), null);
throw new ExecutionException(String.format("Send request failed with status code %s", statusCode),
null);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.openhab.binding.boschshc.internal.exceptions.PairingFailedException;
import org.openhab.binding.boschshc.internal.exceptions.KeystoreException;
import org.openhab.core.OpenHAB;
import org.openhab.core.id.InstanceUUID;
import org.slf4j.Logger;
Expand Down Expand Up @@ -104,7 +104,7 @@ public String getKeystorePath() {
return Paths.get(OpenHAB.getUserDataFolder(), "etc", getBoschShcServerId() + ".jks").toString();
}

public SslContextFactory getSslContextFactory() throws PairingFailedException {
public SslContextFactory getSslContextFactory() throws KeystoreException {
// Instantiate and configure the SslContextFactory
SslContextFactory sslContextFactory = new SslContextFactory.Client.Client(true); // Accept all certificates

Expand All @@ -125,7 +125,7 @@ public SslContextFactory getSslContextFactory() throws PairingFailedException {
return sslContextFactory;
}

public KeyStore getKeyStoreAndCreateIfNecessary() throws PairingFailedException {
public KeyStore getKeyStoreAndCreateIfNecessary() throws KeystoreException {
try {
File file = new File(keystorePath);
if (!file.exists()) {
Expand All @@ -143,8 +143,8 @@ public KeyStore getKeyStoreAndCreateIfNecessary() throws PairingFailedException
}
} catch (OperatorCreationException | GeneralSecurityException | IOException e) {
logger.debug("Exception during keystore creation {}", e.getMessage());
throw new PairingFailedException("Can not create or load keystore file: " + keystorePath
+ ". Check path, write access and JKS content.", e);
throw new KeystoreException("Can not create or load keystore file: " + keystorePath
+ ". Check path, write access and JavaKeyStore content.", e);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
Expand All @@ -33,9 +35,10 @@
import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceStatusUpdate;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.LongPollResult;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.Room;
import org.openhab.binding.boschshc.internal.discovery.ThingDiscoveryService;
import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
import org.openhab.binding.boschshc.internal.exceptions.KeystoreException;
import org.openhab.binding.boschshc.internal.exceptions.LongPollingFailedException;
import org.openhab.binding.boschshc.internal.exceptions.PairingFailedException;
import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
import org.openhab.binding.boschshc.internal.services.dto.JsonRestExceptionResponse;
import org.openhab.core.thing.Bridge;
Expand All @@ -45,6 +48,7 @@
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.osgi.framework.FrameworkUtil;
import org.slf4j.Logger;
Expand Down Expand Up @@ -75,16 +79,32 @@ public class BridgeHandler extends BaseBridgeHandler {
*/
private final LongPolling longPolling;

/**
* HTTP Client for Bosch SHC rest calls and long polling.
*/
private @Nullable BoschHttpClient httpClient;

/**
* Future result to handle successful or failed pairing between bridge and SHC.
*/
private @Nullable ScheduledFuture<?> scheduledPairing;

/**
* Bosch SHC system password
*/
private String password;

public BridgeHandler(Bridge bridge) {
super(bridge);

this.password = "";
this.longPolling = new LongPolling(this.scheduler, this::handleLongPollResult, this::handleLongPollFailure);
}

@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singleton(ThingDiscoveryService.class);
}

@Override
public void initialize() {
logger.debug("Initialize {} Version {}", FrameworkUtil.getBundle(getClass()).getSymbolicName(),
Expand All @@ -100,8 +120,8 @@ public void initialize() {
return;
}

String password = config.password.trim();
if (password.isEmpty()) {
this.password = config.password.trim();
if (this.password.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.conf-error-empty-password");
return;
Expand All @@ -111,14 +131,14 @@ public void initialize() {
try {
// prepare SSL key and certificates
factory = new BoschSslUtil(ipAddress).getSslContextFactory();
} catch (PairingFailedException e) {
} catch (KeystoreException e) {
this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
"@text/offline.conf-error-ssl");
return;
}

// Instantiate HttpClient with the SslContextFactory
BoschHttpClient httpClient = this.httpClient = new BoschHttpClient(ipAddress, password, factory);
BoschHttpClient httpClient = this.httpClient = new BoschHttpClient(ipAddress, factory);

// Start http client
try {
Expand Down Expand Up @@ -203,7 +223,7 @@ private void initialAccess(BoschHttpClient httpClient) {
// update status description to show pairing test
this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE,
"@text/offline.conf-error-pairing");
if (!httpClient.doPairing()) {
if (!httpClient.doPairing(this.password)) {
this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
"@text/offline.conf-error-pairing");
}
Expand All @@ -216,7 +236,7 @@ private void initialAccess(BoschHttpClient httpClient) {
// print rooms and devices
boolean thingReachable = true;
thingReachable &= this.getRooms();
thingReachable &= this.getDevices();
thingReachable &= (this.getDevices() != null);
if (!thingReachable) {
this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
"@text/offline.not-reachable");
Expand All @@ -232,7 +252,6 @@ private void initialAccess(BoschHttpClient httpClient) {
} catch (LongPollingFailedException e) {
this.handleLongPollFailure(e);
}

} catch (InterruptedException e) {
this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE, "@text/offline.interrupted");
Thread.currentThread().interrupt();
Expand All @@ -244,11 +263,11 @@ private void initialAccess(BoschHttpClient httpClient) {
*
* @throws InterruptedException in case bridge is stopped
*/
private boolean getDevices() throws InterruptedException {
public @Nullable ArrayList<Device> getDevices() throws InterruptedException {
@Nullable
BoschHttpClient httpClient = this.httpClient;
if (httpClient == null) {
return false;
return null;
}

try {
Expand All @@ -259,7 +278,7 @@ private boolean getDevices() throws InterruptedException {
// check HTTP status code
if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
logger.debug("Request devices failed with status code: {}", contentResponse.getStatus());
return false;
return null;
}

String content = contentResponse.getContentAsString();
Expand All @@ -270,23 +289,23 @@ private boolean getDevices() throws InterruptedException {
}.getType();
ArrayList<Device> devices = gson.fromJson(content, collectionType);

if (devices != null) {
for (Device d : devices) {
// Write found devices into openhab.log until we have implemented auto discovery
logger.info("Found device: name={} id={}", d.name, d.id);
if (d.deviceSerivceIDs != null) {
for (String s : d.deviceSerivceIDs) {
logger.info(".... service: {}", s);
}
}
}
}
// if (devices != null) {
// for (Device d : devices) {
// // Write found devices into openhab.log until we have implemented auto discovery
// logger.info("Found device: name={} id={}", d.name, d.id);
// if (d.deviceSerivceIDs != null) {
// for (String s : d.deviceSerivceIDs) {
// logger.info(".... service: {}", s);
// }
// }
// }
// }

return devices;
} catch (TimeoutException | ExecutionException e) {
logger.warn("Request devices failed because of {}!", e.getMessage());
return false;
return null;
}

return true;
}

/**
Expand Down Expand Up @@ -386,7 +405,7 @@ private boolean getRooms() throws InterruptedException {

if (rooms != null) {
for (Room r : rooms) {
logger.info("Found room: {}", r.name);
logger.info("Found room: {} (ID={})", r.name, r.id);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
*/
package org.openhab.binding.boschshc.internal.devices.bridge.dto;


/**
* Public Information of the controller.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
import org.openhab.binding.boschshc.internal.devices.bridge.BoschHttpClient;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.PublicInformation;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
Expand All @@ -53,16 +53,9 @@ public class BridgeDiscoveryParticipant implements MDNSDiscoveryParticipant {

private final Gson gson = new Gson();
private final Logger logger = LoggerFactory.getLogger(BridgeDiscoveryParticipant.class);
private final HttpClient httpClient;
private String cachedIpAddress = "";

public BridgeDiscoveryParticipant() {
// create http client upfront to later get public information from SHC
SslContextFactory sslContextFactory = new SslContextFactory.Client.Client(true); // Accept all certificates
sslContextFactory.setTrustAll(true);
sslContextFactory.setValidateCerts(false);
sslContextFactory.setValidatePeerCerts(false);
sslContextFactory.setEndpointIdentificationAlgorithm(null);
httpClient = new HttpClient(sslContextFactory);
}

@Override
Expand Down Expand Up @@ -118,12 +111,29 @@ public String getServiceType() {
}

private @Nullable String getBridgeAddress(String ipAddress) {

String url = String.format("https://%s:8446/smarthome/public/information", ipAddress);
logger.trace("Discovering ipAddress {}", url);
logger.trace("Discovering ipAddress {}", ipAddress);
try {
// return a cached IP address to avoid many REST calls
// the BridgeDiscovery is executed every 5s and this will be too many request for the SHC
if (!cachedIpAddress.isEmpty()) {
logger.debug("Discovered SHC - returning cached IP address {} from first successful discovery",
cachedIpAddress);
return cachedIpAddress;
}
// prepare ssl content and http client
SslContextFactory sslContextFactory = new SslContextFactory.Client.Client(true); // Accept all certificates
sslContextFactory.setTrustAll(true);
sslContextFactory.setValidateCerts(false);
sslContextFactory.setValidatePeerCerts(false);
sslContextFactory.setEndpointIdentificationAlgorithm(null);

BoschHttpClient httpClient = new BoschHttpClient(ipAddress, sslContextFactory);
httpClient.start();
ContentResponse contentResponse = httpClient.newRequest(url).method(HttpMethod.GET).send();
// do rest to get public information including actual IP address of SHC
ContentResponse contentResponse = httpClient.newRequest(httpClient.getPublicInformationUrl())
.method(HttpMethod.GET).send();
httpClient.stop();

// check HTTP status code
if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
logger.debug("Discovering failed with status code: {}", contentResponse.getStatus());
Expand All @@ -135,15 +145,19 @@ public String getServiceType() {
@Nullable
PublicInformation versionInfo = gson.fromJson(content, PublicInformation.class);
if (versionInfo != null) {
cachedIpAddress = versionInfo.shcIpAddress;
return versionInfo.shcIpAddress;
}

return null;

} catch (InterruptedException | TimeoutException | ExecutionException e) {
logger.trace("Discovering failed with exception", e);
logger.debug("Discovering failed with exception {}", e.getMessage());
logger.trace("Discovering failed with exception ", e);
return null;
} catch (Exception e) {
logger.trace("Discovering failed in http client start", e);
logger.debug("Discovering failed in httpClient start {}", e.getMessage());
logger.trace("Discovering failed in httpClient start", e);
return null;
}
}
Expand Down
Loading

0 comments on commit aa4fefd

Please sign in to comment.