2 * Copyright (c) 2010-2021 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.innogysmarthome.internal.client;
15 import static org.openhab.binding.innogysmarthome.internal.client.Constants.*;
17 import java.io.IOException;
20 import java.util.concurrent.ExecutionException;
21 import java.util.concurrent.TimeUnit;
22 import java.util.concurrent.TimeoutException;
23 import java.util.stream.Collectors;
25 import org.apache.commons.lang.StringUtils;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.eclipse.jetty.client.HttpClient;
29 import org.eclipse.jetty.client.api.ContentResponse;
30 import org.eclipse.jetty.client.api.Request;
31 import org.eclipse.jetty.client.util.StringContentProvider;
32 import org.eclipse.jetty.http.HttpHeader;
33 import org.eclipse.jetty.http.HttpMethod;
34 import org.eclipse.jetty.http.HttpStatus;
35 import org.openhab.binding.innogysmarthome.internal.client.entity.StatusResponse;
36 import org.openhab.binding.innogysmarthome.internal.client.entity.action.Action;
37 import org.openhab.binding.innogysmarthome.internal.client.entity.action.ShutterAction;
38 import org.openhab.binding.innogysmarthome.internal.client.entity.action.StateActionSetter;
39 import org.openhab.binding.innogysmarthome.internal.client.entity.capability.Capability;
40 import org.openhab.binding.innogysmarthome.internal.client.entity.capability.CapabilityState;
41 import org.openhab.binding.innogysmarthome.internal.client.entity.device.Device;
42 import org.openhab.binding.innogysmarthome.internal.client.entity.device.DeviceState;
43 import org.openhab.binding.innogysmarthome.internal.client.entity.device.Gateway;
44 import org.openhab.binding.innogysmarthome.internal.client.entity.device.State;
45 import org.openhab.binding.innogysmarthome.internal.client.entity.error.ErrorResponse;
46 import org.openhab.binding.innogysmarthome.internal.client.entity.location.Location;
47 import org.openhab.binding.innogysmarthome.internal.client.entity.message.Message;
48 import org.openhab.binding.innogysmarthome.internal.client.exception.ApiException;
49 import org.openhab.binding.innogysmarthome.internal.client.exception.AuthenticationException;
50 import org.openhab.binding.innogysmarthome.internal.client.exception.ControllerOfflineException;
51 import org.openhab.binding.innogysmarthome.internal.client.exception.InvalidActionTriggeredException;
52 import org.openhab.binding.innogysmarthome.internal.client.exception.RemoteAccessNotAllowedException;
53 import org.openhab.binding.innogysmarthome.internal.client.exception.ServiceUnavailableException;
54 import org.openhab.binding.innogysmarthome.internal.client.exception.SessionExistsException;
55 import org.openhab.binding.innogysmarthome.internal.client.exception.SessionNotFoundException;
56 import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
57 import org.openhab.core.auth.client.oauth2.OAuthClientService;
58 import org.openhab.core.auth.client.oauth2.OAuthException;
59 import org.openhab.core.auth.client.oauth2.OAuthResponseException;
60 import org.slf4j.Logger;
61 import org.slf4j.LoggerFactory;
63 import com.google.gson.Gson;
64 import com.google.gson.GsonBuilder;
65 import com.google.gson.JsonSyntaxException;
68 * The main client that handles the communication with the innogy SmartHome API service.
70 * @author Oliver Kuhl - Initial contribution
71 * @author Hilbrand Bouwkamp - Refactored to use openHAB http and oauth2 libraries
75 public class InnogyClient {
77 private static final String BEARER = "Bearer ";
78 private static final String CONTENT_TYPE = "application/json";
79 private static final int HTTP_REQUEST_TIMEOUT_SECONDS = 10;
80 private static final int HTTP_REQUEST_IDLE_TIMEOUT_SECONDS = 20;
82 private final Logger logger = LoggerFactory.getLogger(InnogyClient.class);
85 * date format as used in json in API. Example: 2016-07-11T10:55:52.3863424Z
87 private static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
89 private final Gson gson = new GsonBuilder().setDateFormat(DATE_FORMAT).create();
90 private final OAuthClientService oAuthService;
91 private final HttpClient httpClient;
92 private @Nullable Gateway bridgeDetails;
93 private String configVersion = "";
95 public InnogyClient(final OAuthClientService oAuthService, final HttpClient httpClient) {
96 this.oAuthService = oAuthService;
97 this.httpClient = httpClient;
103 * As the API returns the details of the SmartHome controller (SHC), the data is saved in {@link #bridgeDetails} and
104 * the {@link #configVersion} is set.
106 * @throws SessionExistsException thrown, if a session already exists
108 public void refreshStatus() throws IOException, ApiException, AuthenticationException {
109 logger.debug("Get innogy SmartHome status...");
110 final StatusResponse status = executeGet(API_URL_STATUS, StatusResponse.class);
112 bridgeDetails = status.gateway;
113 configVersion = bridgeDetails.getConfigVersion();
115 logger.debug("innogy SmartHome Status loaded. Configuration version is {}.", configVersion);
119 * Executes a HTTP GET request with default headers and returns data as object of type T.
121 * @param url request URL
122 * @param clazz type of data to return
123 * @return response content
125 private <T> T executeGet(final String url, final Class<T> clazz)
126 throws IOException, AuthenticationException, ApiException {
127 final ContentResponse response = request(httpClient.newRequest(url).method(HttpMethod.GET));
129 return gson.fromJson(response.getContentAsString(), clazz);
133 * Executes a HTTP GET request with default headers and returns data as List of type T.
135 * @param url request URL
136 * @param clazz array type of data to return as list
137 * @return response content (as a List)
139 private <T> List<T> executeGetList(final String url, final Class<T[]> clazz)
140 throws IOException, AuthenticationException, ApiException {
141 return Arrays.asList(executeGet(url, clazz));
145 * Executes a HTTP POST request with the given {@link Action} as content.
147 * @param url request URL
148 * @param action action to execute
150 private void executePost(final String url, final Action action)
151 throws IOException, AuthenticationException, ApiException {
152 final String json = gson.toJson(action);
153 logger.debug("Action {} JSON: {}", action.getType(), json);
155 request(httpClient.newRequest(url).method(HttpMethod.POST)
156 .content(new StringContentProvider(json), CONTENT_TYPE).accept(CONTENT_TYPE));
159 private ContentResponse request(final Request request) throws IOException, AuthenticationException, ApiException {
160 final ContentResponse response;
162 final AccessTokenResponse accessTokenResponse = getAccessTokenResponse();
164 response = request.header(HttpHeader.ACCEPT, CONTENT_TYPE)
165 .header(HttpHeader.AUTHORIZATION, BEARER + accessTokenResponse.getAccessToken())
166 .idleTimeout(HTTP_REQUEST_IDLE_TIMEOUT_SECONDS, TimeUnit.SECONDS)
167 .timeout(HTTP_REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS).send();
168 } catch (InterruptedException | TimeoutException | ExecutionException e) {
169 throw new IOException(e);
171 handleResponseErrors(response, request.getURI());
175 public AccessTokenResponse getAccessTokenResponse() throws AuthenticationException, IOException {
177 final AccessTokenResponse accessTokenResponse;
179 accessTokenResponse = oAuthService.getAccessTokenResponse();
180 } catch (OAuthException | OAuthResponseException e) {
181 throw new AuthenticationException("Error fetching access token: " + e.getMessage());
183 if (accessTokenResponse == null || StringUtils.isBlank(accessTokenResponse.getAccessToken())) {
184 throw new AuthenticationException("No innogy accesstoken. Is this thing authorized?");
186 return accessTokenResponse;
190 * Handles errors from the {@link ContentResponse} and throws the following errors:
192 * @param response response
193 * @param uri uri of api call made
194 * @throws ControllerOfflineException thrown, if the innogy SmartHome controller (SHC) is offline.
196 private void handleResponseErrors(final ContentResponse response, final URI uri) throws IOException, ApiException {
199 switch (response.getStatus()) {
200 case HttpStatus.OK_200:
201 logger.debug("Statuscode is OK: [{}]", uri);
203 case HttpStatus.SERVICE_UNAVAILABLE_503:
204 logger.debug("innogy service is unavailabe (503).");
205 throw new ServiceUnavailableException("innogy service is unavailabe (503).");
207 logger.debug("Statuscode {} is NOT OK: [{}]", response.getStatus(), uri);
209 content = response.getContentAsString();
210 logger.trace("Response error content: {}", content);
211 final ErrorResponse error = gson.fromJson(content, ErrorResponse.class);
214 logger.debug("Error without JSON message, code: {} / message: {}", response.getStatus(),
215 response.getReason());
216 throw new ApiException("Error code: " + response.getStatus());
219 switch (error.getCode()) {
220 case ErrorResponse.ERR_SESSION_EXISTS:
221 logger.debug("Session exists: {}", error);
222 throw new SessionExistsException(error.getDescription());
223 case ErrorResponse.ERR_SESSION_NOT_FOUND:
224 logger.debug("Session not found: {}", error);
225 throw new SessionNotFoundException(error.getDescription());
226 case ErrorResponse.ERR_CONTROLLER_OFFLINE:
227 logger.debug("Controller offline: {}", error);
228 throw new ControllerOfflineException(error.getDescription());
229 case ErrorResponse.ERR_REMOTE_ACCESS_NOT_ALLOWED:
231 "Remote access not allowed. Access is allowed only from the SHC device network.");
232 throw new RemoteAccessNotAllowedException(
233 "Remote access not allowed. Access is allowed only from the SHC device network.");
234 case ErrorResponse.ERR_INVALID_ACTION_TRIGGERED:
235 logger.debug("Invalid action triggered. Message: {}", error.getMessages());
236 throw new InvalidActionTriggeredException(error.getDescription());
238 logger.debug("Unknown error: {}", error);
239 throw new ApiException("Unknown error: " + error);
241 } catch (final JsonSyntaxException e) {
242 throw new ApiException("Invalid JSON syntax in error response: " + content);
248 * Sets a new state of a SwitchActuator.
250 public void setSwitchActuatorState(final String capabilityId, final boolean state)
251 throws IOException, ApiException, AuthenticationException {
252 executePost(API_URL_ACTION, new StateActionSetter(capabilityId, Capability.TYPE_SWITCHACTUATOR, state));
256 * Sets the dimmer level of a DimmerActuator.
258 public void setDimmerActuatorState(final String capabilityId, final int dimLevel)
259 throws IOException, ApiException, AuthenticationException {
260 executePost(API_URL_ACTION, new StateActionSetter(capabilityId, Capability.TYPE_DIMMERACTUATOR, dimLevel));
264 * Sets the roller shutter level of a RollerShutterActuator.
266 public void setRollerShutterActuatorState(final String capabilityId, final int rollerShutterLevel)
267 throws IOException, ApiException, AuthenticationException {
268 executePost(API_URL_ACTION,
269 new StateActionSetter(capabilityId, Capability.TYPE_ROLLERSHUTTERACTUATOR, rollerShutterLevel));
273 * Starts or stops moving a RollerShutterActuator
275 public void setRollerShutterAction(final String capabilityId,
276 final ShutterAction.ShutterActions rollerShutterAction)
277 throws IOException, ApiException, AuthenticationException {
278 executePost(API_URL_ACTION, new ShutterAction(capabilityId, rollerShutterAction));
282 * Sets a new state of a VariableActuator.
284 public void setVariableActuatorState(final String capabilityId, final boolean state)
285 throws IOException, ApiException, AuthenticationException {
286 executePost(API_URL_ACTION, new StateActionSetter(capabilityId, Capability.TYPE_VARIABLEACTUATOR, state));
290 * Sets the point temperature.
292 public void setPointTemperatureState(final String capabilityId, final double pointTemperature)
293 throws IOException, ApiException, AuthenticationException {
294 executePost(API_URL_ACTION,
295 new StateActionSetter(capabilityId, Capability.TYPE_THERMOSTATACTUATOR, pointTemperature));
299 * Sets the operation mode to "Auto" or "Manu".
301 public void setOperationMode(final String capabilityId, final boolean autoMode)
302 throws IOException, ApiException, AuthenticationException {
303 executePost(API_URL_ACTION,
304 new StateActionSetter(capabilityId, Capability.TYPE_THERMOSTATACTUATOR,
305 autoMode ? CapabilityState.STATE_VALUE_OPERATION_MODE_AUTO
306 : CapabilityState.STATE_VALUE_OPERATION_MODE_MANUAL));
310 * Sets the alarm state.
312 public void setAlarmActuatorState(final String capabilityId, final boolean alarmState)
313 throws IOException, ApiException, AuthenticationException {
314 executePost(API_URL_ACTION, new StateActionSetter(capabilityId, Capability.TYPE_ALARMACTUATOR, alarmState));
318 * Load the device and returns a {@link List} of {@link Device}s..
320 * @param deviceIds Ids of the devices to return
321 * @return List of Devices
323 public List<Device> getDevices(Collection<String> deviceIds)
324 throws IOException, ApiException, AuthenticationException {
325 logger.debug("Loading innogy devices...");
326 List<Device> devices = executeGetList(API_URL_DEVICE, Device[].class);
327 return devices.stream().filter(d -> deviceIds.contains(d.getId())).collect(Collectors.toList());
331 * Loads the {@link Device} with the given deviceId.
333 public Device getDeviceById(final String deviceId) throws IOException, ApiException, AuthenticationException {
334 logger.debug("Loading device with id {}...", deviceId);
335 return executeGet(API_URL_DEVICE_ID.replace("{id}", deviceId), Device.class);
339 * Loads the states for all {@link Device}s.
341 public List<DeviceState> getDeviceStates() throws IOException, ApiException, AuthenticationException {
342 logger.debug("Loading device states...");
343 return executeGetList(API_URL_DEVICE_STATES, DeviceState[].class);
347 * Loads the device state for the given deviceId.
349 public State getDeviceStateByDeviceId(final String deviceId)
350 throws IOException, ApiException, AuthenticationException {
351 logger.debug("Loading device states for device id {}...", deviceId);
352 return executeGet(API_URL_DEVICE_ID_STATE.replace("{id}", deviceId), State.class);
356 * Loads the locations and returns a {@link List} of {@link Location}s.
358 * @return a List of Devices
360 public List<Location> getLocations() throws IOException, ApiException, AuthenticationException {
361 logger.debug("Loading locations...");
362 return executeGetList(API_URL_LOCATION, Location[].class);
366 * Loads and returns a {@link List} of {@link Capability}s for the given deviceId.
368 * @param deviceId the id of the {@link Device}
369 * @return capabilities of the device
371 public List<Capability> getCapabilitiesForDevice(final String deviceId)
372 throws IOException, ApiException, AuthenticationException {
373 logger.debug("Loading capabilities for device {}...", deviceId);
374 return executeGetList(API_URL_DEVICE_CAPABILITIES.replace("{id}", deviceId), Capability[].class);
378 * Loads and returns a {@link List} of all {@link Capability}s.
380 public List<Capability> getCapabilities() throws IOException, ApiException, AuthenticationException {
381 logger.debug("Loading capabilities...");
382 return executeGetList(API_URL_CAPABILITY, Capability[].class);
386 * Loads and returns a {@link List} of all {@link Capability}States.
388 public List<CapabilityState> getCapabilityStates() throws IOException, ApiException, AuthenticationException {
389 logger.debug("Loading capability states...");
390 return executeGetList(API_URL_CAPABILITY_STATES, CapabilityState[].class);
394 * Returns a {@link List} of all {@link Message}s.
396 public List<Message> getMessages() throws IOException, ApiException, AuthenticationException {
397 logger.debug("Loading messages...");
398 return executeGetList(API_URL_MESSAGE, Message[].class);
402 * @return the configVersion
404 public String getConfigVersion() {
405 return configVersion;