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.discovery.ThingDiscoveryService;
45 import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
46 import org.openhab.binding.boschshc.internal.exceptions.LongPollingFailedException;
47 import org.openhab.binding.boschshc.internal.exceptions.PairingFailedException;
48 import org.openhab.binding.boschshc.internal.serialization.GsonUtils;
49 import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
50 import org.openhab.binding.boschshc.internal.services.dto.JsonRestExceptionResponse;
51 import org.openhab.core.library.types.StringType;
52 import org.openhab.core.thing.Bridge;
53 import org.openhab.core.thing.Channel;
54 import org.openhab.core.thing.ChannelUID;
55 import org.openhab.core.thing.Thing;
56 import org.openhab.core.thing.ThingStatus;
57 import org.openhab.core.thing.ThingStatusDetail;
58 import org.openhab.core.thing.binding.BaseBridgeHandler;
59 import org.openhab.core.thing.binding.ThingHandler;
60 import org.openhab.core.thing.binding.ThingHandlerService;
61 import org.openhab.core.types.Command;
62 import org.openhab.core.types.RefreshType;
63 import org.osgi.framework.Bundle;
64 import org.osgi.framework.FrameworkUtil;
65 import org.slf4j.Logger;
66 import org.slf4j.LoggerFactory;
68 import com.google.gson.JsonElement;
69 import com.google.gson.reflect.TypeToken;
72 * Representation of a connection with a Bosch Smart Home Controller bridge.
74 * @author Stefan Kästle - Initial contribution
75 * @author Gerd Zanker - added HttpClient with pairing support
76 * @author Christian Oeing - refactorings of e.g. server registration
77 * @author David Pace - Added support for custom endpoints and HTTP POST requests
78 * @author Gerd Zanker - added thing discovery
81 public class BridgeHandler extends BaseBridgeHandler {
83 private final Logger logger = LoggerFactory.getLogger(BridgeHandler.class);
86 * Handler to do long polling.
88 private final LongPolling longPolling;
91 * HTTP client for all communications to and from the bridge.
93 * This member is package-protected to enable mocking in unit tests.
95 /* package */ @Nullable
96 BoschHttpClient httpClient;
98 private @Nullable ScheduledFuture<?> scheduledPairing;
101 * SHC thing/device discovery service instance.
102 * Registered and unregistered if service is actived/deactived.
103 * Used to scan for things after bridge is paired with SHC.
105 private @Nullable ThingDiscoveryService thingDiscoveryService;
107 private final ScenarioHandler scenarioHandler;
109 public BridgeHandler(Bridge bridge) {
111 scenarioHandler = new ScenarioHandler();
113 this.longPolling = new LongPolling(this.scheduler, this::handleLongPollResult, this::handleLongPollFailure);
117 public Collection<Class<? extends ThingHandlerService>> getServices() {
118 return Set.of(ThingDiscoveryService.class);
122 public void initialize() {
123 Bundle bundle = FrameworkUtil.getBundle(getClass());
124 if (bundle != null) {
125 logger.debug("Initialize {} Version {}", bundle.getSymbolicName(), bundle.getVersion());
128 // Read configuration
129 BridgeConfiguration config = getConfigAs(BridgeConfiguration.class);
131 String ipAddress = config.ipAddress.trim();
132 if (ipAddress.isEmpty()) {
133 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
134 "@text/offline.conf-error-empty-ip");
138 String password = config.password.trim();
139 if (password.isEmpty()) {
140 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
141 "@text/offline.conf-error-empty-password");
145 SslContextFactory factory;
147 // prepare SSL key and certificates
148 factory = new BoschSslUtil(ipAddress).getSslContextFactory();
149 } catch (PairingFailedException e) {
150 logger.debug("Error while obtaining SSL context factory.", e);
151 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
152 "@text/offline.conf-error-ssl");
156 // Instantiate HttpClient with the SslContextFactory
157 BoschHttpClient httpClient = this.httpClient = new BoschHttpClient(ipAddress, password, factory);
162 } catch (Exception e) {
163 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
164 String.format("Could not create http connection to controller: %s", e.getMessage()));
168 // general checks are OK, therefore set the status to unknown and wait for initial access
169 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE);
171 // Initialize bridge in the background.
172 // Start initial access the first time
173 scheduleInitialAccess(httpClient);
177 public void dispose() {
178 // Cancel scheduled pairing.
180 ScheduledFuture<?> scheduledPairing = this.scheduledPairing;
181 if (scheduledPairing != null) {
182 scheduledPairing.cancel(true);
183 this.scheduledPairing = null;
186 // Stop long polling.
187 this.longPolling.stop();
190 BoschHttpClient httpClient = this.httpClient;
191 if (httpClient != null) {
194 } catch (Exception e) {
195 logger.debug("HttpClient failed on bridge disposal: {}", e.getMessage(), e);
197 this.httpClient = null;
204 public void handleCommand(ChannelUID channelUID, Command command) {
205 // commands are handled by individual device handlers
206 BoschHttpClient localHttpClient = httpClient;
207 if (BoschSHCBindingConstants.CHANNEL_TRIGGER_SCENARIO.equals(channelUID.getId())
208 && !RefreshType.REFRESH.equals(command) && localHttpClient != null) {
209 scenarioHandler.triggerScenario(localHttpClient, command.toString());
214 * Schedule the initial access.
215 * Use a delay if pairing fails and next retry is scheduled.
217 private void scheduleInitialAccess(BoschHttpClient httpClient) {
218 this.scheduledPairing = scheduler.schedule(() -> initialAccess(httpClient), 15, TimeUnit.SECONDS);
222 * Execute the initial access.
223 * Uses the HTTP Bosch SHC client
224 * to check if access if possible
225 * pairs this Bosch SHC Bridge with the SHC if necessary
226 * and starts the first log poll.
228 * This method is package-protected to enable unit testing.
230 /* package */ void initialAccess(BoschHttpClient httpClient) {
231 logger.debug("Initializing Bosch SHC Bridge: {} - HTTP client is: {}", this, httpClient);
234 // check if SCH is offline
235 if (!httpClient.isOnline()) {
236 // update status already if access is not possible
237 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE,
238 "@text/offline.conf-error-offline");
239 // restart later initial access
240 scheduleInitialAccess(httpClient);
245 // check if SHC access is not possible and pairing necessary
246 if (!httpClient.isAccessPossible()) {
247 // update status description to show pairing test
248 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE,
249 "@text/offline.conf-error-pairing");
250 if (!httpClient.doPairing()) {
251 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
252 "@text/offline.conf-error-pairing");
254 // restart initial access - needed also in case of successful pairing to check access again
255 scheduleInitialAccess(httpClient);
259 // SHC is online and access should possible
260 if (!checkBridgeAccess()) {
261 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
262 "@text/offline.not-reachable");
263 // restart initial access
264 scheduleInitialAccess(httpClient);
268 // do thing discovery after pairing
269 final ThingDiscoveryService discovery = thingDiscoveryService;
270 if (discovery != null) {
274 // start long polling loop
275 this.updateStatus(ThingStatus.ONLINE);
276 startLongPolling(httpClient);
278 } catch (InterruptedException e) {
279 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE, "@text/offline.interrupted");
280 Thread.currentThread().interrupt();
284 private void startLongPolling(BoschHttpClient httpClient) {
286 this.longPolling.start(httpClient);
287 } catch (LongPollingFailedException e) {
288 this.handleLongPollFailure(e);
293 * Check the bridge access by sending an HTTP request.
294 * Does not throw any exception in case the request fails.
296 public boolean checkBridgeAccess() throws InterruptedException {
298 BoschHttpClient httpClient = this.httpClient;
300 if (httpClient == null) {
305 logger.debug("Sending http request to BoschSHC to check access: {}", httpClient);
306 String url = httpClient.getBoschSmartHomeUrl("devices");
307 ContentResponse contentResponse = httpClient.createRequest(url, GET).send();
309 // check HTTP status code
310 if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
311 logger.debug("Access check failed with status code: {}", contentResponse.getStatus());
317 } catch (TimeoutException | ExecutionException e) {
318 logger.warn("Access check failed because of {}!", e.getMessage());
324 * Get a list of connected devices from the Smart-Home Controller
326 * @throws InterruptedException in case bridge is stopped
328 public List<Device> getDevices() throws InterruptedException {
330 BoschHttpClient httpClient = this.httpClient;
331 if (httpClient == null) {
332 return Collections.emptyList();
336 logger.trace("Sending http request to Bosch to request devices: {}", httpClient);
337 String url = httpClient.getBoschSmartHomeUrl("devices");
338 ContentResponse contentResponse = httpClient.createRequest(url, GET).send();
340 // check HTTP status code
341 if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
342 logger.debug("Request devices failed with status code: {}", contentResponse.getStatus());
343 return Collections.emptyList();
346 String content = contentResponse.getContentAsString();
347 logger.trace("Request devices completed with success: {} - status code: {}", content,
348 contentResponse.getStatus());
350 Type collectionType = new TypeToken<ArrayList<Device>>() {
352 List<Device> nullableDevices = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content, collectionType);
353 return Optional.ofNullable(nullableDevices).orElse(Collections.emptyList());
354 } catch (TimeoutException | ExecutionException e) {
355 logger.debug("Request devices failed because of {}!", e.getMessage(), e);
356 return Collections.emptyList();
361 * Get a list of rooms from the Smart-Home controller
363 * @throws InterruptedException in case bridge is stopped
365 public List<Room> getRooms() throws InterruptedException {
366 List<Room> emptyRooms = new ArrayList<>();
368 BoschHttpClient httpClient = this.httpClient;
369 if (httpClient != null) {
371 logger.trace("Sending http request to Bosch to request rooms");
372 String url = httpClient.getBoschSmartHomeUrl("rooms");
373 ContentResponse contentResponse = httpClient.createRequest(url, GET).send();
375 // check HTTP status code
376 if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
377 logger.debug("Request rooms failed with status code: {}", contentResponse.getStatus());
381 String content = contentResponse.getContentAsString();
382 logger.trace("Request rooms completed with success: {} - status code: {}", content,
383 contentResponse.getStatus());
385 Type collectionType = new TypeToken<ArrayList<Room>>() {
388 ArrayList<Room> rooms = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content, collectionType);
389 return Objects.requireNonNullElse(rooms, emptyRooms);
390 } catch (TimeoutException | ExecutionException e) {
391 logger.debug("Request rooms failed because of {}!", e.getMessage());
399 public boolean registerDiscoveryListener(ThingDiscoveryService listener) {
400 if (thingDiscoveryService == null) {
401 thingDiscoveryService = listener;
408 public boolean unregisterDiscoveryListener() {
409 if (thingDiscoveryService != null) {
410 thingDiscoveryService = null;
418 * Bridge callback handler for the results of long polls.
420 * It will check the results and
421 * forward the received states to the Bosch thing handlers.
423 * @param result Results from Long Polling
425 private void handleLongPollResult(LongPollResult result) {
426 for (BoschSHCServiceState serviceState : result.result) {
427 if (serviceState instanceof DeviceServiceData deviceServiceData) {
428 handleDeviceServiceData(deviceServiceData);
429 } else if (serviceState instanceof Scenario scenario) {
430 final Channel channel = this.getThing().getChannel(BoschSHCBindingConstants.CHANNEL_SCENARIO_TRIGGERED);
431 if (channel != null && isLinked(channel.getUID())) {
432 updateState(channel.getUID(), new StringType(scenario.name));
439 * Processes a single long poll result.
441 * @param deviceServiceData object representing a single long poll result
443 private void handleDeviceServiceData(@Nullable DeviceServiceData deviceServiceData) {
444 if (deviceServiceData != null) {
445 JsonElement state = obtainState(deviceServiceData);
447 logger.debug("Got update for service {} of type {}: {}", deviceServiceData.id, deviceServiceData.type,
450 var updateDeviceId = deviceServiceData.deviceId;
451 if (updateDeviceId == null || state == null) {
455 logger.debug("Got update for device {}", updateDeviceId);
457 forwardStateToHandlers(deviceServiceData, state, updateDeviceId);
462 * Extracts the actual state object from the given {@link DeviceServiceData} instance.
464 * In some special cases like the <code>BatteryLevel</code> service the {@link DeviceServiceData} object itself
465 * contains the state.
466 * In all other cases, the state is contained in a sub-object named <code>state</code>.
468 * @param deviceServiceData the {@link DeviceServiceData} object from which the state should be obtained
469 * @return the state sub-object or the {@link DeviceServiceData} object itself
472 private JsonElement obtainState(DeviceServiceData deviceServiceData) {
473 // the battery level service receives no individual state object but rather requires the DeviceServiceData
475 if ("BatteryLevel".equals(deviceServiceData.id)) {
476 return GsonUtils.DEFAULT_GSON_INSTANCE.toJsonTree(deviceServiceData);
479 return deviceServiceData.state;
483 * Tries to find handlers for the device with the given ID and forwards the received state to the handlers.
485 * @param deviceServiceData object representing updates received in long poll results
486 * @param state the received state object as JSON element
487 * @param updateDeviceId the ID of the device for which the state update was received
489 private void forwardStateToHandlers(DeviceServiceData deviceServiceData, JsonElement state, String updateDeviceId) {
490 boolean handled = false;
492 Bridge bridge = this.getThing();
493 for (Thing childThing : bridge.getThings()) {
494 // All children of this should implement BoschSHCHandler
496 ThingHandler baseHandler = childThing.getHandler();
497 if (baseHandler instanceof BoschSHCHandler handler) {
499 String deviceId = handler.getBoschID();
502 logger.debug("Registered device: {} - looking for {}", deviceId, updateDeviceId);
504 if (deviceId != null && updateDeviceId.equals(deviceId)) {
505 logger.debug("Found child: {} - calling processUpdate (id: {}) with {}", handler,
506 deviceServiceData.id, state);
507 handler.processUpdate(deviceServiceData.id, state);
510 logger.warn("longPoll: child handler for {} does not implement Bosch SHC handler", baseHandler);
515 logger.debug("Could not find a thing for device ID: {}", updateDeviceId);
520 * Bridge callback handler for the failures during long polls.
522 * It will update the bridge status and try to access the SHC again.
524 * @param e error during long polling
526 private void handleLongPollFailure(Throwable e) {
527 logger.warn("Long polling failed, will try to reconnect", e);
529 BoschHttpClient httpClient = this.httpClient;
530 if (httpClient == null) {
531 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
532 "@text/offline.long-polling-failed.http-client-null");
536 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE,
537 "@text/offline.long-polling-failed.trying-to-reconnect");
538 scheduleInitialAccess(httpClient);
541 public Device getDeviceInfo(String deviceId)
542 throws BoschSHCException, InterruptedException, TimeoutException, ExecutionException {
544 BoschHttpClient httpClient = this.httpClient;
545 if (httpClient == null) {
546 throw new BoschSHCException("HTTP client not initialized");
549 String url = httpClient.getBoschSmartHomeUrl(String.format("devices/%s", deviceId));
550 Request request = httpClient.createRequest(url, GET);
552 return httpClient.sendRequest(request, Device.class, Device::isValid, (Integer statusCode, String content) -> {
553 JsonRestExceptionResponse errorResponse = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content,
554 JsonRestExceptionResponse.class);
555 if (errorResponse != null && JsonRestExceptionResponse.isValid(errorResponse)) {
556 if (errorResponse.errorCode.equals(JsonRestExceptionResponse.ENTITY_NOT_FOUND)) {
557 return new BoschSHCException("@text/offline.conf-error.invalid-device-id");
559 return new BoschSHCException(
560 String.format("Request for info of device %s failed with status code %d and error code %s",
561 deviceId, errorResponse.statusCode, errorResponse.errorCode));
564 return new BoschSHCException(String.format("Request for info of device %s failed with status code %d",
565 deviceId, statusCode));
571 * Query the Bosch Smart Home Controller for the state of the given device.
573 * The URL used for retrieving the state has the following structure:
576 * https://{IP}:8444/smarthome/devices/{deviceId}/services/{serviceName}/state
579 * @param deviceId Id of device to get state for
580 * @param stateName Name of the state to query
581 * @param stateClass Class to convert the resulting JSON to
582 * @return the deserialized state object, may be <code>null</code>
583 * @throws ExecutionException
584 * @throws TimeoutException
585 * @throws InterruptedException
586 * @throws BoschSHCException
588 public <T extends BoschSHCServiceState> @Nullable T getState(String deviceId, String stateName, Class<T> stateClass)
589 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
591 BoschHttpClient httpClient = this.httpClient;
592 if (httpClient == null) {
593 logger.warn("HttpClient not initialized");
597 String url = httpClient.getServiceStateUrl(stateName, deviceId);
598 logger.debug("getState(): Requesting \"{}\" from Bosch: {} via {}", stateName, deviceId, url);
599 return getState(httpClient, url, stateClass);
603 * Queries the Bosch Smart Home Controller for the state using an explicit endpoint.
605 * @param <T> Type to which the resulting JSON should be deserialized to
606 * @param endpoint The destination endpoint part of the URL
607 * @param stateClass Class to convert the resulting JSON to
608 * @return the deserialized state object, may be <code>null</code>
609 * @throws InterruptedException
610 * @throws TimeoutException
611 * @throws ExecutionException
612 * @throws BoschSHCException
614 public <T extends BoschSHCServiceState> @Nullable T getState(String endpoint, Class<T> stateClass)
615 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
617 BoschHttpClient httpClient = this.httpClient;
618 if (httpClient == null) {
619 logger.warn("HttpClient not initialized");
623 String url = httpClient.getBoschSmartHomeUrl(endpoint);
624 logger.debug("getState(): Requesting from Bosch: {}", url);
625 return getState(httpClient, url, stateClass);
629 * Sends a HTTP GET request in order to retrieve a state from the Bosch Smart Home Controller.
631 * @param <T> Type to which the resulting JSON should be deserialized to
632 * @param httpClient HTTP client used for sending the request
633 * @param url URL at which the state should be retrieved
634 * @param stateClass Class to convert the resulting JSON to
635 * @return the deserialized state object, may be <code>null</code>
636 * @throws InterruptedException
637 * @throws TimeoutException
638 * @throws ExecutionException
639 * @throws BoschSHCException
641 protected <T extends BoschSHCServiceState> @Nullable T getState(BoschHttpClient httpClient, String url,
642 Class<T> stateClass) throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
643 Request request = httpClient.createRequest(url, GET).header("Accept", "application/json");
645 ContentResponse contentResponse = request.send();
647 String content = contentResponse.getContentAsString();
648 logger.debug("getState(): Request complete: [{}] - return code: {}", content, contentResponse.getStatus());
650 int statusCode = contentResponse.getStatus();
651 if (statusCode != 200) {
652 JsonRestExceptionResponse errorResponse = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content,
653 JsonRestExceptionResponse.class);
654 if (errorResponse != null) {
655 throw new BoschSHCException(
656 String.format("State request with URL %s failed with status code %d and error code %s", url,
657 errorResponse.statusCode, errorResponse.errorCode));
659 throw new BoschSHCException(
660 String.format("State request with URL %s failed with status code %d", url, statusCode));
665 T state = BoschSHCServiceState.fromJson(content, stateClass);
667 throw new BoschSHCException(String.format("Received invalid, expected type %s", stateClass.getName()));
673 * Sends a state change for a device to the controller
675 * @param deviceId Id of device to change state for
676 * @param serviceName Name of service of device to change state for
677 * @param state New state data to set for service
679 * @return Response of request
680 * @throws InterruptedException
681 * @throws ExecutionException
682 * @throws TimeoutException
684 public <T extends BoschSHCServiceState> @Nullable Response putState(String deviceId, String serviceName, T state)
685 throws InterruptedException, TimeoutException, ExecutionException {
687 BoschHttpClient httpClient = this.httpClient;
688 if (httpClient == null) {
689 logger.warn("HttpClient not initialized");
694 String url = httpClient.getServiceStateUrl(serviceName, deviceId);
695 Request request = httpClient.createRequest(url, PUT, state);
698 return request.send();
702 * Sends a HTTP POST request without a request body to the given endpoint.
704 * @param endpoint The destination endpoint part of the URL
705 * @return the HTTP response
706 * @throws InterruptedException
707 * @throws TimeoutException
708 * @throws ExecutionException
710 public @Nullable Response postAction(String endpoint)
711 throws InterruptedException, TimeoutException, ExecutionException {
712 return postAction(endpoint, null);
716 * Sends a HTTP POST request with a request body to the given endpoint.
718 * @param <T> Type of the request
719 * @param endpoint The destination endpoint part of the URL
720 * @param requestBody object representing the request body to be sent, may be <code>null</code>
721 * @return the HTTP response
722 * @throws InterruptedException
723 * @throws TimeoutException
724 * @throws ExecutionException
726 public <T extends BoschSHCServiceState> @Nullable Response postAction(String endpoint, @Nullable T requestBody)
727 throws InterruptedException, TimeoutException, ExecutionException {
729 BoschHttpClient httpClient = this.httpClient;
730 if (httpClient == null) {
731 logger.warn("HttpClient not initialized");
735 String url = httpClient.getBoschSmartHomeUrl(endpoint);
736 Request request = httpClient.createRequest(url, POST, requestBody);
737 return request.send();
740 public @Nullable DeviceServiceData getServiceData(String deviceId, String serviceName)
741 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
743 BoschHttpClient httpClient = this.httpClient;
744 if (httpClient == null) {
745 logger.warn("HttpClient not initialized");
749 String url = httpClient.getServiceUrl(serviceName, deviceId);
750 logger.debug("getState(): Requesting \"{}\" from Bosch: {} via {}", serviceName, deviceId, url);
751 return getState(httpClient, url, DeviceServiceData.class);