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