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.GET;
16 import static org.eclipse.jetty.http.HttpMethod.POST;
17 import static org.eclipse.jetty.http.HttpMethod.PUT;
19 import java.lang.reflect.Type;
20 import java.util.ArrayList;
21 import java.util.Collection;
22 import java.util.Collections;
23 import java.util.List;
24 import java.util.Objects;
26 import java.util.concurrent.ExecutionException;
27 import java.util.concurrent.ScheduledFuture;
28 import java.util.concurrent.TimeUnit;
29 import java.util.concurrent.TimeoutException;
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.eclipse.jetty.client.api.ContentResponse;
34 import org.eclipse.jetty.client.api.Request;
35 import org.eclipse.jetty.client.api.Response;
36 import org.eclipse.jetty.http.HttpStatus;
37 import org.eclipse.jetty.util.ssl.SslContextFactory;
38 import org.openhab.binding.boschshc.internal.devices.BoschDeviceIdUtils;
39 import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
40 import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler;
41 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
42 import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData;
43 import org.openhab.binding.boschshc.internal.devices.bridge.dto.LongPollResult;
44 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Message;
45 import org.openhab.binding.boschshc.internal.devices.bridge.dto.PublicInformation;
46 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Room;
47 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Scenario;
48 import org.openhab.binding.boschshc.internal.devices.bridge.dto.UserDefinedState;
49 import org.openhab.binding.boschshc.internal.discovery.ThingDiscoveryService;
50 import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
51 import org.openhab.binding.boschshc.internal.exceptions.LongPollingFailedException;
52 import org.openhab.binding.boschshc.internal.exceptions.PairingFailedException;
53 import org.openhab.binding.boschshc.internal.serialization.GsonUtils;
54 import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
55 import org.openhab.binding.boschshc.internal.services.dto.JsonRestExceptionResponse;
56 import org.openhab.core.library.types.StringType;
57 import org.openhab.core.thing.Bridge;
58 import org.openhab.core.thing.Channel;
59 import org.openhab.core.thing.ChannelUID;
60 import org.openhab.core.thing.Thing;
61 import org.openhab.core.thing.ThingStatus;
62 import org.openhab.core.thing.ThingStatusDetail;
63 import org.openhab.core.thing.binding.BaseBridgeHandler;
64 import org.openhab.core.thing.binding.ThingHandler;
65 import org.openhab.core.thing.binding.ThingHandlerService;
66 import org.openhab.core.types.Command;
67 import org.openhab.core.types.RefreshType;
68 import org.osgi.framework.Bundle;
69 import org.osgi.framework.FrameworkUtil;
70 import org.slf4j.Logger;
71 import org.slf4j.LoggerFactory;
73 import com.google.gson.JsonElement;
74 import com.google.gson.reflect.TypeToken;
77 * Representation of a connection with a Bosch Smart Home Controller bridge.
79 * @author Stefan Kästle - Initial contribution
80 * @author Gerd Zanker - added HttpClient with pairing support
81 * @author Christian Oeing - refactorings of e.g. server registration
82 * @author David Pace - Added support for custom endpoints and HTTP POST requests
83 * @author Gerd Zanker - added thing discovery
86 public class BridgeHandler extends BaseBridgeHandler {
88 private static final String HTTP_CLIENT_NOT_INITIALIZED = "HttpClient not initialized";
90 private final Logger logger = LoggerFactory.getLogger(BridgeHandler.class);
93 * Handler to do long polling.
95 private final LongPolling longPolling;
98 * HTTP client for all communications to and from the bridge.
100 * This member is package-protected to enable mocking in unit tests.
102 /* package */ @Nullable
103 BoschHttpClient httpClient;
105 private @Nullable ScheduledFuture<?> scheduledPairing;
108 * SHC thing/device discovery service instance.
109 * Registered and unregistered if service is actived/deactived.
110 * Used to scan for things after bridge is paired with SHC.
112 private @Nullable ThingDiscoveryService thingDiscoveryService;
114 private final ScenarioHandler scenarioHandler;
116 public BridgeHandler(Bridge bridge) {
118 scenarioHandler = new ScenarioHandler();
120 this.longPolling = new LongPolling(this.scheduler, this::handleLongPollResult, this::handleLongPollFailure);
124 public Collection<Class<? extends ThingHandlerService>> getServices() {
125 return Set.of(ThingDiscoveryService.class);
129 public void initialize() {
130 Bundle bundle = FrameworkUtil.getBundle(getClass());
131 if (bundle != null) {
132 logger.debug("Initialize {} Version {}", bundle.getSymbolicName(), bundle.getVersion());
135 // Read configuration
136 BridgeConfiguration config = getConfigAs(BridgeConfiguration.class);
138 String ipAddress = config.ipAddress.trim();
139 if (ipAddress.isEmpty()) {
140 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
141 "@text/offline.conf-error-empty-ip");
145 String password = config.password.trim();
146 if (password.isEmpty()) {
147 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
148 "@text/offline.conf-error-empty-password");
152 SslContextFactory factory;
154 // prepare SSL key and certificates
155 factory = new BoschSslUtil(ipAddress).getSslContextFactory();
156 } catch (PairingFailedException e) {
157 logger.debug("Error while obtaining SSL context factory.", e);
158 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
159 "@text/offline.conf-error-ssl");
163 // Instantiate HttpClient with the SslContextFactory
164 BoschHttpClient localHttpClient = this.httpClient = new BoschHttpClient(ipAddress, password, factory);
168 localHttpClient.start();
169 } catch (Exception e) {
170 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
171 String.format("Could not create http connection to controller: %s", e.getMessage()));
175 // general checks are OK, therefore set the status to unknown and wait for initial access
176 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE);
178 // Initialize bridge in the background.
179 // Start initial access the first time
180 scheduleInitialAccess(localHttpClient);
184 public void dispose() {
185 // Cancel scheduled pairing.
187 ScheduledFuture<?> localScheduledPairing = this.scheduledPairing;
188 if (localScheduledPairing != null) {
189 localScheduledPairing.cancel(true);
190 this.scheduledPairing = null;
193 // Stop long polling.
194 this.longPolling.stop();
197 BoschHttpClient localHttpClient = this.httpClient;
198 if (localHttpClient != null) {
200 localHttpClient.stop();
201 } catch (Exception e) {
202 logger.debug("HttpClient failed on bridge disposal: {}", e.getMessage(), e);
204 this.httpClient = null;
211 public void handleCommand(ChannelUID channelUID, Command command) {
212 // commands are handled by individual device handlers
213 BoschHttpClient localHttpClient = httpClient;
214 if (BoschSHCBindingConstants.CHANNEL_TRIGGER_SCENARIO.equals(channelUID.getId())
215 && !RefreshType.REFRESH.equals(command) && localHttpClient != null) {
216 scenarioHandler.triggerScenario(localHttpClient, command.toString());
221 * Schedule the initial access.
222 * Use a delay if pairing fails and next retry is scheduled.
224 private void scheduleInitialAccess(BoschHttpClient httpClient) {
225 this.scheduledPairing = scheduler.schedule(() -> initialAccess(httpClient), 15, TimeUnit.SECONDS);
229 * Execute the initial access.
230 * Uses the HTTP Bosch SHC client
231 * to check if access if possible
232 * pairs this Bosch SHC Bridge with the SHC if necessary
233 * and starts the first log poll.
235 * This method is package-protected to enable unit testing.
237 /* package */ void initialAccess(BoschHttpClient httpClient) {
238 logger.debug("Initializing Bosch SHC Bridge: {} - HTTP client is: {}", this, httpClient);
241 // check if SCH is offline
242 if (!httpClient.isOnline()) {
243 // update status already if access is not possible
244 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE,
245 "@text/offline.conf-error-offline");
246 // restart later initial access
247 scheduleInitialAccess(httpClient);
252 // check if SHC access is not possible and pairing necessary
253 if (!httpClient.isAccessPossible()) {
254 // update status description to show pairing test
255 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE,
256 "@text/offline.conf-error-pairing");
257 if (!httpClient.doPairing()) {
258 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
259 "@text/offline.conf-error-pairing");
261 // restart initial access - needed also in case of successful pairing to check access again
262 scheduleInitialAccess(httpClient);
266 // SHC is online and access should possible
267 if (!checkBridgeAccess()) {
268 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
269 "@text/offline.not-reachable");
270 // restart initial access
271 scheduleInitialAccess(httpClient);
275 // do thing discovery after pairing
276 final ThingDiscoveryService discovery = thingDiscoveryService;
277 if (discovery != null) {
281 // start long polling loop
282 this.updateStatus(ThingStatus.ONLINE);
283 startLongPolling(httpClient);
285 } catch (InterruptedException e) {
286 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE, "@text/offline.interrupted");
287 Thread.currentThread().interrupt();
291 private void startLongPolling(BoschHttpClient httpClient) {
293 this.longPolling.start(httpClient);
294 } catch (LongPollingFailedException e) {
295 this.handleLongPollFailure(e);
300 * Check the bridge access by sending an HTTP request.
301 * Does not throw any exception in case the request fails.
303 public boolean checkBridgeAccess() throws InterruptedException {
305 BoschHttpClient localHttpClient = this.httpClient;
307 if (localHttpClient == null) {
312 logger.debug("Sending http request to BoschSHC to check access: {}", localHttpClient);
313 String url = localHttpClient.getBoschSmartHomeUrl("devices");
314 ContentResponse contentResponse = localHttpClient.createRequest(url, GET).send();
316 // check HTTP status code
317 if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
318 logger.debug("Access check failed with status code: {}", contentResponse.getStatus());
324 } catch (TimeoutException | ExecutionException e) {
325 logger.warn("Access check failed because of {}!", e.getMessage());
331 * Get a list of connected devices from the Smart-Home Controller
333 * @throws InterruptedException in case bridge is stopped
335 public List<Device> getDevices() throws InterruptedException {
337 BoschHttpClient localHttpClient = this.httpClient;
338 if (localHttpClient == null) {
339 return Collections.emptyList();
343 logger.trace("Sending http request to Bosch to request devices: {}", localHttpClient);
344 String url = localHttpClient.getBoschSmartHomeUrl("devices");
345 ContentResponse contentResponse = localHttpClient.createRequest(url, GET).send();
347 // check HTTP status code
348 if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
349 logger.debug("Request devices failed with status code: {}", contentResponse.getStatus());
350 return Collections.emptyList();
353 String content = contentResponse.getContentAsString();
354 logger.trace("Request devices completed with success: {} - status code: {}", content,
355 contentResponse.getStatus());
357 Type collectionType = new TypeToken<ArrayList<Device>>() {
359 List<Device> nullableDevices = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content, collectionType);
360 return nullableDevices != null ? nullableDevices : Collections.emptyList();
361 } catch (TimeoutException | ExecutionException e) {
362 logger.debug("Request devices failed because of {}!", e.getMessage(), e);
363 return Collections.emptyList();
367 public List<UserDefinedState> getUserStates() throws InterruptedException {
369 BoschHttpClient localHttpClient = this.httpClient;
370 if (localHttpClient == null) {
375 logger.trace("Sending http request to Bosch to request user-defined states: {}", localHttpClient);
376 String url = localHttpClient.getBoschSmartHomeUrl("userdefinedstates");
377 ContentResponse contentResponse = localHttpClient.createRequest(url, GET).send();
379 // check HTTP status code
380 if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
381 logger.debug("Request devices failed with status code: {}", contentResponse.getStatus());
385 String content = contentResponse.getContentAsString();
386 logger.trace("Request devices completed with success: {} - status code: {}", content,
387 contentResponse.getStatus());
389 Type collectionType = new TypeToken<ArrayList<UserDefinedState>>() {
391 List<UserDefinedState> nullableUserStates = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content,
393 return nullableUserStates != null ? nullableUserStates : Collections.emptyList();
394 } catch (TimeoutException | ExecutionException e) {
395 logger.debug("Request user-defined states failed because of {}!", e.getMessage(), e);
401 * Get a list of rooms from the Smart-Home controller
403 * @throws InterruptedException in case bridge is stopped
405 public List<Room> getRooms() throws InterruptedException {
406 List<Room> emptyRooms = new ArrayList<>();
408 BoschHttpClient localHttpClient = this.httpClient;
409 if (localHttpClient != null) {
411 logger.trace("Sending http request to Bosch to request rooms");
412 String url = localHttpClient.getBoschSmartHomeUrl("rooms");
413 ContentResponse contentResponse = localHttpClient.createRequest(url, GET).send();
415 // check HTTP status code
416 if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
417 logger.debug("Request rooms failed with status code: {}", contentResponse.getStatus());
421 String content = contentResponse.getContentAsString();
422 logger.trace("Request rooms completed with success: {} - status code: {}", content,
423 contentResponse.getStatus());
425 Type collectionType = new TypeToken<ArrayList<Room>>() {
428 ArrayList<Room> rooms = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content, collectionType);
429 return Objects.requireNonNullElse(rooms, emptyRooms);
430 } catch (TimeoutException | ExecutionException e) {
431 logger.debug("Request rooms failed because of {}!", e.getMessage());
440 * Get public information from Bosch SHC.
442 public PublicInformation getPublicInformation()
443 throws InterruptedException, BoschSHCException, ExecutionException, TimeoutException {
445 BoschHttpClient localHttpClient = this.httpClient;
446 if (localHttpClient == null) {
447 throw new BoschSHCException(HTTP_CLIENT_NOT_INITIALIZED);
450 String url = localHttpClient.getPublicInformationUrl();
451 Request request = localHttpClient.createRequest(url, GET);
453 return localHttpClient.sendRequest(request, PublicInformation.class, PublicInformation::isValid, null);
456 public boolean registerDiscoveryListener(ThingDiscoveryService listener) {
457 if (thingDiscoveryService == null) {
458 thingDiscoveryService = listener;
465 public boolean unregisterDiscoveryListener() {
466 if (thingDiscoveryService != null) {
467 thingDiscoveryService = null;
475 * Bridge callback handler for the results of long polls.
477 * It will check the results and
478 * forward the received states to the Bosch thing handlers.
480 * @param result Results from Long Polling
482 void handleLongPollResult(LongPollResult result) {
483 for (BoschSHCServiceState serviceState : result.result) {
484 if (serviceState instanceof DeviceServiceData deviceServiceData) {
485 handleDeviceServiceData(deviceServiceData);
486 } else if (serviceState instanceof UserDefinedState userDefinedState) {
487 handleUserDefinedState(userDefinedState);
488 } else if (serviceState instanceof Scenario scenario) {
489 final Channel channel = this.getThing().getChannel(BoschSHCBindingConstants.CHANNEL_SCENARIO_TRIGGERED);
490 if (channel != null && isLinked(channel.getUID())) {
491 updateState(channel.getUID(), new StringType(scenario.name));
493 } else if (serviceState instanceof Message message) {
494 handleMessage(message);
499 private void handleMessage(Message message) {
500 if (Message.SOURCE_TYPE_DEVICE.equals(message.sourceType) && message.sourceId != null) {
501 forwardMessageToDevice(message, message.sourceId);
505 private void forwardMessageToDevice(Message message, String deviceId) {
506 BoschSHCHandler deviceHandler = findDeviceHandler(deviceId);
507 if (deviceHandler == null) {
511 deviceHandler.processMessage(message);
515 private BoschSHCHandler findDeviceHandler(String deviceIdToFind) {
516 for (Thing childThing : getThing().getThings()) {
518 ThingHandler baseHandler = childThing.getHandler();
519 if (baseHandler instanceof BoschSHCHandler handler) {
521 String deviceId = handler.getBoschID();
523 if (deviceIdToFind.equals(deviceId)) {
532 * Processes a single long poll result.
534 * @param deviceServiceData object representing a single long poll result
536 private void handleDeviceServiceData(@Nullable DeviceServiceData deviceServiceData) {
537 if (deviceServiceData != null) {
538 JsonElement state = obtainState(deviceServiceData);
540 logger.debug("Got update for service {} of type {}: {}", deviceServiceData.id, deviceServiceData.type,
543 var updateDeviceId = deviceServiceData.deviceId;
544 if (updateDeviceId == null || state == null) {
548 logger.debug("Got update for device {}", updateDeviceId);
550 forwardStateToHandlers(deviceServiceData, state, updateDeviceId);
554 private void handleUserDefinedState(@Nullable UserDefinedState userDefinedState) {
555 if (userDefinedState != null) {
556 JsonElement state = GsonUtils.DEFAULT_GSON_INSTANCE.toJsonTree(userDefinedState.isState());
558 logger.debug("Got update for user-defined state {} with id {}: {}", userDefinedState.getName(),
559 userDefinedState.getId(), state);
561 var stateId = userDefinedState.getId();
562 if (stateId == null || state == null) {
566 logger.debug("Got update for user-defined state {}", userDefinedState);
568 forwardStateToHandlers(userDefinedState, state, stateId);
573 * Extracts the actual state object from the given {@link DeviceServiceData} instance.
575 * In some special cases like the <code>BatteryLevel</code> service the {@link DeviceServiceData} object itself
576 * contains the state.
577 * In all other cases, the state is contained in a sub-object named <code>state</code>.
579 * @param deviceServiceData the {@link DeviceServiceData} object from which the state should be obtained
580 * @return the state sub-object or the {@link DeviceServiceData} object itself
583 private JsonElement obtainState(DeviceServiceData deviceServiceData) {
584 // the battery level service receives no individual state object but rather requires the DeviceServiceData
586 if ("BatteryLevel".equals(deviceServiceData.id)) {
587 return GsonUtils.DEFAULT_GSON_INSTANCE.toJsonTree(deviceServiceData);
590 return deviceServiceData.state;
594 * Tries to find handlers for the device with the given ID and forwards the received state to the handlers.
596 * @param serviceData object representing updates received in long poll results
597 * @param state the received state object as JSON element
598 * @param updateDeviceId the ID of the device for which the state update was received
600 private void forwardStateToHandlers(BoschSHCServiceState serviceData, JsonElement state, String updateDeviceId) {
601 boolean handled = false;
602 final String serviceId = getServiceId(serviceData);
604 Bridge bridge = this.getThing();
605 for (Thing childThing : bridge.getThings()) {
606 // All children of this should implement BoschSHCHandler
608 ThingHandler baseHandler = childThing.getHandler();
609 if (baseHandler instanceof BoschSHCHandler handler) {
611 String deviceId = handler.getBoschID();
613 if (deviceId == null) {
617 logger.trace("Checking device {}, looking for {}", deviceId, updateDeviceId);
619 // handled is a boolean latch that stays true once it becomes true
620 // note that no short-circuiting operators are used, meaning that the method
621 // calls will always be evaluated, even if the latch is already true
622 handled |= notifyHandler(handler, deviceId, updateDeviceId, serviceId, state);
623 handled |= notifyParentHandler(handler, deviceId, updateDeviceId, serviceId, state);
625 logger.warn("longPoll: child handler for {} does not implement Bosch SHC handler", baseHandler);
630 logger.debug("Could not find a thing for device ID: {}", updateDeviceId);
635 * Notifies the given handler if its device ID exactly matches the device ID for which the update was received.
637 * @param handler the handler to be notified if applicable
638 * @param deviceId the device ID associated with the handler
639 * @param updateDeviceId the device ID for which the update was received
640 * @param serviceId the ID of the service for which the update was received
641 * @param state the received state object as JSON element
643 * @return <code>true</code> if the handler matched and was notified, <code>false</code> otherwise
645 private boolean notifyHandler(BoschSHCHandler handler, String deviceId, String updateDeviceId, String serviceId,
647 if (updateDeviceId.equals(deviceId)) {
648 logger.debug("Found handler {}, calling processUpdate() for service {} with state {}", handler, serviceId,
650 handler.processUpdate(serviceId, state);
657 * If an update is received for a logical child device and the given handler is the parent device handler, the
658 * parent handler is notified.
660 * @param handler the handler to be notified if applicable
661 * @param deviceId the device ID associated with the handler
662 * @param updateDeviceId the device ID for which the update was received
663 * @param serviceId the ID of the service for which the update was received
664 * @param state the received state object as JSON element
665 * @return <code>true</code> if the given handler was the corresponding parent handler and was notified,
666 * <code>false</code> otherwise
668 private boolean notifyParentHandler(BoschSHCHandler handler, String deviceId, String updateDeviceId,
669 String serviceId, JsonElement state) {
670 if (BoschDeviceIdUtils.isChildDeviceId(updateDeviceId)) {
671 String parentDeviceId = BoschDeviceIdUtils.getParentDeviceId(updateDeviceId);
672 if (parentDeviceId.equals(deviceId)) {
673 logger.debug("Notifying parent handler {} about update for child device for service {} with state {}",
674 handler, serviceId, state);
675 handler.processChildUpdate(updateDeviceId, serviceId, state);
682 private String getServiceId(BoschSHCServiceState serviceData) {
683 if (serviceData instanceof UserDefinedState userState) {
684 return userState.getId();
686 return ((DeviceServiceData) serviceData).id;
690 * Bridge callback handler for the failures during long polls.
692 * It will update the bridge status and try to access the SHC again.
694 * @param e error during long polling
696 void handleLongPollFailure(Throwable e) {
697 logger.warn("Long polling failed, will try to reconnect", e);
699 BoschHttpClient localHttpClient = this.httpClient;
700 if (localHttpClient == null) {
701 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
702 "@text/offline.long-polling-failed.http-client-null");
706 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE,
707 "@text/offline.long-polling-failed.trying-to-reconnect");
708 scheduleInitialAccess(localHttpClient);
711 public Device getDeviceInfo(String deviceId)
712 throws BoschSHCException, InterruptedException, TimeoutException, ExecutionException {
714 BoschHttpClient localHttpClient = this.httpClient;
715 if (localHttpClient == null) {
716 throw new BoschSHCException(HTTP_CLIENT_NOT_INITIALIZED);
719 String url = localHttpClient.getBoschSmartHomeUrl(String.format("devices/%s", deviceId));
720 Request request = localHttpClient.createRequest(url, GET);
722 return localHttpClient.sendRequest(request, Device.class, Device::isValid,
723 (Integer statusCode, String content) -> {
724 JsonRestExceptionResponse errorResponse = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content,
725 JsonRestExceptionResponse.class);
726 if (errorResponse != null && JsonRestExceptionResponse.isValid(errorResponse)) {
727 if (errorResponse.errorCode.equals(JsonRestExceptionResponse.ENTITY_NOT_FOUND)) {
728 return new BoschSHCException("@text/offline.conf-error.invalid-device-id");
730 return new BoschSHCException(String.format(
731 "Request for info of device %s failed with status code %d and error code %s",
732 deviceId, errorResponse.statusCode, errorResponse.errorCode));
735 return new BoschSHCException(String.format(
736 "Request for info of device %s failed with status code %d", deviceId, statusCode));
741 public UserDefinedState getUserStateInfo(String stateId)
742 throws BoschSHCException, InterruptedException, TimeoutException, ExecutionException {
744 BoschHttpClient locaHttpClient = this.httpClient;
745 if (locaHttpClient == null) {
746 throw new BoschSHCException(HTTP_CLIENT_NOT_INITIALIZED);
749 String url = locaHttpClient.getBoschSmartHomeUrl(String.format("userdefinedstates/%s", stateId));
750 Request request = locaHttpClient.createRequest(url, GET);
752 return locaHttpClient.sendRequest(request, UserDefinedState.class, UserDefinedState::isValid,
753 (Integer statusCode, String content) -> {
754 JsonRestExceptionResponse errorResponse = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content,
755 JsonRestExceptionResponse.class);
756 if (errorResponse != null && JsonRestExceptionResponse.isValid(errorResponse)) {
757 if (errorResponse.errorCode.equals(JsonRestExceptionResponse.ENTITY_NOT_FOUND)) {
758 return new BoschSHCException("@text/offline.conf-error.invalid-state-id");
760 return new BoschSHCException(String.format(
761 "Request for info of user-defined state %s failed with status code %d and error code %s",
762 stateId, errorResponse.statusCode, errorResponse.errorCode));
765 return new BoschSHCException(
766 String.format("Request for info of user-defined state %s failed with status code %d",
767 stateId, statusCode));
773 * Query the Bosch Smart Home Controller for the state of the given device.
775 * The URL used for retrieving the state has the following structure:
778 * https://{IP}:8444/smarthome/devices/{deviceId}/services/{serviceName}/state
781 * @param deviceId Id of device to get state for
782 * @param stateName Name of the state to query
783 * @param stateClass Class to convert the resulting JSON to
784 * @return the deserialized state object, may be <code>null</code>
785 * @throws ExecutionException
786 * @throws TimeoutException
787 * @throws InterruptedException
788 * @throws BoschSHCException
790 public <T extends BoschSHCServiceState> @Nullable T getState(String deviceId, String stateName, Class<T> stateClass)
791 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
793 BoschHttpClient localHttpClient = this.httpClient;
794 if (localHttpClient == null) {
795 logger.warn(HTTP_CLIENT_NOT_INITIALIZED);
799 String url = localHttpClient.getServiceStateUrl(stateName, deviceId, stateClass);
800 logger.debug("getState(): Requesting \"{}\" from Bosch: {} via {}", stateName, deviceId, url);
801 return getState(localHttpClient, url, stateClass);
805 * Queries the Bosch Smart Home Controller for the state using an explicit endpoint.
807 * @param <T> Type to which the resulting JSON should be deserialized to
808 * @param endpoint The destination endpoint part of the URL
809 * @param stateClass Class to convert the resulting JSON to
810 * @return the deserialized state object, may be <code>null</code>
811 * @throws InterruptedException
812 * @throws TimeoutException
813 * @throws ExecutionException
814 * @throws BoschSHCException
816 public <T extends BoschSHCServiceState> @Nullable T getState(String endpoint, Class<T> stateClass)
817 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
819 BoschHttpClient localHttpClient = this.httpClient;
820 if (localHttpClient == null) {
821 logger.warn(HTTP_CLIENT_NOT_INITIALIZED);
825 String url = localHttpClient.getBoschSmartHomeUrl(endpoint);
826 logger.debug("getState(): Requesting from Bosch: {}", url);
827 return getState(localHttpClient, url, stateClass);
831 * Sends a HTTP GET request in order to retrieve a state from the Bosch Smart Home Controller.
833 * @param <T> Type to which the resulting JSON should be deserialized to
834 * @param httpClient HTTP client used for sending the request
835 * @param url URL at which the state should be retrieved
836 * @param stateClass Class to convert the resulting JSON to
837 * @return the deserialized state object, may be <code>null</code>
838 * @throws InterruptedException
839 * @throws TimeoutException
840 * @throws ExecutionException
841 * @throws BoschSHCException
843 protected <T extends BoschSHCServiceState> @Nullable T getState(BoschHttpClient httpClient, String url,
844 Class<T> stateClass) throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
845 Request request = httpClient.createRequest(url, GET).header("Accept", "application/json");
847 ContentResponse contentResponse = request.send();
849 String content = contentResponse.getContentAsString();
850 logger.debug("getState(): Request complete: [{}] - return code: {}", content, contentResponse.getStatus());
852 int statusCode = contentResponse.getStatus();
853 if (statusCode != 200) {
854 JsonRestExceptionResponse errorResponse = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content,
855 JsonRestExceptionResponse.class);
856 if (errorResponse != null) {
857 throw new BoschSHCException(
858 String.format("State request with URL %s failed with status code %d and error code %s", url,
859 errorResponse.statusCode, errorResponse.errorCode));
861 throw new BoschSHCException(
862 String.format("State request with URL %s failed with status code %d", url, statusCode));
867 T state = BoschSHCServiceState.fromJson(content, stateClass);
869 throw new BoschSHCException(String.format("Received invalid, expected type %s", stateClass.getName()));
875 * Sends a state change for a device to the controller
877 * @param deviceId Id of device to change state for
878 * @param serviceName Name of service of device to change state for
879 * @param state New state data to set for service
881 * @return Response of request
882 * @throws InterruptedException
883 * @throws ExecutionException
884 * @throws TimeoutException
886 public <T extends BoschSHCServiceState> @Nullable Response putState(String deviceId, String serviceName, T state)
887 throws InterruptedException, TimeoutException, ExecutionException {
889 BoschHttpClient localHttpClient = this.httpClient;
890 if (localHttpClient == null) {
891 logger.warn(HTTP_CLIENT_NOT_INITIALIZED);
896 String url = localHttpClient.getServiceStateUrl(serviceName, deviceId, state.getClass());
897 Request request = localHttpClient.createRequest(url, PUT, state);
900 return request.send();
904 * Sends a HTTP POST request without a request body to the given endpoint.
906 * @param endpoint The destination endpoint part of the URL
907 * @return the HTTP response
908 * @throws InterruptedException
909 * @throws TimeoutException
910 * @throws ExecutionException
912 public @Nullable Response postAction(String endpoint)
913 throws InterruptedException, TimeoutException, ExecutionException {
914 return postAction(endpoint, null);
918 * Sends a HTTP POST request with a request body to the given endpoint.
920 * @param <T> Type of the request
921 * @param endpoint The destination endpoint part of the URL
922 * @param requestBody object representing the request body to be sent, may be <code>null</code>
923 * @return the HTTP response
924 * @throws InterruptedException
925 * @throws TimeoutException
926 * @throws ExecutionException
928 public <T extends BoschSHCServiceState> @Nullable Response postAction(String endpoint, @Nullable T requestBody)
929 throws InterruptedException, TimeoutException, ExecutionException {
931 BoschHttpClient localHttpClient = this.httpClient;
932 if (localHttpClient == null) {
933 logger.warn(HTTP_CLIENT_NOT_INITIALIZED);
937 String url = localHttpClient.getBoschSmartHomeUrl(endpoint);
938 Request request = localHttpClient.createRequest(url, POST, requestBody);
939 return request.send();
942 public @Nullable DeviceServiceData getServiceData(String deviceId, String serviceName)
943 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
945 BoschHttpClient localHttpClient = this.httpClient;
946 if (localHttpClient == null) {
947 logger.warn(HTTP_CLIENT_NOT_INITIALIZED);
951 String url = localHttpClient.getServiceUrl(serviceName, deviceId);
952 logger.debug("getState(): Requesting \"{}\" from Bosch: {} via {}", serviceName, deviceId, url);
953 return getState(localHttpClient, url, DeviceServiceData.class);