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