2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.boschshc.internal.devices.bridge;
15 import static org.eclipse.jetty.http.HttpMethod.GET;
16 import static org.eclipse.jetty.http.HttpMethod.PUT;
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;
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;
51 import com.google.gson.Gson;
52 import com.google.gson.reflect.TypeToken;
55 * Representation of a connection with a Bosch Smart Home Controller bridge.
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
62 public class BoschSHCBridgeHandler extends BaseBridgeHandler {
64 private final Logger logger = LoggerFactory.getLogger(BoschSHCBridgeHandler.class);
67 * gson instance to convert a class to json string and back.
69 private final Gson gson = new Gson();
72 * Handler to do long polling.
74 private final LongPolling longPolling;
76 private @Nullable BoschHttpClient httpClient;
78 private @Nullable ScheduledFuture<?> scheduledPairing;
80 public BoschSHCBridgeHandler(Bridge bridge) {
83 this.longPolling = new LongPolling(this.scheduler, this::handleLongPollResult, this::handleLongPollFailure);
87 public void initialize() {
88 logger.debug("Initialize {} Version {}", FrameworkUtil.getBundle(getClass()).getSymbolicName(),
89 FrameworkUtil.getBundle(getClass()).getVersion());
92 BoschSHCBridgeConfiguration config = getConfigAs(BoschSHCBridgeConfiguration.class);
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");
101 String password = config.password.trim();
102 if (password.isEmpty()) {
103 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
104 "@text/offline.conf-error-empty-password");
108 SslContextFactory factory;
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");
118 // Instantiate HttpClient with the SslContextFactory
119 BoschHttpClient httpClient = this.httpClient = new BoschHttpClient(ipAddress, password, factory);
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()));
130 // general checks are OK, therefore set the status to unknown and wait for initial access
131 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE);
133 // Initialize bridge in the background.
134 // Start initial access the first time
135 scheduleInitialAccess(httpClient);
139 public void dispose() {
140 // Cancel scheduled pairing.
142 ScheduledFuture<?> scheduledPairing = this.scheduledPairing;
143 if (scheduledPairing != null) {
144 scheduledPairing.cancel(true);
145 this.scheduledPairing = null;
148 // Stop long polling.
149 this.longPolling.stop();
152 BoschHttpClient httpClient = this.httpClient;
153 if (httpClient != null) {
156 } catch (Exception e) {
157 logger.debug("HttpClient failed on bridge disposal: {}", e.getMessage());
159 this.httpClient = null;
166 public void handleCommand(ChannelUID channelUID, Command command) {
170 * Schedule the initial access.
171 * Use a delay if pairing fails and next retry is scheduled.
173 private void scheduleInitialAccess(BoschHttpClient httpClient) {
174 this.scheduledPairing = scheduler.schedule(() -> initialAccess(httpClient), 15, TimeUnit.SECONDS);
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.
184 private void initialAccess(BoschHttpClient httpClient) {
185 logger.debug("Initializing Bosch SHC Bridge: {} - HTTP client is: {}", this, httpClient);
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);
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");
208 // restart initial access - needed also in case of successful pairing to check access again
209 scheduleInitialAccess(httpClient);
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);
226 // start long polling loop
227 this.updateStatus(ThingStatus.ONLINE);
229 this.longPolling.start(httpClient);
230 } catch (LongPollingFailedException e) {
231 this.handleLongPollFailure(e);
234 } catch (InterruptedException e) {
235 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE, "@text/offline.interrupted");
240 * Get a list of connected devices from the Smart-Home Controller
242 * @throws InterruptedException in case bridge is stopped
244 private boolean getDevices() throws InterruptedException {
246 BoschHttpClient httpClient = this.httpClient;
247 if (httpClient == null) {
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();
256 // check HTTP status code
257 if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
258 logger.debug("Request devices failed with status code: {}", contentResponse.getStatus());
262 String content = contentResponse.getContentAsString();
263 logger.debug("Request devices completed with success: {} - status code: {}", content,
264 contentResponse.getStatus());
266 Type collectionType = new TypeToken<ArrayList<Device>>() {
268 ArrayList<Device> devices = gson.fromJson(content, collectionType);
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);
281 } catch (TimeoutException | ExecutionException e) {
282 logger.warn("Request devices failed because of {}!", e.getMessage());
290 * Bridge callback handler for the results of long polls.
292 * It will check the result and
293 * forward the received to the bosch thing handlers.
295 * @param result Results from Long Polling
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);
302 boolean handled = false;
304 Bridge bridge = this.getThing();
305 for (Thing childThing : bridge.getThings()) {
306 // All children of this should implement BoschSHCHandler
308 ThingHandler baseHandler = childThing.getHandler();
309 if (baseHandler != null && baseHandler instanceof BoschSHCHandler) {
310 BoschSHCHandler handler = (BoschSHCHandler) baseHandler;
312 String deviceId = handler.getBoschID();
315 logger.debug("Registered device: {} - looking for {}", deviceId, update.deviceId);
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);
322 logger.warn("longPoll: child handler for {} does not implement Bosch SHC handler", baseHandler);
327 logger.debug("Could not find a thing for device ID: {}", update.deviceId);
334 * Bridge callback handler for the failures during long polls.
336 * It will update the bridge status and try to access the SHC again.
338 * @param e error during long polling
340 private void handleLongPollFailure(Throwable e) {
341 logger.warn("Long polling failed, will try to reconnect", e);
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");
350 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE,
351 "@text/offline.long-polling-failed.trying-to-reconnect");
352 scheduleInitialAccess(httpClient);
356 * Get a list of rooms from the Smart-Home controller
358 * @throws InterruptedException in case bridge is stopped
360 private boolean getRooms() throws InterruptedException {
362 BoschHttpClient httpClient = this.httpClient;
363 if (httpClient != null) {
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();
369 // check HTTP status code
370 if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
371 logger.debug("Request rooms failed with status code: {}", contentResponse.getStatus());
375 String content = contentResponse.getContentAsString();
376 logger.debug("Request rooms completed with success: {} - status code: {}", content,
377 contentResponse.getStatus());
379 Type collectionType = new TypeToken<ArrayList<Room>>() {
382 ArrayList<Room> rooms = gson.fromJson(content, collectionType);
385 for (Room r : rooms) {
386 logger.info("Found room: {}", r.name);
391 } catch (TimeoutException | ExecutionException e) {
392 logger.warn("Request rooms failed because of {}!", e.getMessage());
401 * Query the Bosch Smart Home Controller for the state of the given thing.
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
411 public <T extends BoschSHCServiceState> @Nullable T getState(String deviceId, String stateName, Class<T> stateClass)
412 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
414 BoschHttpClient httpClient = this.httpClient;
415 if (httpClient == null) {
416 logger.warn("HttpClient not initialized");
420 String url = httpClient.getServiceUrl(stateName, deviceId);
421 Request request = httpClient.createRequest(url, GET).header("Accept", "application/json");
423 logger.debug("refreshState: Requesting \"{}\" from Bosch: {} via {}", stateName, deviceId, url);
425 ContentResponse contentResponse = request.send();
427 String content = contentResponse.getContentAsString();
428 logger.debug("refreshState: Request complete: [{}] - return code: {}", content, contentResponse.getStatus());
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));
438 throw new BoschSHCException(
439 String.format("State request for service %s of device %s failed with status code %d", stateName,
440 deviceId, statusCode));
445 T state = gson.fromJson(content, stateClass);
447 throw new BoschSHCException(String.format("Received invalid, expected type %s", stateClass.getName()));
453 * Sends a state change for a device to the controller
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
459 * @return Response of request
460 * @throws InterruptedException
461 * @throws ExecutionException
462 * @throws TimeoutException
464 public <T extends BoschSHCServiceState> @Nullable Response putState(String deviceId, String serviceName, T state)
465 throws InterruptedException, TimeoutException, ExecutionException {
467 BoschHttpClient httpClient = this.httpClient;
468 if (httpClient == null) {
469 logger.warn("HttpClient not initialized");
474 String url = httpClient.getServiceUrl(serviceName, deviceId);
475 Request request = httpClient.createRequest(url, PUT, state);
478 return request.send();