]> git.basschouten.com Git - openhab-addons.git/blob
c5615401a51a118641d3c8b449ae0e53f0b496c0
[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 import static org.eclipse.jetty.http.HttpMethod.PUT;
17
18 import java.lang.reflect.Type;
19 import java.util.ArrayList;
20 import java.util.concurrent.ExecutionException;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
23 import java.util.concurrent.TimeoutException;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.eclipse.jetty.client.api.ContentResponse;
28 import org.eclipse.jetty.client.api.Request;
29 import org.eclipse.jetty.client.api.Response;
30 import org.eclipse.jetty.util.ssl.SslContextFactory;
31 import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler;
32 import org.openhab.binding.boschshc.internal.devices.bridge.dto.*;
33 import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
34 import org.openhab.binding.boschshc.internal.exceptions.LongPollingFailedException;
35 import org.openhab.binding.boschshc.internal.exceptions.PairingFailedException;
36 import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
37 import org.openhab.binding.boschshc.internal.services.dto.JsonRestExceptionResponse;
38 import org.openhab.core.thing.Bridge;
39 import org.openhab.core.thing.ChannelUID;
40 import org.openhab.core.thing.Thing;
41 import org.openhab.core.thing.ThingStatus;
42 import org.openhab.core.thing.ThingStatusDetail;
43 import org.openhab.core.thing.binding.BaseBridgeHandler;
44 import org.openhab.core.thing.binding.ThingHandler;
45 import org.openhab.core.types.Command;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
48
49 import com.google.gson.Gson;
50 import com.google.gson.reflect.TypeToken;
51
52 /**
53  * Representation of a connection with a Bosch Smart Home Controller bridge.
54  *
55  * @author Stefan Kästle - Initial contribution
56  * @author Gerd Zanker - added HttpClient with pairing support
57  * @author Christian Oeing - refactorings of e.g. server registration
58  */
59 @NonNullByDefault
60 public class BoschSHCBridgeHandler extends BaseBridgeHandler {
61
62     private final Logger logger = LoggerFactory.getLogger(BoschSHCBridgeHandler.class);
63
64     /**
65      * gson instance to convert a class to json string and back.
66      */
67     private final Gson gson = new Gson();
68
69     /**
70      * Handler to do long polling.
71      */
72     private final LongPolling longPolling;
73
74     private @Nullable BoschHttpClient httpClient;
75
76     private @Nullable ScheduledFuture<?> scheduledPairing;
77
78     public BoschSHCBridgeHandler(Bridge bridge) {
79         super(bridge);
80
81         this.longPolling = new LongPolling(this.scheduler, this::handleLongPollResult, this::handleLongPollFailure);
82     }
83
84     @Override
85     public void initialize() {
86         // Read configuration
87         BoschSHCBridgeConfiguration config = getConfigAs(BoschSHCBridgeConfiguration.class);
88
89         if (config.ipAddress.isEmpty()) {
90             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No IP address set");
91             return;
92         }
93
94         if (config.password.isEmpty()) {
95             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No system password set");
96             return;
97         }
98
99         SslContextFactory factory;
100         try {
101             // prepare SSL key and certificates
102             factory = new BoschSslUtil(config.ipAddress).getSslContextFactory();
103         } catch (PairingFailedException e) {
104             this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
105                     "@text/offline.conf-error-ssl");
106             return;
107         }
108
109         // Instantiate HttpClient with the SslContextFactory
110         BoschHttpClient httpClient = this.httpClient = new BoschHttpClient(config.ipAddress, config.password, factory);
111
112         // Start http client
113         try {
114             httpClient.start();
115         } catch (Exception e) {
116             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
117                     String.format("Could not create http connection to controller: %s", e.getMessage()));
118             return;
119         }
120
121         // Initialize bridge in the background.
122         // Start initial access the first time
123         scheduleInitialAccess(httpClient);
124     }
125
126     @Override
127     public void dispose() {
128         // Cancel scheduled pairing.
129         ScheduledFuture<?> scheduledPairing = this.scheduledPairing;
130         if (scheduledPairing != null) {
131             scheduledPairing.cancel(true);
132             this.scheduledPairing = null;
133         }
134
135         // Stop long polling.
136         this.longPolling.stop();
137
138         BoschHttpClient httpClient = this.httpClient;
139         if (httpClient != null) {
140             try {
141                 httpClient.stop();
142             } catch (Exception e) {
143                 logger.debug("HttpClient failed on bridge disposal: {}", e.getMessage());
144             }
145             this.httpClient = null;
146         }
147
148         super.dispose();
149     }
150
151     @Override
152     public void handleCommand(ChannelUID channelUID, Command command) {
153     }
154
155     /**
156      * Schedule the initial access.
157      * Use a delay if pairing fails and next retry is scheduled.
158      */
159     private void scheduleInitialAccess(BoschHttpClient httpClient) {
160         this.scheduledPairing = scheduler.schedule(() -> initialAccess(httpClient), 15, TimeUnit.SECONDS);
161     }
162
163     /**
164      * Execute the initial access.
165      * Uses the HTTP Bosch SHC client
166      * to check if access if possible
167      * pairs this Bosch SHC Bridge with the SHC if necessary
168      * and starts the first log poll.
169      */
170     private void initialAccess(BoschHttpClient httpClient) {
171         logger.debug("Initializing Bosch SHC Bridge: {} - HTTP client is: {} - version: 2020-04-05", this, httpClient);
172
173         try {
174             // check access and pair if necessary
175             if (!httpClient.isAccessPossible()) {
176                 // update status already if access is not possible
177                 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE,
178                         "@text/offline.conf-error-pairing");
179                 if (!httpClient.doPairing()) {
180                     this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
181                             "@text/offline.conf-error-pairing");
182                 }
183                 // restart initial access - needed also in case of successful pairing to check access again
184                 scheduleInitialAccess(httpClient);
185             } else {
186                 // print rooms and devices if things are reachable
187                 boolean thingReachable = true;
188                 thingReachable &= this.getRooms();
189                 thingReachable &= this.getDevices();
190
191                 if (thingReachable) {
192                     this.updateStatus(ThingStatus.ONLINE);
193
194                     // Start long polling
195                     try {
196                         this.longPolling.start(httpClient);
197                     } catch (LongPollingFailedException e) {
198                         this.handleLongPollFailure(e);
199                     }
200                 } else {
201                     this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
202                             "@text/offline.not-reachable");
203                     // restart initial access
204                     scheduleInitialAccess(httpClient);
205                 }
206             }
207         } catch (InterruptedException e) {
208             this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE,
209                     String.format("Pairing was interrupted: %s", e.getMessage()));
210         }
211     }
212
213     /**
214      * Get a list of connected devices from the Smart-Home Controller
215      * 
216      * @throws InterruptedException
217      */
218     private boolean getDevices() throws InterruptedException {
219         BoschHttpClient httpClient = this.httpClient;
220         if (httpClient == null) {
221             return false;
222         }
223
224         try {
225             logger.debug("Sending http request to Bosch to request clients: {}", httpClient);
226             String url = httpClient.getBoschSmartHomeUrl("devices");
227             ContentResponse contentResponse = httpClient.createRequest(url, GET).send();
228
229             String content = contentResponse.getContentAsString();
230             logger.debug("Response complete: {} - return code: {}", content, contentResponse.getStatus());
231
232             Type collectionType = new TypeToken<ArrayList<Device>>() {
233             }.getType();
234             ArrayList<Device> devices = gson.fromJson(content, collectionType);
235
236             if (devices != null) {
237                 for (Device d : devices) {
238                     // Write found devices into openhab.log until we have implemented auto discovery
239                     logger.info("Found device: name={} id={}", d.name, d.id);
240                     if (d.deviceSerivceIDs != null) {
241                         for (String s : d.deviceSerivceIDs) {
242                             logger.info(".... service: {}", s);
243                         }
244                     }
245                 }
246             }
247         } catch (TimeoutException | ExecutionException e) {
248             logger.debug("HTTP request failed with exception {}", e.getMessage());
249             return false;
250         }
251
252         return true;
253     }
254
255     private void handleLongPollResult(LongPollResult result) {
256         for (DeviceStatusUpdate update : result.result) {
257             if (update != null && update.state != null) {
258                 logger.debug("Got update for {}", update.deviceId);
259
260                 boolean handled = false;
261
262                 Bridge bridge = this.getThing();
263                 for (Thing childThing : bridge.getThings()) {
264                     // All children of this should implement BoschSHCHandler
265                     ThingHandler baseHandler = childThing.getHandler();
266                     if (baseHandler != null && baseHandler instanceof BoschSHCHandler) {
267                         BoschSHCHandler handler = (BoschSHCHandler) baseHandler;
268                         String deviceId = handler.getBoschID();
269
270                         handled = true;
271                         logger.debug("Registered device: {} - looking for {}", deviceId, update.deviceId);
272
273                         if (deviceId != null && update.deviceId.equals(deviceId)) {
274                             logger.debug("Found child: {} - calling processUpdate with {}", handler, update.state);
275                             handler.processUpdate(update.id, update.state);
276                         }
277                     } else {
278                         logger.warn("longPoll: child handler for {} does not implement Bosch SHC handler", baseHandler);
279                     }
280                 }
281
282                 if (!handled) {
283                     logger.debug("Could not find a thing for device ID: {}", update.deviceId);
284                 }
285             }
286         }
287     }
288
289     private void handleLongPollFailure(Throwable e) {
290         logger.warn("Long polling failed", e);
291         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Long polling failed");
292     }
293
294     /**
295      * Get a list of rooms from the Smart-Home controller
296      * 
297      * @throws InterruptedException
298      */
299     private boolean getRooms() throws InterruptedException {
300         BoschHttpClient httpClient = this.httpClient;
301         if (httpClient != null) {
302             try {
303                 logger.debug("Sending http request to Bosch to request rooms");
304                 String url = httpClient.getBoschSmartHomeUrl("rooms");
305                 ContentResponse contentResponse = httpClient.createRequest(url, GET).send();
306
307                 String content = contentResponse.getContentAsString();
308                 logger.debug("Response complete: {} - return code: {}", content, contentResponse.getStatus());
309
310                 Type collectionType = new TypeToken<ArrayList<Room>>() {
311                 }.getType();
312
313                 ArrayList<Room> rooms = gson.fromJson(content, collectionType);
314
315                 if (rooms != null) {
316                     for (Room r : rooms) {
317                         logger.info("Found room: {}", r.name);
318                     }
319                 }
320
321                 return true;
322             } catch (TimeoutException | ExecutionException e) {
323                 logger.warn("HTTP request failed: {}", e.getMessage());
324                 return false;
325             }
326         } else {
327             return false;
328         }
329     }
330
331     /**
332      * Query the Bosch Smart Home Controller for the state of the given thing.
333      *
334      * @param deviceId Id of device to get state for
335      * @param stateName Name of the state to query
336      * @param stateClass Class to convert the resulting JSON to
337      * @throws ExecutionException
338      * @throws TimeoutException
339      * @throws InterruptedException
340      * @throws BoschSHCException
341      */
342     public <T extends BoschSHCServiceState> @Nullable T getState(String deviceId, String stateName, Class<T> stateClass)
343             throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
344         BoschHttpClient httpClient = this.httpClient;
345         if (httpClient == null) {
346             logger.warn("HttpClient not initialized");
347             return null;
348         }
349
350         String url = httpClient.getServiceUrl(stateName, deviceId);
351         Request request = httpClient.createRequest(url, GET).header("Accept", "application/json");
352
353         logger.debug("refreshState: Requesting \"{}\" from Bosch: {} via {}", stateName, deviceId, url);
354
355         ContentResponse contentResponse = request.send();
356
357         String content = contentResponse.getContentAsString();
358         logger.debug("refreshState: Request complete: [{}] - return code: {}", content, contentResponse.getStatus());
359
360         int statusCode = contentResponse.getStatus();
361         if (statusCode != 200) {
362             JsonRestExceptionResponse errorResponse = gson.fromJson(content, JsonRestExceptionResponse.class);
363             if (errorResponse != null) {
364                 throw new BoschSHCException(String.format(
365                         "State request for service %s of device %s failed with status code %d and error code %s",
366                         stateName, deviceId, errorResponse.statusCode, errorResponse.errorCode));
367             } else {
368                 throw new BoschSHCException(
369                         String.format("State request for service %s of device %s failed with status code %d", stateName,
370                                 deviceId, statusCode));
371             }
372         }
373
374         @Nullable
375         T state = gson.fromJson(content, stateClass);
376         if (state == null) {
377             throw new BoschSHCException(String.format("Received invalid, expected type %s", stateClass.getName()));
378         }
379         return state;
380     }
381
382     /**
383      * Sends a state change for a device to the controller
384      * 
385      * @param deviceId Id of device to change state for
386      * @param serviceName Name of service of device to change state for
387      * @param state New state data to set for service
388      * 
389      * @return Response of request
390      * @throws InterruptedException
391      * @throws ExecutionException
392      * @throws TimeoutException
393      */
394     public <T extends BoschSHCServiceState> @Nullable Response putState(String deviceId, String serviceName, T state)
395             throws InterruptedException, TimeoutException, ExecutionException {
396         BoschHttpClient httpClient = this.httpClient;
397         if (httpClient == null) {
398             logger.warn("HttpClient not initialized");
399             return null;
400         }
401
402         // Create request
403         String url = httpClient.getServiceUrl(serviceName, deviceId);
404         Request request = httpClient.createRequest(url, PUT, state);
405
406         // Send request
407         Response response = request.send();
408         return response;
409     }
410 }