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