From 089e080ca4c876df9f9ccd7c0b2701cb8e026b85 Mon Sep 17 00:00:00 2001 From: Christian Oeing Date: Sat, 13 Feb 2021 21:09:30 +0100 Subject: [PATCH] [boschshc] Release v1.1 (#10097) * #72 changed use units of measure for the twinguard humidity and purity values all other QuantityTypes in bindingcode are fine * #77 changed title of binding to Bosch Smart Home Replaced the SHC occurrences with Smart Home, to avoid technical names. * #62 Try to restart long polling when it fails before taking the thing offline * #62 Run subscribe request on a new thread instead of using the thread of the previous long polling http request This might be the reason why the subscribe request does never finish or finishes with a timeout * #74 Run the whole long polling response handling in a new thread to not get timeout from HTTP client * #74 Schedule initial access when long polling fails unexpected We need to try to reconnect again and again (with 15 seconds between the requests) as the controller may have been restarted (update, manual restart,...). This is already done by the initial access, so I reuse that mechanism. * Use direct formatting of logger.trace instead of String.format * #76 Use i18n texts instead of raw translations for status messages about failed long polling * #76 Use logger.debug instead of logger.warn for long poll error as it is handled now * #78 defined api-version each HTTP request will use now the defined "avp-version=2.1" for request to the smart home controller * logging bundle version removed the old static version string access OSGi bundle version information instead * #75 improved initial access - added isOnline check and isAccessPossible now failed in case HTTPStatus is an error - same HTTPStatus check done to all blocking send() request calls - using i18n strings for all bridge updateStatus calls - skipped the 'controller' and use only 'Bosch Smart Home' in descriptions - added more @Nullable annotations * added newline Signed-off-by: Gerd Zanker Signed-off-by: Christian Oeing --- .../DEVELOPERS.md | 16 +- .../org.openhab.binding.boschshc/README.md | 12 +- bundles/org.openhab.binding.boschshc/pom.xml | 2 +- .../src/main/feature/feature.xml | 2 +- .../devices/bridge/BoschHttpClient.java | 81 +++++++-- .../devices/bridge/BoschSHCBridgeHandler.java | 156 +++++++++++++----- .../internal/devices/bridge/LongPolling.java | 106 +++++++----- .../twinguard/BoschTwinguardHandler.java | 4 +- .../main/resources/OH-INF/binding/binding.xml | 2 +- .../resources/OH-INF/i18n/boschshc.properties | 9 +- .../OH-INF/i18n/boschshc_de.properties | 6 +- .../resources/OH-INF/thing/thing-types.xml | 2 +- .../devices/bridge/BoschHttpClientTest.java | 10 ++ 13 files changed, 289 insertions(+), 119 deletions(-) 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());