2 * Copyright (c) 2010-2022 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;
19 import java.util.Arrays;
20 import java.util.Collection;
21 import java.util.List;
22 import java.util.concurrent.ExecutionException;
23 import java.util.concurrent.TimeUnit;
24 import java.util.concurrent.TimeoutException;
25 import java.util.stream.Collectors;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.eclipse.jetty.client.HttpClient;
30 import org.eclipse.jetty.client.api.ContentResponse;
31 import org.eclipse.jetty.client.api.Request;
32 import org.eclipse.jetty.client.util.StringContentProvider;
33 import org.eclipse.jetty.http.HttpHeader;
34 import org.eclipse.jetty.http.HttpMethod;
35 import org.eclipse.jetty.http.HttpStatus;
36 import org.openhab.binding.innogysmarthome.internal.client.entity.StatusResponse;
37 import org.openhab.binding.innogysmarthome.internal.client.entity.action.Action;
38 import org.openhab.binding.innogysmarthome.internal.client.entity.action.ShutterAction;
39 import org.openhab.binding.innogysmarthome.internal.client.entity.action.StateActionSetter;
40 import org.openhab.binding.innogysmarthome.internal.client.entity.capability.Capability;
41 import org.openhab.binding.innogysmarthome.internal.client.entity.capability.CapabilityState;
42 import org.openhab.binding.innogysmarthome.internal.client.entity.device.Device;
43 import org.openhab.binding.innogysmarthome.internal.client.entity.device.DeviceState;
44 import org.openhab.binding.innogysmarthome.internal.client.entity.device.Gateway;
45 import org.openhab.binding.innogysmarthome.internal.client.entity.device.State;
46 import org.openhab.binding.innogysmarthome.internal.client.entity.error.ErrorResponse;
47 import org.openhab.binding.innogysmarthome.internal.client.entity.location.Location;
48 import org.openhab.binding.innogysmarthome.internal.client.entity.message.Message;
49 import org.openhab.binding.innogysmarthome.internal.client.exception.ApiException;
50 import org.openhab.binding.innogysmarthome.internal.client.exception.AuthenticationException;
51 import org.openhab.binding.innogysmarthome.internal.client.exception.ControllerOfflineException;
52 import org.openhab.binding.innogysmarthome.internal.client.exception.InvalidActionTriggeredException;
53 import org.openhab.binding.innogysmarthome.internal.client.exception.RemoteAccessNotAllowedException;
54 import org.openhab.binding.innogysmarthome.internal.client.exception.ServiceUnavailableException;
55 import org.openhab.binding.innogysmarthome.internal.client.exception.SessionExistsException;
56 import org.openhab.binding.innogysmarthome.internal.client.exception.SessionNotFoundException;
57 import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
58 import org.openhab.core.auth.client.oauth2.OAuthClientService;
59 import org.openhab.core.auth.client.oauth2.OAuthException;
60 import org.openhab.core.auth.client.oauth2.OAuthResponseException;
61 import org.slf4j.Logger;
62 import org.slf4j.LoggerFactory;
64 import com.google.gson.Gson;
65 import com.google.gson.GsonBuilder;
66 import com.google.gson.JsonSyntaxException;
69 * The main client that handles the communication with the innogy SmartHome API service.
71 * @author Oliver Kuhl - Initial contribution
72 * @author Hilbrand Bouwkamp - Refactored to use openHAB http and oauth2 libraries
76 public class InnogyClient {
78 private static final String BEARER = "Bearer ";
79 private static final String CONTENT_TYPE = "application/json";
80 private static final int HTTP_REQUEST_TIMEOUT_SECONDS = 10;
81 private static final int HTTP_REQUEST_IDLE_TIMEOUT_SECONDS = 20;
83 private final Logger logger = LoggerFactory.getLogger(InnogyClient.class);
86 * date format as used in json in API. Example: 2016-07-11T10:55:52.3863424Z
88 private static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
90 private final Gson gson = new GsonBuilder().setDateFormat(DATE_FORMAT).create();
91 private final OAuthClientService oAuthService;
92 private final HttpClient httpClient;
93 private @Nullable Gateway bridgeDetails;
94 private String configVersion = "";
96 public InnogyClient(final OAuthClientService oAuthService, final HttpClient httpClient) {
97 this.oAuthService = oAuthService;
98 this.httpClient = httpClient;
104 * As the API returns the details of the SmartHome controller (SHC), the data is saved in {@link #bridgeDetails} and
105 * the {@link #configVersion} is set.
107 * @throws SessionExistsException thrown, if a session already exists
109 public void refreshStatus() throws IOException, ApiException, AuthenticationException {
110 logger.debug("Get innogy SmartHome status...");
111 final StatusResponse status = executeGet(API_URL_STATUS, StatusResponse.class);
113 bridgeDetails = status.gateway;
114 configVersion = bridgeDetails.getConfigVersion();
116 logger.debug("innogy SmartHome Status loaded. Configuration version is {}.", configVersion);
120 * Executes a HTTP GET request with default headers and returns data as object of type T.
122 * @param url request URL
123 * @param clazz type of data to return
124 * @return response content
126 private <T> T executeGet(final String url, final Class<T> clazz)
127 throws IOException, AuthenticationException, ApiException {
128 final ContentResponse response = request(httpClient.newRequest(url).method(HttpMethod.GET));
130 return gson.fromJson(response.getContentAsString(), clazz);
134 * Executes a HTTP GET request with default headers and returns data as List of type T.
136 * @param url request URL
137 * @param clazz array type of data to return as list
138 * @return response content (as a List)
140 private <T> List<T> executeGetList(final String url, final Class<T[]> clazz)
141 throws IOException, AuthenticationException, ApiException {
142 return Arrays.asList(executeGet(url, clazz));
146 * Executes a HTTP POST request with the given {@link Action} as content.
148 * @param url request URL
149 * @param action action to execute
151 private void executePost(final String url, final Action action)
152 throws IOException, AuthenticationException, ApiException {
153 final String json = gson.toJson(action);
154 logger.debug("Action {} JSON: {}", action.getType(), json);
156 request(httpClient.newRequest(url).method(HttpMethod.POST)
157 .content(new StringContentProvider(json), CONTENT_TYPE).accept(CONTENT_TYPE));
160 private ContentResponse request(final Request request) throws IOException, AuthenticationException, ApiException {
161 final ContentResponse response;
163 final AccessTokenResponse accessTokenResponse = getAccessTokenResponse();
165 response = request.header(HttpHeader.ACCEPT, CONTENT_TYPE)
166 .header(HttpHeader.AUTHORIZATION, BEARER + accessTokenResponse.getAccessToken())
167 .idleTimeout(HTTP_REQUEST_IDLE_TIMEOUT_SECONDS, TimeUnit.SECONDS)
168 .timeout(HTTP_REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS).send();
169 } catch (InterruptedException | TimeoutException | ExecutionException e) {
170 throw new IOException(e);
172 handleResponseErrors(response, request.getURI());
176 public AccessTokenResponse getAccessTokenResponse() throws AuthenticationException, IOException {
178 final AccessTokenResponse accessTokenResponse;
180 accessTokenResponse = oAuthService.getAccessTokenResponse();
181 } catch (OAuthException | OAuthResponseException e) {
182 throw new AuthenticationException("Error fetching access token: " + e.getMessage());
184 if (accessTokenResponse == null || accessTokenResponse.getAccessToken() == null
185 || accessTokenResponse.getAccessToken().isBlank()) {
186 throw new AuthenticationException("No innogy accesstoken. Is this thing authorized?");
188 return accessTokenResponse;
192 * Handles errors from the {@link ContentResponse} and throws the following errors:
194 * @param response response
195 * @param uri uri of api call made
196 * @throws ControllerOfflineException thrown, if the innogy SmartHome controller (SHC) is offline.
198 private void handleResponseErrors(final ContentResponse response, final URI uri) throws IOException, ApiException {
201 switch (response.getStatus()) {
202 case HttpStatus.OK_200:
203 logger.debug("Statuscode is OK: [{}]", uri);
205 case HttpStatus.SERVICE_UNAVAILABLE_503:
206 logger.debug("innogy service is unavailabe (503).");
207 throw new ServiceUnavailableException("innogy service is unavailabe (503).");
209 logger.debug("Statuscode {} is NOT OK: [{}]", response.getStatus(), uri);
211 content = response.getContentAsString();
212 logger.trace("Response error content: {}", content);
213 final ErrorResponse error = gson.fromJson(content, ErrorResponse.class);
216 logger.debug("Error without JSON message, code: {} / message: {}", response.getStatus(),
217 response.getReason());
218 throw new ApiException("Error code: " + response.getStatus());
221 switch (error.getCode()) {
222 case ErrorResponse.ERR_SESSION_EXISTS:
223 logger.debug("Session exists: {}", error);
224 throw new SessionExistsException(error.getDescription());
225 case ErrorResponse.ERR_SESSION_NOT_FOUND:
226 logger.debug("Session not found: {}", error);
227 throw new SessionNotFoundException(error.getDescription());
228 case ErrorResponse.ERR_CONTROLLER_OFFLINE:
229 logger.debug("Controller offline: {}", error);
230 throw new ControllerOfflineException(error.getDescription());
231 case ErrorResponse.ERR_REMOTE_ACCESS_NOT_ALLOWED:
233 "Remote access not allowed. Access is allowed only from the SHC device network.");
234 throw new RemoteAccessNotAllowedException(
235 "Remote access not allowed. Access is allowed only from the SHC device network.");
236 case ErrorResponse.ERR_INVALID_ACTION_TRIGGERED:
237 logger.debug("Invalid action triggered. Message: {}", error.getMessages());
238 throw new InvalidActionTriggeredException(error.getDescription());
240 logger.debug("Unknown error: {}", error);
241 throw new ApiException("Unknown error: " + error);
243 } catch (final JsonSyntaxException e) {
244 throw new ApiException("Invalid JSON syntax in error response: " + content);
250 * Sets a new state of a SwitchActuator.
252 public void setSwitchActuatorState(final String capabilityId, final boolean state)
253 throws IOException, ApiException, AuthenticationException {
254 executePost(API_URL_ACTION, new StateActionSetter(capabilityId, Capability.TYPE_SWITCHACTUATOR, state));
258 * Sets the dimmer level of a DimmerActuator.
260 public void setDimmerActuatorState(final String capabilityId, final int dimLevel)
261 throws IOException, ApiException, AuthenticationException {
262 executePost(API_URL_ACTION, new StateActionSetter(capabilityId, Capability.TYPE_DIMMERACTUATOR, dimLevel));
266 * Sets the roller shutter level of a RollerShutterActuator.
268 public void setRollerShutterActuatorState(final String capabilityId, final int rollerShutterLevel)
269 throws IOException, ApiException, AuthenticationException {
270 executePost(API_URL_ACTION,
271 new StateActionSetter(capabilityId, Capability.TYPE_ROLLERSHUTTERACTUATOR, rollerShutterLevel));
275 * Starts or stops moving a RollerShutterActuator
277 public void setRollerShutterAction(final String capabilityId,
278 final ShutterAction.ShutterActions rollerShutterAction)
279 throws IOException, ApiException, AuthenticationException {
280 executePost(API_URL_ACTION, new ShutterAction(capabilityId, rollerShutterAction));
284 * Sets a new state of a VariableActuator.
286 public void setVariableActuatorState(final String capabilityId, final boolean state)
287 throws IOException, ApiException, AuthenticationException {
288 executePost(API_URL_ACTION, new StateActionSetter(capabilityId, Capability.TYPE_VARIABLEACTUATOR, state));
292 * Sets the point temperature.
294 public void setPointTemperatureState(final String capabilityId, final double pointTemperature)
295 throws IOException, ApiException, AuthenticationException {
296 executePost(API_URL_ACTION,
297 new StateActionSetter(capabilityId, Capability.TYPE_THERMOSTATACTUATOR, pointTemperature));
301 * Sets the operation mode to "Auto" or "Manu".
303 public void setOperationMode(final String capabilityId, final boolean autoMode)
304 throws IOException, ApiException, AuthenticationException {
305 executePost(API_URL_ACTION,
306 new StateActionSetter(capabilityId, Capability.TYPE_THERMOSTATACTUATOR,
307 autoMode ? CapabilityState.STATE_VALUE_OPERATION_MODE_AUTO
308 : CapabilityState.STATE_VALUE_OPERATION_MODE_MANUAL));
312 * Sets the alarm state.
314 public void setAlarmActuatorState(final String capabilityId, final boolean alarmState)
315 throws IOException, ApiException, AuthenticationException {
316 executePost(API_URL_ACTION, new StateActionSetter(capabilityId, Capability.TYPE_ALARMACTUATOR, alarmState));
320 * Load the device and returns a {@link List} of {@link Device}s..
322 * @param deviceIds Ids of the devices to return
323 * @return List of Devices
325 public List<Device> getDevices(Collection<String> deviceIds)
326 throws IOException, ApiException, AuthenticationException {
327 logger.debug("Loading innogy devices...");
328 List<Device> devices = executeGetList(API_URL_DEVICE, Device[].class);
329 return devices.stream().filter(d -> deviceIds.contains(d.getId())).collect(Collectors.toList());
333 * Loads the {@link Device} with the given deviceId.
335 public Device getDeviceById(final String deviceId) throws IOException, ApiException, AuthenticationException {
336 logger.debug("Loading device with id {}...", deviceId);
337 return executeGet(API_URL_DEVICE_ID.replace("{id}", deviceId), Device.class);
341 * Loads the states for all {@link Device}s.
343 public List<DeviceState> getDeviceStates() throws IOException, ApiException, AuthenticationException {
344 logger.debug("Loading device states...");
345 return executeGetList(API_URL_DEVICE_STATES, DeviceState[].class);
349 * Loads the device state for the given deviceId.
351 public State getDeviceStateByDeviceId(final String deviceId)
352 throws IOException, ApiException, AuthenticationException {
353 logger.debug("Loading device states for device id {}...", deviceId);
354 return executeGet(API_URL_DEVICE_ID_STATE.replace("{id}", deviceId), State.class);
358 * Loads the locations and returns a {@link List} of {@link Location}s.
360 * @return a List of Devices
362 public List<Location> getLocations() throws IOException, ApiException, AuthenticationException {
363 logger.debug("Loading locations...");
364 return executeGetList(API_URL_LOCATION, Location[].class);
368 * Loads and returns a {@link List} of {@link Capability}s for the given deviceId.
370 * @param deviceId the id of the {@link Device}
371 * @return capabilities of the device
373 public List<Capability> getCapabilitiesForDevice(final String deviceId)
374 throws IOException, ApiException, AuthenticationException {
375 logger.debug("Loading capabilities for device {}...", deviceId);
376 return executeGetList(API_URL_DEVICE_CAPABILITIES.replace("{id}", deviceId), Capability[].class);
380 * Loads and returns a {@link List} of all {@link Capability}s.
382 public List<Capability> getCapabilities() throws IOException, ApiException, AuthenticationException {
383 logger.debug("Loading capabilities...");
384 return executeGetList(API_URL_CAPABILITY, Capability[].class);
388 * Loads and returns a {@link List} of all {@link Capability}States.
390 public List<CapabilityState> getCapabilityStates() throws IOException, ApiException, AuthenticationException {
391 logger.debug("Loading capability states...");
392 return executeGetList(API_URL_CAPABILITY_STATES, CapabilityState[].class);
396 * Returns a {@link List} of all {@link Message}s.
398 public List<Message> getMessages() throws IOException, ApiException, AuthenticationException {
399 logger.debug("Loading messages...");
400 return executeGetList(API_URL_MESSAGE, Message[].class);
404 * @return the configVersion
406 public String getConfigVersion() {
407 return configVersion;