]> git.basschouten.com Git - openhab-addons.git/blob
08ca9101a9867697d4bf76d9e52e9a15874b9579
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.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;
26
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;
63
64 import com.google.gson.Gson;
65 import com.google.gson.GsonBuilder;
66 import com.google.gson.JsonSyntaxException;
67
68 /**
69  * The main client that handles the communication with the innogy SmartHome API service.
70  *
71  * @author Oliver Kuhl - Initial contribution
72  * @author Hilbrand Bouwkamp - Refactored to use openHAB http and oauth2 libraries
73  *
74  */
75 @NonNullByDefault
76 public class InnogyClient {
77
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;
82
83     private final Logger logger = LoggerFactory.getLogger(InnogyClient.class);
84
85     /**
86      * date format as used in json in API. Example: 2016-07-11T10:55:52.3863424Z
87      */
88     private static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
89
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 = "";
95
96     public InnogyClient(final OAuthClientService oAuthService, final HttpClient httpClient) {
97         this.oAuthService = oAuthService;
98         this.httpClient = httpClient;
99     }
100
101     /**
102      * Gets the status
103      *
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.
106      *
107      * @throws SessionExistsException thrown, if a session already exists
108      */
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);
112
113         bridgeDetails = status.gateway;
114         configVersion = bridgeDetails.getConfigVersion();
115
116         logger.debug("innogy SmartHome Status loaded. Configuration version is {}.", configVersion);
117     }
118
119     /**
120      * Executes a HTTP GET request with default headers and returns data as object of type T.
121      *
122      * @param url request URL
123      * @param clazz type of data to return
124      * @return response content
125      */
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));
129
130         return gson.fromJson(response.getContentAsString(), clazz);
131     }
132
133     /**
134      * Executes a HTTP GET request with default headers and returns data as List of type T.
135      *
136      * @param url request URL
137      * @param clazz array type of data to return as list
138      * @return response content (as a List)
139      */
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));
143     }
144
145     /**
146      * Executes a HTTP POST request with the given {@link Action} as content.
147      *
148      * @param url request URL
149      * @param action action to execute
150      */
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);
155
156         request(httpClient.newRequest(url).method(HttpMethod.POST)
157                 .content(new StringContentProvider(json), CONTENT_TYPE).accept(CONTENT_TYPE));
158     }
159
160     private ContentResponse request(final Request request) throws IOException, AuthenticationException, ApiException {
161         final ContentResponse response;
162         try {
163             final AccessTokenResponse accessTokenResponse = getAccessTokenResponse();
164
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);
171         }
172         handleResponseErrors(response, request.getURI());
173         return response;
174     }
175
176     public AccessTokenResponse getAccessTokenResponse() throws AuthenticationException, IOException {
177         @Nullable
178         final AccessTokenResponse accessTokenResponse;
179         try {
180             accessTokenResponse = oAuthService.getAccessTokenResponse();
181         } catch (OAuthException | OAuthResponseException e) {
182             throw new AuthenticationException("Error fetching access token: " + e.getMessage());
183         }
184         if (accessTokenResponse == null || accessTokenResponse.getAccessToken() == null
185                 || accessTokenResponse.getAccessToken().isBlank()) {
186             throw new AuthenticationException("No innogy accesstoken. Is this thing authorized?");
187         }
188         return accessTokenResponse;
189     }
190
191     /**
192      * Handles errors from the {@link ContentResponse} and throws the following errors:
193      *
194      * @param response response
195      * @param uri uri of api call made
196      * @throws ControllerOfflineException thrown, if the innogy SmartHome controller (SHC) is offline.
197      */
198     private void handleResponseErrors(final ContentResponse response, final URI uri) throws IOException, ApiException {
199         String content = "";
200
201         switch (response.getStatus()) {
202             case HttpStatus.OK_200:
203                 logger.debug("Statuscode is OK: [{}]", uri);
204                 return;
205             case HttpStatus.SERVICE_UNAVAILABLE_503:
206                 logger.debug("innogy service is unavailabe (503).");
207                 throw new ServiceUnavailableException("innogy service is unavailabe (503).");
208             default:
209                 logger.debug("Statuscode {} is NOT OK: [{}]", response.getStatus(), uri);
210                 try {
211                     content = response.getContentAsString();
212                     logger.trace("Response error content: {}", content);
213                     final ErrorResponse error = gson.fromJson(content, ErrorResponse.class);
214
215                     if (error == null) {
216                         logger.debug("Error without JSON message, code: {} / message: {}", response.getStatus(),
217                                 response.getReason());
218                         throw new ApiException("Error code: " + response.getStatus());
219                     }
220
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:
232                             logger.debug(
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());
239                         default:
240                             logger.debug("Unknown error: {}", error);
241                             throw new ApiException("Unknown error: " + error);
242                     }
243                 } catch (final JsonSyntaxException e) {
244                     throw new ApiException("Invalid JSON syntax in error response: " + content);
245                 }
246         }
247     }
248
249     /**
250      * Sets a new state of a SwitchActuator.
251      */
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));
255     }
256
257     /**
258      * Sets the dimmer level of a DimmerActuator.
259      */
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));
263     }
264
265     /**
266      * Sets the roller shutter level of a RollerShutterActuator.
267      */
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));
272     }
273
274     /**
275      * Starts or stops moving a RollerShutterActuator
276      */
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));
281     }
282
283     /**
284      * Sets a new state of a VariableActuator.
285      */
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));
289     }
290
291     /**
292      * Sets the point temperature.
293      */
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));
298     }
299
300     /**
301      * Sets the operation mode to "Auto" or "Manu".
302      */
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));
309     }
310
311     /**
312      * Sets the alarm state.
313      */
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));
317     }
318
319     /**
320      * Load the device and returns a {@link List} of {@link Device}s..
321      *
322      * @param deviceIds Ids of the devices to return
323      * @return List of Devices
324      */
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());
330     }
331
332     /**
333      * Loads the {@link Device} with the given deviceId.
334      */
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);
338     }
339
340     /**
341      * Loads the states for all {@link Device}s.
342      */
343     public List<DeviceState> getDeviceStates() throws IOException, ApiException, AuthenticationException {
344         logger.debug("Loading device states...");
345         return executeGetList(API_URL_DEVICE_STATES, DeviceState[].class);
346     }
347
348     /**
349      * Loads the device state for the given deviceId.
350      */
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);
355     }
356
357     /**
358      * Loads the locations and returns a {@link List} of {@link Location}s.
359      *
360      * @return a List of Devices
361      */
362     public List<Location> getLocations() throws IOException, ApiException, AuthenticationException {
363         logger.debug("Loading locations...");
364         return executeGetList(API_URL_LOCATION, Location[].class);
365     }
366
367     /**
368      * Loads and returns a {@link List} of {@link Capability}s for the given deviceId.
369      *
370      * @param deviceId the id of the {@link Device}
371      * @return capabilities of the device
372      */
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);
377     }
378
379     /**
380      * Loads and returns a {@link List} of all {@link Capability}s.
381      */
382     public List<Capability> getCapabilities() throws IOException, ApiException, AuthenticationException {
383         logger.debug("Loading capabilities...");
384         return executeGetList(API_URL_CAPABILITY, Capability[].class);
385     }
386
387     /**
388      * Loads and returns a {@link List} of all {@link Capability}States.
389      */
390     public List<CapabilityState> getCapabilityStates() throws IOException, ApiException, AuthenticationException {
391         logger.debug("Loading capability states...");
392         return executeGetList(API_URL_CAPABILITY_STATES, CapabilityState[].class);
393     }
394
395     /**
396      * Returns a {@link List} of all {@link Message}s.
397      */
398     public List<Message> getMessages() throws IOException, ApiException, AuthenticationException {
399         logger.debug("Loading messages...");
400         return executeGetList(API_URL_MESSAGE, Message[].class);
401     }
402
403     /**
404      * @return the configVersion
405      */
406     public String getConfigVersion() {
407         return configVersion;
408     }
409 }