2 * Copyright (c) 2010-2020 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.InnogyBindingConstants.*;
16 import static org.openhab.binding.innogysmarthome.internal.client.Constants.*;
18 import java.io.IOException;
20 import java.util.ArrayList;
21 import java.util.Arrays;
22 import java.util.HashMap;
23 import java.util.List;
25 import java.util.concurrent.ExecutionException;
26 import java.util.concurrent.TimeUnit;
27 import java.util.concurrent.TimeoutException;
29 import org.apache.commons.lang.StringUtils;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.eclipse.jetty.client.HttpClient;
33 import org.eclipse.jetty.client.api.ContentResponse;
34 import org.eclipse.jetty.client.api.Request;
35 import org.eclipse.jetty.client.util.StringContentProvider;
36 import org.eclipse.jetty.http.HttpHeader;
37 import org.eclipse.jetty.http.HttpMethod;
38 import org.eclipse.jetty.http.HttpStatus;
39 import org.openhab.binding.innogysmarthome.internal.InnogyBindingConstants;
40 import org.openhab.binding.innogysmarthome.internal.client.entity.StatusResponse;
41 import org.openhab.binding.innogysmarthome.internal.client.entity.action.Action;
42 import org.openhab.binding.innogysmarthome.internal.client.entity.action.ShutterAction;
43 import org.openhab.binding.innogysmarthome.internal.client.entity.action.StateActionSetter;
44 import org.openhab.binding.innogysmarthome.internal.client.entity.capability.Capability;
45 import org.openhab.binding.innogysmarthome.internal.client.entity.capability.CapabilityState;
46 import org.openhab.binding.innogysmarthome.internal.client.entity.device.Device;
47 import org.openhab.binding.innogysmarthome.internal.client.entity.device.DeviceState;
48 import org.openhab.binding.innogysmarthome.internal.client.entity.device.Gateway;
49 import org.openhab.binding.innogysmarthome.internal.client.entity.device.State;
50 import org.openhab.binding.innogysmarthome.internal.client.entity.error.ErrorResponse;
51 import org.openhab.binding.innogysmarthome.internal.client.entity.link.Link;
52 import org.openhab.binding.innogysmarthome.internal.client.entity.location.Location;
53 import org.openhab.binding.innogysmarthome.internal.client.entity.message.Message;
54 import org.openhab.binding.innogysmarthome.internal.client.exception.ApiException;
55 import org.openhab.binding.innogysmarthome.internal.client.exception.AuthenticationException;
56 import org.openhab.binding.innogysmarthome.internal.client.exception.ControllerOfflineException;
57 import org.openhab.binding.innogysmarthome.internal.client.exception.InvalidActionTriggeredException;
58 import org.openhab.binding.innogysmarthome.internal.client.exception.RemoteAccessNotAllowedException;
59 import org.openhab.binding.innogysmarthome.internal.client.exception.ServiceUnavailableException;
60 import org.openhab.binding.innogysmarthome.internal.client.exception.SessionExistsException;
61 import org.openhab.binding.innogysmarthome.internal.client.exception.SessionNotFoundException;
62 import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
63 import org.openhab.core.auth.client.oauth2.OAuthClientService;
64 import org.openhab.core.auth.client.oauth2.OAuthException;
65 import org.openhab.core.auth.client.oauth2.OAuthResponseException;
66 import org.slf4j.Logger;
67 import org.slf4j.LoggerFactory;
69 import com.google.gson.Gson;
70 import com.google.gson.GsonBuilder;
71 import com.google.gson.JsonSyntaxException;
74 * The main client that handles the communication with the innogy SmartHome API service.
76 * @author Oliver Kuhl - Initial contribution
77 * @author Hilbrand Bouwkamp - Refactored to use openHAB http and oauth2 libraries
81 public class InnogyClient {
83 private static final String BEARER = "Bearer ";
84 private static final String CONTENT_TYPE = "application/json";
85 private static final int HTTP_REQUEST_TIMEOUT_SECONDS = 10;
86 private static final int HTTP_REQUEST_IDLE_TIMEOUT_SECONDS = 20;
88 private final Logger logger = LoggerFactory.getLogger(InnogyClient.class);
91 * date format as used in json in API. Example: 2016-07-11T10:55:52.3863424Z
93 private static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
95 private final Gson gson = new GsonBuilder().setDateFormat(DATE_FORMAT).create();
96 private final OAuthClientService oAuthService;
97 private final HttpClient httpClient;
98 private @Nullable Gateway bridgeDetails;
99 private String configVersion = "";
101 public InnogyClient(final OAuthClientService oAuthService, final HttpClient httpClient) {
102 this.oAuthService = oAuthService;
103 this.httpClient = httpClient;
107 * @return the bridgeInfo
109 public @Nullable Gateway getBridgeDetails() {
110 return bridgeDetails;
116 * As the API returns the details of the SmartHome controller (SHC), the data is saved in {@link #bridgeDetails} and
117 * the {@link #configVersion} is set.
119 * @throws SessionExistsException thrown, if a session already exists
120 * @throws IOException
121 * @throws ApiException
123 public void refreshStatus() throws IOException, ApiException, AuthenticationException {
124 logger.debug("Get innogy SmartHome status...");
125 final StatusResponse status = executeGet(API_URL_STATUS, StatusResponse.class);
127 bridgeDetails = status.gateway;
128 configVersion = bridgeDetails.getConfigVersion();
130 logger.debug("innogy SmartHome Status loaded. Configuration version is {}.", configVersion);
134 * Executes a HTTP GET request with default headers and returns data as object of type T.
137 * @param clazz type of data to return
139 * @throws IOException
140 * @throws AuthenticationException
141 * @throws ApiException
143 private <T> T executeGet(final String url, final Class<T> clazz)
144 throws IOException, AuthenticationException, ApiException {
145 final ContentResponse response = request(httpClient.newRequest(url).method(HttpMethod.GET));
147 return gson.fromJson(response.getContentAsString(), clazz);
151 * Executes a HTTP GET request with default headers and returns data as List of type T.
154 * @param clazz array type of data to return as list
155 * @throws IOException
156 * @throws AuthenticationException
157 * @throws ApiException
159 private <T> List<T> executeGetList(final String url, final Class<T[]> clazz)
160 throws IOException, AuthenticationException, ApiException {
161 return Arrays.asList(executeGet(url, clazz));
165 * Executes a HTTP POST request with the given {@link Action} as content.
170 * @throws IOException
171 * @throws AuthenticationException
172 * @throws ApiException
174 private ContentResponse executePost(final String url, final Action action)
175 throws IOException, AuthenticationException, ApiException {
176 final String json = gson.toJson(action);
177 logger.debug("Action {} JSON: {}", action.getType(), json);
179 return request(httpClient.newRequest(url).method(HttpMethod.POST)
180 .content(new StringContentProvider(json), CONTENT_TYPE).accept(CONTENT_TYPE));
183 private ContentResponse request(final Request request) throws IOException, AuthenticationException, ApiException {
184 final ContentResponse response;
186 final AccessTokenResponse accessTokenResponse = getAccessTokenResponse();
188 response = request.header(HttpHeader.ACCEPT, CONTENT_TYPE)
189 .header(HttpHeader.AUTHORIZATION, BEARER + accessTokenResponse.getAccessToken())
190 .idleTimeout(HTTP_REQUEST_IDLE_TIMEOUT_SECONDS, TimeUnit.SECONDS)
191 .timeout(HTTP_REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS).send();
192 } catch (InterruptedException | TimeoutException | ExecutionException e) {
193 throw new IOException(e);
195 handleResponseErrors(response, request.getURI());
199 public AccessTokenResponse getAccessTokenResponse() throws AuthenticationException, IOException {
200 final AccessTokenResponse accessTokenResponse;
202 accessTokenResponse = oAuthService.getAccessTokenResponse();
203 } catch (OAuthException | OAuthResponseException e) {
204 throw new AuthenticationException("Error fetching access token: " + e.getMessage());
206 if (accessTokenResponse == null || StringUtils.isBlank(accessTokenResponse.getAccessToken())) {
207 throw new AuthenticationException("No innogy accesstoken. Is this thing authorized?");
209 return accessTokenResponse;
213 * Handles errors from the {@link ContentResponse} and throws the following errors:
216 * @param uri uri of api call made
217 * @throws SessionExistsException
218 * @throws SessionNotFoundException
219 * @throws ControllerOfflineException thrown, if the innogy SmartHome controller (SHC) is offline.
220 * @throws IOException
221 * @throws ApiException
222 * @throws AuthenticationException
224 private void handleResponseErrors(final ContentResponse response, final URI uri)
225 throws IOException, ApiException, AuthenticationException {
228 switch (response.getStatus()) {
229 case HttpStatus.OK_200:
230 logger.debug("Statuscode is OK: [{}]", uri);
232 case HttpStatus.SERVICE_UNAVAILABLE_503:
233 logger.debug("innogy service is unavailabe (503).");
234 throw new ServiceUnavailableException("innogy service is unavailabe (503).");
236 logger.debug("Statuscode {} is NOT OK: [{}]", response.getStatus(), uri);
238 content = response.getContentAsString();
239 logger.trace("Response error content: {}", content);
240 final ErrorResponse error = gson.fromJson(content, ErrorResponse.class);
243 logger.debug("Error without JSON message, code: {} / message: {}", response.getStatus(),
244 response.getReason());
245 throw new ApiException("Error code: " + response.getStatus());
248 switch (error.getCode()) {
249 case ErrorResponse.ERR_SESSION_EXISTS:
250 logger.debug("Session exists: {}", error);
251 throw new SessionExistsException(error.getDescription());
252 case ErrorResponse.ERR_SESSION_NOT_FOUND:
253 logger.debug("Session not found: {}", error);
254 throw new SessionNotFoundException(error.getDescription());
255 case ErrorResponse.ERR_CONTROLLER_OFFLINE:
256 logger.debug("Controller offline: {}", error);
257 throw new ControllerOfflineException(error.getDescription());
258 case ErrorResponse.ERR_REMOTE_ACCESS_NOT_ALLOWED:
260 "Remote access not allowed. Access is allowed only from the SHC device network.");
261 throw new RemoteAccessNotAllowedException(
262 "Remote access not allowed. Access is allowed only from the SHC device network.");
263 case ErrorResponse.ERR_INVALID_ACTION_TRIGGERED:
264 logger.debug("Invalid action triggered. Message: {}", error.getMessages());
265 throw new InvalidActionTriggeredException(error.getDescription());
267 logger.debug("Unknown error: {}", error);
268 throw new ApiException("Unknown error: " + error);
270 } catch (final JsonSyntaxException e) {
271 throw new ApiException("Invalid JSON syntax in error response: " + content);
277 * Sets a new state of a SwitchActuator.
279 * @param capabilityId
281 * @throws IOException
282 * @throws ApiException
284 public void setSwitchActuatorState(final String capabilityId, final boolean state)
285 throws IOException, ApiException, AuthenticationException {
286 executePost(API_URL_ACTION, new StateActionSetter(capabilityId, Capability.TYPE_SWITCHACTUATOR, state));
290 * Sets the dimmer level of a DimmerActuator.
292 * @param capabilityId
294 * @throws IOException
295 * @throws ApiException
297 public void setDimmerActuatorState(final String capabilityId, final int dimLevel)
298 throws IOException, ApiException, AuthenticationException {
299 executePost(API_URL_ACTION, new StateActionSetter(capabilityId, Capability.TYPE_DIMMERACTUATOR, dimLevel));
303 * Sets the roller shutter level of a RollerShutterActuator.
305 * @param capabilityId
306 * @param rollerShutterLevel
307 * @throws IOException
308 * @throws ApiException
309 * @throws AuthenticationException
311 public void setRollerShutterActuatorState(final String capabilityId, final int rollerShutterLevel)
312 throws IOException, ApiException, AuthenticationException {
313 executePost(API_URL_ACTION,
314 new StateActionSetter(capabilityId, Capability.TYPE_ROLLERSHUTTERACTUATOR, rollerShutterLevel));
318 * Starts or stops moving a RollerShutterActuator
320 * @param capabilityId
321 * @param rollerShutterAction
322 * @throws IOException
323 * @throws ApiException
324 * @throws AuthenticationException
326 public void setRollerShutterAction(final String capabilityId,
327 final ShutterAction.ShutterActions rollerShutterAction)
328 throws IOException, ApiException, AuthenticationException {
329 executePost(API_URL_ACTION, new ShutterAction(capabilityId, rollerShutterAction));
333 * Sets a new state of a VariableActuator.
335 * @param capabilityId
337 * @throws IOException
338 * @throws ApiException
340 public void setVariableActuatorState(final String capabilityId, final boolean state)
341 throws IOException, ApiException, AuthenticationException {
342 executePost(API_URL_ACTION, new StateActionSetter(capabilityId, Capability.TYPE_VARIABLEACTUATOR, state));
346 * Sets the point temperature.
348 * @param capabilityId
349 * @param pointTemperature
350 * @throws IOException
351 * @throws ApiException
353 public void setPointTemperatureState(final String capabilityId, final double pointTemperature)
354 throws IOException, ApiException, AuthenticationException {
355 executePost(API_URL_ACTION,
356 new StateActionSetter(capabilityId, Capability.TYPE_THERMOSTATACTUATOR, pointTemperature));
360 * Sets the operation mode to "Auto" or "Manu".
362 * @param capabilityId
364 * @throws IOException
365 * @throws ApiException
367 public void setOperationMode(final String capabilityId, final boolean autoMode)
368 throws IOException, ApiException, AuthenticationException {
369 executePost(API_URL_ACTION,
370 new StateActionSetter(capabilityId, Capability.TYPE_THERMOSTATACTUATOR,
371 autoMode ? CapabilityState.STATE_VALUE_OPERATION_MODE_AUTO
372 : CapabilityState.STATE_VALUE_OPERATION_MODE_MANUAL));
376 * Sets the alarm state.
378 * @param capabilityId
380 * @throws IOException
381 * @throws ApiException
383 public void setAlarmActuatorState(final String capabilityId, final boolean alarmState)
384 throws IOException, ApiException, AuthenticationException {
385 executePost(API_URL_ACTION, new StateActionSetter(capabilityId, Capability.TYPE_ALARMACTUATOR, alarmState));
389 * Load the device and returns a {@link List} of {@link Device}s..
391 * @return List of Devices
392 * @throws IOException
393 * @throws ApiException
395 public List<Device> getDevices() throws IOException, ApiException, AuthenticationException {
396 logger.debug("Loading innogy devices...");
397 return executeGetList(API_URL_DEVICE, Device[].class);
401 * Loads the {@link Device} with the given deviceId.
405 * @throws IOException
406 * @throws ApiException
408 public Device getDeviceById(final String deviceId) throws IOException, ApiException, AuthenticationException {
409 logger.debug("Loading device with id {}...", deviceId);
410 return executeGet(API_URL_DEVICE_ID.replace("{id}", deviceId), Device.class);
414 * Returns a {@link List} of all {@link Device}s with the full configuration details, {@link Capability}s and
415 * states. Calling this may take a while...
418 * @throws IOException
419 * @throws ApiException
421 public List<Device> getFullDevices() throws IOException, ApiException, AuthenticationException {
423 final List<Location> locationList = getLocations();
424 final Map<String, Location> locationMap = new HashMap<>();
425 for (final Location l : locationList) {
426 locationMap.put(l.getId(), l);
430 final List<Capability> capabilityList = getCapabilities();
431 final Map<String, Capability> capabilityMap = new HashMap<>();
432 for (final Capability c : capabilityList) {
433 capabilityMap.put(c.getId(), c);
437 final List<CapabilityState> capabilityStateList = getCapabilityStates();
438 final Map<String, CapabilityState> capabilityStateMap = new HashMap<>();
439 for (final CapabilityState cs : capabilityStateList) {
440 capabilityStateMap.put(cs.getId(), cs);
444 final List<DeviceState> deviceStateList = getDeviceStates();
445 final Map<String, DeviceState> deviceStateMap = new HashMap<>();
446 for (final DeviceState es : deviceStateList) {
447 deviceStateMap.put(es.getId(), es);
451 final List<Message> messageList = getMessages();
452 final Map<String, List<Message>> deviceMessageMap = new HashMap<>();
453 for (final Message m : messageList) {
454 if (m.getDevices() != null && !m.getDevices().isEmpty()) {
455 final String deviceId = m.getDevices().get(0).replace("/device/", "");
457 if (deviceMessageMap.containsKey(deviceId)) {
458 ml = deviceMessageMap.get(deviceId);
460 ml = new ArrayList<>();
463 deviceMessageMap.put(deviceId, ml);
468 final List<Device> deviceList = getDevices();
469 for (final Device d : deviceList) {
470 if (InnogyBindingConstants.BATTERY_POWERED_DEVICES.contains(d.getType())) {
471 d.setIsBatteryPowered(true);
475 d.setLocation(locationMap.get(d.getLocationId()));
476 final HashMap<String, Capability> deviceCapabilityMap = new HashMap<>();
478 // capabilities and their states
479 for (final String cl : d.getCapabilityLinkList()) {
480 final Capability c = capabilityMap.get(Link.getId(cl));
481 final String capabilityId = c.getId();
482 final CapabilityState capabilityState = capabilityStateMap.get(capabilityId);
483 c.setCapabilityState(capabilityState);
484 deviceCapabilityMap.put(capabilityId, c);
486 d.setCapabilityMap(deviceCapabilityMap);
489 d.setDeviceState(deviceStateMap.get(d.getId()));
492 if (deviceMessageMap.containsKey(d.getId())) {
493 d.setMessageList(deviceMessageMap.get(d.getId()));
494 for (final Message m : d.getMessageList()) {
495 switch (m.getType()) {
496 case Message.TYPE_DEVICE_LOW_BATTERY:
497 d.setLowBattery(true);
498 d.setLowBatteryMessageId(m.getId());
509 * Returns the {@link Device} with the given deviceId with full configuration details, {@link Capability}s and
510 * states. Calling this may take a little bit longer...
514 * @throws IOException
515 * @throws ApiException
517 public Device getFullDeviceById(final String deviceId) throws IOException, ApiException, AuthenticationException {
519 final List<Location> locationList = getLocations();
520 final Map<String, Location> locationMap = new HashMap<>();
521 for (final Location l : locationList) {
522 locationMap.put(l.getId(), l);
525 // CAPABILITIES FOR DEVICE
526 final List<Capability> capabilityList = getCapabilitiesForDevice(deviceId);
527 final Map<String, Capability> capabilityMap = new HashMap<>();
528 for (final Capability c : capabilityList) {
529 capabilityMap.put(c.getId(), c);
533 final List<CapabilityState> capabilityStateList = getCapabilityStates();
534 final Map<String, CapabilityState> capabilityStateMap = new HashMap<>();
535 for (final CapabilityState cs : capabilityStateList) {
536 capabilityStateMap.put(cs.getId(), cs);
540 final State state = getDeviceStateByDeviceId(deviceId);
541 final DeviceState deviceState = new DeviceState();
542 deviceState.setId(deviceId);
543 deviceState.setState(state);
546 final List<Message> messageList = getMessages();
547 final List<Message> ml = new ArrayList<>();
548 final String deviceIdPath = "/device/" + deviceId;
550 for (final Message m : messageList) {
551 logger.trace("Message Type {} with ID {}", m.getType(), m.getId());
552 if (m.getDevices() != null && !m.getDevices().isEmpty()) {
553 for (final String li : m.getDevices()) {
554 if (deviceIdPath.equals(li)) {
562 final Device d = getDeviceById(deviceId);
563 if (BATTERY_POWERED_DEVICES.contains(d.getType())) {
564 d.setIsBatteryPowered(true);
565 d.setLowBattery(false);
569 d.setLocation(locationMap.get(d.getLocationId()));
571 // capabilities and their states
572 final HashMap<String, Capability> deviceCapabilityMap = new HashMap<>();
573 for (final String cl : d.getCapabilityLinkList()) {
575 final Capability c = capabilityMap.get(Link.getId(cl));
576 c.setCapabilityState(capabilityStateMap.get(c.getId()));
577 deviceCapabilityMap.put(c.getId(), c);
580 d.setCapabilityMap(deviceCapabilityMap);
583 d.setDeviceState(deviceState);
587 d.setMessageList(ml);
588 for (final Message m : d.getMessageList()) {
589 switch (m.getType()) {
590 case Message.TYPE_DEVICE_LOW_BATTERY:
591 d.setLowBattery(true);
592 d.setLowBatteryMessageId(m.getId());
602 * Loads the states for all {@link Device}s.
605 * @throws IOException
606 * @throws ApiException
608 public List<DeviceState> getDeviceStates() throws IOException, ApiException, AuthenticationException {
609 logger.debug("Loading device states...");
610 return executeGetList(API_URL_DEVICE_STATES, DeviceState[].class);
614 * Loads the device state for the given deviceId.
618 * @throws IOException
619 * @throws ApiException
621 public State getDeviceStateByDeviceId(final String deviceId)
622 throws IOException, ApiException, AuthenticationException {
623 logger.debug("Loading device states for device id {}...", deviceId);
624 return executeGet(API_URL_DEVICE_ID_STATE.replace("{id}", deviceId), State.class);
628 * Loads the locations and returns a {@link List} of {@link Location}s.
630 * @return a List of Devices
631 * @throws IOException
632 * @throws ApiException
634 public List<Location> getLocations() throws IOException, ApiException, AuthenticationException {
635 logger.debug("Loading locations...");
636 return executeGetList(API_URL_LOCATION, Location[].class);
640 * Loads and returns a {@link List} of {@link Capability}s for the given deviceId.
642 * @param deviceId the id of the {@link Device}
644 * @throws IOException
645 * @throws ApiException
647 public List<Capability> getCapabilitiesForDevice(final String deviceId)
648 throws IOException, ApiException, AuthenticationException {
649 logger.debug("Loading capabilities for device {}...", deviceId);
650 return executeGetList(API_URL_DEVICE_CAPABILITIES.replace("{id}", deviceId), Capability[].class);
654 * Loads and returns a {@link List} of all {@link Capability}s.
657 * @throws IOException
658 * @throws ApiException
660 public List<Capability> getCapabilities() throws IOException, ApiException, AuthenticationException {
661 logger.debug("Loading capabilities...");
662 return executeGetList(API_URL_CAPABILITY, Capability[].class);
666 * Loads and returns a {@link List} of all {@link Capability}States.
669 * @throws IOException
670 * @throws ApiException
672 public List<CapabilityState> getCapabilityStates() throws IOException, ApiException, AuthenticationException {
673 logger.debug("Loading capability states...");
674 return executeGetList(API_URL_CAPABILITY_STATES, CapabilityState[].class);
678 * Returns a {@link List} of all {@link Message}s.
681 * @throws IOException
682 * @throws ApiException
684 public List<Message> getMessages() throws IOException, ApiException, AuthenticationException {
685 logger.debug("Loading messages...");
686 return executeGetList(API_URL_MESSAGE, Message[].class);
690 * @return the configVersion
692 public String getConfigVersion() {
693 return configVersion;
697 * @param configVersion the configVersion to set
699 public void setConfigVersion(final String configVersion) {
700 this.configVersion = configVersion;