mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[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:
parent
51db639853
commit
a796e472ec
@ -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)
|
||||
|
||||
|
@ -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*.
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 {
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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));
|
||||
|
@ -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>
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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>
|
||||
|
@ -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());
|
||||
|
Loading…
Reference in New Issue
Block a user