2 * Copyright (c) 2010-2022 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 BridgeHandler extends BaseBridgeHandler {
66 private final Logger logger = LoggerFactory.getLogger(BridgeHandler.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 BridgeHandler(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 BridgeConfiguration config = getConfigAs(BridgeConfiguration.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.deviceServiceIds != null) {
278 for (String s : d.deviceServiceIds) {
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 of type {}: {}", update.type, update.state);
305 var updateDeviceId = update.deviceId;
306 if (updateDeviceId == null) {
310 logger.debug("Got update for {}", updateDeviceId);
312 boolean handled = false;
314 Bridge bridge = this.getThing();
315 for (Thing childThing : bridge.getThings()) {
316 // All children of this should implement BoschSHCHandler
318 ThingHandler baseHandler = childThing.getHandler();
319 if (baseHandler != null && baseHandler instanceof BoschSHCHandler) {
320 BoschSHCHandler handler = (BoschSHCHandler) baseHandler;
322 String deviceId = handler.getBoschID();
325 logger.debug("Registered device: {} - looking for {}", deviceId, updateDeviceId);
327 if (deviceId != null && updateDeviceId.equals(deviceId)) {
328 logger.debug("Found child: {} - calling processUpdate (id: {}) with {}", handler, update.id,
330 handler.processUpdate(update.id, update.state);
333 logger.warn("longPoll: child handler for {} does not implement Bosch SHC handler", baseHandler);
338 logger.debug("Could not find a thing for device ID: {}", updateDeviceId);
345 * Bridge callback handler for the failures during long polls.
347 * It will update the bridge status and try to access the SHC again.
349 * @param e error during long polling
351 private void handleLongPollFailure(Throwable e) {
352 logger.warn("Long polling failed, will try to reconnect", e);
354 BoschHttpClient httpClient = this.httpClient;
355 if (httpClient == null) {
356 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
357 "@text/offline.long-polling-failed.http-client-null");
361 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE,
362 "@text/offline.long-polling-failed.trying-to-reconnect");
363 scheduleInitialAccess(httpClient);
367 * Get a list of rooms from the Smart-Home controller
369 * @throws InterruptedException in case bridge is stopped
371 private boolean getRooms() throws InterruptedException {
373 BoschHttpClient httpClient = this.httpClient;
374 if (httpClient != null) {
376 logger.debug("Sending http request to Bosch to request rooms");
377 String url = httpClient.getBoschSmartHomeUrl("rooms");
378 ContentResponse contentResponse = httpClient.createRequest(url, GET).send();
380 // check HTTP status code
381 if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
382 logger.debug("Request rooms failed with status code: {}", contentResponse.getStatus());
386 String content = contentResponse.getContentAsString();
387 logger.debug("Request rooms completed with success: {} - status code: {}", content,
388 contentResponse.getStatus());
390 Type collectionType = new TypeToken<ArrayList<Room>>() {
393 ArrayList<Room> rooms = gson.fromJson(content, collectionType);
396 for (Room r : rooms) {
397 logger.info("Found room: {}", r.name);
402 } catch (TimeoutException | ExecutionException e) {
403 logger.warn("Request rooms failed because of {}!", e.getMessage());
411 public Device getDeviceInfo(String deviceId)
412 throws BoschSHCException, InterruptedException, TimeoutException, ExecutionException {
414 BoschHttpClient httpClient = this.httpClient;
415 if (httpClient == null) {
416 throw new BoschSHCException("HTTP client not initialized");
419 String url = httpClient.getBoschSmartHomeUrl(String.format("devices/%s", deviceId));
420 Request request = httpClient.createRequest(url, GET);
422 return httpClient.sendRequest(request, Device.class, Device::isValid, (Integer statusCode, String content) -> {
423 JsonRestExceptionResponse errorResponse = gson.fromJson(content, JsonRestExceptionResponse.class);
424 if (errorResponse != null && JsonRestExceptionResponse.isValid(errorResponse)) {
425 if (errorResponse.errorCode.equals(JsonRestExceptionResponse.ENTITY_NOT_FOUND)) {
426 return new BoschSHCException("@text/offline.conf-error.invalid-device-id");
428 return new BoschSHCException(
429 String.format("Request for info of device %s failed with status code %d and error code %s",
430 deviceId, errorResponse.statusCode, errorResponse.errorCode));
433 return new BoschSHCException(String.format("Request for info for device %s failed with status code %d",
434 deviceId, statusCode));
440 * Query the Bosch Smart Home Controller for the state of the given thing.
442 * @param deviceId Id of device to get state for
443 * @param stateName Name of the state to query
444 * @param stateClass Class to convert the resulting JSON to
445 * @throws ExecutionException
446 * @throws TimeoutException
447 * @throws InterruptedException
448 * @throws BoschSHCException
450 public <T extends BoschSHCServiceState> @Nullable T getState(String deviceId, String stateName, Class<T> stateClass)
451 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
453 BoschHttpClient httpClient = this.httpClient;
454 if (httpClient == null) {
455 logger.warn("HttpClient not initialized");
459 String url = httpClient.getServiceUrl(stateName, deviceId);
460 Request request = httpClient.createRequest(url, GET).header("Accept", "application/json");
462 logger.debug("refreshState: Requesting \"{}\" from Bosch: {} via {}", stateName, deviceId, url);
464 ContentResponse contentResponse = request.send();
466 String content = contentResponse.getContentAsString();
467 logger.debug("refreshState: Request complete: [{}] - return code: {}", content, contentResponse.getStatus());
469 int statusCode = contentResponse.getStatus();
470 if (statusCode != 200) {
471 JsonRestExceptionResponse errorResponse = gson.fromJson(content, JsonRestExceptionResponse.class);
472 if (errorResponse != null) {
473 throw new BoschSHCException(String.format(
474 "State request for service %s of device %s failed with status code %d and error code %s",
475 stateName, deviceId, errorResponse.statusCode, errorResponse.errorCode));
477 throw new BoschSHCException(
478 String.format("State request for service %s of device %s failed with status code %d", stateName,
479 deviceId, statusCode));
484 T state = BoschSHCServiceState.fromJson(content, stateClass);
486 throw new BoschSHCException(String.format("Received invalid, expected type %s", stateClass.getName()));
492 * Sends a state change for a device to the controller
494 * @param deviceId Id of device to change state for
495 * @param serviceName Name of service of device to change state for
496 * @param state New state data to set for service
498 * @return Response of request
499 * @throws InterruptedException
500 * @throws ExecutionException
501 * @throws TimeoutException
503 public <T extends BoschSHCServiceState> @Nullable Response putState(String deviceId, String serviceName, T state)
504 throws InterruptedException, TimeoutException, ExecutionException {
506 BoschHttpClient httpClient = this.httpClient;
507 if (httpClient == null) {
508 logger.warn("HttpClient not initialized");
513 String url = httpClient.getServiceUrl(serviceName, deviceId);
514 Request request = httpClient.createRequest(url, PUT, state);
517 return request.send();