]> git.basschouten.com Git - openhab-addons.git/commitdiff
[boschshc] Release v1.1 (#10097)
authorChristian Oeing <christian.oeing@slashgames.org>
Sat, 13 Feb 2021 20:09:30 +0000 (21:09 +0100)
committerGitHub <noreply@github.com>
Sat, 13 Feb 2021 20:09:30 +0000 (12:09 -0800)
* #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 <gerd.zanker@web.de>
Signed-off-by: Christian Oeing <christian.oeing@slashgames.org>
13 files changed:
bundles/org.openhab.binding.boschshc/DEVELOPERS.md
bundles/org.openhab.binding.boschshc/README.md
bundles/org.openhab.binding.boschshc/pom.xml
bundles/org.openhab.binding.boschshc/src/main/feature/feature.xml
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClient.java
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschSHCBridgeHandler.java
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/bridge/LongPolling.java
bundles/org.openhab.binding.boschshc/src/main/java/org/openhab/binding/boschshc/internal/devices/twinguard/BoschTwinguardHandler.java
bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/binding/binding.xml
bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc.properties
bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/i18n/boschshc_de.properties
bundles/org.openhab.binding.boschshc/src/main/resources/OH-INF/thing/thing-types.xml
bundles/org.openhab.binding.boschshc/src/test/java/org/openhab/binding/boschshc/internal/devices/bridge/BoschHttpClientTest.java

index 9ee8b4bb7e40e9b8da4fbc5cd87917b69bc86945..846d337aa3fee852e10d947d94732b29d79bcbcc 100644 (file)
@@ -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 <ID>
     
 
 ## 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. 
 
     <!-- Bosch SHC for debugging -->
        <Logger level="TRACE" name="org.openhab.binding.boschshc"/>
 
+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)
 
index b55abc659bfbd6003f44b2198ed08d4da6debb01..c1568334794d7e215e8d56765eb0b541302ee9d7 100644 (file)
@@ -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*.
 
index b85d42a4594b1076dd04ed8409c39dddc9ea5bb2..5709b56ebe4a3339fcd81e0b72e9c121e8e2db16 100644 (file)
@@ -12,7 +12,7 @@
 
   <artifactId>org.openhab.binding.boschshc</artifactId>
 
-  <name>openHAB Add-ons :: Bundles :: BoschSHC Binding</name>
+  <name>openHAB Add-ons :: Bundles :: Bosch Smart Home Binding</name>
 
   <dependencies>
     <dependency>
index 314d44d31a791e0df42aa79ad405fd59eafc192c..636ffe26dc83809c51dfef3a21e6ecdacf36083a 100644 (file)
@@ -2,7 +2,7 @@
 <features name="org.openhab.binding.boschshc-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
        <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
 
-       <feature name="openhab-binding-boschshc" description="BoschSHC Binding" version="${project.version}">
+       <feature name="openhab-binding-boschshc" description="Bosch Smart Home Binding" version="${project.version}">
                <feature>openhab-runtime-base</feature>
                <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.boschshc/${project.version}</bundle>
        </feature>
