diff --git a/bundles/org.openhab.binding.boschshc/DEVELOPERS.md b/bundles/org.openhab.binding.boschshc/DEVELOPERS.md index 9ee8b4bb7e40e..846d337aa3fee 100644 --- a/bundles/org.openhab.binding.boschshc/DEVELOPERS.md +++ b/bundles/org.openhab.binding.boschshc/DEVELOPERS.md @@ -2,7 +2,7 @@ ## Build -To only build the Bosch SHC binding code execute +To only build the Bosch Smart Home binding code execute mvn -pl :org.openhab.binding.boschshc install @@ -15,28 +15,32 @@ For the first time the jar is loaded automatically as a bundle. It should also be reloaded automatically when the jar changed. -To reload the bundle manually you need to execute: +To reload the bundle manually you need to execute in the openhab console: - bundle:update "openHAB Add-ons :: Bundles :: BoschSHC Binding" + bundle:update "openHAB Add-ons :: Bundles :: Bosch Smart Home Binding" or get the ID and update the bundle using the ID: bundle:list - -> Get ID for "openHAB Add-ons :: Bundles :: BoschSHC Binding" + -> Get ID for "openHAB Add-ons :: Bundles :: Bosch Smart Home Binding" bundle:update ## Debugging -To get debug output and traces of the Bosch SHC binding code +To get debug output and traces of the Bosch Smart Home binding code add the following lines into ``userdata/etc/log4j2.xml`` Loggers XML section. +or use the openhab console to change the log level + + log:set TRACE org.openhab.binding.boschshc + ## Pairing and Certificates -We need secured and paired connection from the openHAB binding instance to the Bosch SHC. +We need secured and paired connection from the openHAB binding instance to the Bosch Smart Home Controller (SHC). Read more about the pairing process in [register a new client to the bosch smart home controller](https://github.com/BoschSmartHome/bosch-shc-api-docs/tree/master/postman#register-a-new-client-to-the-bosch-smart-home-controller) diff --git a/bundles/org.openhab.binding.boschshc/README.md b/bundles/org.openhab.binding.boschshc/README.md index b55abc659bfbd..c1568334794d7 100644 --- a/bundles/org.openhab.binding.boschshc/README.md +++ b/bundles/org.openhab.binding.boschshc/README.md @@ -1,8 +1,8 @@ -# BoschSHC Binding +# Bosch Smart Home Binding -Binding for the Bosch Smart Home Controller. +Binding for the Bosch Smart Home. -- [BoschSHC Binding](#boschshc-binding) +- [Bosch Smart Home Binding](#bosch-smart-home-binding) - [Supported Things](#supported-things) - [Bosch In-Wall switches & Bosch Smart Plugs](#bosch-in-wall-switches--bosch-smart-plugs) - [Bosch TwinGuard smoke detector](#bosch-twinguard-smoke-detector) @@ -13,7 +13,7 @@ Binding for the Bosch Smart Home Controller. - [Bosch Climate Control](#bosch-climate-control) - [Limitations](#limitations) - [Discovery](#discovery) - - [Binding Configuration](#binding-configuration) + - [Bridge Configuration](#bridge-configuration) - [Getting the device IDs](#getting-the-device-ids) - [Thing Configuration](#thing-configuration) - [Item Configuration](#item-configuration) @@ -102,8 +102,8 @@ You need to provide the IP address and the system password of your Bosch Smart H The IP address of the controller is visible in the Bosch Smart Home Mobile App (More -> System -> Smart Home Controller) or in your network router UI. The system password is set by you during your initial registration steps in the _Bosch Smart Home App_. -A keystore file with a self signed certificate is created automatically. -This certificate is used for pairing between the Bridge and the Bosch SHC. +A keystore file with a self-signed certificate is created automatically. +This certificate is used for pairing between the Bridge and the Bosch Smart Home Controller. *Press and hold the Bosch Smart Home Controller Bridge button until the LED starts blinking after you save your settings for pairing*. diff --git a/bundles/org.openhab.binding.boschshc/pom.xml b/bundles/org.openhab.binding.boschshc/pom.xml index b85d42a4594b1..5709b56ebe4a3 100644 --- a/bundles/org.openhab.binding.boschshc/pom.xml +++ b/bundles/org.openhab.binding.boschshc/pom.xml @@ -12,7 +12,7 @@ org.openhab.binding.boschshc - openHAB Add-ons :: Bundles :: BoschSHC Binding + openHAB Add-ons :: Bundles :: Bosch Smart Home Binding diff --git a/bundles/org.openhab.binding.boschshc/src/main/feature/feature.xml b/bundles/org.openhab.binding.boschshc/src/main/feature/feature.xml index 314d44d31a791..636ffe26dc838 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/feature/feature.xml +++ b/bundles/org.openhab.binding.boschshc/src/main/feature/feature.xml @@ -2,7 +2,7 @@ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features - + openhab-runtime-base mvn:org.openhab.addons.bundles/org.openhab.binding.boschshc/${project.version} diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClient.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClient.java index e5609117ed0af..f342153d55d76 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClient.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClient.java @@ -32,6 +32,7 @@ import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.util.StringContentProvider; import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -60,6 +61,16 @@ public BoschHttpClient(String ipAddress, String systemPassword, SslContextFactor this.systemPassword = systemPassword; } + /** + * Returns the public information URL for the Bosch SHC clients, using port 8446. + * See https://github.com/BoschSmartHome/bosch-shc-api-docs/blob/master/postman/README.md + * + * @return URL for public information + */ + public String getPublicInformationUrl() { + return String.format("https://%s:8446/smarthome/public/information", this.ipAddress); + } + /** * Returns the pairing URL for the Bosch SHC clients, using port 8443. * See https://github.com/BoschSmartHome/bosch-shc-api-docs/blob/master/postman/README.md @@ -102,10 +113,42 @@ public String getServiceUrl(String serviceName, String deviceId) { return this.getBoschSmartHomeUrl(String.format("devices/%s/services/%s/state", deviceId, serviceName)); } + /** + * Checks if the Bosch SHC is online. + * + * The HTTP server could be offline (Timeout of request). + * Or during boot-up the server can response e.g. with SERVICE_UNAVAILABLE_503 + * + * Will return true, if the server responds with the "public information". + * + * + * @return true if HTTP server is online + * @throws InterruptedException in case of an interrupt + */ + public boolean isOnline() throws InterruptedException { + try { + String url = this.getPublicInformationUrl(); + Request request = this.createRequest(url, GET); + ContentResponse contentResponse = request.send(); + if (HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) { + String content = contentResponse.getContentAsString(); + logger.debug("Online check completed with success: {} - status code: {}", content, + contentResponse.getStatus()); + return true; + } else { + logger.debug("Online check failed with status code: {}", contentResponse.getStatus()); + return false; + } + } catch (TimeoutException | ExecutionException | NullPointerException e) { + logger.debug("Online check failed because of {}!", e.getMessage()); + return false; + } + } + /** * Checks if the Bosch SHC can be accessed. - * - * @return true if HTTP access was successful + * + * @return true if HTTP access to SHC devices was successful * @throws InterruptedException in case of an interrupt */ public boolean isAccessPossible() throws InterruptedException { @@ -113,11 +156,17 @@ public boolean isAccessPossible() throws InterruptedException { String url = this.getBoschSmartHomeUrl("devices"); Request request = this.createRequest(url, GET); ContentResponse contentResponse = request.send(); - String content = contentResponse.getContentAsString(); - logger.debug("Access check response complete: {} - return code: {}", content, contentResponse.getStatus()); - return true; + if (HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) { + String content = contentResponse.getContentAsString(); + logger.debug("Access check completed with success: {} - status code: {}", content, + contentResponse.getStatus()); + return true; + } else { + logger.debug("Access check failed with status code: {}", contentResponse.getStatus()); + return false; + } } catch (TimeoutException | ExecutionException | NullPointerException e) { - logger.debug("Access check response failed because of {}!", e.getMessage()); + logger.debug("Access check failed because of {}!", e.getMessage()); return false; } } @@ -130,8 +179,8 @@ public boolean isAccessPossible() throws InterruptedException { * @throws InterruptedException in case of an interrupt */ public boolean doPairing() throws InterruptedException { - logger.trace("Starting pairing openHAB Client with Bosch SmartHomeController!"); - logger.trace("Please press the Bosch SHC button until LED starts blinking"); + 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"); ContentResponse contentResponse; try { @@ -169,7 +218,7 @@ public boolean doPairing() throws InterruptedException { // javax.net.ssl.SSLHandshakeException: General SSLEngine problem // => usually the pairing failed, because hardware button was not pressed. logger.trace("Pairing failed - Details: {}", e.getMessage()); - logger.warn("Pairing failed. Was the Bosch SHC button pressed?"); + logger.warn("Pairing failed. Was the Bosch Smart Home Controller button pressed?"); return false; } } @@ -194,7 +243,12 @@ public Request createRequest(String url, HttpMethod method) { * @return created HTTP request instance */ public Request createRequest(String url, HttpMethod method, @Nullable Object content) { - Request request = this.newRequest(url).method(method).header("Content-Type", "application/json"); + logger.trace("Create request for http client {}", this.toString()); + + Request request = this.newRequest(url).method(method).header("Content-Type", "application/json") + .header("api-version", "2.1") // see https://github.com/BoschSmartHome/bosch-shc-api-docs/issues/46 + .timeout(10, TimeUnit.SECONDS); // Set default timeout + if (content != null) { String body = GSON.toJson(content); logger.trace("create request for {} and content {}", url, body); @@ -203,9 +257,6 @@ public Request createRequest(String url, HttpMethod method, @Nullable Object con logger.trace("create request for {}", url); } - // Set default timeout - request.timeout(10, TimeUnit.SECONDS); - return request; } @@ -220,9 +271,11 @@ public Request createRequest(String url, HttpMethod method, @Nullable Object con */ public TContent sendRequest(Request request, Class responseContentClass) throws InterruptedException, TimeoutException, ExecutionException { + logger.trace("Send request: {}", request.toString()); + ContentResponse contentResponse = request.send(); - logger.debug("BoschHttpClient: response complete: {} - return code: {}", contentResponse.getContentAsString(), + logger.debug("Received response: {} - status: {}", contentResponse.getContentAsString(), contentResponse.getStatus()); try { diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschSHCBridgeHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschSHCBridgeHandler.java index c5615401a51a1..aa1a96e21f564 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschSHCBridgeHandler.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschSHCBridgeHandler.java @@ -27,6 +27,7 @@ import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.http.HttpStatus; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler; import org.openhab.binding.boschshc.internal.devices.bridge.dto.*; @@ -43,6 +44,7 @@ import org.openhab.core.thing.binding.BaseBridgeHandler; import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.types.Command; +import org.osgi.framework.FrameworkUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -83,23 +85,30 @@ public BoschSHCBridgeHandler(Bridge bridge) { @Override public void initialize() { + logger.debug("Initialize {} Version {}", FrameworkUtil.getBundle(getClass()).getSymbolicName(), + FrameworkUtil.getBundle(getClass()).getVersion()); + // Read configuration BoschSHCBridgeConfiguration config = getConfigAs(BoschSHCBridgeConfiguration.class); - if (config.ipAddress.isEmpty()) { - this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No IP address set"); + String ipAddress = config.ipAddress.trim(); + if (ipAddress.isEmpty()) { + this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.conf-error-empty-ip"); return; } - if (config.password.isEmpty()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No system password set"); + String password = config.password.trim(); + if (password.isEmpty()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.conf-error-empty-password"); return; } SslContextFactory factory; try { // prepare SSL key and certificates - factory = new BoschSslUtil(config.ipAddress).getSslContextFactory(); + factory = new BoschSslUtil(ipAddress).getSslContextFactory(); } catch (PairingFailedException e) { this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "@text/offline.conf-error-ssl"); @@ -107,7 +116,7 @@ public void initialize() { } // Instantiate HttpClient with the SslContextFactory - BoschHttpClient httpClient = this.httpClient = new BoschHttpClient(config.ipAddress, config.password, factory); + BoschHttpClient httpClient = this.httpClient = new BoschHttpClient(ipAddress, password, factory); // Start http client try { @@ -118,6 +127,9 @@ public void initialize() { return; } + // general checks are OK, therefore set the status to unknown and wait for initial access + this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE); + // Initialize bridge in the background. // Start initial access the first time scheduleInitialAccess(httpClient); @@ -126,6 +138,7 @@ public void initialize() { @Override public void dispose() { // Cancel scheduled pairing. + @Nullable ScheduledFuture scheduledPairing = this.scheduledPairing; if (scheduledPairing != null) { scheduledPairing.cancel(true); @@ -135,6 +148,7 @@ public void dispose() { // Stop long polling. this.longPolling.stop(); + @Nullable BoschHttpClient httpClient = this.httpClient; if (httpClient != null) { try { @@ -168,12 +182,23 @@ private void scheduleInitialAccess(BoschHttpClient httpClient) { * and starts the first log poll. */ private void initialAccess(BoschHttpClient httpClient) { - logger.debug("Initializing Bosch SHC Bridge: {} - HTTP client is: {} - version: 2020-04-05", this, httpClient); + logger.debug("Initializing Bosch SHC Bridge: {} - HTTP client is: {}", this, httpClient); try { - // check access and pair if necessary - if (!httpClient.isAccessPossible()) { + // check if SCH is offline + if (!httpClient.isOnline()) { // update status already if access is not possible + this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE, + "@text/offline.conf-error-offline"); + // restart later initial access + scheduleInitialAccess(httpClient); + return; + } + + // SHC is online + // check if SHC access is not possible and pairing necessary + if (!httpClient.isAccessPossible()) { + // update status description to show pairing test this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE, "@text/offline.conf-error-pairing"); if (!httpClient.doPairing()) { @@ -182,52 +207,61 @@ private void initialAccess(BoschHttpClient httpClient) { } // restart initial access - needed also in case of successful pairing to check access again scheduleInitialAccess(httpClient); - } else { - // print rooms and devices if things are reachable - boolean thingReachable = true; - thingReachable &= this.getRooms(); - thingReachable &= this.getDevices(); - - if (thingReachable) { - this.updateStatus(ThingStatus.ONLINE); - - // Start long polling - try { - this.longPolling.start(httpClient); - } catch (LongPollingFailedException e) { - this.handleLongPollFailure(e); - } - } else { - this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, - "@text/offline.not-reachable"); - // restart initial access - scheduleInitialAccess(httpClient); - } + return; + } + + // SHC is online and access is possible + // print rooms and devices + boolean thingReachable = true; + thingReachable &= this.getRooms(); + thingReachable &= this.getDevices(); + if (!thingReachable) { + this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + "@text/offline.not-reachable"); + // restart initial access + scheduleInitialAccess(httpClient); + return; + } + + // start long polling loop + this.updateStatus(ThingStatus.ONLINE); + try { + this.longPolling.start(httpClient); + } catch (LongPollingFailedException e) { + this.handleLongPollFailure(e); } + } catch (InterruptedException e) { - this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE, - String.format("Pairing was interrupted: %s", e.getMessage())); + this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE, "@text/offline.interrupted"); } } /** * Get a list of connected devices from the Smart-Home Controller * - * @throws InterruptedException + * @throws InterruptedException in case bridge is stopped */ private boolean getDevices() throws InterruptedException { + @Nullable BoschHttpClient httpClient = this.httpClient; if (httpClient == null) { return false; } try { - logger.debug("Sending http request to Bosch to request clients: {}", httpClient); + logger.debug("Sending http request to Bosch to request devices: {}", httpClient); String url = httpClient.getBoschSmartHomeUrl("devices"); ContentResponse contentResponse = httpClient.createRequest(url, GET).send(); + // check HTTP status code + if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) { + logger.debug("Request devices failed with status code: {}", contentResponse.getStatus()); + return false; + } + String content = contentResponse.getContentAsString(); - logger.debug("Response complete: {} - return code: {}", content, contentResponse.getStatus()); + logger.debug("Request devices completed with success: {} - status code: {}", content, + contentResponse.getStatus()); Type collectionType = new TypeToken>() { }.getType(); @@ -245,13 +279,21 @@ private boolean getDevices() throws InterruptedException { } } } catch (TimeoutException | ExecutionException e) { - logger.debug("HTTP request failed with exception {}", e.getMessage()); + logger.warn("Request devices failed because of {}!", e.getMessage()); return false; } return true; } + /** + * Bridge callback handler for the results of long polls. + * + * It will check the result and + * forward the received to the bosch thing handlers. + * + * @param result Results from Long Polling + */ private void handleLongPollResult(LongPollResult result) { for (DeviceStatusUpdate update : result.result) { if (update != null && update.state != null) { @@ -262,9 +304,11 @@ private void handleLongPollResult(LongPollResult result) { Bridge bridge = this.getThing(); for (Thing childThing : bridge.getThings()) { // All children of this should implement BoschSHCHandler + @Nullable ThingHandler baseHandler = childThing.getHandler(); if (baseHandler != null && baseHandler instanceof BoschSHCHandler) { BoschSHCHandler handler = (BoschSHCHandler) baseHandler; + @Nullable String deviceId = handler.getBoschID(); handled = true; @@ -286,17 +330,35 @@ private void handleLongPollResult(LongPollResult result) { } } + /** + * Bridge callback handler for the failures during long polls. + * + * It will update the bridge status and try to access the SHC again. + * + * @param e error during long polling + */ private void handleLongPollFailure(Throwable e) { - logger.warn("Long polling failed", e); - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Long polling failed"); + logger.warn("Long polling failed, will try to reconnect", e); + @Nullable + BoschHttpClient httpClient = this.httpClient; + if (httpClient == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + "@text/offline.long-polling-failed.http-client-null"); + return; + } + + this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE, + "@text/offline.long-polling-failed.trying-to-reconnect"); + scheduleInitialAccess(httpClient); } /** * Get a list of rooms from the Smart-Home controller * - * @throws InterruptedException + * @throws InterruptedException in case bridge is stopped */ private boolean getRooms() throws InterruptedException { + @Nullable BoschHttpClient httpClient = this.httpClient; if (httpClient != null) { try { @@ -304,8 +366,15 @@ private boolean getRooms() throws InterruptedException { String url = httpClient.getBoschSmartHomeUrl("rooms"); ContentResponse contentResponse = httpClient.createRequest(url, GET).send(); + // check HTTP status code + if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) { + logger.debug("Request rooms failed with status code: {}", contentResponse.getStatus()); + return false; + } + String content = contentResponse.getContentAsString(); - logger.debug("Response complete: {} - return code: {}", content, contentResponse.getStatus()); + logger.debug("Request rooms completed with success: {} - status code: {}", content, + contentResponse.getStatus()); Type collectionType = new TypeToken>() { }.getType(); @@ -320,7 +389,7 @@ private boolean getRooms() throws InterruptedException { return true; } catch (TimeoutException | ExecutionException e) { - logger.warn("HTTP request failed: {}", e.getMessage()); + logger.warn("Request rooms failed because of {}!", e.getMessage()); return false; } } else { @@ -341,6 +410,7 @@ private boolean getRooms() throws InterruptedException { */ public @Nullable T getState(String deviceId, String stateName, Class stateClass) throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException { + @Nullable BoschHttpClient httpClient = this.httpClient; if (httpClient == null) { logger.warn("HttpClient not initialized"); @@ -393,6 +463,7 @@ private boolean getRooms() throws InterruptedException { */ public @Nullable Response putState(String deviceId, String serviceName, T state) throws InterruptedException, TimeoutException, ExecutionException { + @Nullable BoschHttpClient httpClient = this.httpClient; if (httpClient == null) { logger.warn("HttpClient not initialized"); @@ -404,7 +475,6 @@ private boolean getRooms() throws InterruptedException { Request request = httpClient.createRequest(url, PUT, state); // Send request - Response response = request.send(); - return response; + return request.send(); } } diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/LongPolling.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/LongPolling.java index 0c35b91b2c144..96a402251c4a9 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/LongPolling.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/LongPolling.java @@ -117,7 +117,24 @@ private String subscribe(BoschHttpClient httpClient) throws LongPollingFailedExc String subscriptionId = response.getResult(); return subscriptionId; } catch (TimeoutException | ExecutionException | InterruptedException e) { - throw new LongPollingFailedException("Error on subscribe request", e); + throw new LongPollingFailedException( + String.format("Error on subscribe (Http client: %s): %s", httpClient.toString(), e.getMessage()), + e); + } + } + + /** + * Create a new subscription for long polling. + * + * @param httpClient Http client to send requests to + */ + private void resubscribe(BoschHttpClient httpClient) { + try { + String subscriptionId = this.subscribe(httpClient); + this.executeLongPoll(httpClient, subscriptionId); + } catch (LongPollingFailedException e) { + this.handleFailure.accept(e); + return; } } @@ -144,65 +161,74 @@ private void longPoll(BoschHttpClient httpClient, String subscriptionId) { request.send(new BufferingResponseListener() { @Override public void onComplete(@Nullable Result result) { - Throwable failure = result != null ? result.getFailure() : null; - if (failure != null) { - if (failure instanceof ExecutionException) { - if (failure.getCause() instanceof AbortLongPolling) { - logger.debug("Canceling long polling for subscription id {} because it was aborted", - subscriptionId); - } else { - longPolling.handleFailure.accept(new LongPollingFailedException( - "Unexpected exception during long polling request", failure)); - } - } else { - longPolling.handleFailure.accept(new LongPollingFailedException( - "Unexpected exception during long polling request", failure)); - } - } else { - longPolling.onLongPollResponse(httpClient, subscriptionId, this.getContentAsString()); - } + // NOTE: This handler runs inside the HTTP thread, so we schedule the response handling in a new thread + // because the HTTP thread is terminated after the timeout expires. + scheduler.execute(() -> longPolling.onLongPollComplete(httpClient, subscriptionId, result, + this.getContentAsString())); } }); } - private void onLongPollResponse(BoschHttpClient httpClient, String subscriptionId, String content) { + /** + * This is the handler for responses of long poll requests. + * + * @param httpClient HTTP client which received the response + * @param subscriptionId Id of subscription the response is for + * @param result Complete result of the response + * @param content Content of the response + */ + private void onLongPollComplete(BoschHttpClient httpClient, String subscriptionId, @Nullable Result result, + String content) { // Check if thing is still online if (this.aborted) { logger.debug("Canceling long polling for subscription id {} because it was aborted", subscriptionId); return; } - logger.debug("Long poll response: {}", content); + // Check if response was failure or success + Throwable failure = result != null ? result.getFailure() : null; + if (failure != null) { + if (failure instanceof ExecutionException) { + if (failure.getCause() instanceof AbortLongPolling) { + logger.debug("Canceling long polling for subscription id {} because it was aborted", + subscriptionId); + } else { + this.handleFailure.accept(new LongPollingFailedException( + "Unexpected exception during long polling request", failure)); + } + } else { + this.handleFailure.accept( + new LongPollingFailedException("Unexpected exception during long polling request", failure)); + } + } else { + logger.debug("Long poll response: {}", content); - String nextSubscriptionId = subscriptionId; + String nextSubscriptionId = subscriptionId; - LongPollResult longPollResult = gson.fromJson(content, LongPollResult.class); - if (longPollResult != null && longPollResult.result != null) { - this.handleResult.accept(longPollResult); - } else { - logger.warn("Long poll response contained no results: {}", content); + LongPollResult longPollResult = gson.fromJson(content, LongPollResult.class); + if (longPollResult != null && longPollResult.result != null) { + this.handleResult.accept(longPollResult); + } else { + logger.debug("Long poll response contained no result: {}", content); - // Check if we got a proper result from the SHC - LongPollError longPollError = gson.fromJson(content, LongPollError.class); + // Check if we got a proper result from the SHC + LongPollError longPollError = gson.fromJson(content, LongPollError.class); - if (longPollError != null && longPollError.error != null) { - logger.warn("Got long poll error: {} (code: {})", longPollError.error.message, - longPollError.error.code); + if (longPollError != null && longPollError.error != null) { + logger.debug("Got long poll error: {} (code: {})", longPollError.error.message, + longPollError.error.code); - if (longPollError.error.code == LongPollError.SUBSCRIPTION_INVALID) { - logger.warn("Subscription {} became invalid, subscribing again", subscriptionId); - try { - nextSubscriptionId = this.subscribe(httpClient); - } catch (LongPollingFailedException e) { - this.handleFailure.accept(e); + if (longPollError.error.code == LongPollError.SUBSCRIPTION_INVALID) { + logger.debug("Subscription {} became invalid, subscribing again", subscriptionId); + this.resubscribe(httpClient); return; } } } - } - // Execute next run. - this.executeLongPoll(httpClient, nextSubscriptionId); + // Execute next run + this.longPoll(httpClient, nextSubscriptionId); + } } @SuppressWarnings("serial") diff --git a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/twinguard/BoschTwinguardHandler.java b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/twinguard/BoschTwinguardHandler.java index d5d58312075ac..b2f5a1a83e0ea 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/twinguard/BoschTwinguardHandler.java +++ b/bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/twinguard/BoschTwinguardHandler.java @@ -68,9 +68,9 @@ public void handleCommand(ChannelUID channelUID, Command command) { void updateAirQualityState(AirQualityLevelState state) { updateState(CHANNEL_TEMPERATURE, new QuantityType(state.temperature, SIUnits.CELSIUS)); updateState(CHANNEL_TEMPERATURE_RATING, new StringType(state.temperatureRating)); - updateState(CHANNEL_HUMIDITY, new QuantityType(state.humidity, Units.ONE)); + updateState(CHANNEL_HUMIDITY, new QuantityType(state.humidity, Units.PERCENT)); updateState(CHANNEL_HUMIDITY_RATING, new StringType(state.humidityRating)); - updateState(CHANNEL_PURITY, new QuantityType(state.purity, Units.ONE)); + updateState(CHANNEL_PURITY, new QuantityType(state.purity, Units.PARTS_PER_MILLION)); updateState(CHANNEL_AIR_DESCRIPTION, new StringType(state.description)); updateState(CHANNEL_PURITY_RATING, new StringType(state.purityRating)); updateState(CHANNEL_COMBINED_RATING, new StringType(state.combinedRating)); diff --git a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/binding/binding.xml index e7346d10686b1..8d5c3877dd1ea 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/binding/binding.xml +++ b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/binding/binding.xml @@ -4,7 +4,7 @@ xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd"> Bosch Smart Home Binding - This is the binding for Bosch Smart Home Controller. + This is the binding for Bosch Smart Home. Stefan Kästle diff --git a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc.properties b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc.properties index e25fee3096c57..9946491aaa9d3 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc.properties +++ b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc.properties @@ -1,5 +1,12 @@ + # Thing status offline descriptions +offline.conf-error-empty-ip = No network address set. +offline.conf-error-empty-password = No system password set. +offline.conf-error-offline = The Bosch Smart Home Controller is offline or network address is wrong. offline.conf-error-pairing = Press pairing button on the Bosch Smart Home Controller. -offline.not-reachable = Smart Home Controller is not reachable. +offline.not-reachable = The Bosch Smart Home Controller is not reachable. offline.conf-error-ssl = The SSL connection to the Bosch Smart Home Controller is not possible. +offline.long-polling-failed.http-client-null = Long polling failed and could not be restarted because http client is null. +offline.long-polling-failed.trying-to-reconnect = Long polling failed, will try to reconnect. +offline.interrupted = Conneting to Bosch Smart Home Controller was interrupted. diff --git a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc_de.properties b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc_de.properties index 0ef037c73449a..9c493160f5344 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc_de.properties +++ b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc_de.properties @@ -1,7 +1,7 @@ -# binding -binding.boschshc.name = Bosch Smart Home Controller Binding -binding.boschshc.description = Dieses Binding integriert das Bosch Smart Home System. Durch diese können die Bosch Smart Home Geräte verwendet werden. +# binding.xml strings +binding.boschshc.name = Bosch Smart Home Binding +binding.boschshc.description = Dieses Binding integriert das Bosch Smart Home System. Durch diese können die Bosch Smart Home Geräte verwendet werden. # Thing status offline descriptions offline.conf-error-pairing = Bitte betätigen Sie den Taster am Bosch Smart Home Controller zum automatischen Verbinden. diff --git a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/thing/thing-types.xml index 35f4eb5cdb1c1..aa1e30609b087 100644 --- a/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/thing/thing-types.xml @@ -7,7 +7,7 @@ - The Bosch SHC Bridge representing the Bosch Smart Home Controller. + The Bosch Smart Home Bridge representing the Bosch Smart Home Controller. diff --git a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClientTest.java b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClientTest.java index 3bbf82a3acca0..8fa5c61a268a0 100644 --- a/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClientTest.java +++ b/bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClientTest.java @@ -48,6 +48,11 @@ void beforeEach() throws PairingFailedException { assertNotNull(httpClient); } + @Test + void getPublicInformationUrl() { + assertEquals("https://127.0.0.1:8446/smarthome/public/information", httpClient.getPublicInformationUrl()); + } + @Test void getPairingUrl() { assertEquals("https://127.0.0.1:8443/smarthome/clients", httpClient.getPairingUrl()); @@ -75,6 +80,11 @@ void isAccessPossible() throws InterruptedException { assertFalse(httpClient.isAccessPossible()); } + @Test + void isOnline() throws InterruptedException { + assertFalse(httpClient.isOnline()); + } + @Test void doPairing() throws InterruptedException { assertFalse(httpClient.doPairing());