[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 <gerd.zanker@web.de>
Signed-off-by: Christian Oeing <christian.oeing@slashgames.org>
This commit is contained in:
Christian Oeing 2021-02-13 21:09:30 +01:00 committed by GitHub
parent 51db639853
commit a796e472ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 290 additions and 120 deletions

View File

@ -2,7 +2,7 @@
## Build ## 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 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. 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: or get the ID and update the bundle using the ID:
bundle:list 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> bundle:update <ID>
## Debugging ## 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. add the following lines into ``userdata/etc/log4j2.xml`` Loggers XML section.
<!-- Bosch SHC for debugging --> <!-- Bosch SHC for debugging -->
<Logger level="TRACE" name="org.openhab.binding.boschshc"/> <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 ## 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) 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)

View 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) - [Supported Things](#supported-things)
- [Bosch In-Wall switches & Bosch Smart Plugs](#bosch-in-wall-switches--bosch-smart-plugs) - [Bosch In-Wall switches & Bosch Smart Plugs](#bosch-in-wall-switches--bosch-smart-plugs)
- [Bosch TwinGuard smoke detector](#bosch-twinguard-smoke-detector) - [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) - [Bosch Climate Control](#bosch-climate-control)
- [Limitations](#limitations) - [Limitations](#limitations)
- [Discovery](#discovery) - [Discovery](#discovery)
- [Binding Configuration](#binding-configuration) - [Bridge Configuration](#bridge-configuration)
- [Getting the device IDs](#getting-the-device-ids) - [Getting the device IDs](#getting-the-device-ids)
- [Thing Configuration](#thing-configuration) - [Thing Configuration](#thing-configuration)
- [Item Configuration](#item-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 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_. 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. A keystore file with a self-signed certificate is created automatically.
This certificate is used for pairing between the Bridge and the Bosch SHC. 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*. *Press and hold the Bosch Smart Home Controller Bridge button until the LED starts blinking after you save your settings for pairing*.

View File

@ -12,7 +12,7 @@
<artifactId>org.openhab.binding.boschshc</artifactId> <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> <dependencies>
<dependency> <dependency>

View File

@ -2,7 +2,7 @@
<features name="org.openhab.binding.boschshc-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0"> <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> <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> <feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.boschshc/${project.version}</bundle> <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.boschshc/${project.version}</bundle>
</feature> </feature>

View File

@ -32,6 +32,7 @@ import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.StringContentProvider; import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -60,6 +61,16 @@ public class BoschHttpClient extends HttpClient {
this.systemPassword = systemPassword; 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. * 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 * 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)); 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. * 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 * @throws InterruptedException in case of an interrupt
*/ */
public boolean isAccessPossible() throws InterruptedException { public boolean isAccessPossible() throws InterruptedException {
@ -113,11 +156,17 @@ public class BoschHttpClient extends HttpClient {
String url = this.getBoschSmartHomeUrl("devices"); String url = this.getBoschSmartHomeUrl("devices");
Request request = this.createRequest(url, GET); Request request = this.createRequest(url, GET);
ContentResponse contentResponse = request.send(); ContentResponse contentResponse = request.send();
String content = contentResponse.getContentAsString(); if (HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
logger.debug("Access check response complete: {} - return code: {}", content, contentResponse.getStatus()); String content = contentResponse.getContentAsString();
return true; 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) { } 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; return false;
} }
} }
@ -130,8 +179,8 @@ public class BoschHttpClient extends HttpClient {
* @throws InterruptedException in case of an interrupt * @throws InterruptedException in case of an interrupt
*/ */
public boolean doPairing() throws InterruptedException { public boolean doPairing() throws InterruptedException {
logger.trace("Starting pairing openHAB Client with Bosch SmartHomeController!"); logger.trace("Starting pairing openHAB Client with Bosch Smart Home Controller!");
logger.trace("Please press the Bosch SHC button until LED starts blinking"); logger.trace("Please press the Bosch Smart Home Controller button until LED starts blinking");
ContentResponse contentResponse; ContentResponse contentResponse;
try { try {
@ -169,7 +218,7 @@ public class BoschHttpClient extends HttpClient {
// javax.net.ssl.SSLHandshakeException: General SSLEngine problem // javax.net.ssl.SSLHandshakeException: General SSLEngine problem
// => usually the pairing failed, because hardware button was not pressed. // => usually the pairing failed, because hardware button was not pressed.
logger.trace("Pairing failed - Details: {}", e.getMessage()); 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; return false;
} }
} }
@ -194,7 +243,12 @@ public class BoschHttpClient extends HttpClient {
* @return created HTTP request instance * @return created HTTP request instance
*/ */
public Request createRequest(String url, HttpMethod method, @Nullable Object content) { 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) { if (content != null) {
String body = GSON.toJson(content); String body = GSON.toJson(content);
logger.trace("create request for {} and content {}", url, body); logger.trace("create request for {} and content {}", url, body);
@ -203,9 +257,6 @@ public class BoschHttpClient extends HttpClient {
logger.trace("create request for {}", url); logger.trace("create request for {}", url);
} }
// Set default timeout
request.timeout(10, TimeUnit.SECONDS);
return request; return request;
} }
@ -220,9 +271,11 @@ public class BoschHttpClient extends HttpClient {
*/ */
public <TContent> TContent sendRequest(Request request, Class<TContent> responseContentClass) public <TContent> TContent sendRequest(Request request, Class<TContent> responseContentClass)
throws InterruptedException, TimeoutException, ExecutionException { throws InterruptedException, TimeoutException, ExecutionException {
logger.trace("Send request: {}", request.toString());
ContentResponse contentResponse = request.send(); ContentResponse contentResponse = request.send();
logger.debug("BoschHttpClient: response complete: {} - return code: {}", contentResponse.getContentAsString(), logger.debug("Received response: {} - status: {}", contentResponse.getContentAsString(),
contentResponse.getStatus()); contentResponse.getStatus());
try { try {

View File

@ -27,6 +27,7 @@ import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.api.Response; import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler; import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler;
import org.openhab.binding.boschshc.internal.devices.bridge.dto.*; 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.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.types.Command; import org.openhab.core.types.Command;
import org.osgi.framework.FrameworkUtil;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -83,23 +85,30 @@ public class BoschSHCBridgeHandler extends BaseBridgeHandler {
@Override @Override
public void initialize() { public void initialize() {
logger.debug("Initialize {} Version {}", FrameworkUtil.getBundle(getClass()).getSymbolicName(),
FrameworkUtil.getBundle(getClass()).getVersion());
// Read configuration // Read configuration
BoschSHCBridgeConfiguration config = getConfigAs(BoschSHCBridgeConfiguration.class); BoschSHCBridgeConfiguration config = getConfigAs(BoschSHCBridgeConfiguration.class);
if (config.ipAddress.isEmpty()) { String ipAddress = config.ipAddress.trim();
this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No IP address set"); if (ipAddress.isEmpty()) {
this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.conf-error-empty-ip");
return; return;
} }
if (config.password.isEmpty()) { String password = config.password.trim();
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No system password set"); if (password.isEmpty()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.conf-error-empty-password");
return; return;
} }
SslContextFactory factory; SslContextFactory factory;
try { try {
// prepare SSL key and certificates // prepare SSL key and certificates
factory = new BoschSslUtil(config.ipAddress).getSslContextFactory(); factory = new BoschSslUtil(ipAddress).getSslContextFactory();
} catch (PairingFailedException e) { } catch (PairingFailedException e) {
this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
"@text/offline.conf-error-ssl"); "@text/offline.conf-error-ssl");
@ -107,7 +116,7 @@ public class BoschSHCBridgeHandler extends BaseBridgeHandler {
} }
// Instantiate HttpClient with the SslContextFactory // 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 // Start http client
try { try {
@ -118,6 +127,9 @@ public class BoschSHCBridgeHandler extends BaseBridgeHandler {
return; 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. // Initialize bridge in the background.
// Start initial access the first time // Start initial access the first time
scheduleInitialAccess(httpClient); scheduleInitialAccess(httpClient);
@ -126,6 +138,7 @@ public class BoschSHCBridgeHandler extends BaseBridgeHandler {
@Override @Override
public void dispose() { public void dispose() {
// Cancel scheduled pairing. // Cancel scheduled pairing.
@Nullable
ScheduledFuture<?> scheduledPairing = this.scheduledPairing; ScheduledFuture<?> scheduledPairing = this.scheduledPairing;
if (scheduledPairing != null) { if (scheduledPairing != null) {
scheduledPairing.cancel(true); scheduledPairing.cancel(true);
@ -135,6 +148,7 @@ public class BoschSHCBridgeHandler extends BaseBridgeHandler {
// Stop long polling. // Stop long polling.
this.longPolling.stop(); this.longPolling.stop();
@Nullable
BoschHttpClient httpClient = this.httpClient; BoschHttpClient httpClient = this.httpClient;
if (httpClient != null) { if (httpClient != null) {
try { try {
@ -168,12 +182,23 @@ public class BoschSHCBridgeHandler extends BaseBridgeHandler {
* and starts the first log poll. * and starts the first log poll.
*/ */
private void initialAccess(BoschHttpClient httpClient) { 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 { try {
// check access and pair if necessary // check if SCH is offline
if (!httpClient.isAccessPossible()) { if (!httpClient.isOnline()) {
// update status already if access is not possible // 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, this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE,
"@text/offline.conf-error-pairing"); "@text/offline.conf-error-pairing");
if (!httpClient.doPairing()) { 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 // restart initial access - needed also in case of successful pairing to check access again
scheduleInitialAccess(httpClient); scheduleInitialAccess(httpClient);
} else { return;
// 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);
}
} }
// 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) { } catch (InterruptedException e) {
this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE, this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE, "@text/offline.interrupted");
String.format("Pairing was interrupted: %s", e.getMessage()));
} }
} }
/** /**
* Get a list of connected devices from the Smart-Home Controller * 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 { private boolean getDevices() throws InterruptedException {
@Nullable
BoschHttpClient httpClient = this.httpClient; BoschHttpClient httpClient = this.httpClient;
if (httpClient == null) { if (httpClient == null) {
return false; return false;
} }
try { 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"); String url = httpClient.getBoschSmartHomeUrl("devices");
ContentResponse contentResponse = httpClient.createRequest(url, GET).send(); 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(); 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>>() { Type collectionType = new TypeToken<ArrayList<Device>>() {
}.getType(); }.getType();
@ -245,13 +279,21 @@ public class BoschSHCBridgeHandler extends BaseBridgeHandler {
} }
} }
} catch (TimeoutException | ExecutionException e) { } 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 false;
} }
return true; 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) { private void handleLongPollResult(LongPollResult result) {
for (DeviceStatusUpdate update : result.result) { for (DeviceStatusUpdate update : result.result) {
if (update != null && update.state != null) { if (update != null && update.state != null) {
@ -262,9 +304,11 @@ public class BoschSHCBridgeHandler extends BaseBridgeHandler {
Bridge bridge = this.getThing(); Bridge bridge = this.getThing();
for (Thing childThing : bridge.getThings()) { for (Thing childThing : bridge.getThings()) {
// All children of this should implement BoschSHCHandler // All children of this should implement BoschSHCHandler
@Nullable
ThingHandler baseHandler = childThing.getHandler(); ThingHandler baseHandler = childThing.getHandler();
if (baseHandler != null && baseHandler instanceof BoschSHCHandler) { if (baseHandler != null && baseHandler instanceof BoschSHCHandler) {
BoschSHCHandler handler = (BoschSHCHandler) baseHandler; BoschSHCHandler handler = (BoschSHCHandler) baseHandler;
@Nullable
String deviceId = handler.getBoschID(); String deviceId = handler.getBoschID();
handled = true; 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) { private void handleLongPollFailure(Throwable e) {
logger.warn("Long polling failed", e); logger.warn("Long polling failed, will try to reconnect", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Long polling failed"); @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 * Get a list of rooms from the Smart-Home controller
* *
* @throws InterruptedException * @throws InterruptedException in case bridge is stopped
*/ */
private boolean getRooms() throws InterruptedException { private boolean getRooms() throws InterruptedException {
@Nullable
BoschHttpClient httpClient = this.httpClient; BoschHttpClient httpClient = this.httpClient;
if (httpClient != null) { if (httpClient != null) {
try { try {
@ -304,8 +366,15 @@ public class BoschSHCBridgeHandler extends BaseBridgeHandler {
String url = httpClient.getBoschSmartHomeUrl("rooms"); String url = httpClient.getBoschSmartHomeUrl("rooms");
ContentResponse contentResponse = httpClient.createRequest(url, GET).send(); 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(); 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>>() { Type collectionType = new TypeToken<ArrayList<Room>>() {
}.getType(); }.getType();
@ -320,7 +389,7 @@ public class BoschSHCBridgeHandler extends BaseBridgeHandler {
return true; return true;
} catch (TimeoutException | ExecutionException e) { } catch (TimeoutException | ExecutionException e) {
logger.warn("HTTP request failed: {}", e.getMessage()); logger.warn("Request rooms failed because of {}!", e.getMessage());
return false; return false;
} }
} else { } else {
@ -341,6 +410,7 @@ public class BoschSHCBridgeHandler extends BaseBridgeHandler {
*/ */
public <T extends BoschSHCServiceState> @Nullable T getState(String deviceId, String stateName, Class<T> stateClass) public <T extends BoschSHCServiceState> @Nullable T getState(String deviceId, String stateName, Class<T> stateClass)
throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException { throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
@Nullable
BoschHttpClient httpClient = this.httpClient; BoschHttpClient httpClient = this.httpClient;
if (httpClient == null) { if (httpClient == null) {
logger.warn("HttpClient not initialized"); 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) public <T extends BoschSHCServiceState> @Nullable Response putState(String deviceId, String serviceName, T state)
throws InterruptedException, TimeoutException, ExecutionException { throws InterruptedException, TimeoutException, ExecutionException {
@Nullable
BoschHttpClient httpClient = this.httpClient; BoschHttpClient httpClient = this.httpClient;
if (httpClient == null) { if (httpClient == null) {
logger.warn("HttpClient not initialized"); logger.warn("HttpClient not initialized");
@ -404,7 +475,6 @@ public class BoschSHCBridgeHandler extends BaseBridgeHandler {
Request request = httpClient.createRequest(url, PUT, state); Request request = httpClient.createRequest(url, PUT, state);
// Send request // Send request
Response response = request.send(); return request.send();
return response;
} }
} }

View File

@ -117,7 +117,24 @@ public class LongPolling {
String subscriptionId = response.getResult(); String subscriptionId = response.getResult();
return subscriptionId; return subscriptionId;
} catch (TimeoutException | ExecutionException | InterruptedException e) { } 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() { request.send(new BufferingResponseListener() {
@Override @Override
public void onComplete(@Nullable Result result) { public void onComplete(@Nullable Result result) {
Throwable failure = result != null ? result.getFailure() : null; // NOTE: This handler runs inside the HTTP thread, so we schedule the response handling in a new thread
if (failure != null) { // because the HTTP thread is terminated after the timeout expires.
if (failure instanceof ExecutionException) { scheduler.execute(() -> longPolling.onLongPollComplete(httpClient, subscriptionId, result,
if (failure.getCause() instanceof AbortLongPolling) { this.getContentAsString()));
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());
}
} }
}); });
} }
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 // Check if thing is still online
if (this.aborted) { if (this.aborted) {
logger.debug("Canceling long polling for subscription id {} because it was aborted", subscriptionId); logger.debug("Canceling long polling for subscription id {} because it was aborted", subscriptionId);
return; return;
} }
logger.debug("Long poll response: {}", content); // Check if response was failure or success
Throwable failure = result != null ? result.getFailure() : null;
String nextSubscriptionId = subscriptionId; if (failure != null) {
if (failure instanceof ExecutionException) {
LongPollResult longPollResult = gson.fromJson(content, LongPollResult.class); if (failure.getCause() instanceof AbortLongPolling) {
if (longPollResult != null && longPollResult.result != null) { logger.debug("Canceling long polling for subscription id {} because it was aborted",
this.handleResult.accept(longPollResult); 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 { } else {
logger.warn("Long poll response contained no results: {}", content); logger.debug("Long poll response: {}", content);
// Check if we got a proper result from the SHC String nextSubscriptionId = subscriptionId;
LongPollError longPollError = gson.fromJson(content, LongPollError.class);
if (longPollError != null && longPollError.error != null) { LongPollResult longPollResult = gson.fromJson(content, LongPollResult.class);
logger.warn("Got long poll error: {} (code: {})", longPollError.error.message, if (longPollResult != null && longPollResult.result != null) {
longPollError.error.code); this.handleResult.accept(longPollResult);
} else {
logger.debug("Long poll response contained no result: {}", content);
if (longPollError.error.code == LongPollError.SUBSCRIPTION_INVALID) { // Check if we got a proper result from the SHC
logger.warn("Subscription {} became invalid, subscribing again", subscriptionId); LongPollError longPollError = gson.fromJson(content, LongPollError.class);
try {
nextSubscriptionId = this.subscribe(httpClient); if (longPollError != null && longPollError.error != null) {
} catch (LongPollingFailedException e) { logger.debug("Got long poll error: {} (code: {})", longPollError.error.message,
this.handleFailure.accept(e); longPollError.error.code);
if (longPollError.error.code == LongPollError.SUBSCRIPTION_INVALID) {
logger.debug("Subscription {} became invalid, subscribing again", subscriptionId);
this.resubscribe(httpClient);
return; return;
} }
} }
} }
}
// Execute next run. // Execute next run
this.executeLongPoll(httpClient, nextSubscriptionId); this.longPoll(httpClient, nextSubscriptionId);
}
} }
@SuppressWarnings("serial") @SuppressWarnings("serial")

View File

@ -68,9 +68,9 @@ public class BoschTwinguardHandler extends BoschSHCHandler {
void updateAirQualityState(AirQualityLevelState state) { void updateAirQualityState(AirQualityLevelState state) {
updateState(CHANNEL_TEMPERATURE, new QuantityType<Temperature>(state.temperature, SIUnits.CELSIUS)); updateState(CHANNEL_TEMPERATURE, new QuantityType<Temperature>(state.temperature, SIUnits.CELSIUS));
updateState(CHANNEL_TEMPERATURE_RATING, new StringType(state.temperatureRating)); 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_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_AIR_DESCRIPTION, new StringType(state.description));
updateState(CHANNEL_PURITY_RATING, new StringType(state.purityRating)); updateState(CHANNEL_PURITY_RATING, new StringType(state.purityRating));
updateState(CHANNEL_COMBINED_RATING, new StringType(state.combinedRating)); updateState(CHANNEL_COMBINED_RATING, new StringType(state.combinedRating));

View File

@ -4,7 +4,7 @@
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd"> 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> <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> <author>Stefan Kästle</author>
</binding:binding> </binding:binding>

View File

@ -1,5 +1,12 @@
# Thing status offline descriptions # 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.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.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.

View 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 # Thing status offline descriptions
offline.conf-error-pairing = Bitte betätigen Sie den Taster am Bosch Smart Home Controller zum automatischen Verbinden. offline.conf-error-pairing = Bitte betätigen Sie den Taster am Bosch Smart Home Controller zum automatischen Verbinden.

View File

@ -7,7 +7,7 @@
<!-- Bosch Bridge --> <!-- Bosch Bridge -->
<bridge-type id="shc"> <bridge-type id="shc">
<label>Smart Home Controller</label> <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"/> <config-description-ref uri="thing-type:boschshc:bridge"/>
</bridge-type> </bridge-type>

View File

@ -48,6 +48,11 @@ class BoschHttpClientTest {
assertNotNull(httpClient); assertNotNull(httpClient);
} }
@Test
void getPublicInformationUrl() {
assertEquals("https://127.0.0.1:8446/smarthome/public/information", httpClient.getPublicInformationUrl());
}
@Test @Test
void getPairingUrl() { void getPairingUrl() {
assertEquals("https://127.0.0.1:8443/smarthome/clients", httpClient.getPairingUrl()); assertEquals("https://127.0.0.1:8443/smarthome/clients", httpClient.getPairingUrl());
@ -75,6 +80,11 @@ class BoschHttpClientTest {
assertFalse(httpClient.isAccessPossible()); assertFalse(httpClient.isAccessPossible());
} }
@Test
void isOnline() throws InterruptedException {
assertFalse(httpClient.isOnline());
}
@Test @Test
void doPairing() throws InterruptedException { void doPairing() throws InterruptedException {
assertFalse(httpClient.doPairing()); assertFalse(httpClient.doPairing());