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.util.ssl.SslContextFactory;
31 import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler;
32 import org.openhab.binding.boschshc.internal.devices.bridge.dto.*;
33 import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
34 import org.openhab.binding.boschshc.internal.exceptions.LongPollingFailedException;
35 import org.openhab.binding.boschshc.internal.exceptions.PairingFailedException;
36 import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
37 import org.openhab.binding.boschshc.internal.services.dto.JsonRestExceptionResponse;
38 import org.openhab.core.thing.Bridge;
39 import org.openhab.core.thing.ChannelUID;
40 import org.openhab.core.thing.Thing;
41 import org.openhab.core.thing.ThingStatus;
42 import org.openhab.core.thing.ThingStatusDetail;
43 import org.openhab.core.thing.binding.BaseBridgeHandler;
44 import org.openhab.core.thing.binding.ThingHandler;
45 import org.openhab.core.types.Command;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
49 import com.google.gson.Gson;
50 import com.google.gson.reflect.TypeToken;
53 * Representation of a connection with a Bosch Smart Home Controller bridge.
55 * @author Stefan Kästle - Initial contribution
56 * @author Gerd Zanker - added HttpClient with pairing support
57 * @author Christian Oeing - refactorings of e.g. server registration
60 public class BoschSHCBridgeHandler extends BaseBridgeHandler {
62 private final Logger logger = LoggerFactory.getLogger(BoschSHCBridgeHandler.class);
65 * gson instance to convert a class to json string and back.
67 private final Gson gson = new Gson();
70 * Handler to do long polling.
72 private final LongPolling longPolling;
74 private @Nullable BoschHttpClient httpClient;
76 private @Nullable ScheduledFuture<?> scheduledPairing;
78 public BoschSHCBridgeHandler(Bridge bridge) {
81 this.longPolling = new LongPolling(this.scheduler, this::handleLongPollResult, this::handleLongPollFailure);
85 public void initialize() {
87 BoschSHCBridgeConfiguration config = getConfigAs(BoschSHCBridgeConfiguration.class);
89 if (config.ipAddress.isEmpty()) {
90 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No IP address set");
94 if (config.password.isEmpty()) {
95 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No system password set");
99 SslContextFactory factory;
101 // prepare SSL key and certificates
102 factory = new BoschSslUtil(config.ipAddress).getSslContextFactory();
103 } catch (PairingFailedException e) {
104 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
105 "@text/offline.conf-error-ssl");
109 // Instantiate HttpClient with the SslContextFactory
110 BoschHttpClient httpClient = this.httpClient = new BoschHttpClient(config.ipAddress, config.password, factory);
115 } catch (Exception e) {
116 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
117 String.format("Could not create http connection to controller: %s", e.getMessage()));
121 // Initialize bridge in the background.
122 // Start initial access the first time
123 scheduleInitialAccess(httpClient);
127 public void dispose() {
128 // Cancel scheduled pairing.
129 ScheduledFuture<?> scheduledPairing = this.scheduledPairing;
130 if (scheduledPairing != null) {
131 scheduledPairing.cancel(true);
132 this.scheduledPairing = null;
135 // Stop long polling.
136 this.longPolling.stop();
138 BoschHttpClient httpClient = this.httpClient;
139 if (httpClient != null) {
142 } catch (Exception e) {
143 logger.debug("HttpClient failed on bridge disposal: {}", e.getMessage());
145 this.httpClient = null;
152 public void handleCommand(ChannelUID channelUID, Command command) {
156 * Schedule the initial access.
157 * Use a delay if pairing fails and next retry is scheduled.
159 private void scheduleInitialAccess(BoschHttpClient httpClient) {
160 this.scheduledPairing = scheduler.schedule(() -> initialAccess(httpClient), 15, TimeUnit.SECONDS);
164 * Execute the initial access.
165 * Uses the HTTP Bosch SHC client
166 * to check if access if possible
167 * pairs this Bosch SHC Bridge with the SHC if necessary
168 * and starts the first log poll.
170 private void initialAccess(BoschHttpClient httpClient) {
171 logger.debug("Initializing Bosch SHC Bridge: {} - HTTP client is: {} - version: 2020-04-05", this, httpClient);
174 // check access and pair if necessary
175 if (!httpClient.isAccessPossible()) {
176 // update status already if access is not possible
177 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE,
178 "@text/offline.conf-error-pairing");
179 if (!httpClient.doPairing()) {
180 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
181 "@text/offline.conf-error-pairing");
183 // restart initial access - needed also in case of successful pairing to check access again
184 scheduleInitialAccess(httpClient);
186 // print rooms and devices if things are reachable
187 boolean thingReachable = true;
188 thingReachable &= this.getRooms();
189 thingReachable &= this.getDevices();
191 if (thingReachable) {
192 this.updateStatus(ThingStatus.ONLINE);
194 // Start long polling
196 this.longPolling.start(httpClient);
197 } catch (LongPollingFailedException e) {
198 this.handleLongPollFailure(e);
201 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
202 "@text/offline.not-reachable");
203 // restart initial access
204 scheduleInitialAccess(httpClient);
207 } catch (InterruptedException e) {
208 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE,
209 String.format("Pairing was interrupted: %s", e.getMessage()));
214 * Get a list of connected devices from the Smart-Home Controller
216 * @throws InterruptedException
218 private boolean getDevices() throws InterruptedException {
219 BoschHttpClient httpClient = this.httpClient;
220 if (httpClient == null) {
225 logger.debug("Sending http request to Bosch to request clients: {}", httpClient);
226 String url = httpClient.getBoschSmartHomeUrl("devices");
227 ContentResponse contentResponse = httpClient.createRequest(url, GET).send();
229 String content = contentResponse.getContentAsString();
230 logger.debug("Response complete: {} - return code: {}", content, contentResponse.getStatus());
232 Type collectionType = new TypeToken<ArrayList<Device>>() {
234 ArrayList<Device> devices = gson.fromJson(content, collectionType);
236 if (devices != null) {
237 for (Device d : devices) {
238 // Write found devices into openhab.log until we have implemented auto discovery
239 logger.info("Found device: name={} id={}", d.name, d.id);
240 if (d.deviceSerivceIDs != null) {
241 for (String s : d.deviceSerivceIDs) {
242 logger.info(".... service: {}", s);
247 } catch (TimeoutException | ExecutionException e) {
248 logger.debug("HTTP request failed with exception {}", e.getMessage());
255 private void handleLongPollResult(LongPollResult result) {
256 for (DeviceStatusUpdate update : result.result) {
257 if (update != null && update.state != null) {
258 logger.debug("Got update for {}", update.deviceId);
260 boolean handled = false;
262 Bridge bridge = this.getThing();
263 for (Thing childThing : bridge.getThings()) {
264 // All children of this should implement BoschSHCHandler
265 ThingHandler baseHandler = childThing.getHandler();
266 if (baseHandler != null && baseHandler instanceof BoschSHCHandler) {
267 BoschSHCHandler handler = (BoschSHCHandler) baseHandler;
268 String deviceId = handler.getBoschID();
271 logger.debug("Registered device: {} - looking for {}", deviceId, update.deviceId);
273 if (deviceId != null && update.deviceId.equals(deviceId)) {
274 logger.debug("Found child: {} - calling processUpdate with {}", handler, update.state);
275 handler.processUpdate(update.id, update.state);
278 logger.warn("longPoll: child handler for {} does not implement Bosch SHC handler", baseHandler);
283 logger.debug("Could not find a thing for device ID: {}", update.deviceId);
289 private void handleLongPollFailure(Throwable e) {
290 logger.warn("Long polling failed", e);
291 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, "Long polling failed");
295 * Get a list of rooms from the Smart-Home controller
297 * @throws InterruptedException
299 private boolean getRooms() throws InterruptedException {
300 BoschHttpClient httpClient = this.httpClient;
301 if (httpClient != null) {
303 logger.debug("Sending http request to Bosch to request rooms");
304 String url = httpClient.getBoschSmartHomeUrl("rooms");
305 ContentResponse contentResponse = httpClient.createRequest(url, GET).send();
307 String content = contentResponse.getContentAsString();
308 logger.debug("Response complete: {} - return code: {}", content, contentResponse.getStatus());
310 Type collectionType = new TypeToken<ArrayList<Room>>() {
313 ArrayList<Room> rooms = gson.fromJson(content, collectionType);
316 for (Room r : rooms) {
317 logger.info("Found room: {}", r.name);
322 } catch (TimeoutException | ExecutionException e) {
323 logger.warn("HTTP request failed: {}", e.getMessage());
332 * Query the Bosch Smart Home Controller for the state of the given thing.
334 * @param deviceId Id of device to get state for
335 * @param stateName Name of the state to query
336 * @param stateClass Class to convert the resulting JSON to
337 * @throws ExecutionException
338 * @throws TimeoutException
339 * @throws InterruptedException
340 * @throws BoschSHCException
342 public <T extends BoschSHCServiceState> @Nullable T getState(String deviceId, String stateName, Class<T> stateClass)
343 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
344 BoschHttpClient httpClient = this.httpClient;
345 if (httpClient == null) {
346 logger.warn("HttpClient not initialized");
350 String url = httpClient.getServiceUrl(stateName, deviceId);
351 Request request = httpClient.createRequest(url, GET).header("Accept", "application/json");
353 logger.debug("refreshState: Requesting \"{}\" from Bosch: {} via {}", stateName, deviceId, url);
355 ContentResponse contentResponse = request.send();
357 String content = contentResponse.getContentAsString();
358 logger.debug("refreshState: Request complete: [{}] - return code: {}", content, contentResponse.getStatus());
360 int statusCode = contentResponse.getStatus();
361 if (statusCode != 200) {
362 JsonRestExceptionResponse errorResponse = gson.fromJson(content, JsonRestExceptionResponse.class);
363 if (errorResponse != null) {
364 throw new BoschSHCException(String.format(
365 "State request for service %s of device %s failed with status code %d and error code %s",
366 stateName, deviceId, errorResponse.statusCode, errorResponse.errorCode));
368 throw new BoschSHCException(
369 String.format("State request for service %s of device %s failed with status code %d", stateName,
370 deviceId, statusCode));
375 T state = gson.fromJson(content, stateClass);
377 throw new BoschSHCException(String.format("Received invalid, expected type %s", stateClass.getName()));
383 * Sends a state change for a device to the controller
385 * @param deviceId Id of device to change state for
386 * @param serviceName Name of service of device to change state for
387 * @param state New state data to set for service
389 * @return Response of request
390 * @throws InterruptedException
391 * @throws ExecutionException
392 * @throws TimeoutException
394 public <T extends BoschSHCServiceState> @Nullable Response putState(String deviceId, String serviceName, T state)
395 throws InterruptedException, TimeoutException, ExecutionException {
396 BoschHttpClient httpClient = this.httpClient;
397 if (httpClient == null) {
398 logger.warn("HttpClient not initialized");
403 String url = httpClient.getServiceUrl(serviceName, deviceId);
404 Request request = httpClient.createRequest(url, PUT, state);
407 Response response = request.send();