2 * Copyright (c) 2010-2024 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.PublicInformation;
43 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Room;
44 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Scenario;
45 import org.openhab.binding.boschshc.internal.devices.bridge.dto.UserDefinedState;
46 import org.openhab.binding.boschshc.internal.discovery.ThingDiscoveryService;
47 import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
48 import org.openhab.binding.boschshc.internal.exceptions.LongPollingFailedException;
49 import org.openhab.binding.boschshc.internal.exceptions.PairingFailedException;
50 import org.openhab.binding.boschshc.internal.serialization.GsonUtils;
51 import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
52 import org.openhab.binding.boschshc.internal.services.dto.JsonRestExceptionResponse;
53 import org.openhab.core.library.types.StringType;
54 import org.openhab.core.thing.Bridge;
55 import org.openhab.core.thing.Channel;
56 import org.openhab.core.thing.ChannelUID;
57 import org.openhab.core.thing.Thing;
58 import org.openhab.core.thing.ThingStatus;
59 import org.openhab.core.thing.ThingStatusDetail;
60 import org.openhab.core.thing.binding.BaseBridgeHandler;
61 import org.openhab.core.thing.binding.ThingHandler;
62 import org.openhab.core.thing.binding.ThingHandlerService;
63 import org.openhab.core.types.Command;
64 import org.openhab.core.types.RefreshType;
65 import org.osgi.framework.Bundle;
66 import org.osgi.framework.FrameworkUtil;
67 import org.slf4j.Logger;
68 import org.slf4j.LoggerFactory;
70 import com.google.gson.JsonElement;
71 import com.google.gson.reflect.TypeToken;
74 * Representation of a connection with a Bosch Smart Home Controller bridge.
76 * @author Stefan Kästle - Initial contribution
77 * @author Gerd Zanker - added HttpClient with pairing support
78 * @author Christian Oeing - refactorings of e.g. server registration
79 * @author David Pace - Added support for custom endpoints and HTTP POST requests
80 * @author Gerd Zanker - added thing discovery
83 public class BridgeHandler extends BaseBridgeHandler {
85 private static final String HTTP_CLIENT_NOT_INITIALIZED = "HttpClient not initialized";
87 private final Logger logger = LoggerFactory.getLogger(BridgeHandler.class);
90 * Handler to do long polling.
92 private final LongPolling longPolling;
95 * HTTP client for all communications to and from the bridge.
97 * This member is package-protected to enable mocking in unit tests.
99 /* package */ @Nullable
100 BoschHttpClient httpClient;
102 private @Nullable ScheduledFuture<?> scheduledPairing;
105 * SHC thing/device discovery service instance.
106 * Registered and unregistered if service is actived/deactived.
107 * Used to scan for things after bridge is paired with SHC.
109 private @Nullable ThingDiscoveryService thingDiscoveryService;
111 private final ScenarioHandler scenarioHandler;
113 public BridgeHandler(Bridge bridge) {
115 scenarioHandler = new ScenarioHandler();
117 this.longPolling = new LongPolling(this.scheduler, this::handleLongPollResult, this::handleLongPollFailure);
121 public Collection<Class<? extends ThingHandlerService>> getServices() {
122 return Set.of(ThingDiscoveryService.class);
126 public void initialize() {
127 Bundle bundle = FrameworkUtil.getBundle(getClass());
128 if (bundle != null) {
129 logger.debug("Initialize {} Version {}", bundle.getSymbolicName(), bundle.getVersion());
132 // Read configuration
133 BridgeConfiguration config = getConfigAs(BridgeConfiguration.class);
135 String ipAddress = config.ipAddress.trim();
136 if (ipAddress.isEmpty()) {
137 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
138 "@text/offline.conf-error-empty-ip");
142 String password = config.password.trim();
143 if (password.isEmpty()) {
144 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
145 "@text/offline.conf-error-empty-password");
149 SslContextFactory factory;
151 // prepare SSL key and certificates
152 factory = new BoschSslUtil(ipAddress).getSslContextFactory();
153 } catch (PairingFailedException e) {
154 logger.debug("Error while obtaining SSL context factory.", e);
155 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
156 "@text/offline.conf-error-ssl");
160 // Instantiate HttpClient with the SslContextFactory
161 BoschHttpClient localHttpClient = this.httpClient = new BoschHttpClient(ipAddress, password, factory);
165 localHttpClient.start();
166 } catch (Exception e) {
167 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
168 String.format("Could not create http connection to controller: %s", e.getMessage()));
172 // general checks are OK, therefore set the status to unknown and wait for initial access
173 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE);
175 // Initialize bridge in the background.
176 // Start initial access the first time
177 scheduleInitialAccess(localHttpClient);
181 public void dispose() {
182 // Cancel scheduled pairing.
184 ScheduledFuture<?> localScheduledPairing = this.scheduledPairing;
185 if (localScheduledPairing != null) {
186 localScheduledPairing.cancel(true);
187 this.scheduledPairing = null;
190 // Stop long polling.
191 this.longPolling.stop();
194 BoschHttpClient localHttpClient = this.httpClient;
195 if (localHttpClient != null) {
197 localHttpClient.stop();
198 } catch (Exception e) {
199 logger.debug("HttpClient failed on bridge disposal: {}", e.getMessage(), e);
201 this.httpClient = null;
208 public void handleCommand(ChannelUID channelUID, Command command) {
209 // commands are handled by individual device handlers
210 BoschHttpClient localHttpClient = httpClient;
211 if (BoschSHCBindingConstants.CHANNEL_TRIGGER_SCENARIO.equals(channelUID.getId())
212 && !RefreshType.REFRESH.equals(command) && localHttpClient != null) {
213 scenarioHandler.triggerScenario(localHttpClient, command.toString());
218 * Schedule the initial access.
219 * Use a delay if pairing fails and next retry is scheduled.
221 private void scheduleInitialAccess(BoschHttpClient httpClient) {
222 this.scheduledPairing = scheduler.schedule(() -> initialAccess(httpClient), 15, TimeUnit.SECONDS);
226 * Execute the initial access.
227 * Uses the HTTP Bosch SHC client
228 * to check if access if possible
229 * pairs this Bosch SHC Bridge with the SHC if necessary
230 * and starts the first log poll.
232 * This method is package-protected to enable unit testing.
234 /* package */ void initialAccess(BoschHttpClient httpClient) {
235 logger.debug("Initializing Bosch SHC Bridge: {} - HTTP client is: {}", this, httpClient);
238 // check if SCH is offline
239 if (!httpClient.isOnline()) {
240 // update status already if access is not possible
241 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE,
242 "@text/offline.conf-error-offline");
243 // restart later initial access
244 scheduleInitialAccess(httpClient);
249 // check if SHC access is not possible and pairing necessary
250 if (!httpClient.isAccessPossible()) {
251 // update status description to show pairing test
252 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE,
253 "@text/offline.conf-error-pairing");
254 if (!httpClient.doPairing()) {
255 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
256 "@text/offline.conf-error-pairing");
258 // restart initial access - needed also in case of successful pairing to check access again
259 scheduleInitialAccess(httpClient);
263 // SHC is online and access should possible
264 if (!checkBridgeAccess()) {
265 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
266 "@text/offline.not-reachable");
267 // restart initial access
268 scheduleInitialAccess(httpClient);
272 // do thing discovery after pairing
273 final ThingDiscoveryService discovery = thingDiscoveryService;
274 if (discovery != null) {
278 // start long polling loop
279 this.updateStatus(ThingStatus.ONLINE);
280 startLongPolling(httpClient);
282 } catch (InterruptedException e) {
283 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE, "@text/offline.interrupted");
284 Thread.currentThread().interrupt();
288 private void startLongPolling(BoschHttpClient httpClient) {
290 this.longPolling.start(httpClient);
291 } catch (LongPollingFailedException e) {
292 this.handleLongPollFailure(e);
297 * Check the bridge access by sending an HTTP request.
298 * Does not throw any exception in case the request fails.
300 public boolean checkBridgeAccess() throws InterruptedException {
302 BoschHttpClient localHttpClient = this.httpClient;
304 if (localHttpClient == null) {
309 logger.debug("Sending http request to BoschSHC to check access: {}", localHttpClient);
310 String url = localHttpClient.getBoschSmartHomeUrl("devices");
311 ContentResponse contentResponse = localHttpClient.createRequest(url, GET).send();
313 // check HTTP status code
314 if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
315 logger.debug("Access check failed with status code: {}", contentResponse.getStatus());
321 } catch (TimeoutException | ExecutionException e) {
322 logger.warn("Access check failed because of {}!", e.getMessage());
328 * Get a list of connected devices from the Smart-Home Controller
330 * @throws InterruptedException in case bridge is stopped
332 public List<Device> getDevices() throws InterruptedException {
334 BoschHttpClient localHttpClient = this.httpClient;
335 if (localHttpClient == null) {
336 return Collections.emptyList();
340 logger.trace("Sending http request to Bosch to request devices: {}", localHttpClient);
341 String url = localHttpClient.getBoschSmartHomeUrl("devices");
342 ContentResponse contentResponse = localHttpClient.createRequest(url, GET).send();
344 // check HTTP status code
345 if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
346 logger.debug("Request devices failed with status code: {}", contentResponse.getStatus());
347 return Collections.emptyList();
350 String content = contentResponse.getContentAsString();
351 logger.trace("Request devices completed with success: {} - status code: {}", content,
352 contentResponse.getStatus());
354 Type collectionType = new TypeToken<ArrayList<Device>>() {
356 List<Device> nullableDevices = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content, collectionType);
357 return Optional.ofNullable(nullableDevices).orElse(Collections.emptyList());
358 } catch (TimeoutException | ExecutionException e) {
359 logger.debug("Request devices failed because of {}!", e.getMessage(), e);
360 return Collections.emptyList();
364 public List<UserDefinedState> getUserStates() throws InterruptedException {
366 BoschHttpClient localHttpClient = this.httpClient;
367 if (localHttpClient == null) {
372 logger.trace("Sending http request to Bosch to request user-defined states: {}", localHttpClient);
373 String url = localHttpClient.getBoschSmartHomeUrl("userdefinedstates");
374 ContentResponse contentResponse = localHttpClient.createRequest(url, GET).send();
376 // check HTTP status code
377 if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
378 logger.debug("Request devices failed with status code: {}", contentResponse.getStatus());
382 String content = contentResponse.getContentAsString();
383 logger.trace("Request devices completed with success: {} - status code: {}", content,
384 contentResponse.getStatus());
386 Type collectionType = new TypeToken<ArrayList<UserDefinedState>>() {
388 List<UserDefinedState> nullableUserStates = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content,
390 return Optional.ofNullable(nullableUserStates).orElse(Collections.emptyList());
391 } catch (TimeoutException | ExecutionException e) {
392 logger.debug("Request user-defined states failed because of {}!", e.getMessage(), e);
398 * Get a list of rooms from the Smart-Home controller
400 * @throws InterruptedException in case bridge is stopped
402 public List<Room> getRooms() throws InterruptedException {
403 List<Room> emptyRooms = new ArrayList<>();
405 BoschHttpClient localHttpClient = this.httpClient;
406 if (localHttpClient != null) {
408 logger.trace("Sending http request to Bosch to request rooms");
409 String url = localHttpClient.getBoschSmartHomeUrl("rooms");
410 ContentResponse contentResponse = localHttpClient.createRequest(url, GET).send();
412 // check HTTP status code
413 if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
414 logger.debug("Request rooms failed with status code: {}", contentResponse.getStatus());
418 String content = contentResponse.getContentAsString();
419 logger.trace("Request rooms completed with success: {} - status code: {}", content,
420 contentResponse.getStatus());
422 Type collectionType = new TypeToken<ArrayList<Room>>() {
425 ArrayList<Room> rooms = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content, collectionType);
426 return Objects.requireNonNullElse(rooms, emptyRooms);
427 } catch (TimeoutException | ExecutionException e) {
428 logger.debug("Request rooms failed because of {}!", e.getMessage());
437 * Get public information from Bosch SHC.
439 public PublicInformation getPublicInformation()
440 throws InterruptedException, BoschSHCException, ExecutionException, TimeoutException {
442 BoschHttpClient localHttpClient = this.httpClient;
443 if (localHttpClient == null) {
444 throw new BoschSHCException(HTTP_CLIENT_NOT_INITIALIZED);
447 String url = localHttpClient.getPublicInformationUrl();
448 Request request = localHttpClient.createRequest(url, GET);
450 return localHttpClient.sendRequest(request, PublicInformation.class, PublicInformation::isValid, null);
453 public boolean registerDiscoveryListener(ThingDiscoveryService listener) {
454 if (thingDiscoveryService == null) {
455 thingDiscoveryService = listener;
462 public boolean unregisterDiscoveryListener() {
463 if (thingDiscoveryService != null) {
464 thingDiscoveryService = null;
472 * Bridge callback handler for the results of long polls.
474 * It will check the results and
475 * forward the received states to the Bosch thing handlers.
477 * @param result Results from Long Polling
479 private void handleLongPollResult(LongPollResult result) {
480 for (BoschSHCServiceState serviceState : result.result) {
481 if (serviceState instanceof DeviceServiceData deviceServiceData) {
482 handleDeviceServiceData(deviceServiceData);
483 } else if (serviceState instanceof UserDefinedState userDefinedState) {
484 handleUserDefinedState(userDefinedState);
485 } else if (serviceState instanceof Scenario scenario) {
486 final Channel channel = this.getThing().getChannel(BoschSHCBindingConstants.CHANNEL_SCENARIO_TRIGGERED);
487 if (channel != null && isLinked(channel.getUID())) {
488 updateState(channel.getUID(), new StringType(scenario.name));
495 * Processes a single long poll result.
497 * @param deviceServiceData object representing a single long poll result
499 private void handleDeviceServiceData(@Nullable DeviceServiceData deviceServiceData) {
500 if (deviceServiceData != null) {
501 JsonElement state = obtainState(deviceServiceData);
503 logger.debug("Got update for service {} of type {}: {}", deviceServiceData.id, deviceServiceData.type,
506 var updateDeviceId = deviceServiceData.deviceId;
507 if (updateDeviceId == null || state == null) {
511 logger.debug("Got update for device {}", updateDeviceId);
513 forwardStateToHandlers(deviceServiceData, state, updateDeviceId);
517 private void handleUserDefinedState(@Nullable UserDefinedState userDefinedState) {
518 if (userDefinedState != null) {
519 JsonElement state = GsonUtils.DEFAULT_GSON_INSTANCE.toJsonTree(userDefinedState.isState());
521 logger.debug("Got update for user-defined state {} with id {}: {}", userDefinedState.getName(),
522 userDefinedState.getId(), state);
524 var stateId = userDefinedState.getId();
525 if (stateId == null || state == null) {
529 logger.debug("Got update for user-defined state {}", userDefinedState);
531 forwardStateToHandlers(userDefinedState, state, stateId);
536 * Extracts the actual state object from the given {@link DeviceServiceData} instance.
538 * In some special cases like the <code>BatteryLevel</code> service the {@link DeviceServiceData} object itself
539 * contains the state.
540 * In all other cases, the state is contained in a sub-object named <code>state</code>.
542 * @param deviceServiceData the {@link DeviceServiceData} object from which the state should be obtained
543 * @return the state sub-object or the {@link DeviceServiceData} object itself
546 private JsonElement obtainState(DeviceServiceData deviceServiceData) {
547 // the battery level service receives no individual state object but rather requires the DeviceServiceData
549 if ("BatteryLevel".equals(deviceServiceData.id)) {
550 return GsonUtils.DEFAULT_GSON_INSTANCE.toJsonTree(deviceServiceData);
553 return deviceServiceData.state;
557 * Tries to find handlers for the device with the given ID and forwards the received state to the handlers.
559 * @param serviceData object representing updates received in long poll results
560 * @param state the received state object as JSON element
561 * @param updateDeviceId the ID of the device for which the state update was received
563 private void forwardStateToHandlers(BoschSHCServiceState serviceData, JsonElement state, String updateDeviceId) {
564 boolean handled = false;
565 final String serviceId;
566 if (serviceData instanceof UserDefinedState userState) {
567 serviceId = userState.getId();
569 serviceId = ((DeviceServiceData) serviceData).id;
572 Bridge bridge = this.getThing();
573 for (Thing childThing : bridge.getThings()) {
574 // All children of this should implement BoschSHCHandler
576 ThingHandler baseHandler = childThing.getHandler();
577 if (baseHandler instanceof BoschSHCHandler handler) {
579 String deviceId = handler.getBoschID();
582 logger.debug("Registered device: {} - looking for {}", deviceId, updateDeviceId);
584 if (deviceId != null && updateDeviceId.equals(deviceId)) {
585 logger.debug("Found child: {} - calling processUpdate (id: {}) with {}", handler, serviceId, state);
586 handler.processUpdate(serviceId, state);
589 logger.warn("longPoll: child handler for {} does not implement Bosch SHC handler", baseHandler);
594 logger.debug("Could not find a thing for device ID: {}", updateDeviceId);
599 * Bridge callback handler for the failures during long polls.
601 * It will update the bridge status and try to access the SHC again.
603 * @param e error during long polling
605 private void handleLongPollFailure(Throwable e) {
606 logger.warn("Long polling failed, will try to reconnect", e);
608 BoschHttpClient localHttpClient = this.httpClient;
609 if (localHttpClient == null) {
610 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
611 "@text/offline.long-polling-failed.http-client-null");
615 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE,
616 "@text/offline.long-polling-failed.trying-to-reconnect");
617 scheduleInitialAccess(localHttpClient);
620 public Device getDeviceInfo(String deviceId)
621 throws BoschSHCException, InterruptedException, TimeoutException, ExecutionException {
623 BoschHttpClient localHttpClient = this.httpClient;
624 if (localHttpClient == null) {
625 throw new BoschSHCException(HTTP_CLIENT_NOT_INITIALIZED);
628 String url = localHttpClient.getBoschSmartHomeUrl(String.format("devices/%s", deviceId));
629 Request request = localHttpClient.createRequest(url, GET);
631 return localHttpClient.sendRequest(request, Device.class, Device::isValid,
632 (Integer statusCode, String content) -> {
633 JsonRestExceptionResponse errorResponse = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content,
634 JsonRestExceptionResponse.class);
635 if (errorResponse != null && JsonRestExceptionResponse.isValid(errorResponse)) {
636 if (errorResponse.errorCode.equals(JsonRestExceptionResponse.ENTITY_NOT_FOUND)) {
637 return new BoschSHCException("@text/offline.conf-error.invalid-device-id");
639 return new BoschSHCException(String.format(
640 "Request for info of device %s failed with status code %d and error code %s",
641 deviceId, errorResponse.statusCode, errorResponse.errorCode));
644 return new BoschSHCException(String.format(
645 "Request for info of device %s failed with status code %d", deviceId, statusCode));
650 public UserDefinedState getUserStateInfo(String stateId)
651 throws BoschSHCException, InterruptedException, TimeoutException, ExecutionException {
653 BoschHttpClient locaHttpClient = this.httpClient;
654 if (locaHttpClient == null) {
655 throw new BoschSHCException(HTTP_CLIENT_NOT_INITIALIZED);
658 String url = locaHttpClient.getBoschSmartHomeUrl(String.format("userdefinedstates/%s", stateId));
659 Request request = locaHttpClient.createRequest(url, GET);
661 return locaHttpClient.sendRequest(request, UserDefinedState.class, UserDefinedState::isValid,
662 (Integer statusCode, String content) -> {
663 JsonRestExceptionResponse errorResponse = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content,
664 JsonRestExceptionResponse.class);
665 if (errorResponse != null && JsonRestExceptionResponse.isValid(errorResponse)) {
666 if (errorResponse.errorCode.equals(JsonRestExceptionResponse.ENTITY_NOT_FOUND)) {
667 return new BoschSHCException("@text/offline.conf-error.invalid-state-id");
669 return new BoschSHCException(String.format(
670 "Request for info of user-defines state %s failed with status code %d and error code %s",
671 stateId, errorResponse.statusCode, errorResponse.errorCode));
674 return new BoschSHCException(
675 String.format("Request for info of user-defined state %s failed with status code %d",
676 stateId, statusCode));
682 * Query the Bosch Smart Home Controller for the state of the given device.
684 * The URL used for retrieving the state has the following structure:
687 * https://{IP}:8444/smarthome/devices/{deviceId}/services/{serviceName}/state
690 * @param deviceId Id of device to get state for
691 * @param stateName Name of the state to query
692 * @param stateClass Class to convert the resulting JSON to
693 * @return the deserialized state object, may be <code>null</code>
694 * @throws ExecutionException
695 * @throws TimeoutException
696 * @throws InterruptedException
697 * @throws BoschSHCException
699 public <T extends BoschSHCServiceState> @Nullable T getState(String deviceId, String stateName, Class<T> stateClass)
700 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
702 BoschHttpClient localHttpClient = this.httpClient;
703 if (localHttpClient == null) {
704 logger.warn(HTTP_CLIENT_NOT_INITIALIZED);
708 String url = localHttpClient.getServiceStateUrl(stateName, deviceId, stateClass);
709 logger.debug("getState(): Requesting \"{}\" from Bosch: {} via {}", stateName, deviceId, url);
710 return getState(localHttpClient, url, stateClass);
714 * Queries the Bosch Smart Home Controller for the state using an explicit endpoint.
716 * @param <T> Type to which the resulting JSON should be deserialized to
717 * @param endpoint The destination endpoint part of the URL
718 * @param stateClass Class to convert the resulting JSON to
719 * @return the deserialized state object, may be <code>null</code>
720 * @throws InterruptedException
721 * @throws TimeoutException
722 * @throws ExecutionException
723 * @throws BoschSHCException
725 public <T extends BoschSHCServiceState> @Nullable T getState(String endpoint, Class<T> stateClass)
726 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
728 BoschHttpClient localHttpClient = this.httpClient;
729 if (localHttpClient == null) {
730 logger.warn(HTTP_CLIENT_NOT_INITIALIZED);
734 String url = localHttpClient.getBoschSmartHomeUrl(endpoint);
735 logger.debug("getState(): Requesting from Bosch: {}", url);
736 return getState(localHttpClient, url, stateClass);
740 * Sends a HTTP GET request in order to retrieve a state from the Bosch Smart Home Controller.
742 * @param <T> Type to which the resulting JSON should be deserialized to
743 * @param httpClient HTTP client used for sending the request
744 * @param url URL at which the state should be retrieved
745 * @param stateClass Class to convert the resulting JSON to
746 * @return the deserialized state object, may be <code>null</code>
747 * @throws InterruptedException
748 * @throws TimeoutException
749 * @throws ExecutionException
750 * @throws BoschSHCException
752 protected <T extends BoschSHCServiceState> @Nullable T getState(BoschHttpClient httpClient, String url,
753 Class<T> stateClass) throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
754 Request request = httpClient.createRequest(url, GET).header("Accept", "application/json");
756 ContentResponse contentResponse = request.send();
758 String content = contentResponse.getContentAsString();
759 logger.debug("getState(): Request complete: [{}] - return code: {}", content, contentResponse.getStatus());
761 int statusCode = contentResponse.getStatus();
762 if (statusCode != 200) {
763 JsonRestExceptionResponse errorResponse = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content,
764 JsonRestExceptionResponse.class);
765 if (errorResponse != null) {
766 throw new BoschSHCException(
767 String.format("State request with URL %s failed with status code %d and error code %s", url,
768 errorResponse.statusCode, errorResponse.errorCode));
770 throw new BoschSHCException(
771 String.format("State request with URL %s failed with status code %d", url, statusCode));
776 T state = BoschSHCServiceState.fromJson(content, stateClass);
778 throw new BoschSHCException(String.format("Received invalid, expected type %s", stateClass.getName()));
784 * Sends a state change for a device to the controller
786 * @param deviceId Id of device to change state for
787 * @param serviceName Name of service of device to change state for
788 * @param state New state data to set for service
790 * @return Response of request
791 * @throws InterruptedException
792 * @throws ExecutionException
793 * @throws TimeoutException
795 public <T extends BoschSHCServiceState> @Nullable Response putState(String deviceId, String serviceName, T state)
796 throws InterruptedException, TimeoutException, ExecutionException {
798 BoschHttpClient localHttpClient = this.httpClient;
799 if (localHttpClient == null) {
800 logger.warn(HTTP_CLIENT_NOT_INITIALIZED);
805 String url = localHttpClient.getServiceStateUrl(serviceName, deviceId, state.getClass());
806 Request request = localHttpClient.createRequest(url, PUT, state);
809 return request.send();
813 * Sends a HTTP POST request without a request body to the given endpoint.
815 * @param endpoint The destination endpoint part of the URL
816 * @return the HTTP response
817 * @throws InterruptedException
818 * @throws TimeoutException
819 * @throws ExecutionException
821 public @Nullable Response postAction(String endpoint)
822 throws InterruptedException, TimeoutException, ExecutionException {
823 return postAction(endpoint, null);
827 * Sends a HTTP POST request with a request body to the given endpoint.
829 * @param <T> Type of the request
830 * @param endpoint The destination endpoint part of the URL
831 * @param requestBody object representing the request body to be sent, may be <code>null</code>
832 * @return the HTTP response
833 * @throws InterruptedException
834 * @throws TimeoutException
835 * @throws ExecutionException
837 public <T extends BoschSHCServiceState> @Nullable Response postAction(String endpoint, @Nullable T requestBody)
838 throws InterruptedException, TimeoutException, ExecutionException {
840 BoschHttpClient localHttpClient = this.httpClient;
841 if (localHttpClient == null) {
842 logger.warn(HTTP_CLIENT_NOT_INITIALIZED);
846 String url = localHttpClient.getBoschSmartHomeUrl(endpoint);
847 Request request = localHttpClient.createRequest(url, POST, requestBody);
848 return request.send();
851 public @Nullable DeviceServiceData getServiceData(String deviceId, String serviceName)
852 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
854 BoschHttpClient localHttpClient = this.httpClient;
855 if (localHttpClient == null) {
856 logger.warn(HTTP_CLIENT_NOT_INITIALIZED);
860 String url = localHttpClient.getServiceUrl(serviceName, deviceId);
861 logger.debug("getState(): Requesting \"{}\" from Bosch: {} via {}", serviceName, deviceId, url);
862 return getState(localHttpClient, url, DeviceServiceData.class);