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.*;
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;
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;
53 import com.google.gson.Gson;
54 import com.google.gson.reflect.TypeToken;
57 * Representation of a connection with a Bosch Smart Home Controller bridge.
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
64 public class BoschSHCBridgeHandler extends BaseBridgeHandler {
66 private final Logger logger = LoggerFactory.getLogger(BoschSHCBridgeHandler.class);
69 * gson instance to convert a class to json string and back.
71 private final Gson gson = new Gson();
74 * Handler to do long polling.
76 private final LongPolling longPolling;
78 private @Nullable BoschHttpClient httpClient;
80 private @Nullable ScheduledFuture<?> scheduledPairing;
82 public BoschSHCBridgeHandler(Bridge bridge) {
85 this.longPolling = new LongPolling(this.scheduler, this::handleLongPollResult, this::handleLongPollFailure);
89 public void initialize() {
90 logger.debug("Initialize {} Version {}", FrameworkUtil.getBundle(getClass()).getSymbolicName(),
91 FrameworkUtil.getBundle(getClass()).getVersion());
94 BoschSHCBridgeConfiguration config = getConfigAs(BoschSHCBridgeConfiguration.class);
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");
103 String password = config.password.trim();
104 if (password.isEmpty()) {
105 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
106 "@text/offline.conf-error-empty-password");
110 SslContextFactory factory;
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");
120 // Instantiate HttpClient with the SslContextFactory
121 BoschHttpClient httpClient = this.httpClient = new BoschHttpClient(ipAddress, password, factory);
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()));
132 // general checks are OK, therefore set the status to unknown and wait for initial access
133 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE);
135 // Initialize bridge in the background.
136 // Start initial access the first time
137 scheduleInitialAccess(httpClient);
141 public void dispose() {
142 // Cancel scheduled pairing.
144 ScheduledFuture<?> scheduledPairing = this.scheduledPairing;
145 if (scheduledPairing != null) {
146 scheduledPairing.cancel(true);
147 this.scheduledPairing = null;
150 // Stop long polling.
151 this.longPolling.stop();
154 BoschHttpClient httpClient = this.httpClient;
155 if (httpClient != null) {
158 } catch (Exception e) {
159 logger.debug("HttpClient failed on bridge disposal: {}", e.getMessage());
161 this.httpClient = null;
168 public void handleCommand(ChannelUID channelUID, Command command) {
172 * Schedule the initial access.
173 * Use a delay if pairing fails and next retry is scheduled.
175 private void scheduleInitialAccess(BoschHttpClient httpClient) {
176 this.scheduledPairing = scheduler.schedule(() -> initialAccess(httpClient), 15, TimeUnit.SECONDS);
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.
186 private void initialAccess(BoschHttpClient httpClient) {
187 logger.debug("Initializing Bosch SHC Bridge: {} - HTTP client is: {}", this, httpClient);
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);
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");
210 // restart initial access - needed also in case of successful pairing to check access again
211 scheduleInitialAccess(httpClient);
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);
228 // start long polling loop
229 this.updateStatus(ThingStatus.ONLINE);
231 this.longPolling.start(httpClient);
232 } catch (LongPollingFailedException e) {
233 this.handleLongPollFailure(e);
236 } catch (InterruptedException e) {
237 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE, "@text/offline.interrupted");
238 Thread.currentThread().interrupt();
243 * Get a list of connected devices from the Smart-Home Controller
245 * @throws InterruptedException in case bridge is stopped
247 private boolean getDevices() throws InterruptedException {
249 BoschHttpClient httpClient = this.httpClient;
250 if (httpClient == null) {
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();
259 // check HTTP status code
260 if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
261 logger.debug("Request devices failed with status code: {}", contentResponse.getStatus());
265 String content = contentResponse.getContentAsString();
266 logger.debug("Request devices completed with success: {} - status code: {}", content,
267 contentResponse.getStatus());
269 Type collectionType = new TypeToken<ArrayList<Device>>() {
271 ArrayList<Device> devices = gson.fromJson(content, collectionType);
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);
284 } catch (TimeoutException | ExecutionException e) {
285 logger.warn("Request devices failed because of {}!", e.getMessage());
293 * Bridge callback handler for the results of long polls.
295 * It will check the result and
296 * forward the received to the bosch thing handlers.
298 * @param result Results from Long Polling
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);
305 boolean handled = false;
307 Bridge bridge = this.getThing();
308 for (Thing childThing : bridge.getThings()) {
309 // All children of this should implement BoschSHCHandler
311 ThingHandler baseHandler = childThing.getHandler();
312 if (baseHandler != null && baseHandler instanceof BoschSHCHandler) {
313 BoschSHCHandler handler = (BoschSHCHandler) baseHandler;
315 String deviceId = handler.getBoschID();
318 logger.debug("Registered device: {} - looking for {}", deviceId, update.deviceId);
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);
325 logger.warn("longPoll: child handler for {} does not implement Bosch SHC handler", baseHandler);
330 logger.debug("Could not find a thing for device ID: {}", update.deviceId);
337 * Bridge callback handler for the failures during long polls.
339 * It will update the bridge status and try to access the SHC again.
341 * @param e error during long polling
343 private void handleLongPollFailure(Throwable e) {
344 logger.warn("Long polling failed, will try to reconnect", e);
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");
353 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE,
354 "@text/offline.long-polling-failed.trying-to-reconnect");
355 scheduleInitialAccess(httpClient);
359 * Get a list of rooms from the Smart-Home controller
361 * @throws InterruptedException in case bridge is stopped
363 private boolean getRooms() throws InterruptedException {
365 BoschHttpClient httpClient = this.httpClient;
366 if (httpClient != null) {
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();
372 // check HTTP status code
373 if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
374 logger.debug("Request rooms failed with status code: {}", contentResponse.getStatus());
378 String content = contentResponse.getContentAsString();
379 logger.debug("Request rooms completed with success: {} - status code: {}", content,
380 contentResponse.getStatus());
382 Type collectionType = new TypeToken<ArrayList<Room>>() {
385 ArrayList<Room> rooms = gson.fromJson(content, collectionType);
388 for (Room r : rooms) {
389 logger.info("Found room: {}", r.name);
394 } catch (TimeoutException | ExecutionException e) {
395 logger.warn("Request rooms failed because of {}!", e.getMessage());
404 * Query the Bosch Smart Home Controller for the state of the given thing.
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
414 public <T extends BoschSHCServiceState> @Nullable T getState(String deviceId, String stateName, Class<T> stateClass)
415 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
417 BoschHttpClient httpClient = this.httpClient;
418 if (httpClient == null) {
419 logger.warn("HttpClient not initialized");
423 String url = httpClient.getServiceUrl(stateName, deviceId);
424 Request request = httpClient.createRequest(url, GET).header("Accept", "application/json");
426 logger.debug("refreshState: Requesting \"{}\" from Bosch: {} via {}", stateName, deviceId, url);
428 ContentResponse contentResponse = request.send();
430 String content = contentResponse.getContentAsString();
431 logger.debug("refreshState: Request complete: [{}] - return code: {}", content, contentResponse.getStatus());
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));
441 throw new BoschSHCException(
442 String.format("State request for service %s of device %s failed with status code %d", stateName,
443 deviceId, statusCode));
448 T state = gson.fromJson(content, stateClass);
450 throw new BoschSHCException(String.format("Received invalid, expected type %s", stateClass.getName()));
456 * Sends a state change for a device to the controller
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
462 * @return Response of request
463 * @throws InterruptedException
464 * @throws ExecutionException
465 * @throws TimeoutException
467 public <T extends BoschSHCServiceState> @Nullable Response putState(String deviceId, String serviceName, T state)
468 throws InterruptedException, TimeoutException, ExecutionException {
470 BoschHttpClient httpClient = this.httpClient;
471 if (httpClient == null) {
472 logger.warn("HttpClient not initialized");
477 String url = httpClient.getServiceUrl(serviceName, deviceId);
478 Request request = httpClient.createRequest(url, PUT, state);
481 return request.send();