2 * Copyright (c) 2010-2023 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.Collection;
20 import java.util.Collections;
21 import java.util.List;
22 import java.util.Objects;
23 import java.util.Optional;
25 import java.util.concurrent.ExecutionException;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
28 import java.util.concurrent.TimeoutException;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.eclipse.jetty.client.api.ContentResponse;
33 import org.eclipse.jetty.client.api.Request;
34 import org.eclipse.jetty.client.api.Response;
35 import org.eclipse.jetty.http.HttpStatus;
36 import org.eclipse.jetty.util.ssl.SslContextFactory;
37 import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
38 import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler;
39 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
40 import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData;
41 import org.openhab.binding.boschshc.internal.devices.bridge.dto.LongPollResult;
42 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Room;
43 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Scenario;
44 import org.openhab.binding.boschshc.internal.devices.bridge.dto.UserDefinedState;
45 import org.openhab.binding.boschshc.internal.discovery.ThingDiscoveryService;
46 import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
47 import org.openhab.binding.boschshc.internal.exceptions.LongPollingFailedException;
48 import org.openhab.binding.boschshc.internal.exceptions.PairingFailedException;
49 import org.openhab.binding.boschshc.internal.serialization.GsonUtils;
50 import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
51 import org.openhab.binding.boschshc.internal.services.dto.JsonRestExceptionResponse;
52 import org.openhab.core.library.types.StringType;
53 import org.openhab.core.thing.Bridge;
54 import org.openhab.core.thing.Channel;
55 import org.openhab.core.thing.ChannelUID;
56 import org.openhab.core.thing.Thing;
57 import org.openhab.core.thing.ThingStatus;
58 import org.openhab.core.thing.ThingStatusDetail;
59 import org.openhab.core.thing.binding.BaseBridgeHandler;
60 import org.openhab.core.thing.binding.ThingHandler;
61 import org.openhab.core.thing.binding.ThingHandlerService;
62 import org.openhab.core.types.Command;
63 import org.openhab.core.types.RefreshType;
64 import org.osgi.framework.Bundle;
65 import org.osgi.framework.FrameworkUtil;
66 import org.slf4j.Logger;
67 import org.slf4j.LoggerFactory;
69 import com.google.gson.JsonElement;
70 import com.google.gson.reflect.TypeToken;
73 * Representation of a connection with a Bosch Smart Home Controller bridge.
75 * @author Stefan Kästle - Initial contribution
76 * @author Gerd Zanker - added HttpClient with pairing support
77 * @author Christian Oeing - refactorings of e.g. server registration
78 * @author David Pace - Added support for custom endpoints and HTTP POST requests
79 * @author Gerd Zanker - added thing discovery
82 public class BridgeHandler extends BaseBridgeHandler {
84 private static final String HTTP_CLIENT_NOT_INITIALIZED = "HttpClient not initialized";
86 private final Logger logger = LoggerFactory.getLogger(BridgeHandler.class);
89 * Handler to do long polling.
91 private final LongPolling longPolling;
94 * HTTP client for all communications to and from the bridge.
96 * This member is package-protected to enable mocking in unit tests.
98 /* package */ @Nullable
99 BoschHttpClient httpClient;
101 private @Nullable ScheduledFuture<?> scheduledPairing;
104 * SHC thing/device discovery service instance.
105 * Registered and unregistered if service is actived/deactived.
106 * Used to scan for things after bridge is paired with SHC.
108 private @Nullable ThingDiscoveryService thingDiscoveryService;
110 private final ScenarioHandler scenarioHandler;
112 public BridgeHandler(Bridge bridge) {
114 scenarioHandler = new ScenarioHandler();
116 this.longPolling = new LongPolling(this.scheduler, this::handleLongPollResult, this::handleLongPollFailure);
120 public Collection<Class<? extends ThingHandlerService>> getServices() {
121 return Set.of(ThingDiscoveryService.class);
125 public void initialize() {
126 Bundle bundle = FrameworkUtil.getBundle(getClass());
127 if (bundle != null) {
128 logger.debug("Initialize {} Version {}", bundle.getSymbolicName(), bundle.getVersion());
131 // Read configuration
132 BridgeConfiguration config = getConfigAs(BridgeConfiguration.class);
134 String ipAddress = config.ipAddress.trim();
135 if (ipAddress.isEmpty()) {
136 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
137 "@text/offline.conf-error-empty-ip");
141 String password = config.password.trim();
142 if (password.isEmpty()) {
143 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
144 "@text/offline.conf-error-empty-password");
148 SslContextFactory factory;
150 // prepare SSL key and certificates
151 factory = new BoschSslUtil(ipAddress).getSslContextFactory();
152 } catch (PairingFailedException e) {
153 logger.debug("Error while obtaining SSL context factory.", e);
154 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
155 "@text/offline.conf-error-ssl");
159 // Instantiate HttpClient with the SslContextFactory
160 BoschHttpClient localHttpClient = this.httpClient = new BoschHttpClient(ipAddress, password, factory);
164 localHttpClient.start();
165 } catch (Exception e) {
166 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
167 String.format("Could not create http connection to controller: %s", e.getMessage()));
171 // general checks are OK, therefore set the status to unknown and wait for initial access
172 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE);
174 // Initialize bridge in the background.
175 // Start initial access the first time
176 scheduleInitialAccess(localHttpClient);
180 public void dispose() {
181 // Cancel scheduled pairing.
183 ScheduledFuture<?> localScheduledPairing = this.scheduledPairing;
184 if (localScheduledPairing != null) {
185 localScheduledPairing.cancel(true);
186 this.scheduledPairing = null;
189 // Stop long polling.
190 this.longPolling.stop();
193 BoschHttpClient localHttpClient = this.httpClient;
194 if (localHttpClient != null) {
196 localHttpClient.stop();
197 } catch (Exception e) {
198 logger.debug("HttpClient failed on bridge disposal: {}", e.getMessage(), e);
200 this.httpClient = null;
207 public void handleCommand(ChannelUID channelUID, Command command) {
208 // commands are handled by individual device handlers
209 BoschHttpClient localHttpClient = httpClient;
210 if (BoschSHCBindingConstants.CHANNEL_TRIGGER_SCENARIO.equals(channelUID.getId())
211 && !RefreshType.REFRESH.equals(command) && localHttpClient != null) {
212 scenarioHandler.triggerScenario(localHttpClient, command.toString());
217 * Schedule the initial access.
218 * Use a delay if pairing fails and next retry is scheduled.
220 private void scheduleInitialAccess(BoschHttpClient httpClient) {
221 this.scheduledPairing = scheduler.schedule(() -> initialAccess(httpClient), 15, TimeUnit.SECONDS);
225 * Execute the initial access.
226 * Uses the HTTP Bosch SHC client
227 * to check if access if possible
228 * pairs this Bosch SHC Bridge with the SHC if necessary
229 * and starts the first log poll.
231 * This method is package-protected to enable unit testing.
233 /* package */ void initialAccess(BoschHttpClient httpClient) {
234 logger.debug("Initializing Bosch SHC Bridge: {} - HTTP client is: {}", this, httpClient);
237 // check if SCH is offline
238 if (!httpClient.isOnline()) {
239 // update status already if access is not possible
240 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE,
241 "@text/offline.conf-error-offline");
242 // restart later initial access
243 scheduleInitialAccess(httpClient);
248 // check if SHC access is not possible and pairing necessary
249 if (!httpClient.isAccessPossible()) {
250 // update status description to show pairing test
251 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE,
252 "@text/offline.conf-error-pairing");
253 if (!httpClient.doPairing()) {
254 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
255 "@text/offline.conf-error-pairing");
257 // restart initial access - needed also in case of successful pairing to check access again
258 scheduleInitialAccess(httpClient);
262 // SHC is online and access should possible
263 if (!checkBridgeAccess()) {
264 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
265 "@text/offline.not-reachable");
266 // restart initial access
267 scheduleInitialAccess(httpClient);
271 // do thing discovery after pairing
272 final ThingDiscoveryService discovery = thingDiscoveryService;
273 if (discovery != null) {
277 // start long polling loop
278 this.updateStatus(ThingStatus.ONLINE);
279 startLongPolling(httpClient);
281 } catch (InterruptedException e) {
282 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE, "@text/offline.interrupted");
283 Thread.currentThread().interrupt();
287 private void startLongPolling(BoschHttpClient httpClient) {
289 this.longPolling.start(httpClient);
290 } catch (LongPollingFailedException e) {
291 this.handleLongPollFailure(e);
296 * Check the bridge access by sending an HTTP request.
297 * Does not throw any exception in case the request fails.
299 public boolean checkBridgeAccess() throws InterruptedException {
301 BoschHttpClient localHttpClient = this.httpClient;
303 if (localHttpClient == null) {
308 logger.debug("Sending http request to BoschSHC to check access: {}", localHttpClient);
309 String url = localHttpClient.getBoschSmartHomeUrl("devices");
310 ContentResponse contentResponse = localHttpClient.createRequest(url, GET).send();
312 // check HTTP status code
313 if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
314 logger.debug("Access check failed with status code: {}", contentResponse.getStatus());
320 } catch (TimeoutException | ExecutionException e) {
321 logger.warn("Access check failed because of {}!", e.getMessage());
327 * Get a list of connected devices from the Smart-Home Controller
329 * @throws InterruptedException in case bridge is stopped
331 public List<Device> getDevices() throws InterruptedException {
333 BoschHttpClient localHttpClient = this.httpClient;
334 if (localHttpClient == null) {
335 return Collections.emptyList();
339 logger.trace("Sending http request to Bosch to request devices: {}", localHttpClient);
340 String url = localHttpClient.getBoschSmartHomeUrl("devices");
341 ContentResponse contentResponse = localHttpClient.createRequest(url, GET).send();
343 // check HTTP status code
344 if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
345 logger.debug("Request devices failed with status code: {}", contentResponse.getStatus());
346 return Collections.emptyList();
349 String content = contentResponse.getContentAsString();
350 logger.trace("Request devices completed with success: {} - status code: {}", content,
351 contentResponse.getStatus());
353 Type collectionType = new TypeToken<ArrayList<Device>>() {
355 List<Device> nullableDevices = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content, collectionType);
356 return Optional.ofNullable(nullableDevices).orElse(Collections.emptyList());
357 } catch (TimeoutException | ExecutionException e) {
358 logger.debug("Request devices failed because of {}!", e.getMessage(), e);
359 return Collections.emptyList();
363 public List<UserDefinedState> getUserStates() throws InterruptedException {
365 BoschHttpClient localHttpClient = this.httpClient;
366 if (localHttpClient == null) {
371 logger.trace("Sending http request to Bosch to request user-defined states: {}", localHttpClient);
372 String url = localHttpClient.getBoschSmartHomeUrl("userdefinedstates");
373 ContentResponse contentResponse = localHttpClient.createRequest(url, GET).send();
375 // check HTTP status code
376 if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
377 logger.debug("Request devices failed with status code: {}", contentResponse.getStatus());
381 String content = contentResponse.getContentAsString();
382 logger.trace("Request devices completed with success: {} - status code: {}", content,
383 contentResponse.getStatus());
385 Type collectionType = new TypeToken<ArrayList<UserDefinedState>>() {
387 List<UserDefinedState> nullableUserStates = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content,
389 return Optional.ofNullable(nullableUserStates).orElse(Collections.emptyList());
390 } catch (TimeoutException | ExecutionException e) {
391 logger.debug("Request user-defined states failed because of {}!", e.getMessage(), e);
397 * Get a list of rooms from the Smart-Home controller
399 * @throws InterruptedException in case bridge is stopped
401 public List<Room> getRooms() throws InterruptedException {
402 List<Room> emptyRooms = new ArrayList<>();
404 BoschHttpClient localHttpClient = this.httpClient;
405 if (localHttpClient != null) {
407 logger.trace("Sending http request to Bosch to request rooms");
408 String url = localHttpClient.getBoschSmartHomeUrl("rooms");
409 ContentResponse contentResponse = localHttpClient.createRequest(url, GET).send();
411 // check HTTP status code
412 if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
413 logger.debug("Request rooms failed with status code: {}", contentResponse.getStatus());
417 String content = contentResponse.getContentAsString();
418 logger.trace("Request rooms completed with success: {} - status code: {}", content,
419 contentResponse.getStatus());
421 Type collectionType = new TypeToken<ArrayList<Room>>() {
424 ArrayList<Room> rooms = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content, collectionType);
425 return Objects.requireNonNullElse(rooms, emptyRooms);
426 } catch (TimeoutException | ExecutionException e) {
427 logger.debug("Request rooms failed because of {}!", e.getMessage());
435 public boolean registerDiscoveryListener(ThingDiscoveryService listener) {
436 if (thingDiscoveryService == null) {
437 thingDiscoveryService = listener;
444 public boolean unregisterDiscoveryListener() {
445 if (thingDiscoveryService != null) {
446 thingDiscoveryService = null;
454 * Bridge callback handler for the results of long polls.
456 * It will check the results and
457 * forward the received states to the Bosch thing handlers.
459 * @param result Results from Long Polling
461 private void handleLongPollResult(LongPollResult result) {
462 for (BoschSHCServiceState serviceState : result.result) {
463 if (serviceState instanceof DeviceServiceData deviceServiceData) {
464 handleDeviceServiceData(deviceServiceData);
465 } else if (serviceState instanceof UserDefinedState userDefinedState) {
466 handleUserDefinedState(userDefinedState);
467 } else if (serviceState instanceof Scenario scenario) {
468 final Channel channel = this.getThing().getChannel(BoschSHCBindingConstants.CHANNEL_SCENARIO_TRIGGERED);
469 if (channel != null && isLinked(channel.getUID())) {
470 updateState(channel.getUID(), new StringType(scenario.name));
477 * Processes a single long poll result.
479 * @param deviceServiceData object representing a single long poll result
481 private void handleDeviceServiceData(@Nullable DeviceServiceData deviceServiceData) {
482 if (deviceServiceData != null) {
483 JsonElement state = obtainState(deviceServiceData);
485 logger.debug("Got update for service {} of type {}: {}", deviceServiceData.id, deviceServiceData.type,
488 var updateDeviceId = deviceServiceData.deviceId;
489 if (updateDeviceId == null || state == null) {
493 logger.debug("Got update for device {}", updateDeviceId);
495 forwardStateToHandlers(deviceServiceData, state, updateDeviceId);
499 private void handleUserDefinedState(@Nullable UserDefinedState userDefinedState) {
500 if (userDefinedState != null) {
501 JsonElement state = GsonUtils.DEFAULT_GSON_INSTANCE.toJsonTree(userDefinedState.isState());
503 logger.debug("Got update for user-defined state {} with id {}: {}", userDefinedState.getName(),
504 userDefinedState.getId(), state);
506 var stateId = userDefinedState.getId();
507 if (stateId == null || state == null) {
511 logger.debug("Got update for user-defined state {}", userDefinedState);
513 forwardStateToHandlers(userDefinedState, state, stateId);
518 * Extracts the actual state object from the given {@link DeviceServiceData} instance.
520 * In some special cases like the <code>BatteryLevel</code> service the {@link DeviceServiceData} object itself
521 * contains the state.
522 * In all other cases, the state is contained in a sub-object named <code>state</code>.
524 * @param deviceServiceData the {@link DeviceServiceData} object from which the state should be obtained
525 * @return the state sub-object or the {@link DeviceServiceData} object itself
528 private JsonElement obtainState(DeviceServiceData deviceServiceData) {
529 // the battery level service receives no individual state object but rather requires the DeviceServiceData
531 if ("BatteryLevel".equals(deviceServiceData.id)) {
532 return GsonUtils.DEFAULT_GSON_INSTANCE.toJsonTree(deviceServiceData);
535 return deviceServiceData.state;
539 * Tries to find handlers for the device with the given ID and forwards the received state to the handlers.
541 * @param serviceData object representing updates received in long poll results
542 * @param state the received state object as JSON element
543 * @param updateDeviceId the ID of the device for which the state update was received
545 private void forwardStateToHandlers(BoschSHCServiceState serviceData, JsonElement state, String updateDeviceId) {
546 boolean handled = false;
547 final String serviceId;
548 if (serviceData instanceof UserDefinedState userState) {
549 serviceId = userState.getId();
551 serviceId = ((DeviceServiceData) serviceData).id;
554 Bridge bridge = this.getThing();
555 for (Thing childThing : bridge.getThings()) {
556 // All children of this should implement BoschSHCHandler
558 ThingHandler baseHandler = childThing.getHandler();
559 if (baseHandler instanceof BoschSHCHandler handler) {
561 String deviceId = handler.getBoschID();
564 logger.debug("Registered device: {} - looking for {}", deviceId, updateDeviceId);
566 if (deviceId != null && updateDeviceId.equals(deviceId)) {
567 logger.debug("Found child: {} - calling processUpdate (id: {}) with {}", handler, serviceId, state);
568 handler.processUpdate(serviceId, state);
571 logger.warn("longPoll: child handler for {} does not implement Bosch SHC handler", baseHandler);
576 logger.debug("Could not find a thing for device ID: {}", updateDeviceId);
581 * Bridge callback handler for the failures during long polls.
583 * It will update the bridge status and try to access the SHC again.
585 * @param e error during long polling
587 private void handleLongPollFailure(Throwable e) {
588 logger.warn("Long polling failed, will try to reconnect", e);
590 BoschHttpClient localHttpClient = this.httpClient;
591 if (localHttpClient == null) {
592 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
593 "@text/offline.long-polling-failed.http-client-null");
597 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE,
598 "@text/offline.long-polling-failed.trying-to-reconnect");
599 scheduleInitialAccess(localHttpClient);
602 public Device getDeviceInfo(String deviceId)
603 throws BoschSHCException, InterruptedException, TimeoutException, ExecutionException {
605 BoschHttpClient localHttpClient = this.httpClient;
606 if (localHttpClient == null) {
607 throw new BoschSHCException("HTTP client not initialized");
610 String url = localHttpClient.getBoschSmartHomeUrl(String.format("devices/%s", deviceId));
611 Request request = localHttpClient.createRequest(url, GET);
613 return localHttpClient.sendRequest(request, Device.class, Device::isValid,
614 (Integer statusCode, String content) -> {
615 JsonRestExceptionResponse errorResponse = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content,
616 JsonRestExceptionResponse.class);
617 if (errorResponse != null && JsonRestExceptionResponse.isValid(errorResponse)) {
618 if (errorResponse.errorCode.equals(JsonRestExceptionResponse.ENTITY_NOT_FOUND)) {
619 return new BoschSHCException("@text/offline.conf-error.invalid-device-id");
621 return new BoschSHCException(String.format(
622 "Request for info of device %s failed with status code %d and error code %s",
623 deviceId, errorResponse.statusCode, errorResponse.errorCode));
626 return new BoschSHCException(String.format(
627 "Request for info of device %s failed with status code %d", deviceId, statusCode));
632 public UserDefinedState getUserStateInfo(String stateId)
633 throws BoschSHCException, InterruptedException, TimeoutException, ExecutionException {
635 BoschHttpClient locaHttpClient = this.httpClient;
636 if (locaHttpClient == null) {
637 throw new BoschSHCException("HTTP client not initialized");
640 String url = locaHttpClient.getBoschSmartHomeUrl(String.format("userdefinedstates/%s", stateId));
641 Request request = locaHttpClient.createRequest(url, GET);
643 return locaHttpClient.sendRequest(request, UserDefinedState.class, UserDefinedState::isValid,
644 (Integer statusCode, String content) -> {
645 JsonRestExceptionResponse errorResponse = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content,
646 JsonRestExceptionResponse.class);
647 if (errorResponse != null && JsonRestExceptionResponse.isValid(errorResponse)) {
648 if (errorResponse.errorCode.equals(JsonRestExceptionResponse.ENTITY_NOT_FOUND)) {
649 return new BoschSHCException("@text/offline.conf-error.invalid-state-id");
651 return new BoschSHCException(String.format(
652 "Request for info of user-defines state %s failed with status code %d and error code %s",
653 stateId, errorResponse.statusCode, errorResponse.errorCode));
656 return new BoschSHCException(
657 String.format("Request for info of user-defined state %s failed with status code %d",
658 stateId, statusCode));
664 * Query the Bosch Smart Home Controller for the state of the given device.
666 * The URL used for retrieving the state has the following structure:
669 * https://{IP}:8444/smarthome/devices/{deviceId}/services/{serviceName}/state
672 * @param deviceId Id of device to get state for
673 * @param stateName Name of the state to query
674 * @param stateClass Class to convert the resulting JSON to
675 * @return the deserialized state object, may be <code>null</code>
676 * @throws ExecutionException
677 * @throws TimeoutException
678 * @throws InterruptedException
679 * @throws BoschSHCException
681 public <T extends BoschSHCServiceState> @Nullable T getState(String deviceId, String stateName, Class<T> stateClass)
682 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
684 BoschHttpClient localHttpClient = this.httpClient;
685 if (localHttpClient == null) {
686 logger.warn(HTTP_CLIENT_NOT_INITIALIZED);
690 String url = localHttpClient.getServiceStateUrl(stateName, deviceId, stateClass);
691 logger.debug("getState(): Requesting \"{}\" from Bosch: {} via {}", stateName, deviceId, url);
692 return getState(localHttpClient, url, stateClass);
696 * Queries the Bosch Smart Home Controller for the state using an explicit endpoint.
698 * @param <T> Type to which the resulting JSON should be deserialized to
699 * @param endpoint The destination endpoint part of the URL
700 * @param stateClass Class to convert the resulting JSON to
701 * @return the deserialized state object, may be <code>null</code>
702 * @throws InterruptedException
703 * @throws TimeoutException
704 * @throws ExecutionException
705 * @throws BoschSHCException
707 public <T extends BoschSHCServiceState> @Nullable T getState(String endpoint, Class<T> stateClass)
708 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
710 BoschHttpClient localHttpClient = this.httpClient;
711 if (localHttpClient == null) {
712 logger.warn(HTTP_CLIENT_NOT_INITIALIZED);
716 String url = localHttpClient.getBoschSmartHomeUrl(endpoint);
717 logger.debug("getState(): Requesting from Bosch: {}", url);
718 return getState(localHttpClient, url, stateClass);
722 * Sends a HTTP GET request in order to retrieve a state from the Bosch Smart Home Controller.
724 * @param <T> Type to which the resulting JSON should be deserialized to
725 * @param httpClient HTTP client used for sending the request
726 * @param url URL at which the state should be retrieved
727 * @param stateClass Class to convert the resulting JSON to
728 * @return the deserialized state object, may be <code>null</code>
729 * @throws InterruptedException
730 * @throws TimeoutException
731 * @throws ExecutionException
732 * @throws BoschSHCException
734 protected <T extends BoschSHCServiceState> @Nullable T getState(BoschHttpClient httpClient, String url,
735 Class<T> stateClass) throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
736 Request request = httpClient.createRequest(url, GET).header("Accept", "application/json");
738 ContentResponse contentResponse = request.send();
740 String content = contentResponse.getContentAsString();
741 logger.debug("getState(): Request complete: [{}] - return code: {}", content, contentResponse.getStatus());
743 int statusCode = contentResponse.getStatus();
744 if (statusCode != 200) {
745 JsonRestExceptionResponse errorResponse = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content,
746 JsonRestExceptionResponse.class);
747 if (errorResponse != null) {
748 throw new BoschSHCException(
749 String.format("State request with URL %s failed with status code %d and error code %s", url,
750 errorResponse.statusCode, errorResponse.errorCode));
752 throw new BoschSHCException(
753 String.format("State request with URL %s failed with status code %d", url, statusCode));
758 T state = BoschSHCServiceState.fromJson(content, stateClass);
760 throw new BoschSHCException(String.format("Received invalid, expected type %s", stateClass.getName()));
766 * Sends a state change for a device to the controller
768 * @param deviceId Id of device to change state for
769 * @param serviceName Name of service of device to change state for
770 * @param state New state data to set for service
772 * @return Response of request
773 * @throws InterruptedException
774 * @throws ExecutionException
775 * @throws TimeoutException
777 public <T extends BoschSHCServiceState> @Nullable Response putState(String deviceId, String serviceName, T state)
778 throws InterruptedException, TimeoutException, ExecutionException {
780 BoschHttpClient localHttpClient = this.httpClient;
781 if (localHttpClient == null) {
782 logger.warn(HTTP_CLIENT_NOT_INITIALIZED);
787 String url = localHttpClient.getServiceStateUrl(serviceName, deviceId, state.getClass());
788 Request request = localHttpClient.createRequest(url, PUT, state);
791 return request.send();
795 * Sends a HTTP POST request without a request body to the given endpoint.
797 * @param endpoint The destination endpoint part of the URL
798 * @return the HTTP response
799 * @throws InterruptedException
800 * @throws TimeoutException
801 * @throws ExecutionException
803 public @Nullable Response postAction(String endpoint)
804 throws InterruptedException, TimeoutException, ExecutionException {
805 return postAction(endpoint, null);
809 * Sends a HTTP POST request with a request body to the given endpoint.
811 * @param <T> Type of the request
812 * @param endpoint The destination endpoint part of the URL
813 * @param requestBody object representing the request body to be sent, may be <code>null</code>
814 * @return the HTTP response
815 * @throws InterruptedException
816 * @throws TimeoutException
817 * @throws ExecutionException
819 public <T extends BoschSHCServiceState> @Nullable Response postAction(String endpoint, @Nullable T requestBody)
820 throws InterruptedException, TimeoutException, ExecutionException {
822 BoschHttpClient localHttpClient = this.httpClient;
823 if (localHttpClient == null) {
824 logger.warn(HTTP_CLIENT_NOT_INITIALIZED);
828 String url = localHttpClient.getBoschSmartHomeUrl(endpoint);
829 Request request = localHttpClient.createRequest(url, POST, requestBody);
830 return request.send();
833 public @Nullable DeviceServiceData getServiceData(String deviceId, String serviceName)
834 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
836 BoschHttpClient localHttpClient = this.httpClient;
837 if (localHttpClient == null) {
838 logger.warn(HTTP_CLIENT_NOT_INITIALIZED);
842 String url = localHttpClient.getServiceUrl(serviceName, deviceId);
843 logger.debug("getState(): Requesting \"{}\" from Bosch: {} via {}", serviceName, deviceId, url);
844 return getState(localHttpClient, url, DeviceServiceData.class);