]> git.basschouten.com Git - openhab-addons.git/blob
f342153d55d762543d57b7d1fc7e7bcb392250b5
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.boschshc.internal.devices.bridge;
14
15 import static org.eclipse.jetty.http.HttpMethod.GET;
16
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;
23 import java.util.Map;
24 import java.util.concurrent.ExecutionException;
25 import java.util.concurrent.TimeUnit;
26 import java.util.concurrent.TimeoutException;
27
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;
39
40 import com.google.gson.Gson;
41 import com.google.gson.JsonSyntaxException;
42
43 /**
44  * HTTP client using own context with private & Bosch Certs
45  * to pair and connect to the Bosch Smart Home Controller.
46  *
47  * @author Gerd Zanker - Initial contribution
48  */
49 @NonNullByDefault
50 public class BoschHttpClient extends HttpClient {
51     private static final Gson GSON = new Gson();
52
53     private final Logger logger = LoggerFactory.getLogger(BoschHttpClient.class);
54
55     private final String ipAddress;
56     private final String systemPassword;
57
58     public BoschHttpClient(String ipAddress, String systemPassword, SslContextFactory sslContextFactory) {
59         super(sslContextFactory);
60         this.ipAddress = ipAddress;
61         this.systemPassword = systemPassword;
62     }
63
64     /**
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
67      *
68      * @return URL for public information
69      */
70     public String getPublicInformationUrl() {
71         return String.format("https://%s:8446/smarthome/public/information", this.ipAddress);
72     }
73
74     /**
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
77      * 
78      * @return URL for pairing
79      */
80     public String getPairingUrl() {
81         return String.format("https://%s:8443/smarthome/clients", this.ipAddress);
82     }
83
84     /**
85      * Returns a Bosch SHC URL for the endpoint, using port 8444.
86      * 
87      * @param endpoint a endpoint, see https://apidocs.bosch-smarthome.com/local/index.html
88      * @return Bosch SHC URL for passed endpoint
89      */
90     public String getBoschShcUrl(String endpoint) {
91         return String.format("https://%s:8444/%s", this.ipAddress, endpoint);
92     }
93
94     /**
95      * Returns a SmartHome URL for the endpoint - shortcut of {@link BoschSslUtil::getBoschShcUrl()}
96      * 
97      * @param endpoint a endpoint, see https://apidocs.bosch-smarthome.com/local/index.html
98      * @return SmartHome URL for passed endpoint
99      */
100     public String getBoschSmartHomeUrl(String endpoint) {
101         return this.getBoschShcUrl(String.format("smarthome/%s", endpoint));
102     }
103
104     /**
105      * Returns a device & service URL.
106      * see https://apidocs.bosch-smarthome.com/local/index.html
107      * 
108      * @param serviceName the name of the service
109      * @param deviceId the device identifier
110      * @return SmartHome URL for passed endpoint
111      */
112     public String getServiceUrl(String serviceName, String deviceId) {
113         return this.getBoschSmartHomeUrl(String.format("devices/%s/services/%s/state", deviceId, serviceName));
114     }
115
116     /**
117      * Checks if the Bosch SHC is online.
118      *
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
121      *
122      * Will return true, if the server responds with the "public information".
123      *
124      *
125      * @return true if HTTP server is online
126      * @throws InterruptedException in case of an interrupt
127      */
128     public boolean isOnline() throws InterruptedException {
129         try {
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());
137                 return true;
138             } else {
139                 logger.debug("Online check failed with status code: {}", contentResponse.getStatus());
140                 return false;
141             }
142         } catch (TimeoutException | ExecutionException | NullPointerException e) {
143             logger.debug("Online check failed because of {}!", e.getMessage());
144             return false;
145         }
146     }
147
148     /**
149      * Checks if the Bosch SHC can be accessed.
150      *
151      * @return true if HTTP access to SHC devices was successful
152      * @throws InterruptedException in case of an interrupt
153      */
154     public boolean isAccessPossible() throws InterruptedException {
155         try {
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());
163                 return true;
164             } else {
165                 logger.debug("Access check failed with status code: {}", contentResponse.getStatus());
166                 return false;
167             }
168         } catch (TimeoutException | ExecutionException | NullPointerException e) {
169             logger.debug("Access check failed because of {}!", e.getMessage());
170             return false;
171         }
172     }
173
174     /**
175      * Pairs this client with the Bosch SHC.
176      * Press pairing button on the Bosch Smart Home Controller!
177      * 
178      * @return true if pairing was successful, otherwise false
179      * @throws InterruptedException in case of an interrupt
180      */
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");
184
185         ContentResponse contentResponse;
186         try {
187             String publicCert = getCertFromSslContextFactory();
188             logger.trace("Pairing with SHC {}", ipAddress);
189
190             // JSON Rest content
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-----");
198
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)));
202
203             contentResponse = request.send();
204
205             logger.trace("Pairing response complete: {} - return code: {}", contentResponse.getContentAsString(),
206                     contentResponse.getStatus());
207             if (201 == contentResponse.getStatus()) {
208                 logger.debug("Pairing successful.");
209                 return true;
210             } else {
211                 logger.info("Pairing failed with response status {}.", contentResponse.getStatus());
212                 return false;
213             }
214         } catch (TimeoutException | CertificateEncodingException | KeyStoreException | NullPointerException e) {
215             logger.warn("Pairing failed with exception {}", e.getMessage());
216             return false;
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?");
222             return false;
223         }
224     }
225
226     /**
227      * Creates a HTTP request.
228      * 
229      * @param url for the HTTP request
230      * @param method for the HTTP request
231      * @return created HTTP request instance
232      */
233     public Request createRequest(String url, HttpMethod method) {
234         return this.createRequest(url, method, null);
235     }
236
237     /**
238      * Creates a HTTP request.
239      * 
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
244      */
245     public Request createRequest(String url, HttpMethod method, @Nullable Object content) {
246         logger.trace("Create request for http client {}", this.toString());
247
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
251
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));
256         } else {
257             logger.trace("create request for {}", url);
258         }
259
260         return request;
261     }
262
263     /**
264      * Sends a request and expects a response of the specified type.
265      * 
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
271      */
272     public <TContent> TContent sendRequest(Request request, Class<TContent> responseContentClass)
273             throws InterruptedException, TimeoutException, ExecutionException {
274         logger.trace("Send request: {}", request.toString());
275
276         ContentResponse contentResponse = request.send();
277
278         logger.debug("Received response: {} - status: {}", contentResponse.getContentAsString(),
279                 contentResponse.getStatus());
280
281         try {
282             @Nullable
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);
287             }
288             return content;
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);
292         }
293     }
294
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());
299     }
300 }