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;
25 import java.util.Optional;
27 import java.util.concurrent.ExecutionException;
28 import java.util.concurrent.ScheduledFuture;
29 import java.util.concurrent.TimeUnit;
30 import java.util.concurrent.TimeoutException;
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.eclipse.jdt.annotation.Nullable;
34 import org.eclipse.jetty.client.api.ContentResponse;
35 import org.eclipse.jetty.client.api.Request;
36 import org.eclipse.jetty.client.api.Response;
37 import org.eclipse.jetty.http.HttpStatus;
38 import org.eclipse.jetty.util.ssl.SslContextFactory;
39 import org.openhab.binding.boschshc.internal.devices.BoschDeviceIdUtils;
40 import org.openhab.binding.boschshc.internal.devices.BoschSHCBindingConstants;
41 import org.openhab.binding.boschshc.internal.devices.BoschSHCHandler;
42 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Device;
43 import org.openhab.binding.boschshc.internal.devices.bridge.dto.DeviceServiceData;
44 import org.openhab.binding.boschshc.internal.devices.bridge.dto.LongPollResult;
45 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Message;
46 import org.openhab.binding.boschshc.internal.devices.bridge.dto.PublicInformation;
47 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Room;
48 import org.openhab.binding.boschshc.internal.devices.bridge.dto.Scenario;
49 import org.openhab.binding.boschshc.internal.devices.bridge.dto.UserDefinedState;
50 import org.openhab.binding.boschshc.internal.discovery.ThingDiscoveryService;
51 import org.openhab.binding.boschshc.internal.exceptions.BoschSHCException;
52 import org.openhab.binding.boschshc.internal.exceptions.LongPollingFailedException;
53 import org.openhab.binding.boschshc.internal.exceptions.PairingFailedException;
54 import org.openhab.binding.boschshc.internal.serialization.GsonUtils;
55 import org.openhab.binding.boschshc.internal.services.dto.BoschSHCServiceState;
56 import org.openhab.binding.boschshc.internal.services.dto.JsonRestExceptionResponse;
57 import org.openhab.core.library.types.StringType;
58 import org.openhab.core.thing.Bridge;
59 import org.openhab.core.thing.Channel;
60 import org.openhab.core.thing.ChannelUID;
61 import org.openhab.core.thing.Thing;
62 import org.openhab.core.thing.ThingStatus;
63 import org.openhab.core.thing.ThingStatusDetail;
64 import org.openhab.core.thing.binding.BaseBridgeHandler;
65 import org.openhab.core.thing.binding.ThingHandler;
66 import org.openhab.core.thing.binding.ThingHandlerService;
67 import org.openhab.core.types.Command;
68 import org.openhab.core.types.RefreshType;
69 import org.osgi.framework.Bundle;
70 import org.osgi.framework.FrameworkUtil;
71 import org.slf4j.Logger;
72 import org.slf4j.LoggerFactory;
74 import com.google.gson.JsonElement;
75 import com.google.gson.reflect.TypeToken;
78 * Representation of a connection with a Bosch Smart Home Controller bridge.
80 * @author Stefan Kästle - Initial contribution
81 * @author Gerd Zanker - added HttpClient with pairing support
82 * @author Christian Oeing - refactorings of e.g. server registration
83 * @author David Pace - Added support for custom endpoints and HTTP POST requests
84 * @author Gerd Zanker - added thing discovery
87 public class BridgeHandler extends BaseBridgeHandler {
89 private static final String HTTP_CLIENT_NOT_INITIALIZED = "HttpClient not initialized";
91 private final Logger logger = LoggerFactory.getLogger(BridgeHandler.class);
94 * Handler to do long polling.
96 private final LongPolling longPolling;
99 * HTTP client for all communications to and from the bridge.
101 * This member is package-protected to enable mocking in unit tests.
103 /* package */ @Nullable
104 BoschHttpClient httpClient;
106 private @Nullable ScheduledFuture<?> scheduledPairing;
109 * SHC thing/device discovery service instance.
110 * Registered and unregistered if service is actived/deactived.
111 * Used to scan for things after bridge is paired with SHC.
113 private @Nullable ThingDiscoveryService thingDiscoveryService;
115 private final ScenarioHandler scenarioHandler;
117 public BridgeHandler(Bridge bridge) {
119 scenarioHandler = new ScenarioHandler();
121 this.longPolling = new LongPolling(this.scheduler, this::handleLongPollResult, this::handleLongPollFailure);
125 public Collection<Class<? extends ThingHandlerService>> getServices() {
126 return Set.of(ThingDiscoveryService.class);
130 public void initialize() {
131 Bundle bundle = FrameworkUtil.getBundle(getClass());
132 if (bundle != null) {
133 logger.debug("Initialize {} Version {}", bundle.getSymbolicName(), bundle.getVersion());
136 // Read configuration
137 BridgeConfiguration config = getConfigAs(BridgeConfiguration.class);
139 String ipAddress = config.ipAddress.trim();
140 if (ipAddress.isEmpty()) {
141 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
142 "@text/offline.conf-error-empty-ip");
146 String password = config.password.trim();
147 if (password.isEmpty()) {
148 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
149 "@text/offline.conf-error-empty-password");
153 SslContextFactory factory;
155 // prepare SSL key and certificates
156 factory = new BoschSslUtil(ipAddress).getSslContextFactory();
157 } catch (PairingFailedException e) {
158 logger.debug("Error while obtaining SSL context factory.", e);
159 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
160 "@text/offline.conf-error-ssl");
164 // Instantiate HttpClient with the SslContextFactory
165 BoschHttpClient localHttpClient = this.httpClient = new BoschHttpClient(ipAddress, password, factory);
169 localHttpClient.start();
170 } catch (Exception e) {
171 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
172 String.format("Could not create http connection to controller: %s", e.getMessage()));
176 // general checks are OK, therefore set the status to unknown and wait for initial access
177 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE);
179 // Initialize bridge in the background.
180 // Start initial access the first time
181 scheduleInitialAccess(localHttpClient);
185 public void dispose() {
186 // Cancel scheduled pairing.
188 ScheduledFuture<?> localScheduledPairing = this.scheduledPairing;
189 if (localScheduledPairing != null) {
190 localScheduledPairing.cancel(true);
191 this.scheduledPairing = null;
194 // Stop long polling.
195 this.longPolling.stop();
198 BoschHttpClient localHttpClient = this.httpClient;
199 if (localHttpClient != null) {
201 localHttpClient.stop();
202 } catch (Exception e) {
203 logger.debug("HttpClient failed on bridge disposal: {}", e.getMessage(), e);
205 this.httpClient = null;
212 public void handleCommand(ChannelUID channelUID, Command command) {
213 // commands are handled by individual device handlers
214 BoschHttpClient localHttpClient = httpClient;
215 if (BoschSHCBindingConstants.CHANNEL_TRIGGER_SCENARIO.equals(channelUID.getId())
216 && !RefreshType.REFRESH.equals(command) && localHttpClient != null) {
217 scenarioHandler.triggerScenario(localHttpClient, command.toString());
222 * Schedule the initial access.
223 * Use a delay if pairing fails and next retry is scheduled.
225 private void scheduleInitialAccess(BoschHttpClient httpClient) {
226 this.scheduledPairing = scheduler.schedule(() -> initialAccess(httpClient), 15, TimeUnit.SECONDS);
230 * Execute the initial access.
231 * Uses the HTTP Bosch SHC client
232 * to check if access if possible
233 * pairs this Bosch SHC Bridge with the SHC if necessary
234 * and starts the first log poll.
236 * This method is package-protected to enable unit testing.
238 /* package */ void initialAccess(BoschHttpClient httpClient) {
239 logger.debug("Initializing Bosch SHC Bridge: {} - HTTP client is: {}", this, httpClient);
242 // check if SCH is offline
243 if (!httpClient.isOnline()) {
244 // update status already if access is not possible
245 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE,
246 "@text/offline.conf-error-offline");
247 // restart later initial access
248 scheduleInitialAccess(httpClient);
253 // check if SHC access is not possible and pairing necessary
254 if (!httpClient.isAccessPossible()) {
255 // update status description to show pairing test
256 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE,
257 "@text/offline.conf-error-pairing");
258 if (!httpClient.doPairing()) {
259 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
260 "@text/offline.conf-error-pairing");
262 // restart initial access - needed also in case of successful pairing to check access again
263 scheduleInitialAccess(httpClient);
267 // SHC is online and access should possible
268 if (!checkBridgeAccess()) {
269 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
270 "@text/offline.not-reachable");
271 // restart initial access
272 scheduleInitialAccess(httpClient);
276 // do thing discovery after pairing
277 final ThingDiscoveryService discovery = thingDiscoveryService;
278 if (discovery != null) {
282 // start long polling loop
283 this.updateStatus(ThingStatus.ONLINE);
284 startLongPolling(httpClient);
286 } catch (InterruptedException e) {
287 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE, "@text/offline.interrupted");
288 Thread.currentThread().interrupt();
292 private void startLongPolling(BoschHttpClient httpClient) {
294 this.longPolling.start(httpClient);
295 } catch (LongPollingFailedException e) {
296 this.handleLongPollFailure(e);
301 * Check the bridge access by sending an HTTP request.
302 * Does not throw any exception in case the request fails.
304 public boolean checkBridgeAccess() throws InterruptedException {
306 BoschHttpClient localHttpClient = this.httpClient;
308 if (localHttpClient == null) {
313 logger.debug("Sending http request to BoschSHC to check access: {}", localHttpClient);
314 String url = localHttpClient.getBoschSmartHomeUrl("devices");
315 ContentResponse contentResponse = localHttpClient.createRequest(url, GET).send();
317 // check HTTP status code
318 if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
319 logger.debug("Access check failed with status code: {}", contentResponse.getStatus());
325 } catch (TimeoutException | ExecutionException e) {
326 logger.warn("Access check failed because of {}!", e.getMessage());
332 * Get a list of connected devices from the Smart-Home Controller
334 * @throws InterruptedException in case bridge is stopped
336 public List<Device> getDevices() throws InterruptedException {
338 BoschHttpClient localHttpClient = this.httpClient;
339 if (localHttpClient == null) {
340 return Collections.emptyList();
344 logger.trace("Sending http request to Bosch to request devices: {}", localHttpClient);
345 String url = localHttpClient.getBoschSmartHomeUrl("devices");
346 ContentResponse contentResponse = localHttpClient.createRequest(url, GET).send();
348 // check HTTP status code
349 if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
350 logger.debug("Request devices failed with status code: {}", contentResponse.getStatus());
351 return Collections.emptyList();
354 String content = contentResponse.getContentAsString();
355 logger.trace("Request devices completed with success: {} - status code: {}", content,
356 contentResponse.getStatus());
358 Type collectionType = new TypeToken<ArrayList<Device>>() {
360 List<Device> nullableDevices = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content, collectionType);
361 return Optional.ofNullable(nullableDevices).orElse(Collections.emptyList());
362 } catch (TimeoutException | ExecutionException e) {
363 logger.debug("Request devices failed because of {}!", e.getMessage(), e);
364 return Collections.emptyList();
368 public List<UserDefinedState> getUserStates() throws InterruptedException {
370 BoschHttpClient localHttpClient = this.httpClient;
371 if (localHttpClient == null) {
376 logger.trace("Sending http request to Bosch to request user-defined states: {}", localHttpClient);
377 String url = localHttpClient.getBoschSmartHomeUrl("userdefinedstates");
378 ContentResponse contentResponse = localHttpClient.createRequest(url, GET).send();
380 // check HTTP status code
381 if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
382 logger.debug("Request devices failed with status code: {}", contentResponse.getStatus());
386 String content = contentResponse.getContentAsString();
387 logger.trace("Request devices completed with success: {} - status code: {}", content,
388 contentResponse.getStatus());
390 Type collectionType = new TypeToken<ArrayList<UserDefinedState>>() {
392 List<UserDefinedState> nullableUserStates = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content,
394 return Optional.ofNullable(nullableUserStates).orElse(Collections.emptyList());
395 } catch (TimeoutException | ExecutionException e) {
396 logger.debug("Request user-defined states failed because of {}!", e.getMessage(), e);
402 * Get a list of rooms from the Smart-Home controller
404 * @throws InterruptedException in case bridge is stopped
406 public List<Room> getRooms() throws InterruptedException {
407 List<Room> emptyRooms = new ArrayList<>();
409 BoschHttpClient localHttpClient = this.httpClient;
410 if (localHttpClient != null) {
412 logger.trace("Sending http request to Bosch to request rooms");
413 String url = localHttpClient.getBoschSmartHomeUrl("rooms");
414 ContentResponse contentResponse = localHttpClient.createRequest(url, GET).send();
416 // check HTTP status code
417 if (!HttpStatus.getCode(contentResponse.getStatus()).isSuccess()) {
418 logger.debug("Request rooms failed with status code: {}", contentResponse.getStatus());
422 String content = contentResponse.getContentAsString();
423 logger.trace("Request rooms completed with success: {} - status code: {}", content,
424 contentResponse.getStatus());
426 Type collectionType = new TypeToken<ArrayList<Room>>() {
429 ArrayList<Room> rooms = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content, collectionType);
430 return Objects.requireNonNullElse(rooms, emptyRooms);
431 } catch (TimeoutException | ExecutionException e) {
432 logger.debug("Request rooms failed because of {}!", e.getMessage());
441 * Get public information from Bosch SHC.
443 public PublicInformation getPublicInformation()
444 throws InterruptedException, BoschSHCException, ExecutionException, TimeoutException {
446 BoschHttpClient localHttpClient = this.httpClient;
447 if (localHttpClient == null) {
448 throw new BoschSHCException(HTTP_CLIENT_NOT_INITIALIZED);
451 String url = localHttpClient.getPublicInformationUrl();
452 Request request = localHttpClient.createRequest(url, GET);
454 return localHttpClient.sendRequest(request, PublicInformation.class, PublicInformation::isValid, null);
457 public boolean registerDiscoveryListener(ThingDiscoveryService listener) {
458 if (thingDiscoveryService == null) {
459 thingDiscoveryService = listener;
466 public boolean unregisterDiscoveryListener() {
467 if (thingDiscoveryService != null) {
468 thingDiscoveryService = null;
476 * Bridge callback handler for the results of long polls.
478 * It will check the results and
479 * forward the received states to the Bosch thing handlers.
481 * @param result Results from Long Polling
483 void handleLongPollResult(LongPollResult result) {
484 for (BoschSHCServiceState serviceState : result.result) {
485 if (serviceState instanceof DeviceServiceData deviceServiceData) {
486 handleDeviceServiceData(deviceServiceData);
487 } else if (serviceState instanceof UserDefinedState userDefinedState) {
488 handleUserDefinedState(userDefinedState);
489 } else if (serviceState instanceof Scenario scenario) {
490 final Channel channel = this.getThing().getChannel(BoschSHCBindingConstants.CHANNEL_SCENARIO_TRIGGERED);
491 if (channel != null && isLinked(channel.getUID())) {
492 updateState(channel.getUID(), new StringType(scenario.name));
494 } else if (serviceState instanceof Message message) {
495 handleMessage(message);
500 private void handleMessage(Message message) {
501 if (Message.SOURCE_TYPE_DEVICE.equals(message.sourceType) && message.sourceId != null) {
502 forwardMessageToDevice(message, message.sourceId);
506 private void forwardMessageToDevice(Message message, String deviceId) {
507 BoschSHCHandler deviceHandler = findDeviceHandler(deviceId);
508 if (deviceHandler == null) {
512 deviceHandler.processMessage(message);
516 private BoschSHCHandler findDeviceHandler(String deviceIdToFind) {
517 for (Thing childThing : getThing().getThings()) {
519 ThingHandler baseHandler = childThing.getHandler();
520 if (baseHandler instanceof BoschSHCHandler handler) {
522 String deviceId = handler.getBoschID();
524 if (deviceIdToFind.equals(deviceId)) {
533 * Processes a single long poll result.
535 * @param deviceServiceData object representing a single long poll result
537 private void handleDeviceServiceData(@Nullable DeviceServiceData deviceServiceData) {
538 if (deviceServiceData != null) {
539 JsonElement state = obtainState(deviceServiceData);
541 logger.debug("Got update for service {} of type {}: {}", deviceServiceData.id, deviceServiceData.type,
544 var updateDeviceId = deviceServiceData.deviceId;
545 if (updateDeviceId == null || state == null) {
549 logger.debug("Got update for device {}", updateDeviceId);
551 forwardStateToHandlers(deviceServiceData, state, updateDeviceId);
555 private void handleUserDefinedState(@Nullable UserDefinedState userDefinedState) {
556 if (userDefinedState != null) {
557 JsonElement state = GsonUtils.DEFAULT_GSON_INSTANCE.toJsonTree(userDefinedState.isState());
559 logger.debug("Got update for user-defined state {} with id {}: {}", userDefinedState.getName(),
560 userDefinedState.getId(), state);
562 var stateId = userDefinedState.getId();
563 if (stateId == null || state == null) {
567 logger.debug("Got update for user-defined state {}", userDefinedState);
569 forwardStateToHandlers(userDefinedState, state, stateId);
574 * Extracts the actual state object from the given {@link DeviceServiceData} instance.
576 * In some special cases like the <code>BatteryLevel</code> service the {@link DeviceServiceData} object itself
577 * contains the state.
578 * In all other cases, the state is contained in a sub-object named <code>state</code>.
580 * @param deviceServiceData the {@link DeviceServiceData} object from which the state should be obtained
581 * @return the state sub-object or the {@link DeviceServiceData} object itself
584 private JsonElement obtainState(DeviceServiceData deviceServiceData) {
585 // the battery level service receives no individual state object but rather requires the DeviceServiceData
587 if ("BatteryLevel".equals(deviceServiceData.id)) {
588 return GsonUtils.DEFAULT_GSON_INSTANCE.toJsonTree(deviceServiceData);
591 return deviceServiceData.state;
595 * Tries to find handlers for the device with the given ID and forwards the received state to the handlers.
597 * @param serviceData object representing updates received in long poll results
598 * @param state the received state object as JSON element
599 * @param updateDeviceId the ID of the device for which the state update was received
601 private void forwardStateToHandlers(BoschSHCServiceState serviceData, JsonElement state, String updateDeviceId) {
602 boolean handled = false;
603 final String serviceId = getServiceId(serviceData);
605 Bridge bridge = this.getThing();
606 for (Thing childThing : bridge.getThings()) {
607 // All children of this should implement BoschSHCHandler
609 ThingHandler baseHandler = childThing.getHandler();
610 if (baseHandler instanceof BoschSHCHandler handler) {
612 String deviceId = handler.getBoschID();
614 if (deviceId == null) {
618 logger.trace("Checking device {}, looking for {}", deviceId, updateDeviceId);
620 // handled is a boolean latch that stays true once it becomes true
621 // note that no short-circuiting operators are used, meaning that the method
622 // calls will always be evaluated, even if the latch is already true
623 handled |= notifyHandler(handler, deviceId, updateDeviceId, serviceId, state);
624 handled |= notifyParentHandler(handler, deviceId, updateDeviceId, serviceId, state);
626 logger.warn("longPoll: child handler for {} does not implement Bosch SHC handler", baseHandler);
631 logger.debug("Could not find a thing for device ID: {}", updateDeviceId);
636 * Notifies the given handler if its device ID exactly matches the device ID for which the update was received.
638 * @param handler the handler to be notified if applicable
639 * @param deviceId the device ID associated with the handler
640 * @param updateDeviceId the device ID for which the update was received
641 * @param serviceId the ID of the service for which the update was received
642 * @param state the received state object as JSON element
644 * @return <code>true</code> if the handler matched and was notified, <code>false</code> otherwise
646 private boolean notifyHandler(BoschSHCHandler handler, String deviceId, String updateDeviceId, String serviceId,
648 if (updateDeviceId.equals(deviceId)) {
649 logger.debug("Found handler {}, calling processUpdate() for service {} with state {}", handler, serviceId,
651 handler.processUpdate(serviceId, state);
658 * If an update is received for a logical child device and the given handler is the parent device handler, the
659 * parent handler is notified.
661 * @param handler the handler to be notified if applicable
662 * @param deviceId the device ID associated with the handler
663 * @param updateDeviceId the device ID for which the update was received
664 * @param serviceId the ID of the service for which the update was received
665 * @param state the received state object as JSON element
666 * @return <code>true</code> if the given handler was the corresponding parent handler and was notified,
667 * <code>false</code> otherwise
669 private boolean notifyParentHandler(BoschSHCHandler handler, String deviceId, String updateDeviceId,
670 String serviceId, JsonElement state) {
671 if (BoschDeviceIdUtils.isChildDeviceId(updateDeviceId)) {
672 String parentDeviceId = BoschDeviceIdUtils.getParentDeviceId(updateDeviceId);
673 if (parentDeviceId.equals(deviceId)) {
674 logger.debug("Notifying parent handler {} about update for child device for service {} with state {}",
675 handler, serviceId, state);
676 handler.processChildUpdate(updateDeviceId, serviceId, state);
683 private String getServiceId(BoschSHCServiceState serviceData) {
684 if (serviceData instanceof UserDefinedState userState) {
685 return userState.getId();
687 return ((DeviceServiceData) serviceData).id;
691 * Bridge callback handler for the failures during long polls.
693 * It will update the bridge status and try to access the SHC again.
695 * @param e error during long polling
697 void handleLongPollFailure(Throwable e) {
698 logger.warn("Long polling failed, will try to reconnect", e);
700 BoschHttpClient localHttpClient = this.httpClient;
701 if (localHttpClient == null) {
702 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
703 "@text/offline.long-polling-failed.http-client-null");
707 this.updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.UNKNOWN.NONE,
708 "@text/offline.long-polling-failed.trying-to-reconnect");
709 scheduleInitialAccess(localHttpClient);
712 public Device getDeviceInfo(String deviceId)
713 throws BoschSHCException, InterruptedException, TimeoutException, ExecutionException {
715 BoschHttpClient localHttpClient = this.httpClient;
716 if (localHttpClient == null) {
717 throw new BoschSHCException(HTTP_CLIENT_NOT_INITIALIZED);
720 String url = localHttpClient.getBoschSmartHomeUrl(String.format("devices/%s", deviceId));
721 Request request = localHttpClient.createRequest(url, GET);
723 return localHttpClient.sendRequest(request, Device.class, Device::isValid,
724 (Integer statusCode, String content) -> {
725 JsonRestExceptionResponse errorResponse = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content,
726 JsonRestExceptionResponse.class);
727 if (errorResponse != null && JsonRestExceptionResponse.isValid(errorResponse)) {
728 if (errorResponse.errorCode.equals(JsonRestExceptionResponse.ENTITY_NOT_FOUND)) {
729 return new BoschSHCException("@text/offline.conf-error.invalid-device-id");
731 return new BoschSHCException(String.format(
732 "Request for info of device %s failed with status code %d and error code %s",
733 deviceId, errorResponse.statusCode, errorResponse.errorCode));
736 return new BoschSHCException(String.format(
737 "Request for info of device %s failed with status code %d", deviceId, statusCode));
742 public UserDefinedState getUserStateInfo(String stateId)
743 throws BoschSHCException, InterruptedException, TimeoutException, ExecutionException {
745 BoschHttpClient locaHttpClient = this.httpClient;
746 if (locaHttpClient == null) {
747 throw new BoschSHCException(HTTP_CLIENT_NOT_INITIALIZED);
750 String url = locaHttpClient.getBoschSmartHomeUrl(String.format("userdefinedstates/%s", stateId));
751 Request request = locaHttpClient.createRequest(url, GET);
753 return locaHttpClient.sendRequest(request, UserDefinedState.class, UserDefinedState::isValid,
754 (Integer statusCode, String content) -> {
755 JsonRestExceptionResponse errorResponse = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content,
756 JsonRestExceptionResponse.class);
757 if (errorResponse != null && JsonRestExceptionResponse.isValid(errorResponse)) {
758 if (errorResponse.errorCode.equals(JsonRestExceptionResponse.ENTITY_NOT_FOUND)) {
759 return new BoschSHCException("@text/offline.conf-error.invalid-state-id");
761 return new BoschSHCException(String.format(
762 "Request for info of user-defined state %s failed with status code %d and error code %s",
763 stateId, errorResponse.statusCode, errorResponse.errorCode));
766 return new BoschSHCException(
767 String.format("Request for info of user-defined state %s failed with status code %d",
768 stateId, statusCode));
774 * Query the Bosch Smart Home Controller for the state of the given device.
776 * The URL used for retrieving the state has the following structure:
779 * https://{IP}:8444/smarthome/devices/{deviceId}/services/{serviceName}/state
782 * @param deviceId Id of device to get state for
783 * @param stateName Name of the state to query
784 * @param stateClass Class to convert the resulting JSON to
785 * @return the deserialized state object, may be <code>null</code>
786 * @throws ExecutionException
787 * @throws TimeoutException
788 * @throws InterruptedException
789 * @throws BoschSHCException
791 public <T extends BoschSHCServiceState> @Nullable T getState(String deviceId, String stateName, Class<T> stateClass)
792 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
794 BoschHttpClient localHttpClient = this.httpClient;
795 if (localHttpClient == null) {
796 logger.warn(HTTP_CLIENT_NOT_INITIALIZED);
800 String url = localHttpClient.getServiceStateUrl(stateName, deviceId, stateClass);
801 logger.debug("getState(): Requesting \"{}\" from Bosch: {} via {}", stateName, deviceId, url);
802 return getState(localHttpClient, url, stateClass);
806 * Queries the Bosch Smart Home Controller for the state using an explicit endpoint.
808 * @param <T> Type to which the resulting JSON should be deserialized to
809 * @param endpoint The destination endpoint part of the URL
810 * @param stateClass Class to convert the resulting JSON to
811 * @return the deserialized state object, may be <code>null</code>
812 * @throws InterruptedException
813 * @throws TimeoutException
814 * @throws ExecutionException
815 * @throws BoschSHCException
817 public <T extends BoschSHCServiceState> @Nullable T getState(String endpoint, Class<T> stateClass)
818 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
820 BoschHttpClient localHttpClient = this.httpClient;
821 if (localHttpClient == null) {
822 logger.warn(HTTP_CLIENT_NOT_INITIALIZED);
826 String url = localHttpClient.getBoschSmartHomeUrl(endpoint);
827 logger.debug("getState(): Requesting from Bosch: {}", url);
828 return getState(localHttpClient, url, stateClass);
832 * Sends a HTTP GET request in order to retrieve a state from the Bosch Smart Home Controller.
834 * @param <T> Type to which the resulting JSON should be deserialized to
835 * @param httpClient HTTP client used for sending the request
836 * @param url URL at which the state should be retrieved
837 * @param stateClass Class to convert the resulting JSON to
838 * @return the deserialized state object, may be <code>null</code>
839 * @throws InterruptedException
840 * @throws TimeoutException
841 * @throws ExecutionException
842 * @throws BoschSHCException
844 protected <T extends BoschSHCServiceState> @Nullable T getState(BoschHttpClient httpClient, String url,
845 Class<T> stateClass) throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
846 Request request = httpClient.createRequest(url, GET).header("Accept", "application/json");
848 ContentResponse contentResponse = request.send();
850 String content = contentResponse.getContentAsString();
851 logger.debug("getState(): Request complete: [{}] - return code: {}", content, contentResponse.getStatus());
853 int statusCode = contentResponse.getStatus();
854 if (statusCode != 200) {
855 JsonRestExceptionResponse errorResponse = GsonUtils.DEFAULT_GSON_INSTANCE.fromJson(content,
856 JsonRestExceptionResponse.class);
857 if (errorResponse != null) {
858 throw new BoschSHCException(
859 String.format("State request with URL %s failed with status code %d and error code %s", url,
860 errorResponse.statusCode, errorResponse.errorCode));
862 throw new BoschSHCException(
863 String.format("State request with URL %s failed with status code %d", url, statusCode));
868 T state = BoschSHCServiceState.fromJson(content, stateClass);
870 throw new BoschSHCException(String.format("Received invalid, expected type %s", stateClass.getName()));
876 * Sends a state change for a device to the controller
878 * @param deviceId Id of device to change state for
879 * @param serviceName Name of service of device to change state for
880 * @param state New state data to set for service
882 * @return Response of request
883 * @throws InterruptedException
884 * @throws ExecutionException
885 * @throws TimeoutException
887 public <T extends BoschSHCServiceState> @Nullable Response putState(String deviceId, String serviceName, T state)
888 throws InterruptedException, TimeoutException, ExecutionException {
890 BoschHttpClient localHttpClient = this.httpClient;
891 if (localHttpClient == null) {
892 logger.warn(HTTP_CLIENT_NOT_INITIALIZED);
897 String url = localHttpClient.getServiceStateUrl(serviceName, deviceId, state.getClass());
898 Request request = localHttpClient.createRequest(url, PUT, state);
901 return request.send();
905 * Sends a HTTP POST request without a request body to the given endpoint.
907 * @param endpoint The destination endpoint part of the URL
908 * @return the HTTP response
909 * @throws InterruptedException
910 * @throws TimeoutException
911 * @throws ExecutionException
913 public @Nullable Response postAction(String endpoint)
914 throws InterruptedException, TimeoutException, ExecutionException {
915 return postAction(endpoint, null);
919 * Sends a HTTP POST request with a request body to the given endpoint.
921 * @param <T> Type of the request
922 * @param endpoint The destination endpoint part of the URL
923 * @param requestBody object representing the request body to be sent, may be <code>null</code>
924 * @return the HTTP response
925 * @throws InterruptedException
926 * @throws TimeoutException
927 * @throws ExecutionException
929 public <T extends BoschSHCServiceState> @Nullable Response postAction(String endpoint, @Nullable T requestBody)
930 throws InterruptedException, TimeoutException, ExecutionException {
932 BoschHttpClient localHttpClient = this.httpClient;
933 if (localHttpClient == null) {
934 logger.warn(HTTP_CLIENT_NOT_INITIALIZED);
938 String url = localHttpClient.getBoschSmartHomeUrl(endpoint);
939 Request request = localHttpClient.createRequest(url, POST, requestBody);
940 return request.send();
943 public @Nullable DeviceServiceData getServiceData(String deviceId, String serviceName)
944 throws InterruptedException, TimeoutException, ExecutionException, BoschSHCException {
946 BoschHttpClient localHttpClient = this.httpClient;
947 if (localHttpClient == null) {
948 logger.warn(HTTP_CLIENT_NOT_INITIALIZED);
952 String url = localHttpClient.getServiceUrl(serviceName, deviceId);
953 logger.debug("getState(): Requesting \"{}\" from Bosch: {} via {}", serviceName, deviceId, url);
954 return getState(localHttpClient, url, DeviceServiceData.class);