index e5609117ed0af4310efe9e4f8cfd49800a0a1870..f342153d55d762543d57b7d1fc7e7bcb392250b5 100644 (file)
@@ -32,6 +32,7 @@ import org.eclipse.jetty.client.api.ContentResponse;
 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 class BoschHttpClient extends HttpClient {
         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 class BoschHttpClient extends HttpClient {
         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 class BoschHttpClient extends HttpClient {
             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 class BoschHttpClient extends HttpClient {
      * @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 class BoschHttpClient extends HttpClient {
             // 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 class BoschHttpClient extends HttpClient {
      * @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 class BoschHttpClient extends HttpClient {
             logger.trace("create request for {}", url);
         }
 
-        // Set default timeout
-        request.timeout(10, TimeUnit.SECONDS);
-
         return request;
     }
 
@@ -220,9 +271,11 @@ public class BoschHttpClient extends HttpClient {
      */
     public <TContent> TContent sendRequest(Request request, Class<TContent> 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 {
index c5615401a51a118641d3c8b449ae0e53f0b496c0..aa1a96e21f564ed1548d09876279d7e81110bcda 100644 (file)
@@ -27,6 +27,7 @@ import org.eclipse.jdt.annotation.Nullable;
 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.ThingStatusDetail;
 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 class BoschSHCBridgeHandler extends BaseBridgeHandler {
 
     @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 class BoschSHCBridgeHandler extends BaseBridgeHandler {
         }
 
         // 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 class BoschSHCBridgeHandler extends BaseBridgeHandler {
             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 class BoschSHCBridgeHandler extends BaseBridgeHandler {
     @Override
     public void dispose() {
         // Cancel scheduled pairing.
+        @Nullable
         ScheduledFuture<?> scheduledPairing = this.scheduledPairing;
         if (scheduledPairing != null) {
             scheduledPairing.cancel(true);
@@ -135,6 +148,7 @@ public class BoschSHCBridgeHandler extends BaseBridgeHandler {
         // Stop long polling.
         this.longPolling.stop();
 
+        @Nullable
         BoschHttpClient httpClient = this.httpClient;
         if (httpClient != null) {
             try {
@@ -168,12 +182,23 @@ public class BoschSHCBridgeHandler extends BaseBridgeHandler {
      * 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 @@ public class BoschSHCBridgeHandler extends BaseBridgeHandler {
                 }
                 // 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<ArrayList<Device>>() {
             }.getType();
@@ -245,13 +279,21 @@ public class BoschSHCBridgeHandler extends BaseBridgeHandler {
                 }
             }
         } 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 @@ public class BoschSHCBridgeHandler extends BaseBridgeHandler {
                 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 @@ public class BoschSHCBridgeHandler extends BaseBridgeHandler {
         }
     }
 
+    /**
+     * 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 @@ public class BoschSHCBridgeHandler extends BaseBridgeHandler {
                 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<ArrayList<Room>>() {
                 }.getType();
@@ -320,7 +389,7 @@ public class BoschSHCBridgeHandler extends BaseBridgeHandler {
 
                 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 @@ public class BoschSHCBridgeHandler extends BaseBridgeHandler {
      */
     public <T extends BoschSHCServiceState> @Nullable T getState(String deviceId, String stateName, Class<T> stateClass)
             throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
+        @Nullable
         BoschHttpClient httpClient = this.httpClient;
         if (httpClient == null) {
             logger.warn("HttpClient not initialized");
@@ -393,6 +463,7 @@ public class BoschSHCBridgeHandler extends BaseBridgeHandler {
      */
     public <T extends BoschSHCServiceState> @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 @@ public class BoschSHCBridgeHandler extends BaseBridgeHandler {
         Request request = httpClient.createRequest(url, PUT, state);
 
         // Send request
-        Response response = request.send();
-        return response;
+        return request.send();
     }
 }
index 0c35b91b2c144e013c3dd7a45e7f3f23bdf743f6..96a402251c4a97d6aa2ca7830f98a5bfb0080bf6 100644 (file)
@@ -117,7 +117,24 @@ public class LongPolling {
             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 @@ public class LongPolling {
         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")
index d5d58312075ac0cd3bc8b50d09c73b932e19bfb8..b2f5a1a83e0ea1c49cffdac0e1cac7da5925b0a1 100644 (file)
@@ -68,9 +68,9 @@ public class BoschTwinguardHandler extends BoschSHCHandler {
     void updateAirQualityState(AirQualityLevelState state) {
         updateState(CHANNEL_TEMPERATURE, new QuantityType<Temperature>(state.temperature, SIUnits.CELSIUS));
         updateState(CHANNEL_TEMPERATURE_RATING, new StringType(state.temperatureRating));
-        updateState(CHANNEL_HUMIDITY, new QuantityType<Dimensionless>(state.humidity, Units.ONE));
+        updateState(CHANNEL_HUMIDITY, new QuantityType<Dimensionless>(state.humidity, Units.PERCENT));
         updateState(CHANNEL_HUMIDITY_RATING, new StringType(state.humidityRating));
-        updateState(CHANNEL_PURITY, new QuantityType<Dimensionless>(state.purity, Units.ONE));
+        updateState(CHANNEL_PURITY, new QuantityType<Dimensionless>(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));
index e7346d10686b11145f0c8cfdaeb4492b2c000c28..8d5c3877dd1ea82a61f2dd5aa5b596d6cec03c3a 100644 (file)
@@ -4,7 +4,7 @@
        xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
 
        <name>Bosch Smart Home Binding</name>
-       <description>This is the binding for Bosch Smart Home Controller.</description>
+       <description>This is the binding for Bosch Smart Home.</description>
        <author>Stefan Kästle</author>
 
 </binding:binding>
index e25fee3096c57f82992e10431e77cad21389ea2d..9946491aaa9d38d7769e2a44e8c911569e2f69ff 100644 (file)
@@ -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.
index 0ef037c73449a470f02413111c3e76f0cd89fc3c..9c493160f53442cadc0782837e1ef803a5ca55dc 100644 (file)
@@ -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.
index 35f4eb5cdb1c1c77381c2a0a1fd33dd4e1a0f0d2..aa1e30609b0871c42d1ae098bfb6449d8bb3bf7f 100644 (file)
@@ -7,7 +7,7 @@
        <!-- Bosch Bridge -->
        <bridge-type id="shc">
                <label>Smart Home Controller</label>
-               <description>The Bosch SHC Bridge representing the Bosch Smart Home Controller.</description>
+               <description>The Bosch Smart Home Bridge representing the Bosch Smart Home Controller.</description>
 
                <config-description-ref uri="thing-type:boschshc:bridge"/>
        </bridge-type>
index 3bbf82a3acca0ad424a933cef015e1807f9a2d04..8fa5c61a268a054f536d82007da3bc19b3f937d0 100644 (file)
@@ -48,6 +48,11 @@ class BoschHttpClientTest {
         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 @@ class BoschHttpClientTest {
         assertFalse(httpClient.isAccessPossible());
     }
 
+    @Test
+    void isOnline() throws InterruptedException {
+        assertFalse(httpClient.isOnline());
+    }
+
     @Test
     void doPairing() throws InterruptedException {
         assertFalse(httpClient.doPairing());