[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
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)

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)
- [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*.

View 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>

View 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>

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.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 {

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.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();
}
}

View 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);
String nextSubscriptionId = subscriptionId;
LongPollResult longPollResult = gson.fromJson(content, LongPollResult.class);
if (longPollResult != null && longPollResult.result != null) {
this.handleResult.accept(longPollResult);
// 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.warn("Long poll response contained no results: {}", content);
logger.debug("Long poll response: {}", content);
// Check if we got a proper result from the SHC
LongPollError longPollError = gson.fromJson(content, LongPollError.class);
String nextSubscriptionId = subscriptionId;
if (longPollError != null && longPollError.error != null) {
logger.warn("Got long poll error: {} (code: {})", longPollError.error.message,
longPollError.error.code);
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);
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);
// Check if we got a proper result from the SHC
LongPollError longPollError = gson.fromJson(content, LongPollError.class);
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.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")

View 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));

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">
<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>

View 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.

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
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 -->
<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>

View 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());