]> git.basschouten.com Git - openhab-addons.git/blob
e5609117ed0af4310efe9e4f8cfd49800a0a1870
[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.util.ssl.SslContextFactory;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
38
39 import com.google.gson.Gson;
40 import com.google.gson.JsonSyntaxException;
41
42 /**
43  * HTTP client using own context with private & Bosch Certs
44  * to pair and connect to the Bosch Smart Home Controller.
45  *
46  * @author Gerd Zanker - Initial contribution
47  */
48 @NonNullByDefault
49 public class BoschHttpClient extends HttpClient {
50     private static final Gson GSON = new Gson();
51
52     private final Logger logger = LoggerFactory.getLogger(BoschHttpClient.class);
53
54     private final String ipAddress;
55     private final String systemPassword;
56
57     public BoschHttpClient(String ipAddress, String systemPassword, SslContextFactory sslContextFactory) {
58         super(sslContextFactory);
59         this.ipAddress = ipAddress;
60         this.systemPassword = systemPassword;
61     }
62
63     /**
64      * Returns the pairing URL for the Bosch SHC clients, using port 8443.
65      * See https://github.com/BoschSmartHome/bosch-shc-api-docs/blob/master/postman/README.md
66      * 
67      * @return URL for pairing
68      */
69     public String getPairingUrl() {
70         return String.format("https://%s:8443/smarthome/clients", this.ipAddress);
71     }
72
73     /**
74      * Returns a Bosch SHC URL for the endpoint, using port 8444.
75      * 
76      * @param endpoint a endpoint, see https://apidocs.bosch-smarthome.com/local/index.html
77      * @return Bosch SHC URL for passed endpoint
78      */
79     public String getBoschShcUrl(String endpoint) {
80         return String.format("https://%s:8444/%s", this.ipAddress, endpoint);
81     }
82
83     /**
84      * Returns a SmartHome URL for the endpoint - shortcut of {@link BoschSslUtil::getBoschShcUrl()}
85      * 
86      * @param endpoint a endpoint, see https://apidocs.bosch-smarthome.com/local/index.html
87      * @return SmartHome URL for passed endpoint
88      */
89     public String getBoschSmartHomeUrl(String endpoint) {
90         return this.getBoschShcUrl(String.format("smarthome/%s", endpoint));
91     }
92
93     /**
94      * Returns a device & service URL.
95      * see https://apidocs.bosch-smarthome.com/local/index.html
96      * 
97      * @param serviceName the name of the service
98      * @param deviceId the device identifier
99      * @return SmartHome URL for passed endpoint
100      */
101     public String getServiceUrl(String serviceName, String deviceId) {
102         return this.getBoschSmartHomeUrl(String.format("devices/%s/services/%s/state", deviceId, serviceName));
103     }
104
105     /**
106      * Checks if the Bosch SHC can be accessed.
107      * 
108      * @return true if HTTP access was successful
109      * @throws InterruptedException in case of an interrupt
110      */
111     public boolean isAccessPossible() throws InterruptedException {
112         try {
113             String url = this.getBoschSmartHomeUrl("devices");
114             Request request = this.createRequest(url, GET);
115             ContentResponse contentResponse = request.send();
116             String content = contentResponse.getContentAsString();
117             logger.debug("Access check response complete: {} - return code: {}", content, contentResponse.getStatus());
118             return true;
119         } catch (TimeoutException | ExecutionException | NullPointerException e) {
120             logger.debug("Access check response failed because of {}!", e.getMessage());
121             return false;
122         }
123     }
124
125     /**
126      * Pairs this client with the Bosch SHC.
127      * Press pairing button on the Bosch Smart Home Controller!
128      * 
129      * @return true if pairing was successful, otherwise false
130      * @throws InterruptedException in case of an interrupt
131      */
132     public boolean doPairing() throws InterruptedException {
133         logger.trace("Starting pairing openHAB Client with Bosch SmartHomeController!");
134         logger.trace("Please press the Bosch SHC button until LED starts blinking");
135
136         ContentResponse contentResponse;
137         try {
138             String publicCert = getCertFromSslContextFactory();
139             logger.trace("Pairing with SHC {}", ipAddress);
140
141             // JSON Rest content
142             Map<String, String> items = new HashMap<>();
143             items.put("@type", "client");
144             items.put("id", BoschSslUtil.getBoschShcClientId()); // Client Id contains the unique OpenHab instance Id
145             items.put("name", "oss_OpenHAB_Binding"); // Client name according to
146                                                       // https://github.com/BoschSmartHome/bosch-shc-api-docs#terms-and-conditions
147             items.put("primaryRole", "ROLE_RESTRICTED_CLIENT");
148             items.put("certificate", "-----BEGIN CERTIFICATE-----\r" + publicCert + "\r-----END CERTIFICATE-----");
149
150             String url = this.getPairingUrl();
151             Request request = this.createRequest(url, HttpMethod.POST, items).header("Systempassword",
152                     Base64.getEncoder().encodeToString(this.systemPassword.getBytes(StandardCharsets.UTF_8)));
153
154             contentResponse = request.send();
155
156             logger.trace("Pairing response complete: {} - return code: {}", contentResponse.getContentAsString(),
157                     contentResponse.getStatus());
158             if (201 == contentResponse.getStatus()) {
159                 logger.debug("Pairing successful.");
160                 return true;
161             } else {
162                 logger.info("Pairing failed with response status {}.", contentResponse.getStatus());
163                 return false;
164             }
165         } catch (TimeoutException | CertificateEncodingException | KeyStoreException | NullPointerException e) {
166             logger.warn("Pairing failed with exception {}", e.getMessage());
167             return false;
168         } catch (ExecutionException e) {
169             // javax.net.ssl.SSLHandshakeException: General SSLEngine problem
170             // => usually the pairing failed, because hardware button was not pressed.
171             logger.trace("Pairing failed - Details: {}", e.getMessage());
172             logger.warn("Pairing failed. Was the Bosch SHC button pressed?");
173             return false;
174         }
175     }
176
177     /**
178      * Creates a HTTP request.
179      * 
180      * @param url for the HTTP request
181      * @param method for the HTTP request
182      * @return created HTTP request instance
183      */
184     public Request createRequest(String url, HttpMethod method) {
185         return this.createRequest(url, method, null);
186     }
187
188     /**
189      * Creates a HTTP request.
190      * 
191      * @param url for the HTTP request
192      * @param method for the HTTP request
193      * @param content for the HTTP request
194      * @return created HTTP request instance
195      */
196     public Request createRequest(String url, HttpMethod method, @Nullable Object content) {
197         Request request = this.newRequest(url).method(method).header("Content-Type", "application/json");
198         if (content != null) {
199             String body = GSON.toJson(content);
200             logger.trace("create request for {} and content {}", url, body);
201             request = request.content(new StringContentProvider(body));
202         } else {
203             logger.trace("create request for {}", url);
204         }
205
206         // Set default timeout
207         request.timeout(10, TimeUnit.SECONDS);
208
209         return request;
210     }
211
212     /**
213      * Sends a request and expects a response of the specified type.
214      * 
215      * @param request Request to send
216      * @param responseContentClass Type of expected response
217      * @throws ExecutionException in case of invalid HTTP request result
218      * @throws TimeoutException in case of an HTTP request timeout
219      * @throws InterruptedException in case of an interrupt
220      */
221     public <TContent> TContent sendRequest(Request request, Class<TContent> responseContentClass)
222             throws InterruptedException, TimeoutException, ExecutionException {
223         ContentResponse contentResponse = request.send();
224
225         logger.debug("BoschHttpClient: response complete: {} - return code: {}", contentResponse.getContentAsString(),
226                 contentResponse.getStatus());
227
228         try {
229             @Nullable
230             TContent content = GSON.fromJson(contentResponse.getContentAsString(), responseContentClass);
231             if (content == null) {
232                 throw new ExecutionException(String.format("Received no content in response, expected type %s",
233                         responseContentClass.getName()), null);
234             }
235             return content;
236         } catch (JsonSyntaxException e) {
237             throw new ExecutionException(String.format("Received invalid content in response, expected type %s: %s",
238                     responseContentClass.getName(), e.getMessage()), e);
239         }
240     }
241
242     private String getCertFromSslContextFactory() throws KeyStoreException, CertificateEncodingException {
243         Certificate cert = this.getSslContextFactory().getKeyStore()
244                 .getCertificate(BoschSslUtil.getBoschShcServerId(ipAddress));
245         return Base64.getEncoder().encodeToString(cert.getEncoded());
246     }
247 }