]> git.basschouten.com Git - openhab-addons.git/blob
3a841962a3270c7cac08760f1d771d5b0e176d1b
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.innogysmarthome.internal.client;
14
15 import static org.openhab.binding.innogysmarthome.internal.client.Constants.*;
16
17 import java.io.IOException;
18 import java.net.URI;
19 import java.util.*;
20 import java.util.concurrent.ExecutionException;
21 import java.util.concurrent.TimeUnit;
22 import java.util.concurrent.TimeoutException;
23 import java.util.stream.Collectors;
24
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;
62
63 import com.google.gson.Gson;
64 import com.google.gson.GsonBuilder;
65 import com.google.gson.JsonSyntaxException;
66
67 /**
68  * The main client that handles the communication with the innogy SmartHome API service.
69  *
70  * @author Oliver Kuhl - Initial contribution
71  * @author Hilbrand Bouwkamp - Refactored to use openHAB http and oauth2 libraries
72  *
73  */
74 @NonNullByDefault
75 public class InnogyClient {
76
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;
81
82     private final Logger logger = LoggerFactory.getLogger(InnogyClient.class);
83
84     /**
85      * date format as used in json in API. Example: 2016-07-11T10:55:52.3863424Z
86      */
87     private static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
88
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 = "";
94
95     public InnogyClient(final OAuthClientService oAuthService, final HttpClient httpClient) {
96         this.oAuthService = oAuthService;
97         this.httpClient = httpClient;
98     }
99
100     /**
101      * Gets the status
102      *
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.
105      *
106      * @throws SessionExistsException thrown, if a session already exists
107      */
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);
111
112         bridgeDetails = status.gateway;
113         configVersion = bridgeDetails.getConfigVersion();
114
115         logger.debug("innogy SmartHome Status loaded. Configuration version is {}.", configVersion);
116     }
117
118     /**
119      * Executes a HTTP GET request with default headers and returns data as object of type T.
120      *
121      * @param url request URL
122      * @param clazz type of data to return
123      * @return response content
124      */
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));
128
129         return gson.fromJson(response.getContentAsString(), clazz);
130     }
131
132     /**
133      * Executes a HTTP GET request with default headers and returns data as List of type T.
134      *
135      * @param url request URL
136      * @param clazz array type of data to return as list
137      * @return response content (as a List)
138      */
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));
142     }
143
144     /**
145      * Executes a HTTP POST request with the given {@link Action} as content.
146      *
147      * @param url request URL
148      * @param action action to execute
149      */
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);
154
155         request(httpClient.newRequest(url).method(HttpMethod.POST)
156                 .content(new StringContentProvider(json), CONTENT_TYPE).accept(CONTENT_TYPE));
157     }
158
159     private ContentResponse request(final Request request) throws IOException, AuthenticationException, ApiException {
160         final ContentResponse response;
161         try {
162             final AccessTokenResponse accessTokenResponse = getAccessTokenResponse();
163
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);
170         }
171         handleResponseErrors(response, request.getURI());
172         return response;
173     }
174
175     public AccessTokenResponse getAccessTokenResponse() throws AuthenticationException, IOException {
176         @Nullable
177         final AccessTokenResponse accessTokenResponse;
178         try {
179             accessTokenResponse = oAuthService.getAccessTokenResponse();
180         } catch (OAuthException | OAuthResponseException e) {
181             throw new AuthenticationException("Error fetching access token: " + e.getMessage());
182         }
183         if (accessTokenResponse == null || StringUtils.isBlank(accessTokenResponse.getAccessToken())) {
184             throw new AuthenticationException("No innogy accesstoken. Is this thing authorized?");
185         }
186         return accessTokenResponse;
187     }
188
189     /**
190      * Handles errors from the {@link ContentResponse} and throws the following errors:
191      *
192      * @param response response
193      * @param uri uri of api call made
194      * @throws ControllerOfflineException thrown, if the innogy SmartHome controller (SHC) is offline.
195      */
196     private void handleResponseErrors(final ContentResponse response, final URI uri) throws IOException, ApiException {
197         String content = "";
198
199         switch (response.getStatus()) {
200             case HttpStatus.OK_200:
201                 logger.debug("Statuscode is OK: [{}]", uri);
202                 return;
203             case HttpStatus.SERVICE_UNAVAILABLE_503:
204                 logger.debug("innogy service is unavailabe (503).");
205                 throw new ServiceUnavailableException("innogy service is unavailabe (503).");
206             default:
207                 logger.debug("Statuscode {} is NOT OK: [{}]", response.getStatus(), uri);
208                 try {
209                     content = response.getContentAsString();
210                     logger.trace("Response error content: {}", content);
211                     final ErrorResponse error = gson.fromJson(content, ErrorResponse.class);
212
213                     if (error == null) {
214                         logger.debug("Error without JSON message, code: {} / message: {}", response.getStatus(),
215                                 response.getReason());
216                         throw new ApiException("Error code: " + response.getStatus());
217                     }
218
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:
230                             logger.debug(
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());
237                         default:
238                             logger.debug("Unknown error: {}", error);
239                             throw new ApiException("Unknown error: " + error);
240                     }
241                 } catch (final JsonSyntaxException e) {
242                     throw new ApiException("Invalid JSON syntax in error response: " + content);
243                 }
244         }
245     }
246
247     /**
248      * Sets a new state of a SwitchActuator.
249      */
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));
253     }
254
255     /**
256      * Sets the dimmer level of a DimmerActuator.
257      */
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));
261     }
262
263     /**
264      * Sets the roller shutter level of a RollerShutterActuator.
265      */
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));
270     }
271
272     /**
273      * Starts or stops moving a RollerShutterActuator
274      */
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));
279     }
280
281     /**
282      * Sets a new state of a VariableActuator.
283      */
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));
287     }
288
289     /**
290      * Sets the point temperature.
291      */
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));
296     }
297
298     /**
299      * Sets the operation mode to "Auto" or "Manu".
300      */
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));
307     }
308
309     /**
310      * Sets the alarm state.
311      */
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));
315     }
316
317     /**
318      * Load the device and returns a {@link List} of {@link Device}s..
319      *
320      * @param deviceIds Ids of the devices to return
321      * @return List of Devices
322      */
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());
328     }
329
330     /**
331      * Loads the {@link Device} with the given deviceId.
332      */
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);
336     }
337
338     /**
339      * Loads the states for all {@link Device}s.
340      */
341     public List<DeviceState> getDeviceStates() throws IOException, ApiException, AuthenticationException {
342         logger.debug("Loading device states...");
343         return executeGetList(API_URL_DEVICE_STATES, DeviceState[].class);
344     }
345
346     /**
347      * Loads the device state for the given deviceId.
348      */
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);
353     }
354
355     /**
356      * Loads the locations and returns a {@link List} of {@link Location}s.
357      *
358      * @return a List of Devices
359      */
360     public List<Location> getLocations() throws IOException, ApiException, AuthenticationException {
361         logger.debug("Loading locations...");
362         return executeGetList(API_URL_LOCATION, Location[].class);
363     }
364
365     /**
366      * Loads and returns a {@link List} of {@link Capability}s for the given deviceId.
367      *
368      * @param deviceId the id of the {@link Device}
369      * @return capabilities of the device
370      */
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);
375     }
376
377     /**
378      * Loads and returns a {@link List} of all {@link Capability}s.
379      */
380     public List<Capability> getCapabilities() throws IOException, ApiException, AuthenticationException {
381         logger.debug("Loading capabilities...");
382         return executeGetList(API_URL_CAPABILITY, Capability[].class);
383     }
384
385     /**
386      * Loads and returns a {@link List} of all {@link Capability}States.
387      */
388     public List<CapabilityState> getCapabilityStates() throws IOException, ApiException, AuthenticationException {
389         logger.debug("Loading capability states...");
390         return executeGetList(API_URL_CAPABILITY_STATES, CapabilityState[].class);
391     }
392
393     /**
394      * Returns a {@link List} of all {@link Message}s.
395      */
396     public List<Message> getMessages() throws IOException, ApiException, AuthenticationException {
397         logger.debug("Loading messages...");
398         return executeGetList(API_URL_MESSAGE, Message[].class);
399     }
400
401     /**
402      * @return the configVersion
403      */
404     public String getConfigVersion() {
405         return configVersion;
406     }
407 }