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.BoschDeviceIdUtils;
38 import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
39 import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler;
40 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
41 import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData;
42 import org.openhab.binding.boschshc.internal.devices.bridge.dto.LongPollResult;
43 import org.openhab.binding.boschshc.internal.devices.bridge.dto.PublicInformation;
44 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Room;
45 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Scenario;
46 import org.openhab.binding.boschshc.internal.devices.bridge.dto.UserDefinedState;
47 import org.openhab.binding.boschshc.internal.discovery.ThingDiscoveryService;
48 import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
49 import org.openhab.binding.boschshc.internal.exceptions.LongPollingFailedException;
50 import org.openhab.binding.boschshc.internal.exceptions.PairingFailedException;
51 import org.openhab.binding.boschshc.internal.serialization.GsonUtils;
52 import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
53 import org.openhab.binding.boschshc.internal.services.dto.JsonRestExceptionResponse;
54 import org.openhab.core.library.types.StringType;
55 import org.openhab.core.thing.Bridge;
56 import org.openhab.core.thing.Channel;
57 import org.openhab.core.thing.ChannelUID;
58 import org.openhab.core.thing.Thing;
59 import org.openhab.core.thing.ThingStatus;
60 import org.openhab.core.thing.ThingStatusDetail;
61 import org.openhab.core.thing.binding.BaseBridgeHandler;
62 import org.openhab.core.thing.binding.ThingHandler;
63 import org.openhab.core.thing.binding.ThingHandlerService;
64 import org.openhab.core.types.Command;
65 import org.openhab.core.types.RefreshType;
66 import org.osgi.framework.Bundle;
67 import org.osgi.framework.FrameworkUtil;
68 import org.slf4j.Logger;
69 import org.slf4j.LoggerFactory;
71 import com.google.gson.JsonElement;
72 import com.google.gson.reflect.TypeToken;
75 * Representation of a connection with a Bosch Smart Home Controller bridge.
77 * @author Stefan Kästle - Initial contribution
78 * @author Gerd Zanker - added HttpClient with pairing support
79 * @author Christian Oeing - refactorings of e.g. server registration
80 * @author David Pace - Added support for custom endpoints and HTTP POST requests
81 * @author Gerd Zanker - added thing discovery
84 public class BridgeHandler extends BaseBridgeHandler {
86 private static final String HTTP_CLIENT_NOT_INITIALIZED = "HttpClient not initialized";
88 private final Logger logger = LoggerFactory.getLogger(BridgeHandler.class);
91 * Handler to do long polling.
93 private final LongPolling longPolling;
96 * HTTP client for all communications to and from the bridge.
98 * This member is package-protected to enable mocking in unit tests.
100 /* package */ @Nullable
101 BoschHttpClient httpClient;
103 private @Nullable ScheduledFuture<?> scheduledPairing;
106 * SHC thing/device discovery service instance.
107 * Registered and unregistered if service is actived/deactived.
108 * Used to scan for things after bridge is paired with SHC.
110 private @Nullable ThingDiscoveryService thingDiscoveryService;
112 private final ScenarioHandler scenarioHandler;
114 public BridgeHandler(Bridge bridge) {
116 scenarioHandler = new ScenarioHandler();
118 this.longPolling = new LongPolling(this.scheduler, this::handleLongPollResult, this::handleLongPollFailure);
122 public Collection<Class<? extends ThingHandlerService>> getServices() {
123 return Set.of(ThingDiscoveryService.class);
127 public void initialize() {
128 Bundle bundle = FrameworkUtil.getBundle(getClass());
129 if (bundle != null) {
130 logger.debug("Initialize {} Version {}", bundle.getSymbolicName(), bundle.getVersion());
133 // Read configuration
134 BridgeConfiguration config = getConfigAs(BridgeConfiguration.class);
136 String ipAddress = config.ipAddress.trim();
137 if (ipAddress.isEmpty()) {
138 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
139 "@text/offline.conf-error-empty-ip");
143 String password = config.password.trim();
144 if (password.isEmpty()) {
145 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
146 "@text/offline.conf-error-empty-password");
150 SslContextFactory factory;
152 // prepare SSL key and certificates
153 factory = new BoschSslUtil(ipAddress).getSslContextFactory();
154 } catch (PairingFailedException e) {
155 logger.debug("Error while obtaining SSL context factory.", e);
156 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
157 "@text/offline.conf-error-ssl");
161 // Instantiate HttpClient with the SslContextFactory
162 BoschHttpClient localHttpClient = this.httpClient = new BoschHttpClient(ipAddress, password, factory);
166 localHttpClient.start();
167 } catch (Exception e) {
168 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
169 String.format("Could not create http connection to controller: %s", e.getMessage()));
173 // general checks are OK, therefore set the status to unknown and wait for initial access
174 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE);
176 // Initialize bridge in the background.
177 // Start initial access the first time
178 scheduleInitialAccess(localHttpClient);
182 public void dispose() {
183 // Cancel scheduled pairing.
185 ScheduledFuture<?> localScheduledPairing = this.scheduledPairing;
186 if (localScheduledPairing != null) {
187 localScheduledPairing.cancel(true);
188 this.scheduledPairing = null;
191 // Stop long polling.
192 this.longPolling.stop();
195 BoschHttpClient localHttpClient = this.httpClient;
196 if (localHttpClient != null) {
198 localHttpClient.stop();
199 } catch (Exception e) {
200 logger.debug("HttpClient failed on bridge disposal: {}", e.getMessage(), e);
202 this.httpClient = null;
209 public void handleCommand(ChannelUID channelUID, Command command) {
210 // commands are handled by individual device handlers
211 BoschHttpClient localHttpClient = httpClient;
212 if (BoschSHCBindingConstants.CHANNEL_TRIGGER_SCENARIO.equals(channelUID.getId())
213 && !RefreshType.REFRESH.equals(command) && localHttpClient != null) {
214 scenarioHandler.triggerScenario(localHttpClient, command.toString());
219 * Schedule the initial access.
220 * Use a delay if pairing fails and next retry is scheduled.
222 private void scheduleInitialAccess(BoschHttpClient httpClient) {
223 this.scheduledPairing = scheduler.schedule(() -> initialAccess(httpClient), 15, TimeUnit.SECONDS);
227 * Execute the initial access.
228 * Uses the HTTP Bosch SHC client
229 * to check if access if possible
230 * pairs this Bosch SHC Bridge with the SHC if necessary
231 * and starts the first log poll.
233 * This method is package-protected to enable unit testing.
235 /* package */ void initialAccess(BoschHttpClient httpClient) {
236 logger.debug("Initializing Bosch SHC Bridge: {} - HTTP client is: {}", this, httpClient);
239 // check if SCH is offline
240 if (!httpClient.isOnline()) {
241 // update status already if access is not possible
242 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE,
243 "@text/offline.conf-error-offline");
244 // restart later initial access
245 scheduleInitialAccess(httpClient);
250 // check if SHC access is not possible and pairing necessary
251 if (!httpClient.isAccessPossible()) {
252 // update status description to show pairing test
253 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE,
254 "@text/offline.conf-error-pairing");
255 if (!httpClient.doPairing()) {
256 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
257 "@text/offline.conf-error-pairing");
259 // restart initial access - needed also in case of successful pairing to check access again
260 scheduleInitialAccess(httpClient);
264 // SHC is online and access should possible
265 if (!checkBridgeAccess()) {
266 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
267 "@text/offline.not-reachable");
268 // restart initial access
269 scheduleInitialAccess(httpClient);
273 // do thing discovery after pairing
274 final ThingDiscoveryService discovery = thingDiscoveryService;
275 if (discovery != null) {
279 // start long polling loop
280 this.updateStatus(ThingStatus.ONLINE);
281 startLongPolling(httpClient);
283 } catch (InterruptedException e) {
284 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE, "@text/offline.interrupted");
285 Thread.currentThread().interrupt();
289 private void startLongPolling(BoschHttpClient httpClient) {
291 this.longPolling.start(httpClient);
292 } catch (LongPollingFailedException e) {
293 this.handleLongPollFailure(e);
298 * Check the bridge access by sending an HTTP request.
299 * Does not throw any exception in case the request fails.
301 public boolean checkBridgeAccess() throws InterruptedException {
303 BoschHttpClient localHttpClient = this.httpClient;
305 if (localHttpClient == null) {
310 logger.debug("Sending http request to BoschSHC to check access: {}", localHttpClient);
311 String url = localHttpClient.getBoschSmartHomeUrl("devices");
312 ContentResponse contentResponse = localHttpClient.createRequest(url, GET).send();
314 // check HTTP status code
315 if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
316 logger.debug("Access check failed with status code: {}", contentResponse.getStatus());
322 } catch (TimeoutException | ExecutionException e) {
323 logger.warn("Access check failed because of {}!", e.getMessage());
329 * Get a list of connected devices from the Smart-Home Controller
331 * @throws InterruptedException in case bridge is stopped
333 public List<Device> getDevices() throws InterruptedException {
335 BoschHttpClient localHttpClient = this.httpClient;
336 if (localHttpClient == null) {
337 return Collections.emptyList();
341 logger.trace("Sending http request to Bosch to request devices: {}", localHttpClient);
342 String url = localHttpClient.getBoschSmartHomeUrl("devices");
343 ContentResponse contentResponse = localHttpClient.createRequest(url, GET).send();
345 // check HTTP status code
346 if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
347 logger.debug("Request devices failed with status code: {}", contentResponse.getStatus());
348 return Collections.emptyList();
351 String content = contentResponse.getContentAsString();
352 logger.trace("Request devices completed with success: {} - status code: {}", content,
353 contentResponse.getStatus());
355 Type collectionType = new TypeToken<ArrayList<Device>>() {
357 List<Device> nullableDevices = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content, collectionType);
358 return Optional.ofNullable(nullableDevices).orElse(Collections.emptyList());
359 } catch (TimeoutException | ExecutionException e) {
360 logger.debug("Request devices failed because of {}!", e.getMessage(), e);
361 return Collections.emptyList();
365 public List<UserDefinedState> getUserStates() throws InterruptedException {
367 BoschHttpClient localHttpClient = this.httpClient;
368 if (localHttpClient == null) {
373 logger.trace("Sending http request to Bosch to request user-defined states: {}", localHttpClient);
374 String url = localHttpClient.getBoschSmartHomeUrl("userdefinedstates");
375 ContentResponse contentResponse = localHttpClient.createRequest(url, GET).send();
377 // check HTTP status code
378 if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
379 logger.debug("Request devices failed with status code: {}", contentResponse.getStatus());
383 String content = contentResponse.getContentAsString();
384 logger.trace("Request devices completed with success: {} - status code: {}", content,
385 contentResponse.getStatus());
387 Type collectionType = new TypeToken<ArrayList<UserDefinedState>>() {
389 List<UserDefinedState> nullableUserStates = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content,
391 return Optional.ofNullable(nullableUserStates).orElse(Collections.emptyList());
392 } catch (TimeoutException | ExecutionException e) {
393 logger.debug("Request user-defined states failed because of {}!", e.getMessage(), e);
399 * Get a list of rooms from the Smart-Home controller
401 * @throws InterruptedException in case bridge is stopped
403 public List<Room> getRooms() throws InterruptedException {
404 List<Room> emptyRooms = new ArrayList<>();
406 BoschHttpClient localHttpClient = this.httpClient;
407 if (localHttpClient != null) {
409 logger.trace("Sending http request to Bosch to request rooms");
410 String url = localHttpClient.getBoschSmartHomeUrl("rooms");
411 ContentResponse contentResponse = localHttpClient.createRequest(url, GET).send();
413 // check HTTP status code
414 if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
415 logger.debug("Request rooms failed with status code: {}", contentResponse.getStatus());
419 String content = contentResponse.getContentAsString();
420 logger.trace("Request rooms completed with success: {} - status code: {}", content,
421 contentResponse.getStatus());
423 Type collectionType = new TypeToken<ArrayList<Room>>() {
426 ArrayList<Room> rooms = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content, collectionType);
427 return Objects.requireNonNullElse(rooms, emptyRooms);
428 } catch (TimeoutException | ExecutionException e) {
429 logger.debug("Request rooms failed because of {}!", e.getMessage());
438 * Get public information from Bosch SHC.
440 public PublicInformation getPublicInformation()
441 throws InterruptedException, BoschSHCException, ExecutionException, TimeoutException {
443 BoschHttpClient localHttpClient = this.httpClient;
444 if (localHttpClient == null) {
445 throw new BoschSHCException(HTTP_CLIENT_NOT_INITIALIZED);
448 String url = localHttpClient.getPublicInformationUrl();
449 Request request = localHttpClient.createRequest(url, GET);
451 return localHttpClient.sendRequest(request, PublicInformation.class, PublicInformation::isValid, null);
454 public boolean registerDiscoveryListener(ThingDiscoveryService listener) {
455 if (thingDiscoveryService == null) {
456 thingDiscoveryService = listener;
463 public boolean unregisterDiscoveryListener() {
464 if (thingDiscoveryService != null) {
465 thingDiscoveryService = null;
473 * Bridge callback handler for the results of long polls.
475 * It will check the results and
476 * forward the received states to the Bosch thing handlers.
478 * @param result Results from Long Polling
480 void handleLongPollResult(LongPollResult result) {
481 for (BoschSHCServiceState serviceState : result.result) {
482 if (serviceState instanceof DeviceServiceData deviceServiceData) {
483 handleDeviceServiceData(deviceServiceData);
484 } else if (serviceState instanceof UserDefinedState userDefinedState) {
485 handleUserDefinedState(userDefinedState);
486 } else if (serviceState instanceof Scenario scenario) {
487 final Channel channel = this.getThing().getChannel(BoschSHCBindingConstants.CHANNEL_SCENARIO_TRIGGERED);
488 if (channel != null && isLinked(channel.getUID())) {
489 updateState(channel.getUID(), new StringType(scenario.name));
496 * Processes a single long poll result.
498 * @param deviceServiceData object representing a single long poll result
500 private void handleDeviceServiceData(@Nullable DeviceServiceData deviceServiceData) {
501 if (deviceServiceData != null) {
502 JsonElement state = obtainState(deviceServiceData);
504 logger.debug("Got update for service {} of type {}: {}", deviceServiceData.id, deviceServiceData.type,
507 var updateDeviceId = deviceServiceData.deviceId;
508 if (updateDeviceId == null || state == null) {
512 logger.debug("Got update for device {}", updateDeviceId);
514 forwardStateToHandlers(deviceServiceData, state, updateDeviceId);
518 private void handleUserDefinedState(@Nullable UserDefinedState userDefinedState) {
519 if (userDefinedState != null) {
520 JsonElement state = GsonUtils.DEFAULT_GSON_INSTANCE.toJsonTree(userDefinedState.isState());
522 logger.debug("Got update for user-defined state {} with id {}: {}", userDefinedState.getName(),
523 userDefinedState.getId(), state);
525 var stateId = userDefinedState.getId();
526 if (stateId == null || state == null) {
530 logger.debug("Got update for user-defined state {}", userDefinedState);
532 forwardStateToHandlers(userDefinedState, state, stateId);
537 * Extracts the actual state object from the given {@link DeviceServiceData} instance.
539 * In some special cases like the <code>BatteryLevel</code> service the {@link DeviceServiceData} object itself
540 * contains the state.
541 * In all other cases, the state is contained in a sub-object named <code>state</code>.
543 * @param deviceServiceData the {@link DeviceServiceData} object from which the state should be obtained
544 * @return the state sub-object or the {@link DeviceServiceData} object itself
547 private JsonElement obtainState(DeviceServiceData deviceServiceData) {
548 // the battery level service receives no individual state object but rather requires the DeviceServiceData
550 if ("BatteryLevel".equals(deviceServiceData.id)) {
551 return GsonUtils.DEFAULT_GSON_INSTANCE.toJsonTree(deviceServiceData);
554 return deviceServiceData.state;
558 * Tries to find handlers for the device with the given ID and forwards the received state to the handlers.
560 * @param serviceData object representing updates received in long poll results
561 * @param state the received state object as JSON element
562 * @param updateDeviceId the ID of the device for which the state update was received
564 private void forwardStateToHandlers(BoschSHCServiceState serviceData, JsonElement state, String updateDeviceId) {
565 boolean handled = false;
566 final String serviceId = getServiceId(serviceData);
568 Bridge bridge = this.getThing();
569 for (Thing childThing : bridge.getThings()) {
570 // All children of this should implement BoschSHCHandler
572 ThingHandler baseHandler = childThing.getHandler();
573 if (baseHandler instanceof BoschSHCHandler handler) {
575 String deviceId = handler.getBoschID();
577 if (deviceId == null) {
581 logger.trace("Checking device {}, looking for {}", deviceId, updateDeviceId);
583 // handled is a boolean latch that stays true once it becomes true
584 // note that no short-circuiting operators are used, meaning that the method
585 // calls will always be evaluated, even if the latch is already true
586 handled |= notifyHandler(handler, deviceId, updateDeviceId, serviceId, state);
587 handled |= notifyParentHandler(handler, deviceId, updateDeviceId, 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 * Notifies the given handler if its device ID exactly matches the device ID for which the update was received.
601 * @param handler the handler to be notified if applicable
602 * @param deviceId the device ID associated with the handler
603 * @param updateDeviceId the device ID for which the update was received
604 * @param serviceId the ID of the service for which the update was received
605 * @param state the received state object as JSON element
607 * @return <code>true</code> if the handler matched and was notified, <code>false</code> otherwise
609 private boolean notifyHandler(BoschSHCHandler handler, String deviceId, String updateDeviceId, String serviceId,
611 if (updateDeviceId.equals(deviceId)) {
612 logger.debug("Found handler {}, calling processUpdate() for service {} with state {}", handler, serviceId,
614 handler.processUpdate(serviceId, state);
621 * If an update is received for a logical child device and the given handler is the parent device handler, the
622 * parent handler is notified.
624 * @param handler the handler to be notified if applicable
625 * @param deviceId the device ID associated with the handler
626 * @param updateDeviceId the device ID for which the update was received
627 * @param serviceId the ID of the service for which the update was received
628 * @param state the received state object as JSON element
629 * @return <code>true</code> if the given handler was the corresponding parent handler and was notified,
630 * <code>false</code> otherwise
632 private boolean notifyParentHandler(BoschSHCHandler handler, String deviceId, String updateDeviceId,
633 String serviceId, JsonElement state) {
634 if (BoschDeviceIdUtils.isChildDeviceId(updateDeviceId)) {
635 String parentDeviceId = BoschDeviceIdUtils.getParentDeviceId(updateDeviceId);
636 if (parentDeviceId.equals(deviceId)) {
637 logger.debug("Notifying parent handler {} about update for child device for service {} with state {}",
638 handler, serviceId, state);
639 handler.processChildUpdate(updateDeviceId, serviceId, state);
646 private String getServiceId(BoschSHCServiceState serviceData) {
647 if (serviceData instanceof UserDefinedState userState) {
648 return userState.getId();
650 return ((DeviceServiceData) serviceData).id;
654 * Bridge callback handler for the failures during long polls.
656 * It will update the bridge status and try to access the SHC again.
658 * @param e error during long polling
660 private void handleLongPollFailure(Throwable e) {
661 logger.warn("Long polling failed, will try to reconnect", e);
663 BoschHttpClient localHttpClient = this.httpClient;
664 if (localHttpClient == null) {
665 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
666 "@text/offline.long-polling-failed.http-client-null");
670 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE,
671 "@text/offline.long-polling-failed.trying-to-reconnect");
672 scheduleInitialAccess(localHttpClient);
675 public Device getDeviceInfo(String deviceId)
676 throws BoschSHCException, InterruptedException, TimeoutException, ExecutionException {
678 BoschHttpClient localHttpClient = this.httpClient;
679 if (localHttpClient == null) {
680 throw new BoschSHCException(HTTP_CLIENT_NOT_INITIALIZED);
683 String url = localHttpClient.getBoschSmartHomeUrl(String.format("devices/%s", deviceId));
684 Request request = localHttpClient.createRequest(url, GET);
686 return localHttpClient.sendRequest(request, Device.class, Device::isValid,
687 (Integer statusCode, String content) -> {
688 JsonRestExceptionResponse errorResponse = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content,
689 JsonRestExceptionResponse.class);
690 if (errorResponse != null && JsonRestExceptionResponse.isValid(errorResponse)) {
691 if (errorResponse.errorCode.equals(JsonRestExceptionResponse.ENTITY_NOT_FOUND)) {
692 return new BoschSHCException("@text/offline.conf-error.invalid-device-id");
694 return new BoschSHCException(String.format(
695 "Request for info of device %s failed with status code %d and error code %s",
696 deviceId, errorResponse.statusCode, errorResponse.errorCode));
699 return new BoschSHCException(String.format(
700 "Request for info of device %s failed with status code %d", deviceId, statusCode));
705 public UserDefinedState getUserStateInfo(String stateId)
706 throws BoschSHCException, InterruptedException, TimeoutException, ExecutionException {
708 BoschHttpClient locaHttpClient = this.httpClient;
709 if (locaHttpClient == null) {
710 throw new BoschSHCException(HTTP_CLIENT_NOT_INITIALIZED);
713 String url = locaHttpClient.getBoschSmartHomeUrl(String.format("userdefinedstates/%s", stateId));
714 Request request = locaHttpClient.createRequest(url, GET);
716 return locaHttpClient.sendRequest(request, UserDefinedState.class, UserDefinedState::isValid,
717 (Integer statusCode, String content) -> {
718 JsonRestExceptionResponse errorResponse = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content,
719 JsonRestExceptionResponse.class);
720 if (errorResponse != null && JsonRestExceptionResponse.isValid(errorResponse)) {
721 if (errorResponse.errorCode.equals(JsonRestExceptionResponse.ENTITY_NOT_FOUND)) {
722 return new BoschSHCException("@text/offline.conf-error.invalid-state-id");
724 return new BoschSHCException(String.format(
725 "Request for info of user-defines state %s failed with status code %d and error code %s",
726 stateId, errorResponse.statusCode, errorResponse.errorCode));
729 return new BoschSHCException(
730 String.format("Request for info of user-defined state %s failed with status code %d",
731 stateId, statusCode));
737 * Query the Bosch Smart Home Controller for the state of the given device.
739 * The URL used for retrieving the state has the following structure:
742 * https://{IP}:8444/smarthome/devices/{deviceId}/services/{serviceName}/state
745 * @param deviceId Id of device to get state for
746 * @param stateName Name of the state to query
747 * @param stateClass Class to convert the resulting JSON to
748 * @return the deserialized state object, may be <code>null</code>
749 * @throws ExecutionException
750 * @throws TimeoutException
751 * @throws InterruptedException
752 * @throws BoschSHCException
754 public <T extends BoschSHCServiceState> @Nullable T getState(String deviceId, String stateName, Class<T> stateClass)
755 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
757 BoschHttpClient localHttpClient = this.httpClient;
758 if (localHttpClient == null) {
759 logger.warn(HTTP_CLIENT_NOT_INITIALIZED);
763 String url = localHttpClient.getServiceStateUrl(stateName, deviceId, stateClass);
764 logger.debug("getState(): Requesting \"{}\" from Bosch: {} via {}", stateName, deviceId, url);
765 return getState(localHttpClient, url, stateClass);
769 * Queries the Bosch Smart Home Controller for the state using an explicit endpoint.
771 * @param <T> Type to which the resulting JSON should be deserialized to
772 * @param endpoint The destination endpoint part of the URL
773 * @param stateClass Class to convert the resulting JSON to
774 * @return the deserialized state object, may be <code>null</code>
775 * @throws InterruptedException
776 * @throws TimeoutException
777 * @throws ExecutionException
778 * @throws BoschSHCException
780 public <T extends BoschSHCServiceState> @Nullable T getState(String endpoint, Class<T> stateClass)
781 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
783 BoschHttpClient localHttpClient = this.httpClient;
784 if (localHttpClient == null) {
785 logger.warn(HTTP_CLIENT_NOT_INITIALIZED);
789 String url = localHttpClient.getBoschSmartHomeUrl(endpoint);
790 logger.debug("getState(): Requesting from Bosch: {}", url);
791 return getState(localHttpClient, url, stateClass);
795 * Sends a HTTP GET request in order to retrieve a state from the Bosch Smart Home Controller.
797 * @param <T> Type to which the resulting JSON should be deserialized to
798 * @param httpClient HTTP client used for sending the request
799 * @param url URL at which the state should be retrieved
800 * @param stateClass Class to convert the resulting JSON to
801 * @return the deserialized state object, may be <code>null</code>
802 * @throws InterruptedException
803 * @throws TimeoutException
804 * @throws ExecutionException
805 * @throws BoschSHCException
807 protected <T extends BoschSHCServiceState> @Nullable T getState(BoschHttpClient httpClient, String url,
808 Class<T> stateClass) throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
809 Request request = httpClient.createRequest(url, GET).header("Accept", "application/json");
811 ContentResponse contentResponse = request.send();
813 String content = contentResponse.getContentAsString();
814 logger.debug("getState(): Request complete: [{}] - return code: {}", content, contentResponse.getStatus());
816 int statusCode = contentResponse.getStatus();
817 if (statusCode != 200) {
818 JsonRestExceptionResponse errorResponse = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content,
819 JsonRestExceptionResponse.class);
820 if (errorResponse != null) {
821 throw new BoschSHCException(
822 String.format("State request with URL %s failed with status code %d and error code %s", url,
823 errorResponse.statusCode, errorResponse.errorCode));
825 throw new BoschSHCException(
826 String.format("State request with URL %s failed with status code %d", url, statusCode));
831 T state = BoschSHCServiceState.fromJson(content, stateClass);
833 throw new BoschSHCException(String.format("Received invalid, expected type %s", stateClass.getName()));
839 * Sends a state change for a device to the controller
841 * @param deviceId Id of device to change state for
842 * @param serviceName Name of service of device to change state for
843 * @param state New state data to set for service
845 * @return Response of request
846 * @throws InterruptedException
847 * @throws ExecutionException
848 * @throws TimeoutException
850 public <T extends BoschSHCServiceState> @Nullable Response putState(String deviceId, String serviceName, T state)
851 throws InterruptedException, TimeoutException, ExecutionException {
853 BoschHttpClient localHttpClient = this.httpClient;
854 if (localHttpClient == null) {
855 logger.warn(HTTP_CLIENT_NOT_INITIALIZED);
860 String url = localHttpClient.getServiceStateUrl(serviceName, deviceId, state.getClass());
861 Request request = localHttpClient.createRequest(url, PUT, state);
864 return request.send();
868 * Sends a HTTP POST request without a request body to the given endpoint.
870 * @param endpoint The destination endpoint part of the URL
871 * @return the HTTP response
872 * @throws InterruptedException
873 * @throws TimeoutException
874 * @throws ExecutionException
876 public @Nullable Response postAction(String endpoint)
877 throws InterruptedException, TimeoutException, ExecutionException {
878 return postAction(endpoint, null);
882 * Sends a HTTP POST request with a request body to the given endpoint.
884 * @param <T> Type of the request
885 * @param endpoint The destination endpoint part of the URL
886 * @param requestBody object representing the request body to be sent, may be <code>null</code>
887 * @return the HTTP response
888 * @throws InterruptedException
889 * @throws TimeoutException
890 * @throws ExecutionException
892 public <T extends BoschSHCServiceState> @Nullable Response postAction(String endpoint, @Nullable T requestBody)
893 throws InterruptedException, TimeoutException, ExecutionException {
895 BoschHttpClient localHttpClient = this.httpClient;
896 if (localHttpClient == null) {
897 logger.warn(HTTP_CLIENT_NOT_INITIALIZED);
901 String url = localHttpClient.getBoschSmartHomeUrl(endpoint);
902 Request request = localHttpClient.createRequest(url, POST, requestBody);
903 return request.send();
906 public @Nullable DeviceServiceData getServiceData(String deviceId, String serviceName)
907 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
909 BoschHttpClient localHttpClient = this.httpClient;
910 if (localHttpClient == null) {
911 logger.warn(HTTP_CLIENT_NOT_INITIALIZED);
915 String url = localHttpClient.getServiceUrl(serviceName, deviceId);
916 logger.debug("getState(): Requesting \"{}\" from Bosch: {} via {}", serviceName, deviceId, url);
917 return getState(localHttpClient, url, DeviceServiceData.class);