2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.boschshc.internal.devices.bridge;
15 import static org.eclipse.jetty.http.HttpMethod.GET;
17 import java.nio.charset.StandardCharsets;
18 import java.security.KeyStoreException;
19 import java.security.cert.Certificate;
20 import java.security.cert.CertificateEncodingException;
21 import java.util.Base64;
22 import java.util.HashMap;
24 import java.util.concurrent.ExecutionException;
25 import java.util.concurrent.TimeUnit;
26 import java.util.concurrent.TimeoutException;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.eclipse.jetty.client.HttpClient;
31 import org.eclipse.jetty.client.api.ContentResponse;
32 import org.eclipse.jetty.client.api.Request;
33 import org.eclipse.jetty.client.util.StringContentProvider;
34 import org.eclipse.jetty.http.HttpMethod;
35 import org.eclipse.jetty.http.HttpStatus;
36 import org.eclipse.jetty.util.ssl.SslContextFactory;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
40 import com.google.gson.Gson;
41 import com.google.gson.JsonSyntaxException;
44 * HTTP client using own context with private & Bosch Certs
45 * to pair and connect to the Bosch Smart Home Controller.
47 * @author Gerd Zanker - Initial contribution
50 public class BoschHttpClient extends HttpClient {
51 private static final Gson GSON = new Gson();
53 private final Logger logger = LoggerFactory.getLogger(BoschHttpClient.class);
55 private final String ipAddress;
56 private final String systemPassword;
58 public BoschHttpClient(String ipAddress, String systemPassword, SslContextFactory sslContextFactory) {
59 super(sslContextFactory);
60 this.ipAddress = ipAddress;
61 this.systemPassword = systemPassword;
65 * Returns the public information URL for the Bosch SHC clients, using port 8446.
66 * See https://github.com/BoschSmartHome/bosch-shc-api-docs/blob/master/postman/README.md
68 * @return URL for public information
70 public String getPublicInformationUrl() {
71 return String.format("https://%s:8446/smarthome/public/information", this.ipAddress);
75 * Returns the pairing URL for the Bosch SHC clients, using port 8443.
76 * See https://github.com/BoschSmartHome/bosch-shc-api-docs/blob/master/postman/README.md
78 * @return URL for pairing
80 public String getPairingUrl() {
81 return String.format("https://%s:8443/smarthome/clients", this.ipAddress);
85 * Returns a Bosch SHC URL for the endpoint, using port 8444.
87 * @param endpoint a endpoint, see https://apidocs.bosch-smarthome.com/local/index.html
88 * @return Bosch SHC URL for passed endpoint
90 public String getBoschShcUrl(String endpoint) {
91 return String.format("https://%s:8444/%s", this.ipAddress, endpoint);
95 * Returns a SmartHome URL for the endpoint - shortcut of {@link BoschSslUtil::getBoschShcUrl()}
97 * @param endpoint a endpoint, see https://apidocs.bosch-smarthome.com/local/index.html
98 * @return SmartHome URL for passed endpoint
100 public String getBoschSmartHomeUrl(String endpoint) {
101 return this.getBoschShcUrl(String.format("smarthome/%s", endpoint));
105 * Returns a device & service URL.
106 * see https://apidocs.bosch-smarthome.com/local/index.html
108 * @param serviceName the name of the service
109 * @param deviceId the device identifier
110 * @return SmartHome URL for passed endpoint
112 public String getServiceUrl(String serviceName, String deviceId) {
113 return this.getBoschSmartHomeUrl(String.format("devices/%s/services/%s/state", deviceId, serviceName));
117 * Checks if the Bosch SHC is online.
119 * The HTTP server could be offline (Timeout of request).
120 * Or during boot-up the server can response e.g. with SERVICE_UNAVAILABLE_503
122 * Will return true, if the server responds with the "public information".
125 * @return true if HTTP server is online
126 * @throws InterruptedException in case of an interrupt
128 public boolean isOnline() throws InterruptedException {
130 String url = this.getPublicInformationUrl();
131 Request request = this.createRequest(url, GET);
132 ContentResponse contentResponse = request.send();
133 if (HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
134 String content = contentResponse.getContentAsString();
135 logger.debug("Online check completed with success: {} - status code: {}", content,
136 contentResponse.getStatus());
139 logger.debug("Online check failed with status code: {}", contentResponse.getStatus());
142 } catch (TimeoutException | ExecutionException | NullPointerException e) {
143 logger.debug("Online check failed because of {}!", e.getMessage());
149 * Checks if the Bosch SHC can be accessed.
151 * @return true if HTTP access to SHC devices was successful
152 * @throws InterruptedException in case of an interrupt
154 public boolean isAccessPossible() throws InterruptedException {
156 String url = this.getBoschSmartHomeUrl("devices");
157 Request request = this.createRequest(url, GET);
158 ContentResponse contentResponse = request.send();
159 if (HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
160 String content = contentResponse.getContentAsString();
161 logger.debug("Access check completed with success: {} - status code: {}", content,
162 contentResponse.getStatus());
165 logger.debug("Access check failed with status code: {}", contentResponse.getStatus());
168 } catch (TimeoutException | ExecutionException | NullPointerException e) {
169 logger.debug("Access check failed because of {}!", e.getMessage());
175 * Pairs this client with the Bosch SHC.
176 * Press pairing button on the Bosch Smart Home Controller!
178 * @return true if pairing was successful, otherwise false
179 * @throws InterruptedException in case of an interrupt
181 public boolean doPairing() throws InterruptedException {
182 logger.trace("Starting pairing openHAB Client with Bosch Smart Home Controller!");
183 logger.trace("Please press the Bosch Smart Home Controller button until LED starts blinking");
185 ContentResponse contentResponse;
187 String publicCert = getCertFromSslContextFactory();
188 logger.trace("Pairing with SHC {}", ipAddress);
191 Map<String, String> items = new HashMap<>();
192 items.put("@type", "client");
193 items.put("id", BoschSslUtil.getBoschShcClientId()); // Client Id contains the unique OpenHab instance Id
194 items.put("name", "oss_OpenHAB_Binding"); // Client name according to
195 // https://github.com/BoschSmartHome/bosch-shc-api-docs#terms-and-conditions
196 items.put("primaryRole", "ROLE_RESTRICTED_CLIENT");
197 items.put("certificate", "-----BEGIN CERTIFICATE-----\r" + publicCert + "\r-----END CERTIFICATE-----");
199 String url = this.getPairingUrl();
200 Request request = this.createRequest(url, HttpMethod.POST, items).header("Systempassword",
201 Base64.getEncoder().encodeToString(this.systemPassword.getBytes(StandardCharsets.UTF_8)));
203 contentResponse = request.send();
205 logger.trace("Pairing response complete: {} - return code: {}", contentResponse.getContentAsString(),
206 contentResponse.getStatus());
207 if (201 == contentResponse.getStatus()) {
208 logger.debug("Pairing successful.");
211 logger.info("Pairing failed with response status {}.", contentResponse.getStatus());
214 } catch (TimeoutException | CertificateEncodingException | KeyStoreException | NullPointerException e) {
215 logger.warn("Pairing failed with exception {}", e.getMessage());
217 } catch (ExecutionException e) {
218 // javax.net.ssl.SSLHandshakeException: General SSLEngine problem
219 // => usually the pairing failed, because hardware button was not pressed.
220 logger.trace("Pairing failed - Details: {}", e.getMessage());
221 logger.warn("Pairing failed. Was the Bosch Smart Home Controller button pressed?");
227 * Creates a HTTP request.
229 * @param url for the HTTP request
230 * @param method for the HTTP request
231 * @return created HTTP request instance
233 public Request createRequest(String url, HttpMethod method) {
234 return this.createRequest(url, method, null);
238 * Creates a HTTP request.
240 * @param url for the HTTP request
241 * @param method for the HTTP request
242 * @param content for the HTTP request
243 * @return created HTTP request instance
245 public Request createRequest(String url, HttpMethod method, @Nullable Object content) {
246 logger.trace("Create request for http client {}", this.toString());
248 Request request = this.newRequest(url).method(method).header("Content-Type", "application/json")
249 .header("api-version", "2.1") // see https://github.com/BoschSmartHome/bosch-shc-api-docs/issues/46
250 .timeout(10, TimeUnit.SECONDS); // Set default timeout
252 if (content != null) {
253 String body = GSON.toJson(content);
254 logger.trace("create request for {} and content {}", url, body);
255 request = request.content(new StringContentProvider(body));
257 logger.trace("create request for {}", url);
264 * Sends a request and expects a response of the specified type.
266 * @param request Request to send
267 * @param responseContentClass Type of expected response
268 * @throws ExecutionException in case of invalid HTTP request result
269 * @throws TimeoutException in case of an HTTP request timeout
270 * @throws InterruptedException in case of an interrupt
272 public <TContent> TContent sendRequest(Request request, Class<TContent> responseContentClass)
273 throws InterruptedException, TimeoutException, ExecutionException {
274 logger.trace("Send request: {}", request.toString());
276 ContentResponse contentResponse = request.send();
278 logger.debug("Received response: {} - status: {}", contentResponse.getContentAsString(),
279 contentResponse.getStatus());
283 TContent content = GSON.fromJson(contentResponse.getContentAsString(), responseContentClass);
284 if (content == null) {
285 throw new ExecutionException(String.format("Received no content in response, expected type %s",
286 responseContentClass.getName()), null);
289 } catch (JsonSyntaxException e) {
290 throw new ExecutionException(String.format("Received invalid content in response, expected type %s: %s",
291 responseContentClass.getName(), e.getMessage()), e);
295 private String getCertFromSslContextFactory() throws KeyStoreException, CertificateEncodingException {
296 Certificate cert = this.getSslContextFactory().getKeyStore()
297 .getCertificate(BoschSslUtil.getBoschShcServerId(ipAddress));
298 return Base64.getEncoder().encodeToString(cert.getEncoded());