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 {
82 private static final String BEARER = "Bearer ";
83 private static final String CONTENT_TYPE = "application/json";
84 private static final int HTTP_CLIENT_TIMEOUT_SECONDS = 10;
86 private final Logger logger = LoggerFactory.getLogger(InnogyClient.class);
89 * date format as used in json in API. Example: 2016-07-11T10:55:52.3863424Z
91 private static final String DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
93 private final Gson gson = new GsonBuilder().setDateFormat(DATE_FORMAT).create();
94 private final OAuthClientService oAuthService;
95 private final HttpClient httpClient;
96 private @Nullable Gateway bridgeDetails;
97 private String configVersion = "";
99 public InnogyClient(final OAuthClientService oAuthService, final HttpClient httpClient) {
100 this.oAuthService = oAuthService;
101 this.httpClient = httpClient;
105 * @return the bridgeInfo
107 public @Nullable Gateway getBridgeDetails() {
108 return bridgeDetails;
114 * As the API returns the details of the SmartHome controller (SHC), the data is saved in {@link #bridgeDetails} and
115 * the {@link #configVersion} is set.
117 * @throws SessionExistsException thrown, if a session already exists
118 * @throws IOException
119 * @throws ApiException
121 public void refreshStatus() throws IOException, ApiException, AuthenticationException {
122 logger.debug("Get innogy SmartHome status...");
123 final StatusResponse status = executeGet(API_URL_STATUS, StatusResponse.class);
125 bridgeDetails = status.gateway;
126 configVersion = bridgeDetails.getConfigVersion();
128 logger.debug("innogy SmartHome Status loaded. Configuration version is {}.", configVersion);
132 * Executes a HTTP GET request with default headers and returns data as object of type T.
135 * @param clazz type of data to return
137 * @throws IOException
138 * @throws AuthenticationException
139 * @throws ApiException
141 private <T> T executeGet(final String url, final Class<T> clazz)
142 throws IOException, AuthenticationException, ApiException {
143 final ContentResponse response = request(httpClient.newRequest(url).method(HttpMethod.GET));
145 return gson.fromJson(response.getContentAsString(), clazz);
149 * Executes a HTTP GET request with default headers and returns data as List of type T.
152 * @param clazz array type of data to return as list
153 * @throws IOException
154 * @throws AuthenticationException
155 * @throws ApiException
157 private <T> List<T> executeGetList(final String url, final Class<T[]> clazz)
158 throws IOException, AuthenticationException, ApiException {
159 return Arrays.asList(executeGet(url, clazz));
163 * Executes a HTTP POST request with the given {@link Action} as content.
168 * @throws IOException
169 * @throws AuthenticationException
170 * @throws ApiException
172 private ContentResponse executePost(final String url, final Action action)
173 throws IOException, AuthenticationException, ApiException {
174 final String json = gson.toJson(action);
175 logger.debug("Action {} JSON: {}", action.getType(), json);
177 return request(httpClient.newRequest(url).method(HttpMethod.POST)
178 .content(new StringContentProvider(json), CONTENT_TYPE).accept(CONTENT_TYPE));
181 private ContentResponse request(final Request request) throws IOException, AuthenticationException, ApiException {
182 final ContentResponse response;
184 final AccessTokenResponse accessTokenResponse = getAccessTokenResponse();
186 response = request.header(HttpHeader.ACCEPT, CONTENT_TYPE)
187 .header(HttpHeader.AUTHORIZATION, BEARER + accessTokenResponse.getAccessToken())
188 .timeout(HTTP_CLIENT_TIMEOUT_SECONDS, TimeUnit.SECONDS).send();
189 } catch (InterruptedException | TimeoutException | ExecutionException e) {
190 throw new IOException(e);
192 handleResponseErrors(response, request.getURI());
196 public AccessTokenResponse getAccessTokenResponse() throws AuthenticationException, IOException {
197 final AccessTokenResponse accessTokenResponse;
199 accessTokenResponse = oAuthService.getAccessTokenResponse();
200 } catch (OAuthException | OAuthResponseException e) {
201 throw new AuthenticationException("Error fetching access token: " + e.getMessage());
203 if (accessTokenResponse == null || StringUtils.isBlank(accessTokenResponse.getAccessToken())) {
204 throw new AuthenticationException("No innogy accesstoken. Is this thing authorized?");
206 return accessTokenResponse;
210 * Handles errors from the {@link ContentResponse} and throws the following errors:
213 * @param uri uri of api call made
214 * @throws SessionExistsException
215 * @throws SessionNotFoundException
216 * @throws ControllerOfflineException thrown, if the innogy SmartHome controller (SHC) is offline.
217 * @throws IOException
218 * @throws ApiException
219 * @throws AuthenticationException
221 private void handleResponseErrors(final ContentResponse response, final URI uri)
222 throws IOException, ApiException, AuthenticationException {
225 switch (response.getStatus()) {
226 case HttpStatus.OK_200:
227 logger.debug("Statuscode is OK: [{}]", uri);
229 case HttpStatus.SERVICE_UNAVAILABLE_503:
230 logger.debug("innogy service is unavailabe (503).");
231 throw new ServiceUnavailableException("innogy service is unavailabe (503).");
233 logger.debug("Statuscode {} is NOT OK: [{}]", response.getStatus(), uri);
235 content = response.getContentAsString();
236 logger.trace("Response error content: {}", content);
237 final ErrorResponse error = gson.fromJson(content, ErrorResponse.class);
240 logger.debug("Error without JSON message, code: {} / message: {}", response.getStatus(),
241 response.getReason());
242 throw new ApiException("Error code: " + response.getStatus());
245 switch (error.getCode()) {
246 case ErrorResponse.ERR_SESSION_EXISTS:
247 logger.debug("Session exists: {}", error);
248 throw new SessionExistsException(error.getDescription());
249 case ErrorResponse.ERR_SESSION_NOT_FOUND:
250 logger.debug("Session not found: {}", error);
251 throw new SessionNotFoundException(error.getDescription());
252 case ErrorResponse.ERR_CONTROLLER_OFFLINE:
253 logger.debug("Controller offline: {}", error);
254 throw new ControllerOfflineException(error.getDescription());
255 case ErrorResponse.ERR_REMOTE_ACCESS_NOT_ALLOWED:
257 "Remote access not allowed. Access is allowed only from the SHC device network.");
258 throw new RemoteAccessNotAllowedException(
259 "Remote access not allowed. Access is allowed only from the SHC device network.");
260 case ErrorResponse.ERR_INVALID_ACTION_TRIGGERED:
261 logger.debug("Invalid action triggered. Message: {}", error.getMessages());
262 throw new InvalidActionTriggeredException(error.getDescription());
264 logger.debug("Unknown error: {}", error);
265 throw new ApiException("Unknown error: " + error);
267 } catch (final JsonSyntaxException e) {
268 throw new ApiException("Invalid JSON syntax in error response: " + content);
274 * Sets a new state of a SwitchActuator.
276 * @param capabilityId
278 * @throws IOException
279 * @throws ApiException
281 public void setSwitchActuatorState(final String capabilityId, final boolean state)
282 throws IOException, ApiException, AuthenticationException {
283 executePost(API_URL_ACTION, new StateActionSetter(capabilityId, Capability.TYPE_SWITCHACTUATOR, state));
287 * Sets the dimmer level of a DimmerActuator.
289 * @param capabilityId
291 * @throws IOException
292 * @throws ApiException
294 public void setDimmerActuatorState(final String capabilityId, final int dimLevel)
295 throws IOException, ApiException, AuthenticationException {
296 executePost(API_URL_ACTION, new StateActionSetter(capabilityId, Capability.TYPE_DIMMERACTUATOR, dimLevel));
300 * Sets the roller shutter level of a RollerShutterActuator.
302 * @param capabilityId
303 * @param rollerShutterLevel
304 * @throws IOException
305 * @throws ApiException
306 * @throws AuthenticationException
308 public void setRollerShutterActuatorState(final String capabilityId, final int rollerShutterLevel)
309 throws IOException, ApiException, AuthenticationException {
310 executePost(API_URL_ACTION,
311 new StateActionSetter(capabilityId, Capability.TYPE_ROLLERSHUTTERACTUATOR, rollerShutterLevel));
315 * Starts or stops moving a RollerShutterActuator
317 * @param capabilityId
318 * @param rollerShutterAction
319 * @throws IOException
320 * @throws ApiException
321 * @throws AuthenticationException
323 public void setRollerShutterAction(final String capabilityId,
324 final ShutterAction.ShutterActions rollerShutterAction)
325 throws IOException, ApiException, AuthenticationException {
326 executePost(API_URL_ACTION, new ShutterAction(capabilityId, rollerShutterAction));
330 * Sets a new state of a VariableActuator.
332 * @param capabilityId
334 * @throws IOException
335 * @throws ApiException
337 public void setVariableActuatorState(final String capabilityId, final boolean state)
338 throws IOException, ApiException, AuthenticationException {
339 executePost(API_URL_ACTION, new StateActionSetter(capabilityId, Capability.TYPE_VARIABLEACTUATOR, state));
343 * Sets the point temperature.
345 * @param capabilityId
346 * @param pointTemperature
347 * @throws IOException
348 * @throws ApiException
350 public void setPointTemperatureState(final String capabilityId, final double pointTemperature)
351 throws IOException, ApiException, AuthenticationException {
352 executePost(API_URL_ACTION,
353 new StateActionSetter(capabilityId, Capability.TYPE_THERMOSTATACTUATOR, pointTemperature));
357 * Sets the operation mode to "Auto" or "Manu".
359 * @param capabilityId
361 * @throws IOException
362 * @throws ApiException
364 public void setOperationMode(final String capabilityId, final boolean autoMode)
365 throws IOException, ApiException, AuthenticationException {
366 executePost(API_URL_ACTION,
367 new StateActionSetter(capabilityId, Capability.TYPE_THERMOSTATACTUATOR,
368 autoMode ? CapabilityState.STATE_VALUE_OPERATION_MODE_AUTO
369 : CapabilityState.STATE_VALUE_OPERATION_MODE_MANUAL));
373 * Sets the alarm state.
375 * @param capabilityId
377 * @throws IOException
378 * @throws ApiException
380 public void setAlarmActuatorState(final String capabilityId, final boolean alarmState)
381 throws IOException, ApiException, AuthenticationException {
382 executePost(API_URL_ACTION, new StateActionSetter(capabilityId, Capability.TYPE_ALARMACTUATOR, alarmState));
386 * Load the device and returns a {@link List} of {@link Device}s..
388 * @return List of Devices
389 * @throws IOException
390 * @throws ApiException
392 public List<Device> getDevices() throws IOException, ApiException, AuthenticationException {
393 logger.debug("Loading innogy devices...");
394 return executeGetList(API_URL_DEVICE, Device[].class);
398 * Loads the {@link Device} with the given deviceId.
402 * @throws IOException
403 * @throws ApiException
405 public Device getDeviceById(final String deviceId) throws IOException, ApiException, AuthenticationException {
406 logger.debug("Loading device with id {}...", deviceId);
407 return executeGet(API_URL_DEVICE_ID.replace("{id}", deviceId), Device.class);
411 * Returns a {@link List} of all {@link Device}s with the full configuration details, {@link Capability}s and
412 * states. Calling this may take a while...
415 * @throws IOException
416 * @throws ApiException
418 public List<Device> getFullDevices() throws IOException, ApiException, AuthenticationException {
420 final List<Location> locationList = getLocations();
421 final Map<String, Location> locationMap = new HashMap<>();
422 for (final Location l : locationList) {
423 locationMap.put(l.getId(), l);
427 final List<Capability> capabilityList = getCapabilities();
428 final Map<String, Capability> capabilityMap = new HashMap<>();
429 for (final Capability c : capabilityList) {
430 capabilityMap.put(c.getId(), c);
434 final List<CapabilityState> capabilityStateList = getCapabilityStates();
435 final Map<String, CapabilityState> capabilityStateMap = new HashMap<>();
436 for (final CapabilityState cs : capabilityStateList) {
437 capabilityStateMap.put(cs.getId(), cs);
441 final List<DeviceState> deviceStateList = getDeviceStates();
442 final Map<String, DeviceState> deviceStateMap = new HashMap<>();
443 for (final DeviceState es : deviceStateList) {
444 deviceStateMap.put(es.getId(), es);
448 final List<Message> messageList = getMessages();
449 final Map<String, List<Message>> deviceMessageMap = new HashMap<>();
450 for (final Message m : messageList) {
451 if (m.getDevices() != null && !m.getDevices().isEmpty()) {
452 final String deviceId = m.getDevices().get(0).replace("/device/", "");
454 if (deviceMessageMap.containsKey(deviceId)) {
455 ml = deviceMessageMap.get(deviceId);
457 ml = new ArrayList<>();
460 deviceMessageMap.put(deviceId, ml);
465 final List<Device> deviceList = getDevices();
466 for (final Device d : deviceList) {
467 if (InnogyBindingConstants.BATTERY_POWERED_DEVICES.contains(d.getType())) {
468 d.setIsBatteryPowered(true);
472 d.setLocation(locationMap.get(d.getLocationId()));
473 final HashMap<String, Capability> deviceCapabilityMap = new HashMap<>();
475 // capabilities and their states
476 for (final String cl : d.getCapabilityLinkList()) {
477 final Capability c = capabilityMap.get(Link.getId(cl));
478 final String capabilityId = c.getId();
479 final CapabilityState capabilityState = capabilityStateMap.get(capabilityId);
480 c.setCapabilityState(capabilityState);
481 deviceCapabilityMap.put(capabilityId, c);
483 d.setCapabilityMap(deviceCapabilityMap);
486 d.setDeviceState(deviceStateMap.get(d.getId()));
489 if (deviceMessageMap.containsKey(d.getId())) {
490 d.setMessageList(deviceMessageMap.get(d.getId()));
491 for (final Message m : d.getMessageList()) {
492 switch (m.getType()) {
493 case Message.TYPE_DEVICE_LOW_BATTERY:
494 d.setLowBattery(true);
495 d.setLowBatteryMessageId(m.getId());
506 * Returns the {@link Device} with the given deviceId with full configuration details, {@link Capability}s and
507 * states. Calling this may take a little bit longer...
511 * @throws IOException
512 * @throws ApiException
514 public Device getFullDeviceById(final String deviceId) throws IOException, ApiException, AuthenticationException {
516 final List<Location> locationList = getLocations();
517 final Map<String, Location> locationMap = new HashMap<>();
518 for (final Location l : locationList) {
519 locationMap.put(l.getId(), l);
522 // CAPABILITIES FOR DEVICE
523 final List<Capability> capabilityList = getCapabilitiesForDevice(deviceId);
524 final Map<String, Capability> capabilityMap = new HashMap<>();
525 for (final Capability c : capabilityList) {
526 capabilityMap.put(c.getId(), c);
530 final List<CapabilityState> capabilityStateList = getCapabilityStates();
531 final Map<String, CapabilityState> capabilityStateMap = new HashMap<>();
532 for (final CapabilityState cs : capabilityStateList) {
533 capabilityStateMap.put(cs.getId(), cs);
537 final State state = getDeviceStateByDeviceId(deviceId);
538 final DeviceState deviceState = new DeviceState();
539 deviceState.setId(deviceId);
540 deviceState.setState(state);
543 final List<Message> messageList = getMessages();
544 final List<Message> ml = new ArrayList<>();
545 final String deviceIdPath = "/device/" + deviceId;
547 for (final Message m : messageList) {
548 logger.trace("Message Type {} with ID {}", m.getType(), m.getId());
549 if (m.getDevices() != null && !m.getDevices().isEmpty()) {
550 for (final String li : m.getDevices()) {
551 if (deviceIdPath.equals(li)) {
559 final Device d = getDeviceById(deviceId);
560 if (BATTERY_POWERED_DEVICES.contains(d.getType())) {
561 d.setIsBatteryPowered(true);
562 d.setLowBattery(false);
566 d.setLocation(locationMap.get(d.getLocationId()));
568 // capabilities and their states
569 final HashMap<String, Capability> deviceCapabilityMap = new HashMap<>();
570 for (final String cl : d.getCapabilityLinkList()) {
572 final Capability c = capabilityMap.get(Link.getId(cl));
573 c.setCapabilityState(capabilityStateMap.get(c.getId()));
574 deviceCapabilityMap.put(c.getId(), c);
577 d.setCapabilityMap(deviceCapabilityMap);
580 d.setDeviceState(deviceState);
584 d.setMessageList(ml);
585 for (final Message m : d.getMessageList()) {
586 switch (m.getType()) {
587 case Message.TYPE_DEVICE_LOW_BATTERY:
588 d.setLowBattery(true);
589 d.setLowBatteryMessageId(m.getId());
599 * Loads the states for all {@link Device}s.
602 * @throws IOException
603 * @throws ApiException
605 public List<DeviceState> getDeviceStates() throws IOException, ApiException, AuthenticationException {
606 logger.debug("Loading device states...");
607 return executeGetList(API_URL_DEVICE_STATES, DeviceState[].class);
611 * Loads the device state for the given deviceId.
615 * @throws IOException
616 * @throws ApiException
618 public State getDeviceStateByDeviceId(final String deviceId)
619 throws IOException, ApiException, AuthenticationException {
620 logger.debug("Loading device states for device id {}...", deviceId);
621 return executeGet(API_URL_DEVICE_ID_STATE.replace("{id}", deviceId), State.class);
625 * Loads the locations and returns a {@link List} of {@link Location}s.
627 * @return a List of Devices
628 * @throws IOException
629 * @throws ApiException
631 public List<Location> getLocations() throws IOException, ApiException, AuthenticationException {
632 logger.debug("Loading locations...");
633 return executeGetList(API_URL_LOCATION, Location[].class);
637 * Loads and returns a {@link List} of {@link Capability}s for the given deviceId.
639 * @param deviceId the id of the {@link Device}
641 * @throws IOException
642 * @throws ApiException
644 public List<Capability> getCapabilitiesForDevice(final String deviceId)
645 throws IOException, ApiException, AuthenticationException {
646 logger.debug("Loading capabilities for device {}...", deviceId);
647 return executeGetList(API_URL_DEVICE_CAPABILITIES.replace("{id}", deviceId), Capability[].class);
651 * Loads and returns a {@link List} of all {@link Capability}s.
654 * @throws IOException
655 * @throws ApiException
657 public List<Capability> getCapabilities() throws IOException, ApiException, AuthenticationException {
658 logger.debug("Loading capabilities...");
659 return executeGetList(API_URL_CAPABILITY, Capability[].class);
663 * Loads and returns a {@link List} of all {@link Capability}States.
666 * @throws IOException
667 * @throws ApiException
669 public List<CapabilityState> getCapabilityStates() throws IOException, ApiException, AuthenticationException {
670 logger.debug("Loading capability states...");
671 return executeGetList(API_URL_CAPABILITY_STATES, CapabilityState[].class);
675 * Returns a {@link List} of all {@link Message}s.
678 * @throws IOException
679 * @throws ApiException
681 public List<Message> getMessages() throws IOException, ApiException, AuthenticationException {
682 logger.debug("Loading messages...");
683 return executeGetList(API_URL_MESSAGE, Message[].class);
687 * @return the configVersion
689 public String getConfigVersion() {
690 return configVersion;
694 * @param configVersion the configVersion to set
696 public void setConfigVersion(final String configVersion) {
697 this.configVersion = configVersion;