2 * Copyright (c) 2010-2023 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.InnogyBindingConstants;
37 import org.openhab.binding.innogysmarthome.internal.client.entity.StatusResponse;
38 import org.openhab.binding.innogysmarthome.internal.client.entity.action.Action;
39 import org.openhab.binding.innogysmarthome.internal.client.entity.action.ShutterAction;
40 import org.openhab.binding.innogysmarthome.internal.client.entity.action.StateActionSetter;
41 import org.openhab.binding.innogysmarthome.internal.client.entity.capability.Capability;
42 import org.openhab.binding.innogysmarthome.internal.client.entity.capability.CapabilityState;
43 import org.openhab.binding.innogysmarthome.internal.client.entity.device.Device;
44 import org.openhab.binding.innogysmarthome.internal.client.entity.device.DeviceState;
45 import org.openhab.binding.innogysmarthome.internal.client.entity.device.Gateway;
46 import org.openhab.binding.innogysmarthome.internal.client.entity.device.State;
47 import org.openhab.binding.innogysmarthome.internal.client.entity.error.ErrorResponse;
48 import org.openhab.binding.innogysmarthome.internal.client.entity.location.Location;
49 import org.openhab.binding.innogysmarthome.internal.client.entity.message.Message;
50 import org.openhab.binding.innogysmarthome.internal.client.exception.ApiException;
51 import org.openhab.binding.innogysmarthome.internal.client.exception.AuthenticationException;
52 import org.openhab.binding.innogysmarthome.internal.client.exception.ControllerOfflineException;
53 import org.openhab.binding.innogysmarthome.internal.client.exception.InvalidActionTriggeredException;
54 import org.openhab.binding.innogysmarthome.internal.client.exception.RemoteAccessNotAllowedException;
55 import org.openhab.binding.innogysmarthome.internal.client.exception.ServiceUnavailableException;
56 import org.openhab.binding.innogysmarthome.internal.client.exception.SessionExistsException;
57 import org.openhab.binding.innogysmarthome.internal.client.exception.SessionNotFoundException;
58 import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
59 import org.openhab.core.auth.client.oauth2.OAuthClientService;
60 import org.openhab.core.auth.client.oauth2.OAuthException;
61 import org.openhab.core.auth.client.oauth2.OAuthResponseException;
62 import org.slf4j.Logger;
63 import org.slf4j.LoggerFactory;
65 import com.google.gson.Gson;
66 import com.google.gson.GsonBuilder;
67 import com.google.gson.JsonSyntaxException;
70 * The main client that handles the communication with the innogy SmartHome API service.
72 * @author Oliver Kuhl - Initial contribution
73 * @author Hilbrand Bouwkamp - Refactored to use openHAB http and oauth2 libraries
77 public class InnogyClient {
79 private static final String BEARER = "Bearer ";
80 private static final String CONTENT_TYPE = "application/json";
81 private static final int HTTP_REQUEST_TIMEOUT_SECONDS = 10;
82 private static final int HTTP_REQUEST_IDLE_TIMEOUT_SECONDS = 20;
84 private final Logger logger = LoggerFactory.getLogger(InnogyClient.class);
87 * date format as used in json in API. Example: 2016-07-11T10:55:52.3863424Z
89 private static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
91 private final Gson gson = new GsonBuilder().setDateFormat(DATE_FORMAT).create();
92 private final OAuthClientService oAuthService;
93 private final HttpClient httpClient;
94 private @Nullable Gateway bridgeDetails;
95 private String configVersion = "";
97 public InnogyClient(final OAuthClientService oAuthService, final HttpClient httpClient) {
98 this.oAuthService = oAuthService;
99 this.httpClient = httpClient;
105 * As the API returns the details of the SmartHome controller (SHC), the data is saved in {@link #bridgeDetails} and
106 * the {@link #configVersion} is set.
108 * @throws SessionExistsException thrown, if a session already exists
110 public void refreshStatus() throws IOException, ApiException, AuthenticationException {
111 logger.debug("Get innogy SmartHome status...");
112 final StatusResponse status = executeGet(API_URL_STATUS, StatusResponse.class);
114 bridgeDetails = status.gateway;
115 configVersion = bridgeDetails.getConfigVersion();
117 logger.debug("innogy SmartHome Status loaded. Configuration version is {}.", configVersion);
121 * Executes a HTTP GET request with default headers and returns data as object of type T.
123 * @param url request URL
124 * @param clazz type of data to return
125 * @return response content
127 private <T> T executeGet(final String url, final Class<T> clazz)
128 throws IOException, AuthenticationException, ApiException {
129 final ContentResponse response = request(httpClient.newRequest(url).method(HttpMethod.GET));
131 return gson.fromJson(response.getContentAsString(), clazz);
135 * Executes a HTTP GET request with default headers and returns data as List of type T.
137 * @param url request URL
138 * @param clazz array type of data to return as list
139 * @return response content (as a List)
141 private <T> List<T> executeGetList(final String url, final Class<T[]> clazz)
142 throws IOException, AuthenticationException, ApiException {
143 return Arrays.asList(executeGet(url, clazz));
147 * Executes a HTTP POST request with the given {@link Action} as content.
149 * @param url request URL
150 * @param action action to execute
152 private void executePost(final String url, final Action action)
153 throws IOException, AuthenticationException, ApiException {
154 final String json = gson.toJson(action);
155 logger.debug("Action {} JSON: {}", action.getType(), json);
157 request(httpClient.newRequest(url).method(HttpMethod.POST)
158 .content(new StringContentProvider(json), CONTENT_TYPE).accept(CONTENT_TYPE));
161 private ContentResponse request(final Request request) throws IOException, AuthenticationException, ApiException {
162 final ContentResponse response;
164 final AccessTokenResponse accessTokenResponse = getAccessTokenResponse();
166 response = request.header(HttpHeader.ACCEPT, CONTENT_TYPE)
167 .header(HttpHeader.AUTHORIZATION, BEARER + accessTokenResponse.getAccessToken())
168 .idleTimeout(HTTP_REQUEST_IDLE_TIMEOUT_SECONDS, TimeUnit.SECONDS)
169 .timeout(HTTP_REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS).send();
170 } catch (InterruptedException | TimeoutException | ExecutionException e) {
171 throw new IOException(e);
173 handleResponseErrors(response, request.getURI());
177 public AccessTokenResponse getAccessTokenResponse() throws AuthenticationException, IOException {
179 final AccessTokenResponse accessTokenResponse;
181 accessTokenResponse = oAuthService.getAccessTokenResponse();
182 } catch (OAuthException | OAuthResponseException e) {
183 throw new AuthenticationException("Error fetching access token: " + e.getMessage());
185 if (accessTokenResponse == null || accessTokenResponse.getAccessToken() == null
186 || accessTokenResponse.getAccessToken().isBlank()) {
187 throw new AuthenticationException("No innogy accesstoken. Is this thing authorized?");
189 return accessTokenResponse;
193 * Handles errors from the {@link ContentResponse} and throws the following errors:
195 * @param response response
196 * @param uri uri of api call made
197 * @throws ControllerOfflineException thrown, if the innogy SmartHome controller (SHC) is offline.
199 private void handleResponseErrors(final ContentResponse response, final URI uri) throws IOException, ApiException {
202 switch (response.getStatus()) {
203 case HttpStatus.OK_200:
204 logger.debug("Statuscode is OK: [{}]", uri);
206 case HttpStatus.SERVICE_UNAVAILABLE_503:
207 logger.debug("innogy service is unavailabe (503).");
208 throw new ServiceUnavailableException("innogy service is unavailabe (503).");
210 logger.debug("Statuscode {} is NOT OK: [{}]", response.getStatus(), uri);
212 content = response.getContentAsString();
213 logger.trace("Response error content: {}", content);
214 final ErrorResponse error = gson.fromJson(content, ErrorResponse.class);
217 logger.debug("Error without JSON message, code: {} / message: {}", response.getStatus(),
218 response.getReason());
219 throw new ApiException("Error code: " + response.getStatus());
222 switch (error.getCode()) {
223 case ErrorResponse.ERR_SESSION_EXISTS:
224 logger.debug("Session exists: {}", error);
225 throw new SessionExistsException(error.getDescription());
226 case ErrorResponse.ERR_SESSION_NOT_FOUND:
227 logger.debug("Session not found: {}", error);
228 throw new SessionNotFoundException(error.getDescription());
229 case ErrorResponse.ERR_CONTROLLER_OFFLINE:
230 logger.debug("Controller offline: {}", error);
231 throw new ControllerOfflineException(error.getDescription());
232 case ErrorResponse.ERR_REMOTE_ACCESS_NOT_ALLOWED:
234 "Remote access not allowed. Access is allowed only from the SHC device network.");
235 throw new RemoteAccessNotAllowedException(
236 "Remote access not allowed. Access is allowed only from the SHC device network.");
237 case ErrorResponse.ERR_INVALID_ACTION_TRIGGERED:
238 logger.debug("Invalid action triggered. Message: {}", error.getMessages());
239 throw new InvalidActionTriggeredException(error.getDescription());
241 logger.debug("Unknown error: {}", error);
242 throw new ApiException("Unknown error: " + error);
244 } catch (final JsonSyntaxException e) {
245 throw new ApiException("Invalid JSON syntax in error response: " + content);
251 * Sets a new state of a SwitchActuator.
253 public void setSwitchActuatorState(final String capabilityId, final boolean state)
254 throws IOException, ApiException, AuthenticationException {
255 executePost(API_URL_ACTION, new StateActionSetter(capabilityId, Capability.TYPE_SWITCHACTUATOR, state));
259 * Sets the dimmer level of a DimmerActuator.
261 public void setDimmerActuatorState(final String capabilityId, final int dimLevel)
262 throws IOException, ApiException, AuthenticationException {
263 executePost(API_URL_ACTION, new StateActionSetter(capabilityId, Capability.TYPE_DIMMERACTUATOR, dimLevel));
267 * Sets the roller shutter level of a RollerShutterActuator.
269 public void setRollerShutterActuatorState(final String capabilityId, final int rollerShutterLevel)
270 throws IOException, ApiException, AuthenticationException {
271 executePost(API_URL_ACTION,
272 new StateActionSetter(capabilityId, Capability.TYPE_ROLLERSHUTTERACTUATOR, rollerShutterLevel));
276 * Starts or stops moving a RollerShutterActuator
278 public void setRollerShutterAction(final String capabilityId,
279 final ShutterAction.ShutterActions rollerShutterAction)
280 throws IOException, ApiException, AuthenticationException {
281 executePost(API_URL_ACTION, new ShutterAction(capabilityId, rollerShutterAction));
285 * Sets a new state of a VariableActuator.
287 public void setVariableActuatorState(final String capabilityId, final boolean state)
288 throws IOException, ApiException, AuthenticationException {
289 executePost(API_URL_ACTION, new StateActionSetter(capabilityId, Capability.TYPE_VARIABLEACTUATOR, state));
293 * Sets the point temperature.
295 public void setPointTemperatureState(final String capabilityId, final double pointTemperature)
296 throws IOException, ApiException, AuthenticationException {
297 executePost(API_URL_ACTION,
298 new StateActionSetter(capabilityId, Capability.TYPE_THERMOSTATACTUATOR, pointTemperature));
302 * Sets the operation mode to "Auto" or "Manu".
304 public void setOperationMode(final String capabilityId, final boolean autoMode)
305 throws IOException, ApiException, AuthenticationException {
306 executePost(API_URL_ACTION,
307 new StateActionSetter(capabilityId, Capability.TYPE_THERMOSTATACTUATOR,
308 autoMode ? CapabilityState.STATE_VALUE_OPERATION_MODE_AUTO
309 : CapabilityState.STATE_VALUE_OPERATION_MODE_MANUAL));
313 * Sets the alarm state.
315 public void setAlarmActuatorState(final String capabilityId, final boolean alarmState)
316 throws IOException, ApiException, AuthenticationException {
317 executePost(API_URL_ACTION, new StateActionSetter(capabilityId, Capability.TYPE_ALARMACTUATOR, alarmState));
321 * Load the device and returns a {@link List} of {@link Device}s..
322 * VariableActuators are returned additionally (independent from the device ids),
323 * because VariableActuators are everytime available and never have a device state.
325 * @param deviceIds Ids of the devices to return
326 * @return List of Devices
328 public List<Device> getDevices(Collection<String> deviceIds)
329 throws IOException, ApiException, AuthenticationException {
330 logger.debug("Loading innogy devices...");
331 List<Device> devices = executeGetList(API_URL_DEVICE, Device[].class);
332 return devices.stream().filter(d -> isDeviceUsable(d, deviceIds)).collect(Collectors.toList());
336 * Loads the {@link Device} with the given deviceId.
338 public Device getDeviceById(final String deviceId) throws IOException, ApiException, AuthenticationException {
339 logger.debug("Loading device with id {}...", deviceId);
340 return executeGet(API_URL_DEVICE_ID.replace("{id}", deviceId), Device.class);
344 * Loads the states for all {@link Device}s.
346 public List<DeviceState> getDeviceStates() throws IOException, ApiException, AuthenticationException {
347 logger.debug("Loading device states...");
348 return executeGetList(API_URL_DEVICE_STATES, DeviceState[].class);
352 * Loads the device state for the given deviceId.
354 public State getDeviceStateByDeviceId(final String deviceId)
355 throws IOException, ApiException, AuthenticationException {
356 logger.debug("Loading device states for device id {}...", deviceId);
357 return executeGet(API_URL_DEVICE_ID_STATE.replace("{id}", deviceId), State.class);
361 * Loads the locations and returns a {@link List} of {@link Location}s.
363 * @return a List of Devices
365 public List<Location> getLocations() throws IOException, ApiException, AuthenticationException {
366 logger.debug("Loading locations...");
367 return executeGetList(API_URL_LOCATION, Location[].class);
371 * Loads and returns a {@link List} of {@link Capability}s for the given deviceId.
373 * @param deviceId the id of the {@link Device}
374 * @return capabilities of the device
376 public List<Capability> getCapabilitiesForDevice(final String deviceId)
377 throws IOException, ApiException, AuthenticationException {
378 logger.debug("Loading capabilities for device {}...", deviceId);
379 return executeGetList(API_URL_DEVICE_CAPABILITIES.replace("{id}", deviceId), Capability[].class);
383 * Loads and returns a {@link List} of all {@link Capability}s.
385 public List<Capability> getCapabilities() throws IOException, ApiException, AuthenticationException {
386 logger.debug("Loading capabilities...");
387 return executeGetList(API_URL_CAPABILITY, Capability[].class);
391 * Loads and returns a {@link List} of all {@link Capability}States.
393 public List<CapabilityState> getCapabilityStates() throws IOException, ApiException, AuthenticationException {
394 logger.debug("Loading capability states...");
395 return executeGetList(API_URL_CAPABILITY_STATES, CapabilityState[].class);
399 * Returns a {@link List} of all {@link Message}s.
401 public List<Message> getMessages() throws IOException, ApiException, AuthenticationException {
402 logger.debug("Loading messages...");
403 return executeGetList(API_URL_MESSAGE, Message[].class);
407 * @return the configVersion
409 public String getConfigVersion() {
410 return configVersion;
414 * Decides if a (discovered) device is usable (available and supported).
416 * @param device device to check
417 * @param activeDeviceIds active device id (devices with an according available device state)
418 * @return true when usable, otherwise false
420 private static boolean isDeviceUsable(Device device, Collection<String> activeDeviceIds) {
421 return activeDeviceIds.contains(device.getId())
422 || InnogyBindingConstants.DEVICE_VARIABLE_ACTUATOR.equals(device.getType());